Tabs Subtle
nexvyn/tabs-subtleFreeComponent
A pill-style tab list where the selected indicator springs between tabs, built on the WAI-ARIA tabs pattern with full roving-focus keyboard support.
Live preview
live · rendered by the registry
Rendered by nexvyn on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.nexvyn.dev/r/tabs-subtle.jsonSource
1'use client'23import {4 Children,5 createContext,6 forwardRef,7 isValidElement,8 useContext,9 useId,10 useMemo,11 useRef,12 useState,13 type HTMLAttributes,14 type KeyboardEvent,15 type ReactElement,16 type ReactNode,17} from 'react'18import { motion, useReducedMotion } from 'motion/react'19import { cn } from '@/lib/utils'20import { springs } from '@/lib/motion-tokens'2122interface TabsSubtleContextValue {23 selectedValue: string24 onSelect: (value: string) => void25 idPrefix: string26 layoutId: string27}2829const TabsSubtleContext = createContext<TabsSubtleContextValue | null>(null)3031function useTabsSubtleContext() {32 const ctx = useContext(TabsSubtleContext)33 if (!ctx) throw new Error('TabsSubtleItem/TabsSubtlePanel must be used within TabsSubtle')34 return ctx35}3637export interface TabsSubtleProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> {38 children: ReactNode39 value?: string40 defaultValue?: string41 onValueChange?: (value: string) => void42 idPrefix?: string43}4445export const TabsSubtle = forwardRef<HTMLDivElement, TabsSubtleProps>(46 (47 { children, value, defaultValue, onValueChange, idPrefix: idPrefixProp, className, ...props },48 ref,49 ) => {50 const containerRef = useRef<HTMLDivElement>(null)51 const generatedId = useId()52 const layoutId = useId()53 const idPrefix = idPrefixProp ?? generatedId5455 const items = useMemo(56 () =>57 Children.toArray(children).filter(58 (child): child is ReactElement<{ value: string }> =>59 isValidElement(child) &&60 typeof child.props === 'object' &&61 child.props !== null &&62 'value' in child.props,63 ),64 [children],65 )6667 const isControlled = value !== undefined68 const [internal, setInternal] = useState(defaultValue ?? items[0]?.props.value ?? '')69 const selectedValue = isControlled ? value : internal7071 const onSelect = (next: string) => {72 if (!isControlled) setInternal(next)73 onValueChange?.(next)74 }7576 const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {77 const els = Array.from(78 containerRef.current?.querySelectorAll('[role="tab"]') ?? [],79 ) as HTMLElement[]80 const currentIdx = els.findIndex((el) => el === document.activeElement)81 if (currentIdx === -1) return8283 if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {84 e.preventDefault()85 const dir = ['ArrowRight', 'ArrowDown'].includes(e.key) ? 1 : -186// … truncated
What it pulls in
npm packages
Files it writes
More from nexvyn
All 33 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Badgebadge | nexvyn | Components | Free | 2 deps · 2 files | |
| Bars Themebars-theme | nexvyn | Components | Free | 1 dep · 3 files | |
| Bounce Sidebarbounce-sidebar | nexvyn | Components | Free | 1 dep · 2 files | |
| Breadcrumbsbreadcrumbs | nexvyn | Components | Free | 1 dep · 2 files | |
| Checkboxcheckbox | nexvyn | Components | Free | 1 dep · 3 files | |
| Clipboard Fieldclipboard-field | nexvyn | Components | Free | no deps · 2 files | |
| Color Pickercolor-picker | nexvyn | Components | Free | no deps · 16 files | |
| Comboboxcombobox | nexvyn | Components | Free | 1 dep · 2 files |
