Skip to main content

Project 05 · Beginner

Tabs

A recipe card with three tabs: Ingredients, Method, and Tips. You'll build it three ways: with radio buttons and no JavaScript, with buttons that swap a class, and finally as real tabs that keyboard and screen reader users can rely on.

  • 6 HTML ideas
  • 6 CSS ideas
  • 8 JavaScript ideas
  • 3 ways to build it

Why build it

With data attributes, ARIA roles, and arrow-key handling, you can build product pages that switch between Description, Reviews, and Shipping, code samples with a tab per language, and settings screens split into sections. The keyboard pattern you learn here, one stop in the Tab order and arrow keys to move inside, also works for toolbars, menus, and radio groups.

Skills you'll earn

  • Link each button to the content it shows with data attributes, and switch it with classList
  • Tell screen readers what a custom widget is with ARIA roles
  • Respond to the arrow keys, Home, and End with a keydown listener
  • Give a group of controls one stop in the Tab order, with a roving tabindex

01

Try it

Play with it first, then read the code that makes it work.

3 ways to build it

Hidden radio buttons and CSS's :checked switch the panels, and the arrow keys work for free. But screen readers hear radio buttons, not tabs.

Result0px
New tab (opens in a new tab)
Download
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Tabs</title>
		<link rel="stylesheet" href="style.css" />
	</head>
	<body>
		<main class="recipe">
			<h1 class="recipe-title">Pancakes</h1>

			<div class="tabs">
				<!-- Radios that share a name work as one choice: only one is picked. -->
				<input
					class="tab-radio"
					type="radio"
					name="recipe-tab"
					id="tab-ingredients"
					checked
				/>
				<!-- for="..." connects each label to its radio, so clicking it picks it. -->
				<label class="tab" for="tab-ingredients">Ingredients</label>
				<input class="tab-radio" type="radio" name="recipe-tab" id="tab-method" />
				<label class="tab" for="tab-method">Method</label>
				<input class="tab-radio" type="radio" name="recipe-tab" id="tab-tips" />
				<label class="tab" for="tab-tips">Tips</label>

				<!-- The panels come after the radios, so CSS can show the picked one. -->
				<div class="tab-panel panel-ingredients">
					<ul>
						<li>1 cup of flour</li>
						<li>1 cup of milk</li>
						<li>1 egg</li>
						<li>2 tablespoons of melted butter</li>
					</ul>
				</div>
				<div class="tab-panel panel-method">
					<ol>
						<li>Whisk everything into a smooth batter.</li>
						<li>Pour a little into a hot, buttered pan.</li>
						<li>Flip when bubbles appear, then cook the other side.</li>
					</ol>
				</div>
				<div class="tab-panel panel-tips">
					<p>
						Let the batter rest for ten minutes, and keep the first pancake for
						yourself: it's always the odd one.
					</p>
				</div>
			</div>
		</main>
	</body>
</html>

02

How it works

The ideas behind it, one at a time, each next to the code that does it.

3 ways to build it

Hidden radio buttons and CSS's :checked switch the panels, and the arrow keys work for free. But screen readers hear radio buttons, not tabs.

HTMLthe structure

  1. 01

    Use radios that share a name

    Radio buttons let someone pick one option from a set. Radios with the same name, here recipe-tab, count as one choice: picking one unpicks the others. That's how tabs behave too, so the browser keeps track of the selected tab for you. checked on the first radio makes Ingredients the tab that's open to start with.

    				<!-- Radios that share a name work as one choice: only one is picked. -->
    				<input
    					class="tab-radio"
    					type="radio"
    					name="recipe-tab"
    					id="tab-ingredients"
    					checked
    				/>
  2. 02

    Connect each label with for

    A <label> is text that names a form control. Its for attribute matches a radio's id, which connects the two, so clicking the label picks that radio. The radios themselves are hidden, so the labels are what you see and click, styled to look like tabs.

    				<!-- for="..." connects each label to its radio, so clicking it picks it. -->
    				<label class="tab" for="tab-ingredients">Ingredients</label>
    				<input class="tab-radio" type="radio" name="recipe-tab" id="tab-method" />
    				<label class="tab" for="tab-method">Method</label>
    				<input class="tab-radio" type="radio" name="recipe-tab" id="tab-tips" />
    				<label class="tab" for="tab-tips">Tips</label>

CSSthe look

  1. 03

    Hide the radios but keep them working

    opacity: 0 makes the radios see-through without removing them, and position: absolute takes them out of the layout so they don't leave a gap. They're still on the page, so Tab reaches them and the arrow keys move between them. The :focus-visible rule draws the focus outline on the label after the focused radio, so keyboard users can see where they are.

    /* The radios stay in the page for keyboards, but out of sight. */
    .tab-radio {
    	position: absolute;
    	opacity: 0;
    }
    
    .tab {
    	padding: 0.625rem 0.75rem;
    	border-radius: 999px;
    	background: #f4f4f6;
    	font-weight: 600;
    	text-align: center;
    	color: #6b6b76;
    	cursor: pointer;
    }
    
    .tab:hover {
    	color: #1c1c22;
    }
    
    /* + picks the label straight after the checked radio: the selected tab. */
    .tab-radio:checked + .tab {
    	background: #1c1c22;
    	color: #ffffff;
    }
    
    .tab-radio:focus-visible + .tab {
    	outline: 3px solid #2563eb;
    	outline-offset: 3px;
    }
  2. 04

    Style the picked tab with :checked +

    :checked matches a radio that's picked. + selects the element straight after another, when both share the same parent. So .tab-radio:checked + .tab finds the label right after the picked radio and styles it as the selected tab. This only works because each label comes directly after its radio in the HTML.

    /* + picks the label straight after the checked radio: the selected tab. */
    .tab-radio:checked + .tab {
    	background: #1c1c22;
    	color: #ffffff;
    }
  3. 05

    Show the matching panel with :checked ~

    ~ selects any later sibling, not only the next one. Every panel starts with display: none, and rules like #tab-method:checked ~ .panel-method show the panel that matches the picked radio. These selectors can only reach elements that come later, never earlier, which is why the panels have to come after the radios in the HTML.

    /* ~ reaches any later sibling, so each checked radio shows its own panel. */
    #tab-ingredients:checked ~ .panel-ingredients,
    #tab-method:checked ~ .panel-method,
    #tab-tips:checked ~ .panel-tips {
    	display: block;
    }
  4. 06

    Stop the card jumping with min-height

    Each panel holds a different amount of text, so each one would be a different height. Without a fixed size, the card would grow and shrink as you switch, and the tabs could move under your cursor. min-height: 9rem makes every panel at least that tall, so the card stays the same height.

    /* A fixed minimum height stops the card jumping when panels change size. */
    .tab-panel {
    	display: none;
    	grid-column: 1 / -1;
    	min-height: 9rem;
    	margin-top: 1.25rem;
    	line-height: 1.6;
    	color: #4b4b55;
    }

03

Wrap up

Track your progress, then go deeper or keep going.

Take it further

Once it works, try these on your own. Each one uses what you've just learned, plus one new idea.

  1. Challenge 01

    Update the page's address when a tab is picked, so a link can open a specific tab.

  2. Challenge 02

    Turn the tabs into an accordion on narrow screens.

  3. Challenge 03

    Remember the last tab picked with localStorage.

Finished Tabs?

Mark it done to track your progress.

You finished Tabs!

0 of 15 projects done.

Go deeper

The full article explains each idea in more depth, why the code is written this way, and more than fits on this page.

Article coming soon. See the series so far (opens in a new tab)