CSS-First UI Patterns · Content · 06

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, flexbox order, container queries
Browsers
All current browsers (Chrome, Edge, Firefox, Safari)
Updated
Can you make tabs without JavaScript?

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.

Live demo 0 KB JavaScript Click · Tab · ← →
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

How we do Baidu SEO

Script
Traditional Chinese (zh-TW)
Search
Google leads
Social
LINE, Facebook, Instagram
Note
Vocabulary differs from Hong Kong — converting the script is not enough

Convert Simplified to Traditional

Script
Traditional Chinese (zh-HK); English widely used
Search
Google leads
Social
WhatsApp, Facebook, Instagram
Note
Spoken Cantonese shows up in informal copy

Check which script a text uses

How the CSS tabs work

  1. 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.

  2. Radio, label, panel — in that order

    .ui-tabs__radio:checked + .ui-tabs__tab + .ui-tabs__panel means “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.

  3. Flexbox order builds the layout

    In the HTML, labels and panels alternate. order: 0 on the labels and order: 1 on the full-width panels moves all labels into one row above the panels.

  4. 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.

  5. A visible focus ring

    The focus sits on the invisible radio, so :focus-visible + .ui-tabs__tab draws the outline on its label.

  6. 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.

tabs.html
<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.css
/* 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

SituationWithout JSDetails
KeyboardWorksTab reaches the selected tab, arrow keys switch, Tab again moves into the panel's links.
Screen readersOKAnnounced as radio buttons — “Taiwan, radio button, 2 of 3” — not as tabs. Understandable, and honest about how it works.
Mouse and touchWorksLabels are large click targets.
Link to a tab (#taiwan)Needs JSCSS can't check a radio from the URL. Use :target tabs if deep links matter, or add a few lines of script.
Remember the tabNeeds JSThe first tab is selected again after a reload.
Don't add 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.

One language, several markets

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 →