Tabs Without JavaScript
Tabbed content built from radio buttons and a few lines of CSS. It works with mouse, touch and keyboard, handles any number of tabs, and keeps every panel in the HTML where search engines can read it.
- JavaScript
- 0 KB
- Built with
:checked, flexboxorder, container queries- Browsers
- All current browsers (Chrome, Edge, Firefox, Safari)
- Updated
Yes. Give each tab a radio button, a <label> and a panel. Radio buttons with the same name allow exactly one choice — that is the tab state. CSS shows the panel next to the checked radio with :checked + label + .panel. All panels stay in the HTML.
Live demo
Three Chinese-speaking markets, three tabs — with links to the right tool or service in each. Click a tab, or Tab into the demo and use the ← → arrow keys: radio buttons switch with arrow keys natively.
- Script
- Simplified Chinese (zh-CN)
- Search
- Baidu leads; Bing, Sogou, 360 and Shenma also matter
- Social
- WeChat, Douyin, Rednote, Weibo
- Note
- Google, Facebook and YouTube are not accessible
- Script
- Traditional Chinese (zh-TW)
- Search
- Google leads
- Social
- LINE, Facebook, Instagram
- Note
- Vocabulary differs from Hong Kong — converting the script is not enough
- Script
- Traditional Chinese (zh-HK); English widely used
- Search
- Google leads
- Social
- WhatsApp, Facebook, Instagram
- Note
- Spoken Cantonese shows up in informal copy
How the CSS tabs work
- Radio buttons hold the state
All radios share
name="ui-market", so exactly one is checked. Clicking a<label for="…">checks its radio — that is the whole “click a tab” logic. - Radio, label, panel — in that order
.ui-tabs__radio:checked + .ui-tabs__tab + .ui-tabs__panelmeans “the panel right after the label of the checked radio”. The same rule works for 3 tabs or 30, without a single ID in the CSS. - Flexbox
orderbuilds the layoutIn the HTML, labels and panels alternate.
order: 0on the labels andorder: 1on the full-width panels moves all labels into one row above the panels. - Keyboard support for free
Most CSS tab tutorials hide the radios with
display: none, which makes them unreachable by keyboard. Kept visually hidden instead, they get focus with Tab — and the arrow keys move between them, like real tabs. - A visible focus ring
The focus sits on the invisible radio, so
:focus-visible + .ui-tabs__tabdraws the outline on its label. - Works in small spaces
A container query lets the tabs share the row evenly and stacks the panel content when the component is narrow — on phones or in a sidebar.
Copy the HTML and CSS
Exactly the code running in the demo. To add a tab, copy one radio–label–panel block and give the radio a new id (and the label a matching for). Use a different name for each tab group on the same page.
<div class="ui-tabs">
<input class="ui-tabs__radio" type="radio" name="ui-market" id="ui-tab-cn" checked>
<label class="ui-tabs__tab" for="ui-tab-cn">Mainland China</label>
<div class="ui-tabs__panel">
<dl>
<dt>Script</dt><dd>Simplified Chinese (zh-CN)</dd>
<dt>Search</dt><dd>Baidu leads; Bing, Sogou, 360 and Shenma also matter</dd>
<dt>Social</dt><dd>WeChat, Douyin, Rednote, Weibo</dd>
<dt>Note</dt><dd>Google, Facebook and YouTube are not accessible</dd>
</dl>
<p><a href="/services/seo/china/">How we do Baidu SEO</a></p>
</div>
<input class="ui-tabs__radio" type="radio" name="ui-market" id="ui-tab-tw">
<label class="ui-tabs__tab" for="ui-tab-tw">Taiwan</label>
<div class="ui-tabs__panel">
<dl>
<dt>Script</dt><dd>Traditional Chinese (zh-TW)</dd>
<dt>Search</dt><dd>Google leads</dd>
<dt>Social</dt><dd>LINE, Facebook, Instagram</dd>
<dt>Note</dt><dd>Vocabulary differs from Hong Kong — converting the script is not enough</dd>
</dl>
<p><a href="/tools/simplified-to-traditional/">Convert Simplified to Traditional</a></p>
</div>
<input class="ui-tabs__radio" type="radio" name="ui-market" id="ui-tab-hk">
<label class="ui-tabs__tab" for="ui-tab-hk">Hong Kong</label>
<div class="ui-tabs__panel">
<dl>
<dt>Script</dt><dd>Traditional Chinese (zh-HK); English widely used</dd>
<dt>Search</dt><dd>Google leads</dd>
<dt>Social</dt><dd>WhatsApp, Facebook, Instagram</dd>
<dt>Note</dt><dd>Spoken Cantonese shows up in informal copy</dd>
</dl>
<p><a href="/tools/detect-chinese/">Check which script a text uses</a></p>
</div>
</div>/* Tabs — no JavaScript.
Each tab is a radio button, its label and its panel.
Flexbox "order" moves all labels into one row above the panels,
so the CSS works for any number of tabs — no IDs needed. */
.ui-tabs {
--ui-bg: #ffffff;
--ui-tab: #e6e8f5;
--ui-text: #1b1f3b;
--ui-muted: #4b5070;
--ui-accent: #4f46e5;
--ui-border: #d7daea;
container-type: inline-size;
position: relative; /* keeps the hidden radios inside the component */
display: flex;
flex-wrap: wrap;
align-items: flex-end;
font: 400 0.9375rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
color: var(--ui-text);
}
/* The radio holds the state. Visually hidden, NOT display:none,
so keyboard users can reach it — arrow keys then switch tabs. */
.ui-tabs__radio {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* Tab labels: first row */
.ui-tabs__tab {
order: 0;
margin: 0 0.25rem 0 0;
padding: 0.65rem 1.1rem;
border: 1px solid var(--ui-border);
border-bottom: 0;
border-radius: 10px 10px 0 0;
background: var(--ui-tab);
font-weight: 600;
cursor: pointer;
}
.ui-tabs__tab:hover {
background: var(--ui-bg);
}
/* Panels: second row, full width, hidden until their radio is checked */
.ui-tabs__panel {
order: 1;
width: 100%;
display: none;
padding: 1.25rem 1.5rem;
border: 1px solid var(--ui-border);
border-radius: 0 12px 12px 12px;
background: var(--ui-bg);
color: var(--ui-muted);
}
/* Active tab and its panel */
.ui-tabs__radio:checked + .ui-tabs__tab {
position: relative;
z-index: 1;
margin-bottom: -1px;
padding-bottom: calc(0.65rem + 1px);
background: var(--ui-bg);
color: var(--ui-accent);
}
.ui-tabs__radio:checked + .ui-tabs__tab + .ui-tabs__panel {
display: block;
}
/* Keyboard focus is on the hidden radio — show it on the label */
.ui-tabs__radio:focus-visible + .ui-tabs__tab {
outline: 2px solid var(--ui-accent);
outline-offset: 2px;
}
/* Panel content */
.ui-tabs__panel dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.4rem 1.25rem;
margin: 0;
}
.ui-tabs__panel dt {
font-weight: 700;
color: var(--ui-text);
}
.ui-tabs__panel dd {
margin: 0;
}
.ui-tabs__panel p {
margin: 1rem 0 0;
}
.ui-tabs__panel a {
color: var(--ui-accent);
font-weight: 600;
}
/* Narrow space: tabs share the row, panel content stacks */
@container (max-width: 30rem) {
.ui-tabs__tab {
flex: 1 1 0;
min-width: 0;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
margin-right: 2px;
padding-inline: 0.4rem;
font-size: 0.875rem;
line-height: 1.25;
text-align: center;
}
.ui-tabs__panel {
border-radius: 0 0 12px 12px;
}
.ui-tabs__panel dl {
grid-template-columns: 1fr;
gap: 0 0;
}
.ui-tabs__panel dd {
margin-bottom: 0.5rem;
}
}All class names start with ui-, and the CSS sets its own spacing — it works inside a page with a global reset.
Accessibility: what CSS tabs can and can't do
| Situation | Without JS | Details |
|---|---|---|
| Keyboard | Works | Tab reaches the selected tab, arrow keys switch, Tab again moves into the panel's links. |
| Screen readers | OK | Announced as radio buttons — “Taiwan, radio button, 2 of 3” — not as tabs. Understandable, and honest about how it works. |
| Mouse and touch | Works | Labels are large click targets. |
Link to a tab (#taiwan) | Needs JS | CSS can't check a radio from the URL. Use :target tabs if deep links matter, or add a few lines of script. |
| Remember the tab | Needs JS | The first tab is selected again after a reload. |
role="tab" without JavaScript
Many CSS tab examples put role="tablist" and role="tab" on the labels. That promises screen-reader users a tab widget — with aria-selected states and panel relationships — that only JavaScript can keep up to date. Without the script, the roles describe something that isn't there. Plain radio buttons are the more honest choice. If you need full tab semantics, build the ARIA tab pattern with JavaScript on top of this HTML.
Tabs and SEO
All three panels in the demo are in the HTML. Search engines read the Taiwan and Hong Kong panels just like the visible one — no click needed. Google has said that with mobile-first indexing, content in tabs and accordions is treated like visible content.
The risk is tab systems that only load a panel's content when its tab is clicked. Crawlers don't click. Baidu and many AI crawlers also don't run JavaScript reliably, so content that a script adds later may never be indexed.
One practical rule: keep the content a page should rank for in the first, visible tab — or outside the tabs altogether. Tabs are best for parallel details that visitors choose between: product variants, regions, plans.
Questions about CSS tabs
Can you make tabs with only HTML and CSS?
Yes. Radio buttons store which tab is active, labels act as the tab buttons, and :checked shows the matching panel. The demo on this page uses no JavaScript.
Are CSS-only tabs accessible?
They are keyboard- and screen-reader-usable if the radios stay focusable. Screen readers announce radio buttons rather than tabs. For the full ARIA tab pattern you need JavaScript.
Does Google index content in hidden tabs?
Yes, if the content is in the HTML. Content that is loaded only after a click is the problem — crawlers don't click.
How many tabs can I have?
Technically any number — the CSS doesn't change. For usability, keep it to what fits in one row on a phone, usually two to four.
Tabs or accordion?
Tabs suit short, parallel content that visitors compare. Accordions suit longer content and phones. See the accordion pattern.
This FAQ is built without JavaScript — see the accordion pattern.
Mainland China, Taiwan or Hong Kong — where should you start?
Each market has its own script, search engines and platforms. We plan market-specific content and run Baidu SEO and Chinese PPC for Western companies — with native editors for every region.
CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →