Addons

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.json
pnpm dlx shadcn@latest add https://nitso.fun/r/token-list-demo.json
yarn dlx shadcn@latest add https://nitso.fun/r/token-list-demo.json
bunx --bun shadcn@latest add https://nitso.fun/r/token-list-demo.json

This 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.json
pnpm dlx shadcn@latest add https://nitso.fun/r/token-list.json
yarn dlx shadcn@latest add https://nitso.fun/r/token-list.json
bunx --bun shadcn@latest add https://nitso.fun/r/token-list.json

Install shadcn UI components

npx shadcn@latest accordion
pnpm dlx shadcn@latest accordion
yarn dlx shadcn@latest accordion
bunx --bun shadcn@latest accordion

Copy 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:

components/header.tsx
import { ConnectButton } from "@/components/nitso/connect-button/";

export function Header() {
  return (
    <nav>
      <ConnectButton />
    </nav>
  );
}

After:

components/header.tsx
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:

components/header.tsx
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

PropTypeDefaultDescription
limitnumber5Maximum 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.

On this page