One coherent interface language
The component preserves keyboard behavior, clear state, and calm visual hierarchy.
Controlled or uncontrolled tabs with roving keyboard navigation.
The component preserves keyboard behavior, clear state, and calm visual hierarchy.
The command adds the component source and its DesignOS base dependency.
pnpm dlx shadcn@latest add https://designos.h4rithd.com/r/tabs.json"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>
)
}