Tab switch transition
Cross-fade between tab panels with a subtle slide-up — pure CSS, no JavaScript.
Overview
This tab panel fades in with a subtle upward slide when selected. The transition uses opacity and transform for a lightweight, GPU-composited animation.
Features
Each panel is positioned absolutely and layered. The active panel gets opacity: 1 and position: relative, pushing inactive panels out of flow so the container sizes to the active content.
Pricing
The active tab indicator uses border-bottom with a transition on color. Pair with View Transitions for cross-document tab switching in multi-page apps.
Quick implementation
/* HTML: radio inputs + labels + panels */
/* <input type="radio" name="tabs" id="tab-a" checked> */
/* <label for="tab-a">Tab</label> */
/* <div class="panel panel--a">...</div> */
.panel {
position: absolute;
inset: 0;
opacity: 0;
transform: translateY(0.5rem);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
/* Show active panel via :has() */
.tabs:has(#tab-a:checked) .panel--a,
.tabs:has(#tab-b:checked) .panel--b {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
position: relative;
}
/* Active tab indicator */
.tab-bar label {
border-bottom: 2px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.tabs:has(#tab-a:checked) label[for="tab-a"] {
color: oklch(0.72 0.19 265);
border-color: oklch(0.72 0.19 265);
}
@media (prefers-reduced-motion: reduce) {
.panel { transition-duration: 0.01s; }
}
Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer building tab navigation components.
Goal: Animated tab switching where the active panel cross-fades in with a subtle slide-up — pure CSS using radio inputs and :has().
Technical constraints:
- Use radio inputs + labels as the tab trigger mechanism — no JavaScript.
- Panels fade in with opacity 0→1 and transform: translateY(0.5rem→0).
- Active tab indicator uses border-bottom with a color transition.
- Use :has() to detect the checked radio and show the corresponding panel.
- Use oklch() for all color values — no hex or rgba().
- Transition duration 0.25–0.35s with ease timing.
Framework variant (pick one):
A) Vanilla HTML + CSS only — radio-driven tab switching.
B) React component — accept tabs[] prop with label and content, manage activeIndex state, CSS handles animation.
Edge cases to handle:
- Respect prefers-reduced-motion: reduce duration to near-instant.
- Panels must be accessible: use role="tabpanel" and aria-labelledby on each panel.
- The container height should adapt to the active panel's content height, not the tallest panel.
Return HTML + CSS.
Why this matters in 2026
Tab interfaces are everywhere — settings pages, dashboards, product detail views. Historically, animating between panels required JavaScript to toggle classes or swap content. With :has() and radio inputs, the entire tab mechanism — including the panel transition — is pure CSS. The browser handles state, the stylesheet handles presentation, and JavaScript is reserved for enhancement, not basic functionality.
The logic
Hidden radio inputs act as state holders. Labels trigger them. :has(#tab-a:checked) on a parent container selects the active panel and applies opacity: 1 and transform: translateY(0). Inactive panels stay at opacity: 0 with position: absolute, while the active panel switches to position: relative so the container sizes to its content. The tab indicator transitions border-color to match the accent color.
Accessibility & performance
opacity and transform transitions are compositor-only — zero layout cost. For screen readers, add role="tabpanel" and aria-labelledby attributes to each panel so the relationship between tab and content is clear. The radio-input pattern gives keyboard users native tab navigation via arrow keys. Gate all animations behind prefers-reduced-motion.