Expandable Tabs
beui/expandable-tabsFreeBlock
Icon tab bar where the active tab expands to a labelled pill, with a panel above that morphs height and slides content direction-aware on switch.
Install it
npx shadcn@latest add https://beui.dev/r/expandable-tabs.jsonSource
1"use client";2// beui.dev/components/blocks/expandable-tabs34import {5 AnimatePresence,6 motion,7 useReducedMotion,8 type Variants,9} from "motion/react";10import {11 useCallback,12 useEffect,13 useLayoutEffect,14 useRef,15 useState,16 type ReactNode,17} from "react";18import { EASE_OUT } from "@/lib/ease";19import { cn } from "@/lib/utils";2021export type ExpandableTabsItem = {22 id: string;23 /** String label — shown inside the active tab and used as the button's accessible name. */24 label: string;25 icon: ReactNode;26 /** Panel shown above the bar when this tab is active. */27 content: ReactNode;28};2930export type ExpandableTabsClassNames = {31 root?: string;32 panel?: string;33 bar?: string;34 tab?: string;35 activeTab?: string;36 icon?: string;37 label?: string;38 pill?: string;39};4041export interface ExpandableTabsProps {42 items: ExpandableTabsItem[];43 /** Active tab id, or null/undefined for the closed (bar-only) state. */44 value?: string | null;45 defaultValue?: string | null;46 onValueChange?: (id: string | null) => void;47 className?: string;48 classNames?: ExpandableTabsClassNames;49}5051type Size = { width: number; height: number };5253// DynamicIsland-style real width/height motion, tuned tighter here so the tab54// bar feels controlled instead of elastic.55const SHELL_SPRING = { type: "spring", duration: 0.58, bounce: 0.06 } as const;5657// Position-only tab layout motion keeps switching loose without stretching58// icons or letting the label linger.59const TAB_CHANGE_SPRING = {60 type: "spring",61 duration: 0.46,62 bounce: 0.04,63} as const;64const LABEL_OPEN = { type: "spring", duration: 0.38, bounce: 0.03 } as const;65const LABEL_CLOSE = { duration: 0.16, ease: EASE_OUT } as const;6667// Fixed bar height keeps the content panel's bottom reserve static so the open68// height is right on the first frame. p-2 (16) + h-9 button (36).69const BAR_H = 52;70const TAB_W = 32;71const BAR_X = 16;72const BAR_GAP = 4;73const ROOT_BORDER = 2;74const ICON_W = 16;75const ACTIVE_LEFT_PAD = 10;76const ACTIVE_RIGHT_PAD = 16;77const LABEL_GAP = 7;78const PANEL_DOCK_GAP = 4;7980// Content is clipped above the dock so rows never pass through the icon bar.81// It enters from slightly above instead of from the dock line.82const CONTENT_VARIANTS: Variants = {83 enter: { y: -8, scale: 0.98, opacity: 0, filter: "blur(4px)" },84 center: { y: 0, scale: 1, opacity: 1, filter: "blur(0px)" },85 exit: {86 y: -6,87 scale: 0.98,88 opacity: 0,89 filter: "blur(4px)",90// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| File Upload Attachment Uploadattachment-upload | beui | Blocks | Free | 4 deps · 5 files | |
| Availability Scheduleravailability-scheduler | beui | Blocks | Free | 4 deps · 15 files | |
| Bloom Menubloom-menu | beui | Blocks | Free | 4 deps · 3 files | |
| Command Palettecommand-palette | beui | Blocks | Free | 4 deps · 3 files | |
| Dynamic Islanddynamic-island | beui | Blocks | Free | 3 deps · 3 files | |
| Expandable Action Barexpandable-action-bar | beui | Blocks | Free | 3 deps · 2 files | |
| Feedback Widgetfeedback-widget | beui | Blocks | Free | 4 deps · 9 files | |
| File Upload Upload Queuefile-upload | beui | Blocks | Free | 4 deps · 3 files |
