SolanaUISolanaUI

Token Command

A command palette component for token selection with optional grouping

Command Palette

Search for a command to run...

import { TokenCommand } from "@/components/sol/token-command";

const SOL_ICON =
  "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png";
const USDC_ICON =
  "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png";
const BONK_ICON =
  "https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I";
const JITOSOL_ICON =
  "https://storage.googleapis.com/token-metadata/JitoSOL-256.png";

export function TokenCommandDemo() {
  return (
    <TokenCommand
      groups={[
        {
          heading: "Global Pool",
          tokens: [
            { icon: SOL_ICON, symbol: "SOL" },
            { icon: USDC_ICON, symbol: "USDC" },
            { icon: BONK_ICON, symbol: "BONK" },
          ],
        },
        {
          heading: "Isolated Pools",
          tokens: [
            { icon: JITOSOL_ICON, symbol: "JitoSOL" },
          ],
        },
      ]}
    />
  );
}

Installation

pnpm dlx shadcn@latest add @solanaui/token-command
npx shadcn@latest add @solanaui/token-command
yarn dlx shadcn@latest add @solanaui/token-command

Usage

Flat Token List

<TokenCommand
  tokens={[
    { icon: SOL_ICON, symbol: "SOL" },
    { icon: USDC_ICON, symbol: "USDC" },
  ]}
/>

Grouped Tokens

Use the groups prop to organize tokens into labeled sections. Pass either tokens or groups, not both.

<TokenCommand
  groups={[
    {
      heading: "Global Pool",
      tokens: [
        { icon: SOL_ICON, symbol: "SOL" },
        { icon: USDC_ICON, symbol: "USDC" },
      ],
    },
    {
      heading: "Isolated Pools",
      tokens: [
        { icon: JITOSOL_ICON, symbol: "JitoSOL" },
      ],
    },
  ]}
/>

With Selection Callback

<TokenCommand
  tokens={[
    { icon: SOL_ICON, symbol: "SOL" },
    { icon: USDC_ICON, symbol: "USDC" },
  ]}
  onSelect={(token) => console.log("Selected:", token.symbol)}
/>

Source Code

"use client";import { ChevronsUpDown } from "lucide-react";import React from "react";import { TokenIcon } from "@/registry/sol/token-icon";import { Button } from "@/components/ui/button";import {  CommandDialog,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,} from "@/components/ui/command";import { cn } from "@/lib/utils";interface TokenCommandToken {  icon: string;  symbol: string;}interface TokenCommandGroup {  heading: string;  tokens: TokenCommandToken[];}type TokenCommandProps = {  onSelect?: (token: TokenCommandToken) => void;  className?: string;} & (  | { tokens: TokenCommandToken[]; groups?: never }  | { groups: TokenCommandGroup[]; tokens?: never });const TokenCommand = ({  tokens,  groups,  onSelect,  className,}: TokenCommandProps) => {  const [open, setOpen] = React.useState(false);  const [value, setValue] = React.useState("");  const allTokens = React.useMemo(() => {    if (tokens) return tokens;    if (groups) return groups.flatMap((g) => g.tokens);    return [];  }, [tokens, groups]);  const activeToken = allTokens.find(    (token) => token.symbol.toLowerCase() === value.toLowerCase(),  );  React.useEffect(() => {    const down = (e: KeyboardEvent) => {      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {        e.preventDefault();        setOpen((open) => !open);      }    };    document.addEventListener("keydown", down);    return () => document.removeEventListener("keydown", down);  }, []);  const renderItem = (token: TokenCommandToken) => (    <CommandItem      key={token.symbol}      value={token.symbol}      onSelect={(currentValue) => {        const newValue = currentValue === value ? "" : currentValue;        setValue(newValue);        setOpen(false);        if (newValue) {          onSelect?.(token);        }      }}    >      <TokenIcon src={token.icon} alt={token.symbol} width={20} height={20} />      {token.symbol}    </CommandItem>  );  return (    <>      <Button        variant="outline"        className={cn("w-[200px] justify-between", className)}        onClick={() => setOpen(true)}      >        {activeToken ? (          <div className="flex items-center gap-2.5">            <TokenIcon              src={activeToken.icon}              alt={activeToken.symbol}              width={20}              height={20}            />            {activeToken.symbol}          </div>        ) : (          "Select token..."        )}        <ChevronsUpDown className="opacity-50" />      </Button>      <CommandDialog open={open} onOpenChange={setOpen}>        <CommandInput placeholder="Search token..." />        <CommandList>          <CommandEmpty>No tokens found.</CommandEmpty>          {groups ? (            groups.map((group) => (              <CommandGroup key={group.heading} heading={group.heading}>                {group.tokens.map(renderItem)}              </CommandGroup>            ))          ) : (            <CommandGroup heading="Tokens">              {allTokens.map(renderItem)}            </CommandGroup>          )}        </CommandList>      </CommandDialog>    </>  );};export type { TokenCommandProps, TokenCommandToken, TokenCommandGroup };export { TokenCommand };