SolanaUISolanaUI

Wallet Sheet

A Phantom-style slide-out wallet panel with balance, action buttons, and token list

import { WalletSheet } from "@/components/sol/wallet-sheet";

const SOL_ICON =
  "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png";

export function WalletSheetDemo() {
  return (
    <WalletSheet
      address="MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA"
      balance="$8,241.50"
      balanceChange="+$342.18"
      balanceChangePercent="+4.33%"
      tokens={[
        {
          icon: SOL_ICON,
          name: "Solana",
          symbol: "SOL",
          balance: "24.58",
          value: "$5,996.73",
          change: "+$248.30",
        },
      ]}
    />
  );
}

Installation

pnpm dlx shadcn@latest add @solanaui/wallet-sheet
npx shadcn@latest add @solanaui/wallet-sheet
yarn dlx shadcn@latest add @solanaui/wallet-sheet

Usage

const SOL_ICON =
  "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png";

<WalletSheet
  address="MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA"
  balance="$8,241.50"
  balanceChange="+$342.18"
  balanceChangePercent="+4.33%"
  tokens={[
    {
      icon: SOL_ICON,
      name: "Solana",
      symbol: "SOL",
      balance: "24.58",
      value: "$5,996.73",
      change: "+$248.30",
    },
  ]}
>
  {/* Optional footer content (import Button from @/components/ui/button) */}
  <Button variant="outline" className="w-full">Disconnect</Button>
</WalletSheet>

Source Code

"use client";import {  ArrowUpIcon,  DollarSignIcon,  QrCodeIcon,  RepeatIcon,  WalletIcon,} from "lucide-react";import type React from "react";import { Button } from "@/components/ui/button";import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";import { cn } from "@/lib/utils";import { AddressDisplay } from "@/registry/sol/address-display";import { TokenIcon } from "@/registry/sol/token-icon";interface WalletSheetProps {  address?: string;  balance?: string;  balanceChange?: string;  balanceChangePercent?: string;  tokens?: {    icon: string;    name: string;    symbol: string;    balance: string;    value: string;    change?: string;  }[];  actions?: {    label: string;    icon?: React.ReactNode;  }[];  children?: React.ReactNode;  trigger?: React.ReactNode;  className?: string;}const DEFAULT_ACTIONS = [  { label: "Send", icon: <ArrowUpIcon className="size-4" /> },  { label: "Swap", icon: <RepeatIcon className="size-4" /> },  { label: "Receive", icon: <QrCodeIcon className="size-4" /> },  { label: "Buy", icon: <DollarSignIcon className="size-4" /> },];const truncateAddress = (address: string) => {  if (address.length <= 12) return address;  return `${address.slice(0, 4)}...${address.slice(-4)}`;};const isNegative = (value: string) => {  return value.trim().startsWith("-");};const WalletSheet = ({  address,  balance,  balanceChange,  balanceChangePercent,  tokens,  actions = DEFAULT_ACTIONS,  children,  trigger,  className,}: WalletSheetProps) => {  return (    <Sheet>      <SheetTrigger asChild>        {trigger ?? (          <Button variant="outline" size="sm" className="gap-2">            <WalletIcon className="size-4" />            {address ? truncateAddress(address) : "Connect"}          </Button>        )}      </SheetTrigger>      <SheetContent className={cn("flex flex-col p-0 gap-0", className)}>        {/* Header: address + copy */}        {address && (          <div className="flex items-center justify-center pt-6 pb-2 px-6">            <AddressDisplay address={address} />          </div>        )}        {/* Balance hero */}        {balance && (          <div className="flex flex-col items-center gap-1 px-6 pb-4 pt-2">            <span className="text-4xl font-semibold tracking-tight">              {balance}            </span>            {(balanceChange || balanceChangePercent) && (              <div className="flex items-center gap-1.5 text-sm">                {balanceChange && (                  <span                    className={cn(                      isNegative(balanceChange)                        ? "text-red-400"                        : "text-emerald-500",                    )}                  >                    {balanceChange}                  </span>                )}                {balanceChangePercent && (                  <span                    className={cn(                      isNegative(balanceChangePercent)                        ? "text-red-400"                        : "text-emerald-500",                    )}                  >                    {balanceChangePercent}                  </span>                )}              </div>            )}          </div>        )}        {/* Action buttons row */}        {actions.length > 0 && (          <div className="flex items-center justify-center gap-4 px-6 pb-5">            {actions.map((action) => (              <div                key={action.label}                className="flex flex-col items-center gap-1.5"              >                <div className="flex items-center justify-center size-10 rounded-full bg-muted">                  {action.icon}                </div>                <span className="text-xs text-muted-foreground">                  {action.label}                </span>              </div>            ))}          </div>        )}        {/* Token list */}        {tokens && tokens.length > 0 && (          <div className="flex flex-col flex-1 overflow-auto border-t">            <span className="text-sm font-medium px-6 pt-4 pb-2">Tokens</span>            <div className="flex flex-col">              {tokens.map((token) => {                const changeNegative = token.change                  ? isNegative(token.change)                  : false;                return (                  <div                    key={token.symbol}                    className="flex items-center justify-between px-6 py-3 hover:bg-muted/50 transition-colors"                  >                    <div className="flex items-center gap-3">                      <TokenIcon                        src={token.icon}                        alt={token.symbol}                        width={36}                        height={36}                      />                      <div className="flex flex-col">                        <span className="text-sm font-medium">                          {token.name}                        </span>                        <span className="text-xs text-muted-foreground">                          {token.balance} {token.symbol}                        </span>                      </div>                    </div>                    <div className="flex flex-col items-end">                      <span className="text-sm font-medium">{token.value}</span>                      {token.change && (                        <span                          className={cn(                            "text-xs",                            changeNegative                              ? "text-red-400"                              : "text-emerald-500",                          )}                        >                          {token.change}                        </span>                      )}                    </div>                  </div>                );              })}            </div>          </div>        )}        {/* Consumer-provided footer content (e.g. disconnect button) */}        {children && <div className="mt-auto p-4 border-t">{children}</div>}      </SheetContent>    </Sheet>  );};export type { WalletSheetProps };export { WalletSheet };