Expandable Tabs
dominik-portfolio/expandable-tabsFreeComponent
Animated icon tabs with a sliding active pill and an expanding label, built on the shadcn tooltip.
Install it
npx shadcn@latest add https://dominikkoch.dev/r/expandable-tabs.jsonSource
1"use client";23import {4 type ReactNode,5 useCallback,6 useEffect,7 useLayoutEffect,8 useRef,9 useState,10} from "react";11import {12 Tooltip,13 TooltipContent,14 TooltipTrigger,15} from "@/components/ui/tooltip";16import { cn } from "@/lib/utils";1718interface ExpandableTabsItem {19 value: string;20 label: string;21 icon: ReactNode;22 href?: string;23}2425interface ExpandableTabsProps {26 items: ExpandableTabsItem[];27 value?: string;28 defaultValue?: string;29 onValueChange?: (value: string) => void;30 label?: string;31 className?: string;32}3334interface ExpandableTabProps {35 item: ExpandableTabsItem;36 isActive: boolean;37 onSelect: (value: string) => void;38}3940function ExpandableTab({ item, isActive, onSelect }: ExpandableTabProps) {41 const tabClassName =42 "relative z-10 flex shrink-0 cursor-pointer items-center rounded-[calc(var(--radius-2xl)-6px)] p-2 outline-none transition-colors duration-200 ease-out focus-visible:ring-2 focus-visible:ring-ring";4344 const content = (45 <>46 {item.icon}47 <span48 className={cn(49 "grid transition-[grid-template-columns,opacity] duration-300 ease-out",50 isActive ? "grid-cols-[1fr] opacity-100" : "grid-cols-[0fr] opacity-0"51 )}52 >53 <span className="min-w-0 overflow-hidden whitespace-nowrap font-medium text-foreground text-sm">54 <span className="block w-max pr-1 pl-2">{item.label}</span>55 </span>56 </span>57 </>58 );5960 return (61 <Tooltip disabled={isActive}>62 <TooltipTrigger63 render={64 item.href ? (65 <a66 aria-current={isActive ? "page" : undefined}67 className={tabClassName}68 data-value={item.value}69 href={item.href}70 >71 {content}72 </a>73 ) : (74 <button75 aria-pressed={isActive}76 className={tabClassName}77 data-value={item.value}78 onClick={() => onSelect(item.value)}79 type="button"80 >81 {content}82 </button>83 )84 }85 />86 <TooltipContent>{item.label}</TooltipContent>87 </Tooltip>88 );89}9091function useActivePill(activeValue: string | undefined) {92 const menuRef = useRef<HTMLMenuElement>(null);93 const [pill, setPill] = useState<{94 left: number;95 top: number;96 width: number;97 height: number;98 } | null>(null);99100 const measure = useCallback(() => {101// … truncated
What it pulls in
Other registry items
Files it writes
More from dominik-portfolio
All 4 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Braille Loaderbraille-loader | dominik-portfolio | Components | Free | no deps · 2 files | |
| Command Tabscommand-tabs | dominik-portfolio | Components | Free | 1 dep | |
| Permission Selectorpermission-selector | dominik-portfolio | Components | Free | 1 dep |
