Primitives
Tabs
Segmented navigation with a stable active indicator.
Composition
The preferred shape for composing the installed primitive from its exported parts.
Parts
Exported compound parts from the installed source.
Tabs├── TabsList├── TabsTab└── TabsPanelThe refined source installs this primitive from src/registry/sources/refined/ui/tabs.tsx. Install it on its own with the command above, or inspect the source below.
npx shadcn@latest add http://127.0.0.1:3000/r/refined/tabs.jsonRaw code
Primary installed primitive source
Base UI Tabs slot
src/registry/sources/refined/ui/tabs.tsx
"use client";import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";import type { CSSProperties, ReactNode } from "react";import { Children, Fragment, isValidElement, useRef } from "react";import type { ControlSize, TabsListProps, TabsPanelProps, TabsProps, TabsTabProps } from "@/components/refined-ui/contracts";import { cn } from "@/components/refined-ui/lib/cn";import { skinSlot } from "@/components/refined-ui/skin";// Active indicator driven by Base UI's --active-tab-width/--active-tab-left — slides between tabs with one transition.export function Tabs({ className, onValueChange, ...props }: TabsProps) { const rootRef = useRef<HTMLDivElement>(null); const clearPrevHeight = useRef(0); // The cross-slide's height morph needs the outgoing panel's height as the entering panel's starting // height — auto→auto never transitions, and CSS cannot measure a sibling. Captured here pre-commit // (the active panel is still the old one), cleared right after the starting frame so a later switch // that skips onValueChange (externally controlled value) never morphs from a stale height. const handleValueChange = (value: string) => { const root = rootRef.current; if (root) { for (const panel of root.querySelectorAll('[data-ui="agent"][data-slot="tab-panel"]')) { if (!(panel instanceof HTMLElement) || panel.inert || panel.hidden) continue; if (panel.closest('[data-slot="tabs"]') !== root) continue; root.style.setProperty("--aui-tabs-prev-height", `${panel.getBoundingClientRect().height}px`); cancelAnimationFrame(clearPrevHeight.current); clearPrevHeight.current = requestAnimationFrame(() => { clearPrevHeight.current = requestAnimationFrame(() => root.style.removeProperty("--aui-tabs-prev-height")); }); break; } } onValueChange?.(value); }; return ( <TabsPrimitive.Root data-ui="agent" data-slot="tabs" className={cn(skinSlot("tabs", {}), className)} onValueChange={handleValueChange} {...props} ref={rootRef} /> );}const controlHeights: Record<ControlSize, string> = { xs: "var(--control-h-xs)", sm: "var(--control-h-sm)", md: "var(--control-h-md)", lg: "var(--control-h-lg)",};type TabsListStyle = CSSProperties & { "--tabs-trigger-h"?: string;};function countTabs(children: ReactNode): number { return Children.toArray(children).reduce<number>((count, child) => { if (!isValidElement<{ children?: ReactNode }>(child)) return count; if (child.type === TabsTab) return count + 1; if (child.type === Fragment) return count + countTabs(child.props.children); return count; }, 0);}export function TabsList({ size = "sm", className, children, style, ...props }: TabsListProps) { const isSingle = countTabs(children) === 1; const controlStyle = { "--tabs-trigger-h": controlHeights[size], ...style, } satisfies TabsListStyle; return ( <TabsPrimitive.List data-ui="agent" data-slot="tabs-list" data-size={size} data-single={isSingle ? "true" : undefined} className={cn("group/tabs-list", skinSlot("tabs-list", { size }), className)} style={controlStyle} {...props} > {children} {isSingle ? null : ( <TabsPrimitive.Indicator data-ui="agent" data-slot="tabs-indicator" className={cn(skinSlot("tabs-indicator", {}))} /> )} </TabsPrimitive.List> );}export function TabsTab({ className, ...props }: TabsTabProps) { return <TabsPrimitive.Tab data-ui="agent" data-slot="tab" className={cn(skinSlot("tab", {}), className)} {...props} />;}export function TabsPanel({ className, ...props }: TabsPanelProps) { return ( <TabsPrimitive.Panel data-ui="agent" data-slot="tab-panel" className={cn( "outline-none data-[hidden]:hidden focus-visible:ring-2 focus-visible:ring-foreground/20 [&[hidden]]:hidden", skinSlot("tab-panel", {}), className, )} {...props} /> );}