Highlight Tabs
ericts-ui/highlight-tabsFreeComponent
A Motion-powered tab list with a shared highlight indicator and keyboard navigation.
Live preview
live · rendered by the registry
Rendered by ericts-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.ericts.com/r/highlight-tabs.jsonSource
1"use client";23import * as React from "react";4import { LayoutGroup, motion, useReducedMotion } from "motion/react";56import { cn } from "@/lib/utils";78export type HighlightTab = {9 value: string;10 label: React.ReactNode;11 disabled?: boolean;12};1314export type HighlightTabsProps = Omit<15 React.ComponentPropsWithoutRef<"div">,16 "defaultValue" | "onChange"17> & {18 tabs: HighlightTab[];19 value?: string;20 defaultValue?: string;21 onValueChange?: (value: string) => void;22 selectOnHover?: boolean;23 listClassName?: string;24 tabClassName?: string;25 indicatorClassName?: string;26 "aria-label"?: string;27};2829function getEnabledTabs(tabs: HighlightTab[]) {30 return tabs.filter((tab) => !tab.disabled);31}3233function getInitialValue(tabs: HighlightTab[], value?: string) {34 const enabledTabs = getEnabledTabs(tabs);3536 return enabledTabs.some((tab) => tab.value === value)37 ? value38 : enabledTabs[0]?.value;39}4041export function HighlightTabs({42 tabs,43 value,44 defaultValue,45 onValueChange,46 selectOnHover = true,47 className,48 listClassName,49 tabClassName,50 indicatorClassName,51 "aria-label": ariaLabel = "Tabs",52 ...props53}: HighlightTabsProps) {54 const shouldReduceMotion = useReducedMotion();55 const reactId = React.useId();56 const layoutId = `highlight-tabs-${reactId}`;57 const isControlled = value !== undefined;58 const tabRefs = React.useRef(new Map<string, HTMLButtonElement>());59 const [uncontrolledValue, setUncontrolledValue] = React.useState(60 () => getInitialValue(tabs, defaultValue) ?? "",61 );6263 const enabledTabs = React.useMemo(() => getEnabledTabs(tabs), [tabs]);64 const activeValue = isControlled65 ? value66 : enabledTabs.some((tab) => tab.value === uncontrolledValue)67 ? uncontrolledValue68 : enabledTabs[0]?.value;6970 const setActiveValue = React.useCallback(71 (nextValue: string) => {72 const nextTab = tabs.find((tab) => tab.value === nextValue);7374 if (!nextTab || nextTab.disabled || nextValue === activeValue) return;7576 if (!isControlled) {77 setUncontrolledValue(nextValue);78 }7980 onValueChange?.(nextValue);81 },82 [activeValue, isControlled, onValueChange, tabs],83 );8485 const focusTab = React.useCallback((nextValue: string) => {86 tabRefs.current.get(nextValue)?.focus();87 }, []);8889 const handleKeyDown = React.useCallback(90 (event: React.KeyboardEvent<HTMLButtonElement>, currentValue: string) => {91 if (enabledTabs.length === 0) return;9293// … truncated
What it pulls in
npm packages
Files it writes
More from ericts-ui
All 38 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Adaptive Draweradaptive-drawer | ericts-ui | Components | Free | 2 deps | |
| Check Animationcheck-animation | ericts-ui | Components | Free | no deps · 2 files | |
| Context Cursorcontext-cursor | ericts-ui | Components | Free | 1 dep | |
| Copy Buttoncopy-button | ericts-ui | Components | Free | 2 deps | |
| Expandable Dialogexpandable-modal | ericts-ui | Components | Free | 1 dep | |
| Expandable Tabsexpandable-tabs | ericts-ui | Components | Free | 1 dep | |
| Expandable Toolbarexpandable-toolbar | ericts-ui | Components | Free | 1 dep | |
| Expanding Panelexpanding-panel | ericts-ui | Components | Free | 2 deps |
