Token List
Lists SPL token holdings in a collapsible accordion. Requires connect-button.
Preview
"use client";import { useTokens } from "@solana/connector/react";import { Coins, RefreshCw } from "lucide-react";import { useRef } from "react";import { Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";import { cn } from "@/lib/utils";interface TokenListProps { limit?: number;}export function TokenList({ limit = 5 }: TokenListProps) { const { tokens, isLoading, refetch } = useTokens(); const hasEverLoaded = useRef(false); if (!isLoading) { hasEverLoaded.current = true; } const displayTokens = tokens.slice(0, limit); const showSkeleton = isLoading && !hasEverLoaded.current; return ( <Accordion type="single" collapsible className="w-full"> <AccordionItem value="tokens" className="rounded-xl border px-3"> <div className="flex items-center justify-between"> <AccordionTrigger className="flex-1 gap-2 py-3 hover:cursor-pointer hover:no-underline"> <div className="flex items-center gap-2"> <Coins className="h-4 w-4" /> <span className="font-medium">Tokens</span> </div> </AccordionTrigger> <button type="button" onClick={(e) => { e.stopPropagation(); refetch(); }} disabled={isLoading} className="hover:bg-accent rounded p-1 transition-colors disabled:opacity-50" > <RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} /> </button> </div> <AccordionContent className="h-auto"> {showSkeleton ? ( <div className="space-y-2"> {[1, 2, 3].map((i) => ( <div key={i} className="flex items-center gap-3"> <div className="bg-muted h-8 w-8 animate-pulse rounded-full" /> <div className="flex-1"> <div className="bg-muted mb-1 h-4 w-16 animate-pulse rounded" /> <div className="bg-muted h-3 w-24 animate-pulse rounded" /> </div> </div> ))} </div> ) : displayTokens.length === 0 ? ( <p className="text-muted-foreground py-2 text-center text-sm"> No tokens found </p> ) : ( <div className="space-y-2 pb-2"> {displayTokens.map((token) => ( <div key={token.mint} className="flex items-center gap-3 py-1"> {token.logo ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={token.logo} className="h-8 w-8 rounded-full" alt={token.symbol} /> ) : ( <div className="bg-muted flex h-8 w-8 items-center justify-center rounded-full"> <Coins className="h-4 w-4" /> </div> )} <div className="flex-1"> <span className="block truncate text-sm font-medium"> {token.symbol} </span> <span className="text-muted-foreground block truncate text-xs"> {token.name} </span> </div> <span className="font-mono text-sm tabular-nums"> {token.formatted} </span> </div> ))} </div> )} </AccordionContent> </AccordionItem> </Accordion> );}Prerequisites
Complete the installation guide and install Connect Button before using this addon.
Try the Demo
Want to see it working immediately without manual wiring? Install the demo:
npx shadcn@latest add https://nitso.fun/r/token-list-demo.jsonpnpm dlx shadcn@latest add https://nitso.fun/r/token-list-demo.jsonyarn dlx shadcn@latest add https://nitso.fun/r/token-list-demo.jsonbunx --bun shadcn@latest add https://nitso.fun/r/token-list-demo.jsonThis installs token-list-demo.tsx into your components/ folder. Drop it anywhere:
import { TokenListDemo } from '@/components/token-list-demo';
export default function Page() {
return <TokenListDemo />;
}Delete it when you're ready to wire things up yourself.
Installation
npx shadcn@latest add https://nitso.fun/r/token-list.jsonpnpm dlx shadcn@latest add https://nitso.fun/r/token-list.jsonyarn dlx shadcn@latest add https://nitso.fun/r/token-list.jsonbunx --bun shadcn@latest add https://nitso.fun/r/token-list.jsonInstall shadcn UI components
npx shadcn@latest accordionpnpm dlx shadcn@latest accordionyarn dlx shadcn@latest accordionbunx --bun shadcn@latest accordionCopy the source code
"use client";import { useTokens } from "@solana/connector/react";import { Coins, RefreshCw } from "lucide-react";import { useRef } from "react";import { Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion";import { cn } from "@/lib/utils";interface TokenListProps { limit?: number;}export function TokenList({ limit = 5 }: TokenListProps) { const { tokens, isLoading, refetch } = useTokens(); const hasEverLoaded = useRef(false); if (!isLoading) { hasEverLoaded.current = true; } const displayTokens = tokens.slice(0, limit); const showSkeleton = isLoading && !hasEverLoaded.current; return ( <Accordion type="single" collapsible className="w-full"> <AccordionItem value="tokens" className="rounded-xl border px-3"> <div className="flex items-center justify-between"> <AccordionTrigger className="flex-1 gap-2 py-3 hover:cursor-pointer hover:no-underline"> <div className="flex items-center gap-2"> <Coins className="h-4 w-4" /> <span className="font-medium">Tokens</span> </div> </AccordionTrigger> <button type="button" onClick={(e) => { e.stopPropagation(); refetch(); }} disabled={isLoading} className="hover:bg-accent rounded p-1 transition-colors disabled:opacity-50" > <RefreshCw className={cn("h-3.5 w-3.5", isLoading && "animate-spin")} /> </button> </div> <AccordionContent className="h-auto"> {showSkeleton ? ( <div className="space-y-2"> {[1, 2, 3].map((i) => ( <div key={i} className="flex items-center gap-3"> <div className="bg-muted h-8 w-8 animate-pulse rounded-full" /> <div className="flex-1"> <div className="bg-muted mb-1 h-4 w-16 animate-pulse rounded" /> <div className="bg-muted h-3 w-24 animate-pulse rounded" /> </div> </div> ))} </div> ) : displayTokens.length === 0 ? ( <p className="text-muted-foreground py-2 text-center text-sm"> No tokens found </p> ) : ( <div className="space-y-2 pb-2"> {displayTokens.map((token) => ( <div key={token.mint} className="flex items-center gap-3 py-1"> {token.logo ? ( // biome-ignore lint/performance/noImgElement: token logos are dynamic external URLs <img src={token.logo} className="h-8 w-8 rounded-full" alt={token.symbol} /> ) : ( <div className="bg-muted flex h-8 w-8 items-center justify-center rounded-full"> <Coins className="h-4 w-4" /> </div> )} <div className="flex-1"> <span className="block truncate text-sm font-medium"> {token.symbol} </span> <span className="text-muted-foreground block truncate text-xs"> {token.name} </span> </div> <span className="font-mono text-sm tabular-nums"> {token.formatted} </span> </div> ))} </div> )} </AccordionContent> </AccordionItem> </Accordion> );}Wiring
Update the file where you use <ConnectButton /> (e.g. your header or layout):
Before:
import { ConnectButton } from "@/components/nitso/connect-button/";
export function Header() {
return (
<nav>
<ConnectButton />
</nav>
);
}After:
import { ConnectButton } from "@/components/nitso/connect-button/";
import {
WalletDropdownContent,
type WalletDropdownContentProps,
} from "@/components/nitso/connect-button/";
import { TokenList } from "@/components/nitso/addons/token-list/";
function WalletDropdownWithTokens(props: WalletDropdownContentProps) {
return (
<WalletDropdownContent {...props}>
<TokenList /> // [!code ++]
</WalletDropdownContent>
);
}
export function Header() {
return (
<nav>
<ConnectButton /> // [!code --]
<ConnectButton dropdownContent={WalletDropdownWithTokens} /> // [!code ++]
</nav>
);
}TokenList plugs into the children slot of WalletDropdownContent, rendering a collapsible accordion with SPL token holdings between the wallet header and the disconnect button.
Combining with other addons
If you already have other addons wired up, add TokenList as another child:
import { useState } from "react";
import { ConnectButton } from "@/components/nitso/connect-button/";
import {
WalletDropdownContent,
type WalletDropdownContentProps,
} from "@/components/nitso/connect-button/";
import {
NetworkSwitcherButton,
NetworkSwitcherView,
} from "@/components/nitso/addons/network-switcher/";
import { WalletBalance } from "@/components/nitso/addons/wallet-balance/";
import { TokenList } from "@/components/nitso/addons/token-list/";
function WalletDropdownFull(props: WalletDropdownContentProps) {
const [view, setView] = useState<"wallet" | "network">("wallet");
if (view === "network") {
return <NetworkSwitcherView onBack={() => setView("wallet")} />;
}
return (
<WalletDropdownContent
{...props}
actions={<NetworkSwitcherButton onClick={() => setView("network")} />}
>
<WalletBalance />
<TokenList /> // [!code ++]
</WalletDropdownContent>
);
}
export function Header() {
return (
<nav>
<ConnectButton dropdownContent={WalletDropdownFull} />
</nav>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
limit | number | 5 | Maximum number of tokens to display. |
Notes
Token metadata (name, symbol, logo) is fetched from the Solana Token List API. Token balances are fetched from your configured RPC endpoint.