Animated Tabs
Tailwind UI's code switcher re-built with React Aria Components and animated with Framer Motion.
Preview
Code
Preview content
This example is built on BuildUI's already excellent Animated Tabs recipe. It provides improved accessibility by using React Aria Components under the hood and demonstrates how to animate a tab handle that lives between a background and a label.
Best Practices
Prefix React Aria Components' imports to avoid confusion with custom components.
import { Tab as AriaTab } from 'react-aria-components';Easily style children inside the parent using
data-slot
.'[&>[data-slot=label]]:text-slate-600 [&>[data-slot=label]]:transition-colors group-hover:[&>[data-slot=label]]:text-slate-900'Group styles by splitting them into multiple lines with
clsx
.className={clsx(// Tab text wrapper'relative z-20 flex items-center gap-x-2',// Tab icon'[&>[data-slot=icon]]:duration-[400ms] [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:text-slate-600 [&>[data-slot=icon]]:transition-colors [&>[data-slot=icon]]:group-rac-selected:text-sky-500',// Tab text'[&>[data-slot=label]]:text-slate-600 [&>[data-slot=label]]:transition-colors group-hover:[&>[data-slot=label]]:text-slate-900 [&>[data-slot=label]]:group-rac-selected:text-slate-900',)}
Requirements
Code
import { CodeBracketIcon, EyeIcon } from '@heroicons/react/20/solid';import { clsx } from 'clsx';import { motion } from 'framer-motion';import { type TabPanelProps as AriaTabPanelProps, type TabProps as AriaTabProps, Tab as AriaTab, TabList as AriaTabList, TabPanel as AriaTabPanel, Tabs as AriaTabs,} from 'react-aria-components';
interface TabProps extends Omit<AriaTabProps, 'children' | 'className'> { children: React.ReactNode;}
function AnimatedTabs() { return ( <AriaTabs className="flex flex-col items-stretch gap-y-4"> <AriaTabList className="flex gap-x-1 rounded-lg bg-slate-100 p-0.5"> <Tab id="preview"> <EyeIcon /> <span data-slot="label">Preview</span> </Tab> <Tab id="code"> <CodeBracketIcon /> <span data-slot="label">Code</span> </Tab> </AriaTabList> <TabPanel id="preview"> <p>Preview content</p> </TabPanel> <TabPanel id="code"> <code>Code content</code> </TabPanel> </AriaTabs> );}
function Tab({ children, ...props }: TabProps) { return ( <AriaTab className="group relative flex cursor-pointer items-center rounded-md px-2 py-[0.4375rem] text-sm font-semibold focus-visible:outline-none rac-selected:cursor-default" {...props} > {({ isSelected }) => ( <> {isSelected && <TabHandle />} <div className={clsx( 'relative z-20 flex items-center gap-x-2', '[&>[data-slot=icon]]:duration-[400ms] [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:text-slate-600 [&>[data-slot=icon]]:transition-colors [&>[data-slot=icon]]:group-rac-selected:text-sky-500', '[&>[data-slot=label]]:text-slate-600 [&>[data-slot=label]]:transition-colors group-hover:[&>[data-slot=label]]:text-slate-900 [&>[data-slot=label]]:group-rac-selected:text-slate-900', )} > {children} </div> </> )} </AriaTab> );}
function TabPanel({ children, ...props}: Omit<AriaTabPanelProps, 'className'>) { return ( <AriaTabPanel className="rounded-lg bg-white p-8 ring-1 ring-slate-900/10" {...props} > {children} </AriaTabPanel> );}
function TabHandle() { return ( <motion.span className="absolute inset-0 z-10 rounded-md bg-white shadow" layoutId="bubble" transition={{ type: 'spring', bounce: 0, duration: 0.4 }} /> );}