Tabs
Usage
Branch tab content
Commit tab content
Pull Request tab content
// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";
export function TabsDemo() {
return (
<Tabs defaultValue="branch" color="#f08c00" classNames={{ panel: "min-w-56 p-4" }}>
<Tabs.List>
<Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
<Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit</Tabs.Tab>
<Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
<Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
<Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
</Tabs>
);
}
Properties
Round
Color
// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";
export function TabsDemo() {
return (
<Tabs defaultValue="commit" classNames={{ root: "data-[orientation=horizontal]:flex data-[orientation=horizontal]:flex-col data-[inverted]:data-[orientation=horizontal]:flex-col-reverse size-full", panel: "min-w-56 p-4" }}>
<Tabs.List>
<Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
<Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit
</Tabs.Tab>
<Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
<Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
<Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
</Tabs>
);
}
Tabs Position
// Using Compound Components is only supported on client side rendering
"use client";
import { Tabs } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";
export function TabsDemo() {
return (
<Tabs defaultValue="commit" classNames={{ panel: "min-w-56 p-4" }}>
<Tabs.List>
<Tabs.Tab value="branch" leftSection={<GitBranchIcon size={14} />}>Branch</Tabs.Tab>
<Tabs.Tab value="commit" leftSection={<GitCommitIcon size={14} />}>Commit
</Tabs.Tab>
<Tabs.Tab value="pullrequest" leftSection={<GitPullRequestIcon size={14} />}>Pull Request</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="branch">Branch tab content</Tabs.Panel>
<Tabs.Panel value="commit">Commit tab content</Tabs.Panel>
<Tabs.Panel value="pullrequest">Pull Request tab content</Tabs.Panel>
</Tabs>
);
}
Tabs Customs
"use client";
import React from "react";
import { Tabs, type TabsProps, type TabsListProps } from "@/ui/tabs";
import { GitBranchIcon, GitCommitIcon, GitPullRequestIcon } from "@/icons/*";
import { FloatingIndicator } from "@/ui/floating-indicator";
const data = [
{
label: "Branch",
description: "Branches are a core feature of version control systems, allowing you to isolate and manage changes in your codebase. A branch represents a separate line of development where you can experiment with new features, fix bugs, or make changes without impacting the main codebase. Once your work is complete, you can merge the branch back into the main development stream.",
icon: <GitBranchIcon size={14} />
},
{
label: "Commit",
description: "Commits are snapshots of your project at a specific point in time. They serve as a way to document changes in your code, providing a history of development that can be referenced or reverted to if needed. Each commit includes a message describing the changes, making it easier to track the evolution of your codebase over time.",
icon: <GitCommitIcon size={14} />
},
{
label: "Pull Request",
description: "Pull requests facilitate collaboration in version control workflows, enabling developers to propose changes, review code, and discuss potential improvements before merging into the main branch. They promote code quality, encourage team collaboration, and provide a platform to ensure that every change meets the required standards before integration.",
icon: <GitPullRequestIcon size={14} />
}
];
const validatedValue = (value: string) => value.trim().toLowerCase();
const DEFAULT_VALUE: string = validatedValue(data[0]?.label);
export function TabsDemo() {
const [parentRef, setParentRef] = React.useState<HTMLDivElement | null>(null);
const [controlsRefs, setControlsRefs] = React.useState<Record<string, HTMLButtonElement | null>>({});
const [active, setActive] = React.useState<string>(DEFAULT_VALUE);
const [hover, setHover] = React.useState<string | null>(null);
const setControlRef = (key: string) => (node: HTMLButtonElement) => {
controlsRefs[key] = node;
setControlsRefs(controlsRefs);
};
const propsTab = (key: string) => ({
ref: setControlRef(key),
onClick: () => setActive(key),
onMouseEnter: () => setHover(key),
onMouseLeave: () => setHover(null),
});
return (
<Tabs defaultValue={DEFAULT_VALUE} classNames={{ root: "size-full", panel: "min-w-56 p-4" }}>
<Tabs.List ref={setParentRef}>
{data.map((i, _i) => (
<Tabs.Tab key={_i} value={validatedValue(i.label)} leftSection={i.icon} {...propsTab(validatedValue(i.label))}>
{i.label}
</Tabs.Tab>
))}
<FloatingIndicator
target={controlsRefs[hover ?? active]}
parent={parentRef}
transitionDuration={350}
className="bg-muted/60 border-muted-foreground/60 rounded-md rounded-b-none border-b-transparent -z-1 shadow-md border-2"
/>
</Tabs.List>
{data.map((i, _i) => (
<Tabs.Panel key={_i} value={validatedValue(i.label)} className="overflow-auto max-h-full max-w-full">
{i.description}
</Tabs.Panel>
))}
</Tabs>
);
}
API References
Styles API
type T = "root" | "list" | "tab" | "tabLabel" | "tabSection" | "panel";
Styles API | Type | Default | Annotation |
---|---|---|---|
unstyled? | Partial<Record<T, boolean>> | false | if true , default styles will be removed |
className? | string | undefined | pass to root component <div> |
classNames? | Partial<Record<T, string>> | undefined | |
style? | CSSProperties | undefined | pass to root component <div> |
styles? | Partial<Record<T, CSSProperties>> | undefined |
Props API
Props API | Type | Default | Annotation |
---|---|---|---|
defaultValue? | string | null | Default value for uncontrolled component | |
value? | string | null | Value for controlled component | |
onChange? | (value: string | null) => void | Called when value changes | |
orientation? | 'vertical' | 'horizontal' | horizontal | Tabs orientation |
placement? | 'left' | 'right' | 'left' | Tabs.List placement relative to Tabs.Panel , applicable only when orientation="vertical" |
id? | string | generated randomly | Base id, used to generate ids to connect labels with controls |
loop? | boolean | true | Determines whether arrow key presses should loop though items (first to last and last to first) |
activateTabWithKeyboard? | boolean | true | Determines whether tab should be activated with arrow key press |
allowTabDeactivation? | boolean | false | Determines whether tab can be deactivated |
children? | React.ReactNode | undefined | Tabs content |
color? | Colors | `` | Changes colors of Tabs.Tab components when variant is pills or default , does nothing for other variants |
radius? | (string & {}) | number | `` | Key of valid CSS value to set border-radius |
inverted? | boolean | false | Determines whether tabs should have inverted styles |
keepMounted? | boolean | true | If set to false , Tabs.Panel content will be unmounted when the associated tab is not active |
Source Codes
tabs.tsx
globals.css
/* tabs */
@layer base {
.stylelayer-tabs {
@apply [--tab-border-color:hsl(var(--border))] [display:--tabs-display] [flex-direction:--tabs-flex-direction] [--tab-justify:flex-start] [--tabs-panel-grow:unset] [--tabs-display:block] [--tabs-flex-direction:row] [--tabs-list-border-width:0] [--tabs-list-border-size:0_0_var(--tabs-list-border-width)_0] [--tabs-list-gap:unset] [--tabs-list-line-bottom:0] [--tabs-list-line-top:unset] [--tabs-list-line-start:0] [--tabs-list-line-end:0] [--tab-round:var(--tabs-round)_var(--tabs-round)_0_0] [--tab-border-width:0_0_var(--tabs-list-border-width)_0];
&[data-inverted] {
@apply [--tabs-list-line-bottom:unset] [--tabs-list-line-top:0] [--tab-round:0_0_var(--tabs-round)_var(--tabs-round)] [--tab-border-width:var(--tabs-list-border-width)_0_0_0];
& .tabs-list[data-variant="default"]::before,
& .tabs-list[data-variant="outline"]::before {
@apply top-0 bottom-[unset];
}
}
&[data-orientation="vertical"] {
@apply [--tabs-list-line-start:unset] [--tabs-list-line-end:0] [--tabs-list-line-top:0] [--tabs-list-line-bottom:0] [--tabs-list-border-size:0_var(--tabs-list-border-width)_0_0] [--tab-border-width:0_var(--tabs-list-border-width)_0_0] [--tab-round:var(--tabs-round)_0_0_var(--tabs-round)] [--tabs-panel-grow:1] [--tabs-display:flex];
&:where([dir="rtl"]) {
@apply [--tabs-list-border-size:0_0_0_var(--tabs-list-border-width)] [--tab-border-width:0_0_0_var(--tabs-list-border-width)] [--tab-round:0_var(--tabs-round)_var(--tabs-round)_0];
}
&[data-placement="right"] {
@apply [--tabs-flex-direction:row-reverse] [--tabs-list-line-start:0] [--tabs-list-line-end:unset] [--tabs-list-border-size:0_0_0_var(--tabs-list-border-width)] [--tab-border-width:0_0_0_var(--tabs-list-border-width)] [--tab-round:_0_var(--tabs-round)_var(--tabs-round)_0];
&:where([dir="rtl"]) {
@apply [--tabs-list-border-size:0_var(--tabs-list-border-width)_0_0] [--tab-border-width:0_var(--tabs-list-border-width)_0_0] [--tab-round:var(--tabs-round)_0_0_var(--tabs-round)];
}
}
}
&[data-orientation="horizontal"] {
@apply [--tab-justify:center];
}
&[data-variant="default"] {
@apply [--tabs-list-border-width:2px] [--tab-hover-color:hsl(var(--muted)/0.5)];
}
&[data-variant="outline"] {
@apply [--tabs-list-border-width:0.0625rem];
}
&[data-variant="pills"] {
@apply [--tabs-list-gap:calc(0.75rem/2)] [--tab-hover-color:hsl(var(--muted)/0.5)];
}
& .tabs-list {
&[data-grow] {
@apply [--tab-grow:1];
}
&[data-variant="default"],
&[data-variant="outline"] {
@apply relative;
&::before {
@apply [content:--content,''] absolute border-solid border-[--tab-border-color] [border-width:--tabs-list-border-size] bottom-[--tabs-list-line-bottom] left-[--tabs-list-line-start] right-[--tabs-list-line-end] top-[--tabs-list-line-top];
}
}
}
& .tabs-panel {
@apply [flex-grow:--tabs-panel-grow];
}
& .tabs-tab {
@apply relative py-2 px-4 text-sm whitespace-nowrap z-0 flex items-center leading-none select-none [flex-grow:--tab-grow] [justify-content:--tab-justify] focus:z-1 focus-visible:z-1;
&:disabled,
&[data-disabled],
&[aria-disabled="true"] {
@apply opacity-50 cursor-not-allowed;
}
&[data-variant="default"] {
@apply [--tab-bg:transparent] rounded-[--tab-round] border-solid border-transparent [border-width:--tab-border-width] bg-[--tab-bg] active:[&:where(:not([data-active]))]:border-[--tab-border-color] active:[&:where(:not([data-active]))]:[--tab-bg:hsl(var(--muted))] [&:where([data-active])]:border-[--tabs-color] [&:where([data-active])]:active:[--tab-bg:--tab-hover-color];
&:disabled,
&[data-disabled] {
@apply hover:[--tab-bg:transparent];
}
}
&[data-variant="outline"] {
@apply relative border border-solid border-transparent rounded-[--tab-round] border-t-[--tab-border-top-color] border-b-[--tab-border-bottom-color] [--tab-border-bottom-color:transparent] [--tab-border-top-color:transparent] [--tab-border-inline-end-color:transparent] [--tab-border-inline-start-color:transparent];
&:where([data-active]) {
@apply border-t-[--tab-border-top-color] border-b-[--tab-border-bottom-color] border-l-[--tab-border-inline-start-color] border-r-[--tab-border-inline-end-color] [--tab-border-top-color:--tab-border-color] [--tab-border-inline-start-color:--tab-border-color] [--tab-border-inline-end-color:--tab-border-color] [--tab-border-bottom-color:hsl(var(--background))];
&::before {
@apply content-[''] absolute bg-[--tab-border-color] bottom-[--tab-before-bottom,-0.0625rem] left-[--tab-before-left,-0.0625rem] right-[--tab-before-right,auto] top-[--tab-before-top,auto] size-px;
}
&::after {
@apply content-[''] absolute bg-[--tab-border-color] bottom-[--tab-after-bottom,-0.0625rem] right-[--tab-after-right,-0.0625rem] left-[--tab-after-left,auto] top-[--tab-after-top,auto] size-px;
}
&[data-inverted] {
@apply [--tab-border-bottom-color:--tab-border-color] [--tab-border-top-color:--tab-border-color] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-bottom:auto] [--tab-after-top:-0.0625rem];
&[data-orientation="horizontal"] {
@apply [--tab-border-top-color:hsl(var(--background))];
}
}
&[data-orientation="vertical"] {
&[data-placement="left"] {
@apply [--tab-border-inline-end-color:hsl(var(--background))] [--tab-border-inline-start-color:--tab-border-color] [--tab-border-bottom-color:--tab-border-color] [--tab-before-right:-0.0625rem] [--tab-before-left:auto] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-left:auto] [--tab-after-right:-0.0625rem];
&:where([dir="rtl"]) {
@apply [--tab-before-right:auto] [--tab-before-left:-0.0625rem] [--tab-after-left:-0.0625rem] [--tab-after-right:auto];
}
}
&[data-placement="right"] {
@apply [--tab-border-inline-start-color:hsl(var(--background))] [--tab-border-inline-end-color:--tab-border-color] [--tab-border-bottom-color:--tab-border-color] [--tab-before-left:-0.0625rem] [--tab-before-right:auto] [--tab-before-bottom:auto] [--tab-before-top:-0.0625rem] [--tab-after-right:auto] [--tab-after-left:-0.0625rem];
&:where([dir="rtl"]) {
@apply [--tab-before-left:auto] [--tab-before-right:-0.0625rem] [--tab-after-right:-0.0625rem] [--tab-after-left:auto];
}
}
}
}
}
&[data-variant="pills"] {
@apply rounded-[--tabs-round] bg-[--tab-bg] text-[--tab-color] [--tab-bg:transparent] [--tab-color:inherit];
&:not([data-disabled]) {
@apply hover:[--tab-bg:--tab-hover-color];
}
&[data-active][data-active] {
@apply [--tab-bg:--tabs-color] [--tab-color:--tabs-text-color] hover:[--tab-bg:--tabs-color];
}
}
}
& .tabs-tab-section {
@apply flex items-center justify-center ml-[--tab-section-margin-left,0] mr-[--tab-section-margin-right,0];
&[data-position="left"] {
&:not(:only-child) {
@apply [--tab-section-margin-right:0.625rem];
&:where([dir="rtl"]) {
@apply [--tab-section-margin-right:0rem] [--tab-section-margin-left:0.625rem];
}
}
}
&[data-position="right"] {
&:not(:only-child) {
@apply [--tab-section-margin-left:0.625rem];
&:where([dir="rtl"]) {
@apply [--tab-section-margin-left:0rem] [--tab-section-margin-right:0.625rem];
}
}
}
}
}
}