Tabs that swap content panels using only hidden <input type="radio"> + sibling-combinator CSS. Zero JavaScript, works in iOS Quick Look, keyboard-accessible by default. The canonical "swap content without a framework" pattern.
Click any pill. The active state is held by a hidden radio input.
Construcción multicapa, tejido exterior flexible, color negro mate. Resiste hasta 12 bares de presión.
Pistola multifunción con 10 modos de riego. Activación con un solo dedo, empuñadura ergonómica antideslizante.
Acabado anodizado mate, no se oxidan. Sellado anti-fugas en cada conexión. Roscado europeo estándar.
Sistema click-and-go, conexión en 2 segundos. Compatible con cualquier marca. Junta tórica reemplazable.
<div class="tabset">
<!-- Hidden radios — these hold the state -->
<input type="radio" name="tabs" id="t1" checked>
<input type="radio" name="tabs" id="t2">
<input type="radio" name="tabs" id="t3">
<!-- Pills — each <label> targets a radio via for= -->
<div class="pills">
<label class="pill" for="t1">Spec A</label>
<label class="pill" for="t2">Spec B</label>
<label class="pill" for="t3">Spec C</label>
</div>
<!-- Panels — sibling-combinator CSS shows/hides based on :checked -->
<div class="panels">
<div class="panel" data-t="1">...content for tab 1...</div>
<div class="panel" data-t="2">...content for tab 2...</div>
<div class="panel" data-t="3">...content for tab 3...</div>
</div>
</div>
.tabset { position: relative; }
/* Hide the radios visually but keep them accessible to keyboard + screen readers */
.tabset input[type="radio"] {
position: absolute;
width: 1px; height: 1px;
opacity: 0;
pointer-events: none;
clip: rect(0 0 0 0);
}
/* Pills — clickable labels */
.pill {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: 999px;
cursor: pointer;
min-height: 36px; /* touch target ≥ 44px ideally */
display: inline-flex; align-items: center;
transition: all 0.25s ease;
}
/* Panels — hidden by default */
.panel { display: none; }
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THE TRICK — radio :checked + sibling combinator ~
targets labels and panels anywhere AFTER the radio in DOM order
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
/* Active pill (visual highlight) */
#t1:checked ~ .pills .pill[for="t1"],
#t2:checked ~ .pills .pill[for="t2"],
#t3:checked ~ .pills .pill[for="t3"] {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
/* Active panel (visible) */
#t1:checked ~ .panels .panel[data-t="1"],
#t2:checked ~ .panels .panel[data-t="2"],
#t3:checked ~ .panels .panel[data-t="3"] {
display: block;
}
Radios = state holder. Browsers natively maintain "which
radio in a group is checked." Clicking a <label for="t2">
toggles #t2 on, which un-checks #t1. We never
touch this state with JS.
Sibling combinator ~. The CSS rule
#t1:checked ~ .panels .panel[data-t="1"] reads: "when the
element with id t1 is checked, find any later sibling that
matches .panels, then within that find .panel[data-t="1"]."
The pills and panels must appear AFTER the radios in DOM order for this
to work.
Hidden radios are still accessible. The
clip + opacity + position:absolute recipe is the canonical
"visually hide but keep accessible" pattern (vs. display: none,
which removes from the tab order entirely). Keyboard users can Tab to the
radios and arrow-key through tabs.
Default tab. The radio you mark checked
is the initial active tab. Conventionally the first one, but you can
default to whichever makes sense (e.g. "Most popular" tier in pricing).
Pill placement. Pills can sit above the panels (most common), inside the same visual frame as the content (overlay style, e.g. on top of an image), or stacked vertically as a sidebar — the sibling combinator doesn't care about layout, only DOM order.
Number of tabs. The pattern scales to 7+ tabs but the CSS rules grow linearly — one selector per tab. For 10+ tabs, consider a JS-driven solution to avoid 30+ lines of repetitive selectors.
Transitions between panels. Because panels switch via
display: none/block, you can't animate the transition (no
transition on display). Workaround: use opacity + position
layering (all panels stacked absolutely, only one fully opaque).
DOM order matters. The pills and panels must come AFTER the radios in the DOM. If a pill is rendered BEFORE its radio, the sibling selector can't reach it.
Don't nest tabsets. If you have a tabset inside another
tabset, the inner radios' name attribute must differ from the
outer's, or you'll get cross-group state contamination.
iOS Safari tap delay. Add -webkit-tap-highlight-color:
transparent to the pills to suppress the blue tap flash.
Animating the active state. You CAN transition properties
like background, border-color, color on
the pill — they animate smoothly between checked states. Only display
doesn't animate.
Source: Mithjem product pages + homepage tech/spray sections (2026-05-22+). Pattern works flawlessly in iOS Quick Look + Safari Reader + Files preview thanks to its zero-JS nature.