Anatomy of the Magic Tab
How a segmented control borrows Magic Button's three-layer stack, but only the selected tab ever pays for it.
↓Scroll to step through it
Three layers, one tab
Magic Tab is Magic Button's sibling: the same three-layer illusion of a
pushed-up surface and the same springy bezier, on a role="tablist"
instead of a lone button. The difference is selection. A button is either
pressed or not; a tab has to hold a rest state for every item it isn't,
and spend the 3D budget only on the one it is.
Every tab is a <button> with the same absolutely-stacked shadow, edge,
and front-face spans as Magic Button. The difference: on an unselected tab
the shadow and edge sit at opacity-0 and the front is flat
(bg-transparent, no lift). Only the selected tab's stack materializes.
The scene goes flat → the three spans → assembled lift:
Unselected tabs still preview the selected color flat on hover/focus
(frontVariantPreview) as a hint of what clicking does, but never lift.
Only the selected tab lifts.
Three lift stops
A selected tab sits at -4px and goes one step further on
focus-visible, to -6px, the same way Magic Button's hover lift previews
what a press would feel like. Rest → selected uses the slower settle
bezier; selected → focus overshoots on the snappier one:
600ms at rest→selected is deliberately unhurried; 250ms with a 1.5
overshoot control point on selected→focus is the snap that marks the tab
as interactive. Both offsets animate translate, never top. Magic Button
follows the same compositor-only rule for the same reason: moving a layer
is free, re-laying it out is not.
Rainbow, paused offscreen
The selected tab's edge and shadow can run the same sliding rainbow
gradient as Magic Button: background-size: 200% 100% with a
background-position keyframe. That keyframe costs main-thread work, so an
IntersectionObserver on the tablist root pauses every
.animate-magic-rainbow layer the instant it scrolls out of view:
rootMargin: "128px" resumes the sweep just before the tablist re-enters
the viewport, so scrolling back to it never catches a frozen mid-cycle
frame snapping back to life.
The result
Magic Tab uses the same three spans and bezier as Magic Button. On each render it decides which single tab may spend them.
unselected
three spans
selected
- Front
- label face, lifts −4px when selected
- Edge
- gradient wall, fakes the 3D side
- Shadow
- blurred, grounds the raised tab
const shadowOpacity = selected ? (rainbow ? "opacity-70" : "opacity-100") : "opacity-0";const edgeFill = selected ? `opacity-100 ${...}` : "opacity-0";const frontState = selected? `-translate-y-[4px] ${frontVariantSelected[variant]} ...`: `bg-transparent text-muted-foreground ${frontVariantPreview[variant]}`;Anatomy of the Magic Tab
How a segmented control borrows Magic Button's three-layer stack, but only the selected tab ever pays for it.
unselected
three spans
selected
- Front
- label face, lifts −4px when selected
- Edge
- gradient wall, fakes the 3D side
- Shadow
- blurred, grounds the raised tab
const shadowOpacity = selected ? (rainbow ? "opacity-70" : "opacity-100") : "opacity-0";const edgeFill = selected ? `opacity-100 ${...}` : "opacity-0";const frontState = selected? `-translate-y-[4px] ${frontVariantSelected[variant]} ...`: `bg-transparent text-muted-foreground ${frontVariantPreview[variant]}`;Three layers, one tab
Magic Tab is Magic Button's sibling: the same three-layer illusion of a
pushed-up surface and the same springy bezier, on a role="tablist"
instead of a lone button. The difference is selection. A button is either
pressed or not; a tab has to hold a rest state for every item it isn't,
and spend the 3D budget only on the one it is.
Every tab is a <button> with the same absolutely-stacked shadow, edge,
and front-face spans as Magic Button. The difference: on an unselected tab
the shadow and edge sit at opacity-0 and the front is flat
(bg-transparent, no lift). Only the selected tab's stack materializes.
The scene goes flat → the three spans → assembled lift:
Unselected tabs still preview the selected color flat on hover/focus
(frontVariantPreview) as a hint of what clicking does, but never lift.
Only the selected tab lifts.
- rest
- selected
- focus
- none
- 600ms
- 250ms
- 0px
- -4px · …,1
- -6px · …,1.5
// front face"-translate-y-[4px] group-focus-visible:-translate-y-[6px][transition:translate_600ms_cubic-bezier(0.3,0.7,0.4,1)]group-focus-visible:[transition:translate_250ms_cubic-bezier(0.3,0.7,0.4,1.5)]"Three lift stops
A selected tab sits at -4px and goes one step further on
focus-visible, to -6px, the same way Magic Button's hover lift previews
what a press would feel like. Rest → selected uses the slower settle
bezier; selected → focus overshoots on the snappier one:
600ms at rest→selected is deliberately unhurried; 250ms with a 1.5
overshoot control point on selected→focus is the snap that marks the tab
as interactive. Both offsets animate translate, never top. Magic Button
follows the same compositor-only rule for the same reason: moving a layer
is free, re-laying it out is not.
animationPlayState flips to “paused” once the tablist clears the 128px margin, and resumes where it left off on the way back in
const io = new IntersectionObserver(([entry]) => { for (const layer of root.querySelectorAll<HTMLElement>(".animate-magic-rainbow")) layer.style.animationPlayState = entry.isIntersecting ? "" : "paused";},{ rootMargin: "128px" },);Rainbow, paused offscreen
The selected tab's edge and shadow can run the same sliding rainbow
gradient as Magic Button: background-size: 200% 100% with a
background-position keyframe. That keyframe costs main-thread work, so an
IntersectionObserver on the tablist root pauses every
.animate-magic-rainbow layer the instant it scrolls out of view:
rootMargin: "128px" resumes the sweep just before the tablist re-enters
the viewport, so scrolling back to it never catches a frozen mid-cycle
frame snapping back to life.
The result
Magic Tab uses the same three spans and bezier as Magic Button. On each render it decides which single tab may spend them.
Accessibility
The container is role="tablist" with aria-orientation="horizontal";
each button is role="tab" with aria-selected. Focus and selection are
separate: arrow keys move a roving tabIndex between tabs
without selecting them, and Enter/Space commits whichever tab currently
holds focus:
case "ArrowRight":
moveFocus(enabled[(currentIndex + 1) % enabled.length].value);
break;
case "Enter":
case " ":
if (rovingValue !== undefined) select(rovingValue);
break;This is the WAI-ARIA "manual activation" tab pattern: you can arrow through every tab to read its label before committing, instead of every keypress switching panels. Tabbing out of the list resets the roving stop back to the selected tab.
Motion Score
translateSelected tab face / indicator motionbackground-positionSelected tab rainbow keyframe loop