Animated Tabs
zyeon/animated-tabsFreeComponent
A controlled tab strip with a sliding active indicator — underline and pill variants animated via motion layout animation.
See it running
Open the demo on zyeon
ui.zyeon.ai/r/animated-tabsInstall it
npx shadcn@latest add https://ui.zyeon.ai/r/animated-tabs.jsonSource
1"use client"23import * as React from "react"4import { motion, useReducedMotion, type Transition } from "motion/react"5import { cn } from "@/lib/utils"67export interface AnimatedTab {8 id: string9 label: string10}1112export interface AnimatedTabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {13 /** Tab items, rendered in order. */14 tabs: AnimatedTab[]15 /** Controlled: id of the selected tab. */16 value: string17 /** Controlled: called with the id of the tab to select. */18 onChange: (id: string) => void19 variant?: "underline" | "pill"20}2122/**23 * Tab strip only — panels belong to the consumer. The active indicator glides24 * between tabs via motion's shared layout animation (one `layoutId` per25 * instance, scoped with useId so multiple strips never cross-animate).26 */27export const AnimatedTabs = React.forwardRef<HTMLDivElement, AnimatedTabsProps>(28 ({ tabs, value, onChange, variant = "underline", className, onKeyDown, ...props }, ref) => {29 const uid = React.useId()30 const tabRefs = React.useRef<(HTMLButtonElement | null)[]>([])31 const reducedMotion = useReducedMotion()32 const transition: Transition = reducedMotion33 ? { duration: 0 }34 : { type: "spring", stiffness: 500, damping: 40 }3536 const activeIndex = tabs.findIndex(t => t.id === value)37 // roving tabindex:活动 tab 可聚焦;value 不匹配时兜底到第一个,保证键盘可达38 const focusIndex = activeIndex === -1 ? 0 : activeIndex3940 const selectAndFocus = (index: number) => {41 const tab = tabs[index]42 if (!tab) return43 onChange(tab.id)44 tabRefs.current[index]?.focus()45 }4647 const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {48 onKeyDown?.(e)49 if (e.defaultPrevented || tabs.length === 0) return50 if (e.key === "ArrowRight") {51 e.preventDefault()52 selectAndFocus((focusIndex + 1) % tabs.length)53 } else if (e.key === "ArrowLeft") {54 e.preventDefault()55 selectAndFocus((focusIndex - 1 + tabs.length) % tabs.length)56 } else if (e.key === "Home") {57 e.preventDefault()58 selectAndFocus(0)59 } else if (e.key === "End") {60 e.preventDefault()61 selectAndFocus(tabs.length - 1)62 }63 }6465 return (66 <div67 className={cn(68 "inline-flex max-w-full items-center overflow-x-auto",69 variant === "underline" ? "gap-1 border-b" : "gap-1 rounded-lg bg-muted p-1",70 className,71 )}72// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from zyeon
All 517 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordionaccordion | zyeon | Components | Free | 1 dep | |
| Activity Heatmapactivity-heatmap | zyeon | Components | Paid | 1 dep · 2 files | |
| Address Formaddress-form | zyeon | Components | Paid | 1 dep | |
| Agent Graphagent-graph | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Handoffagent-handoff | zyeon | Components | Free | 1 dep | |
| Agent Inboxagent-inbox | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Permissionsagent-permissions | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Planagent-plan | zyeon | Components | Free | 2 deps · 2 files |
