Refined UI

An opinionated, customizable superset of shadcn/ui

AgentsPrimitivesSkillsSkins
Guides
  • Overview
  • Get started
  • Shadcn compatibility
  • Architecture
  • Agent surface
Primitives
  • Accordion
  • Alert
  • Alert dialog
  • Aspect ratio
  • Autocomplete
  • Avatar
  • Badge
  • Button
  • Button group
  • Calendar
  • Card
  • Checkbox
  • Checkbox Group
  • Code
  • Code Diff
  • Collapsible
  • Color Picker
  • Combobox
  • Command
  • Date Picker
  • Dialog
  • Drawer
  • Empty
  • Field
  • Form
  • Gradient Editor
  • Hover card
  • Input
  • Input group
  • Input OTP
  • Item
  • Kbd
  • Markdown
  • Menu
  • Menubar
  • Meter
  • Native select
  • Navigation menu
  • Number Field
  • Pagination
  • Popover
  • Progress
  • Radio group
  • Resizable
  • Scroll area
  • Select
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Table of contents
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toolbar
  • Tooltip
  • Tree
  • Trigger Menu
  • Typography
Hooks
  • Use Chat Message
  • Use Chat Input
  • Use User Ask
  • Use Audio Recorder
  • Use Environment Variables
  • Use Copy To Clipboard
  • Use Tool Call
  • Use Sidebar Resize
Utils
  • cn
  • Skin
  • Contracts
  • Serialize
Extensions
  • Control Effects
  • View Transition
  • Send Aurora
  1. Primitives
  2. Tabs
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
└── TabsPanel

The 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.

Registry command
npx shadcn@latest add http://127.0.0.1:3000/r/refined/tabs.json
See registry manifest

Raw 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}
/>
);
}
​

On this page

  • Preview
  • Composition
  • Installation
  • Source
Rendered output of the component.