Tabs That Don't Hide What Matters: Building Tab Interfaces in React
Tabs are a promise about what is behind them
A row of tabs tells people three things at once: there is more here than you can see, these sections are siblings, and you can move between them without losing your place. When that promise holds, tabs are one of the calmest patterns on the web. When it breaks, they become a game of hide and seek with the one setting someone actually needed.
Most problems with tabs are not about styling. They come from putting the wrong content behind them, or from building something that looks like tabs but does not behave like tabs. This guide walks through both, then gets into indicators and motion, using a few of today's additions to the Uncode catalog as examples.
Decide whether the content belongs in tabs at all
Tabs work when the panels are parallel views of the same thing and people usually need only one at a time. Account settings split into Profile, Security and Billing is a good fit. So is a pricing page that switches between monthly and yearly, or a dashboard card that flips between this week and this month.
Tabs work badly when people need to compare panels side by side, when one panel is far more important than the others, or when the content is a sequence. If someone has to click back and forth to compare two tables, put the tables next to each other. If step two depends on step one, you are building a wizard, not tabs. And if ninety percent of visitors only ever need the first panel, the rest might be better as a link or a collapsed section further down.
Labels do most of the work
Short, specific labels beat clever ones. "Overview", "Activity" and "Settings" tell people exactly what they will find. "Insights" and "Explore" make them click to find out. Keep labels to one or two words, keep them in the same grammatical form, and avoid wrapping them onto two lines.
Icons can help in tight spaces, but an icon alone is a guess. The expandable tabs component handles this nicely: every tab shows an icon, and the active one expands to reveal its label, so the current location is always named even when the bar is compact. Each button still carries an aria-label, which means screen readers never hear an unnamed icon.
Counts belong next to labels when they change what someone does next. The badge tabs component puts a small number beside Messages, Tasks and Alerts. That number is useful because it answers "is there anything waiting for me here" before the click. Counts that never change, or that are always zero, are just noise.
Get the keyboard and ARIA right first
Real tabs have a specific contract with assistive technology. The container has role="tablist", each tab is a button with role="tab" and aria-selected, and each panel has role="tabpanel" and is labelled by its tab. Only the active tab sits in the normal Tab order; the arrow keys move between tabs, and Home and End jump to the first and last.
Pressing Tab should move into the panel, not through every tab, because a settings page with eight tabs would otherwise cost eight extra key presses every time.
You can write this yourself, and the expandable tabs component does exactly that with a small onKeyDown handler. For anything more involved, a headless primitive saves a lot of edge cases. The fade slide tabs component is built on Radix Tabs, which takes care of roving focus, selection and the ARIA wiring, leaving only the visual layer to style.
One decision to make on purpose: should arrow keys select a tab immediately, or only move focus until Enter is pressed? Automatic activation feels faster when panels are light. Manual activation is better when switching a panel triggers a network request or a heavy render, because people arrowing past three tabs should not kick off three loads.
Uncode UI
Build with AI, not against it.
Over 1,400 React and Tailwind components that run live in the browser, with a prompt ready to paste into Claude, Cursor or v0. Plus a community of people building real sites with AI.
The indicator is the whole interaction
The active indicator, whether it is an underline, a pill or a highlight, is what tells people where they are. It needs to be obvious at a glance, with enough contrast against the inactive tabs that someone with low vision or a washed out laptop screen can still spot it. Colour alone is not enough; pair it with weight, background or a shape change.
Animating the indicator between positions is one of the few tab animations that genuinely helps. When the pill slides from one tab to the next, the eye follows it, and the change of location becomes physical instead of abrupt. In React with framer-motion, the simplest way is a shared layoutId: render the pill only inside the active tab and give it the same layout id everywhere, and the library animates it between positions for you.
Two practical notes. First, if more than one tab bar can appear on the same page, make that layout id unique per instance, for example by combining it with useId(). Otherwise the pill from one bar can fly across the screen into another. Second, keep the spring short, around 200 milliseconds with little bounce.
Animate panels lightly, or not at all
Panel transitions are where tabs most often get slower than they need to be. A gentle fade with a few pixels of horizontal movement, like the one in fade slide tabs, gives a hint of direction without delaying the content. Full slides, flips and long cross fades make people wait for something they asked for instantly.
Watch the height. If panels have different heights, the page below them jumps every time the tab changes, which is disorienting and can push the very button someone was about to press. Either give the panel area a minimum height, animate the height smoothly, or accept the jump but keep everything above the tabs stable.
Respect reduced motion. Read prefers-reduced-motion and drop the slide, keeping only an instant swap or a very short fade. framer-motion's useReducedMotion hook makes this a one line check.
Autoplay tabs need a pause button and a reason
Some tab layouts cycle on their own, the way a product page might rotate through features. The vertical tabs component does this, with a progress line that fills down the side of the active item and a large image that changes with each step. It pauses while the pointer is over the image, which is the minimum you want.
Autoplay is fine for marketing sections where the content is a teaser and nobody is reading carefully. It is not fine for anything people need to read in full, fill in, or compare. If you use it, make sure the timer pauses on hover and on focus, restart it only after a real interaction, and stop it entirely for people who prefer reduced motion.
Small screens change the rules
A row of five text tabs that fits nicely on a laptop will overflow on a phone. There are three honest ways to handle that. You can let the tab list scroll horizontally, with a fade at the edge so people know there is more. You can switch to icon tabs that expand the active label, like expandable tabs. Or you can collapse the whole thing into a select menu below a certain width. Never shrink the text until it fits.
A short checklist before you ship
- The panels are siblings, and people rarely need two at once.
- Labels are short, specific and parallel. Icons have accessible names.
- Arrow keys move between tabs, Tab moves into the panel.
- The active tab is clear without relying on colour alone.
- Indicator motion is short, and each tab bar uses its own layout id.
- Panel changes do not shove the rest of the page around.
- Reduced motion turns slides into instant swaps.
- Autoplay, if any, pauses on hover and focus.
Tabs are a small component with a lot of quiet responsibility. Get the content and the keyboard right, keep the motion honest, and people will stop noticing the tabs at all, which is exactly the goal. If you work in Cursor, Claude Code or Lovable, you can pull any of the components mentioned here straight into your editor through the Uncode MCP connection and adapt them from there.
Uncode UI
Build with AI, not against it.
Over 1,400 React and Tailwind components that run live in the browser, with a prompt ready to paste into Claude, Cursor or v0. Plus a community of people building real sites with AI.