Skip to content
DesignOSby h4rithd
ui / navigation

Tabs

Controlled or uncontrolled tabs with roving keyboard navigation.

PREVIEW READY
LIVE PREVIEWDARK / LIGHT COMPATIBLE
SYSTEM / OVERVIEW

One coherent interface language

The component preserves keyboard behavior, clear state, and calm visual hierarchy.

INSTALL / SOURCE REGISTRY

Add Tabs to your project

The command adds the component source and its DesignOS base dependency.

$pnpm dlx shadcn@latest add https://designos.h4rithd.com/r/tabs.json
registry/default/ui/tabs.tsx
"use client"

import * as React from "react"
import { clsx as cn } from "clsx"
import "@/components/designos/designos.css"

export type TabItem = { id: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean }

export function Tabs({ items, defaultValue, value, onValueChange, label = "Content sections", className }: { items: TabItem[]; defaultValue?: string; value?: string; onValueChange?: (value: string) => void; label?: string; className?: string }) {
  const [internal, setInternal] = React.useState(defaultValue ?? items[0]?.id)
  const active = value ?? internal
  const select = (id: string) => { if (value === undefined) setInternal(id); onValueChange?.(id) }
  const move = (event: React.KeyboardEvent, index: number) => {
    if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return
    event.preventDefault()
    const enabled = items.filter((item) => !item.disabled)
    const current = enabled.findIndex((item) => item.id === items[index].id)
    const next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (current + (event.key === "ArrowRight" ? 1 : -1) + enabled.length) % enabled.length
    select(enabled[next].id)
    document.getElementById(`dos-tab-${enabled[next].id}`)?.focus()
  }
  return (
    <div className={cn("dos-tabs", className)}>
      <div role="tablist" aria-label={label} className="dos-tabs__list">
        {items.map((item, index) => <button key={item.id} id={`dos-tab-${item.id}`} role="tab" type="button" disabled={item.disabled} aria-selected={active === item.id} aria-controls={`dos-panel-${item.id}`} tabIndex={active === item.id ? 0 : -1} className="dos-tabs__trigger" onClick={() => select(item.id)} onKeyDown={(event) => move(event, index)}>{item.label}</button>)}
      </div>
      {items.map((item) => <div key={item.id} id={`dos-panel-${item.id}`} role="tabpanel" aria-labelledby={`dos-tab-${item.id}`} hidden={active !== item.id} tabIndex={0} className="dos-tabs__panel">{item.content}</div>)}
    </div>
  )
}

Component contract

  • Source-owned after installation
  • Dark and light theme variables
  • Keyboard and visible-focus behavior
  • Reduced-motion fallback
  • Responsive from 320px upward
  • IBM Plex Sans + JetBrains Mono hierarchy