For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Tabs

The Tabs component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/tabs

Options

  • <TabsList variant="line"> underline tabs; <TabsTrigger count> adds a trailing count.
  • <TabsList variant="pill"> fully rounded blue pill that slides between triggers; variant="pill-muted" is the quieter 10px neutral version.
  • <TabsList variant="pill-soft"> text tabs with a fully rounded, primary-tinted active tab.
  • <TabsList variant="ghost"> plain text tabs; the active tab turns darker and medium weight.
  • <TabsList variant="segmented"> default tabs on the toggle-group track with wider triggers.
  • <Tabs size="flush"> drops the gap between the list and the content.
  • <TabsList indicator> slides one active surface between triggers on the default variant.

Source

"use client";

import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "cn";

function Tabs({
  className,
  orientation = "horizontal",
  size = "default",
  ...props
}: TabsPrimitive.Root.Props & { size?: "default" | "flush" }) {
  return (
    <TabsPrimitive.Root
      data-slot="tabs"
      orientation={orientation}
      className={cn(
        "group/tabs flex gap-4 data-horizontal:flex-col",
        size === "flush" && "gap-0",
        className
      )}
      {...props}
    />
  );
}

const tabsListVariants = cva(
  "group/tabs-list inline-flex w-fit items-center justify-center rounded-[8px] p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
  {
    defaultVariants: {
      variant: "default",
    },
    variants: {
      variant: {
        default: "bg-muted",
        pill: "relative gap-1 bg-transparent p-0 group-data-horizontal/tabs:h-auto",
        "pill-muted":
          "relative gap-1 bg-transparent p-0 group-data-horizontal/tabs:h-auto",
        ghost: "gap-0.5 bg-transparent p-0 group-data-horizontal/tabs:h-auto",
        "pill-soft":
          "gap-1 bg-transparent p-0 group-data-horizontal/tabs:h-auto",
        segmented: "bg-[var(--toggle-group-track,var(--muted))]",
        line: "relative w-full justify-start gap-1 border-b border-[var(--surface-strong,var(--border))] bg-transparent p-0 group-data-horizontal/tabs:h-auto group-data-vertical/tabs:w-fit group-data-vertical/tabs:items-stretch group-data-vertical/tabs:border-r group-data-vertical/tabs:border-b-0",
      },
    },
  }
);

const pillTriggerClassName =
  "group-data-[variant=pill]/tabs-list:h-auto group-data-[variant=pill]/tabs-list:flex-none group-data-[variant=pill]/tabs-list:cursor-pointer group-data-[variant=pill]/tabs-list:gap-1 group-data-[variant=pill]/tabs-list:rounded-full group-data-[variant=pill]/tabs-list:border-0 group-data-[variant=pill]/tabs-list:px-2 group-data-[variant=pill]/tabs-list:py-[5px] group-data-[variant=pill]/tabs-list:text-muted-foreground! group-data-[variant=pill]/tabs-list:shadow-none group-data-[variant=pill]/tabs-list:hover:bg-[var(--surface-hover,var(--muted))] group-data-[variant=pill]/tabs-list:data-active:bg-transparent! group-data-[variant=pill]/tabs-list:data-active:text-[var(--tabs-pill-selected-foreground,var(--primary))]! group-data-[variant=pill]/tabs-list:focus-visible:border-0 group-data-[variant=pill]/tabs-list:focus-visible:ring-2 group-data-[variant=pill]/tabs-list:focus-visible:ring-ring group-data-[variant=pill]/tabs-list:focus-visible:outline-none dark:group-data-[variant=pill]/tabs-list:data-active:border-0 " +
  "group-data-[variant=pill-muted]/tabs-list:h-auto group-data-[variant=pill-muted]/tabs-list:flex-none group-data-[variant=pill-muted]/tabs-list:cursor-pointer group-data-[variant=pill-muted]/tabs-list:gap-1 group-data-[variant=pill-muted]/tabs-list:rounded-[10px] group-data-[variant=pill-muted]/tabs-list:border-0 group-data-[variant=pill-muted]/tabs-list:px-2 group-data-[variant=pill-muted]/tabs-list:py-[5px] group-data-[variant=pill-muted]/tabs-list:text-muted-foreground! group-data-[variant=pill-muted]/tabs-list:shadow-none group-data-[variant=pill-muted]/tabs-list:hover:bg-[var(--surface-hover,var(--muted))] group-data-[variant=pill-muted]/tabs-list:data-active:bg-transparent! group-data-[variant=pill-muted]/tabs-list:data-active:text-foreground! group-data-[variant=pill-muted]/tabs-list:focus-visible:border-0 group-data-[variant=pill-muted]/tabs-list:focus-visible:ring-2 group-data-[variant=pill-muted]/tabs-list:focus-visible:ring-ring group-data-[variant=pill-muted]/tabs-list:focus-visible:outline-none dark:group-data-[variant=pill-muted]/tabs-list:data-active:border-0";

// The sliding indicator (variant "default") can be restyled from the list:
// set --tabs-indicator-background: and --tabs-indicator-radius: on TabsList.
function TabsList({
  className,
  variant = "default",
  indicator = false,
  children,
  ...props
}: TabsPrimitive.List.Props &
  VariantProps<typeof tabsListVariants> & { indicator?: boolean }) {
  return (
    <TabsPrimitive.List
      data-slot="tabs-list"
      data-variant={variant}
      data-indicator={indicator && variant === "default" ? "" : undefined}
      className={cn(
        tabsListVariants({ variant }),
        indicator &&
          variant === "segmented" &&
          "relative **:data-[slot=tabs-trigger]:before:hidden",
        "data-indicator:relative data-indicator:**:data-[slot=tabs-trigger]:before:hidden",
        indicator &&
          variant === "pill-soft" &&
          "relative **:data-[slot=tabs-trigger]:before:hidden",
        className
      )}
      {...props}
    >
      {indicator && variant === "pill-soft" && (
        <TabsPrimitive.Indicator className="pointer-events-none absolute top-0 left-0 h-[var(--active-tab-height)] w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] translate-y-[var(--active-tab-top)] rounded-full bg-[var(--tabs-pill-selected-background,color-mix(in_srgb,var(--primary)_15%,transparent))] transition-[translate,width,height] duration-300 ease-[cubic-bezier(0.34,1.2,0.64,1)] motion-reduce:transition-none" />
      )}
      {indicator && variant === "default" && (
        <TabsPrimitive.Indicator className="pointer-events-none absolute top-0 left-0 h-[var(--active-tab-height)] w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] translate-y-[var(--active-tab-top)] rounded-[var(--tabs-indicator-radius,6px)] bg-[var(--tabs-indicator-background,var(--background))] shadow-[0_1px_0_0_rgb(0_0_0/0.05)] transition-[translate,width,height] duration-200 ease-[ease] motion-reduce:transition-none dark:bg-[var(--tabs-indicator-background,color-mix(in_oklab,var(--input)_30%,transparent))]" />
      )}
      {indicator && variant === "segmented" && (
        <TabsPrimitive.Indicator className="pointer-events-none absolute top-0 left-0 h-[var(--active-tab-height)] w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] translate-y-[var(--active-tab-top)] rounded-[6px] bg-background shadow-sm transition-[translate,width,height] duration-200 ease-[ease] motion-reduce:transition-none dark:bg-[var(--surface-card,var(--input))]" />
      )}
      {(variant === "pill" || variant === "pill-muted") && (
        <TabsPrimitive.Indicator
          className={cn(
            "pointer-events-none absolute top-0 left-0 h-[var(--active-tab-height)] w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] translate-y-[var(--active-tab-top)] transition-[translate,width,height] duration-300 ease-[cubic-bezier(0.34,1.2,0.64,1)] motion-reduce:transition-none",
            variant === "pill"
              ? "rounded-full bg-[var(--tabs-pill-selected-background,color-mix(in_srgb,var(--primary)_10%,transparent))]"
              : "rounded-[10px] bg-[var(--tabs-pill-muted-background,var(--muted))]"
          )}
        />
      )}
      {children}
      {variant === "line" && (
        <TabsPrimitive.Indicator className="pointer-events-none absolute bottom-0 left-0 h-0.5 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] bg-primary transition-[translate,width] duration-200 ease-[ease] motion-reduce:transition-none group-data-vertical/tabs:top-0 group-data-vertical/tabs:right-0 group-data-vertical/tabs:bottom-auto group-data-vertical/tabs:left-auto group-data-vertical/tabs:h-[var(--active-tab-height)] group-data-vertical/tabs:w-0.5 group-data-vertical/tabs:translate-x-0 group-data-vertical/tabs:translate-y-[var(--active-tab-top)]" />
      )}
    </TabsPrimitive.List>
  );
}

function TabsTrigger({
  className,
  count,
  children,
  ...props
}: TabsPrimitive.Tab.Props & { count?: React.ReactNode }) {
  return (
    <TabsPrimitive.Tab
      data-slot="tabs-trigger"
      className={cn(
        "relative isolate inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-[6px] border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-[background-color,border-color,box-shadow] data-active:transition-none group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
        "data-active:text-foreground dark:data-active:text-foreground",
        "before:pointer-events-none before:absolute before:-inset-px before:-z-10 before:rounded-[inherit] before:opacity-0 data-active:before:opacity-100 group-data-[variant=default]/tabs-list:before:bg-background group-data-[variant=default]/tabs-list:before:shadow-sm dark:group-data-[variant=default]/tabs-list:before:bg-input/30",
        "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
        pillTriggerClassName,
        "group-data-[variant=ghost]/tabs-list:h-auto group-data-[variant=ghost]/tabs-list:flex-none group-data-[variant=ghost]/tabs-list:cursor-pointer group-data-[variant=ghost]/tabs-list:rounded-[6px] group-data-[variant=ghost]/tabs-list:px-2.5 group-data-[variant=ghost]/tabs-list:py-1 group-data-[variant=ghost]/tabs-list:font-normal group-data-[variant=ghost]/tabs-list:text-muted-foreground! group-data-[variant=ghost]/tabs-list:data-active:bg-transparent! group-data-[variant=ghost]/tabs-list:data-active:font-medium group-data-[variant=ghost]/tabs-list:data-active:text-foreground! group-data-[variant=ghost]/tabs-list:shadow-none group-data-[variant=ghost]/tabs-list:not-data-active:hover:text-foreground! dark:group-data-[variant=ghost]/tabs-list:data-active:border-transparent",
        "group-data-[variant=pill-soft]/tabs-list:h-auto group-data-[variant=pill-soft]/tabs-list:flex-none group-data-[variant=pill-soft]/tabs-list:cursor-pointer group-data-[variant=pill-soft]/tabs-list:gap-2 group-data-[variant=pill-soft]/tabs-list:rounded-full group-data-[variant=pill-soft]/tabs-list:px-2.5 group-data-[variant=pill-soft]/tabs-list:py-1 group-data-[variant=pill-soft]/tabs-list:font-normal group-data-[variant=pill-soft]/tabs-list:text-muted-foreground! group-data-[variant=pill-soft]/tabs-list:before:bg-[var(--tabs-pill-selected-background,color-mix(in_srgb,var(--primary)_15%,transparent))] group-data-[variant=pill-soft]/tabs-list:data-active:font-medium group-data-[variant=pill-soft]/tabs-list:data-active:text-primary! group-data-[variant=pill-soft]/tabs-list:shadow-none group-data-[variant=pill-soft]/tabs-list:not-data-active:hover:text-foreground! dark:group-data-[variant=pill-soft]/tabs-list:data-active:border-transparent",
        "group-data-[variant=segmented]/tabs-list:px-3 group-data-[variant=segmented]/tabs-list:text-muted-foreground/75 group-data-[variant=segmented]/tabs-list:hover:text-foreground group-data-[variant=segmented]/tabs-list:data-active:text-foreground dark:group-data-[variant=segmented]/tabs-list:data-active:border-transparent dark:group-data-[variant=segmented]/tabs-list:data-active:focus-visible:border-transparent group-data-[variant=segmented]/tabs-list:before:bg-background group-data-[variant=segmented]/tabs-list:before:shadow-sm dark:group-data-[variant=segmented]/tabs-list:before:bg-[var(--surface-card,var(--input))]",
        "group-data-[variant=line]/tabs-list:h-auto group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:cursor-pointer group-data-[variant=line]/tabs-list:gap-2.5 group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-0 group-data-[variant=line]/tabs-list:px-2.5 group-data-[variant=line]/tabs-list:py-2 group-data-[variant=line]/tabs-list:font-normal group-data-[variant=line]/tabs-list:text-foreground group-data-[variant=line]/tabs-list:shadow-none group-data-[variant=line]/tabs-list:after:hidden group-data-[variant=line]/tabs-list:data-active:font-medium group-data-[variant=line]/tabs-list:data-active:text-primary dark:group-data-[variant=line]/tabs-list:text-foreground dark:group-data-[variant=line]/tabs-list:data-active:text-primary group-data-[variant=line]/tabs-list:data-disabled:cursor-not-allowed group-data-[variant=line]/tabs-list:focus-visible:rounded-[4px] group-data-[variant=line]/tabs-list:focus-visible:border-transparent group-data-[variant=line]/tabs-list:focus-visible:ring-2 group-data-[variant=line]/tabs-list:focus-visible:ring-ring group-data-[variant=line]/tabs-list:focus-visible:outline-none data-disabled:cursor-not-allowed data-disabled:opacity-50 motion-reduce:transition-none",
        className
      )}
      {...props}
    >
      {count != null ? (
        <span className="inline-flex items-center gap-1.5">{children}</span>
      ) : (
        children
      )}
      {count != null && (
        <span className="inline-flex items-center justify-center rounded-[4px] bg-[var(--tabs-count-background,color-mix(in_srgb,var(--foreground)_10%,transparent))] px-1 py-px text-xs leading-4 font-medium tracking-[0.15px] text-foreground opacity-50 [[data-active]>&]:bg-[var(--tabs-count-selected-background,color-mix(in_srgb,var(--primary)_15%,transparent))] [[data-active]>&]:text-primary [[data-active]>&]:opacity-100 group-data-[variant=segmented]/tabs-list:min-w-5 group-data-[variant=segmented]/tabs-list:bg-[var(--badge-neutral-background,var(--muted))] group-data-[variant=segmented]/tabs-list:text-muted-foreground group-data-[variant=segmented]/tabs-list:opacity-100 group-data-[variant=segmented]/tabs-list:[[data-active]>&]:bg-[var(--badge-neutral-background,var(--muted))] group-data-[variant=segmented]/tabs-list:[[data-active]>&]:text-muted-foreground">
          {count}
        </span>
      )}
    </TabsPrimitive.Tab>
  );
}

function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
  return (
    <TabsPrimitive.Panel
      data-slot="tabs-content"
      className={cn("flex-1 text-sm outline-none", className)}
      {...props}
    />
  );
}

export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };