Skip to content

RESOURCES / BLOG

How are HTML tabs created for switching between content sections?

Building clean, accessible tabbed interfaces is a common front-end task. Whether you are creating a docs layout, product detail views, or a settings page, tabs help users switch context without page reloads.

Hi folks,
I’m building a small component library and need a reliable tabs pattern. How are HTML tabs created for switching between content sections? I’d like both a no-JS fallback and an accessible JS version with proper keyboard navigation. Guidance on performance with heavy images or videos inside tabs would also help. Thanks!

Tabs can be implemented in a few ways depending on your constraints. Here are two practical approaches you can mix and match: a pure CSS version and a fully accessible JavaScript version.

This approach works without JavaScript, is easy to style, and keeps state within the page. It is not as accessible as a proper ARIA tabs pattern, but it provides a reasonable baseline.

<div class="tabs">
  <input type="radio" name="tabs" id="tab1" checked>
  <label for="tab1">Overview</label>

  <input type="radio" name="tabs" id="tab2">
  <label for="tab2">Code</label>

  <input type="radio" name="tabs" id="tab3">
  <label for="tab3">FAQ</label>

  <div class="panel" id="panel1">Overview content</div>
  <div class="panel" id="panel2">Code content</div>
  <div class="panel" id="panel3">FAQ content</div>
</div>

<style>
.tabs { display: grid; grid-template-columns: auto auto auto; grid-auto-rows: min-content; gap: .5rem; }
.tabs > input { position: absolute; left: -9999px; } /* hide */
.tabs > label { padding: .5rem 1rem; cursor: pointer; border-bottom: 2px solid transparent; }
#tab1:checked + label { border-color: #4a90e2; }
#tab2:checked + label ~ input + label { border-color: #4a90e2; }
#tab3:checked + label ~ .panel { display: none; } /* ensures later rules win */

.panel { grid-column: 1 / -1; display: none; padding: 1rem; border: 1px solid #ddd; }
#tab1:checked ~ #panel1 { display: block; }
#tab2:checked ~ #panel2 { display: block; }
#tab3:checked ~ #panel3 { display: block; }
</style>Code language: HTML, XML (xml)

Alternative: a :target-based approach uses anchor links and hash navigation. It’s SEO and share-link friendly but requires more CSS specificity to show the targeted panel and hide others.

For production UI, implement the WAI-ARIA tabs pattern with proper roles, states, and keyboard interaction. This ensures screen readers and keyboard users can use your tabs.

<div role="tablist" aria-label="Sample tabs" class="tablist">
  <button role="tab" id="t1" aria-controls="p1" aria-selected="true" tabindex="0">Overview</button>
  <button role="tab" id="t2" aria-controls="p2" aria-selected="false" tabindex="-1">Code</button>
  <button role="tab" id="t3" aria-controls="p3" aria-selected="false" tabindex="-1">FAQ</button>
</div>

<div id="p1" role="tabpanel" aria-labelledby="t1" tabindex="0">Overview content</div>
<div id="p2" role="tabpanel" aria-labelledby="t2" hidden tabindex="0">Code content</div>
<div id="p3" role="tabpanel" aria-labelledby="t3" hidden tabindex="0">FAQ content</div>

<script>
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
const panels = Array.from(document.querySelectorAll('[role="tabpanel"]'));

function activateTab(tab) {
  tabs.forEach(t => {
    const selected = t === tab;
    t.setAttribute('aria-selected', selected);
    t.tabIndex = selected ? 0 : -1;
  });
  panels.forEach(p => {
    p.hidden = p.id !== tab.getAttribute('aria-controls');
  });
  tab.focus();
}

document.querySelector('.tablist').addEventListener('click', e => {
  if (e.target.getAttribute('role') === 'tab') activateTab(e.target);
});

document.querySelector('.tablist').addEventListener('keydown', e => {
  const idx = tabs.indexOf(document.activeElement);
  if (idx < 0) return;
  if (e.key === 'ArrowRight') activateTab(tabs[(idx + 1) % tabs.length]);
  if (e.key === 'ArrowLeft') activateTab(tabs[(idx - 1 + tabs.length) % tabs.length]);
  if (e.key === 'Home') activateTab(tabs[0]);
  if (e.key === 'End') activateTab(tabs[tabs.length - 1]);
});
</script>Code language: HTML, XML (xml)
  • Use role="tablist", role="tab", and role="tabpanel".
  • Toggle aria-selected on tabs and hidden on panels.
  • Ensure only the active tab is focusable (tabindex=0), others are -1.
  • Support arrow keys and hom
  • Keep headings inside panels for clear structure.
  • Defer heavy content. Render non-active panels on demand or lazy load images with loading="lazy".
  • Use semantic <img> and alt text in panels. See this guide on the HTML image tag.
  • Prefer modern formats like WebP or AVIF when possible. This overview explains tradeoffs: WebP format guide.
  • Audit initial render size and reduce offscreen work. Practical ideas are summarized here: Optimized website basics.

If your tabs include images or videos, you can optimize delivery and responsiveness with Cloudinary. For example, serve the best format and quality automatically, and scale to the panel width.

<!-- Inside an inactive tab panel -->
<img
  src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/sample.jpg"
  alt="Product view"
  loading="lazy"
  width="800" height="533"
/>Code language: HTML, XML (xml)
  • f_auto picks a modern format like WebP if supported, falling back when needed.
  • q_auto balances visual quality and file size.
  • w_ sets a max width so images match panel layout without overserving bytes.

You can also combine transformations, generate thumbnails per tab, and manage assets at scale. Explore handy utilities at Cloudinary tools.

.tablist { display: flex; gap: .5rem; border-bottom: 1px solid #ddd; }
[role="tab"] { padding: .5rem 1rem; border: 0; background: none; cursor: pointer; }
[role="tab"][aria-selected="true"] { border-bottom: 2px solid #4a90e2; font-weight: 600; }
[role="tabpanel"] { padding: 1rem 0; }Code language: CSS (css)
  • Basic: CSS-only tabs can work with radio inputs or :target.
  • Production: Use ARIA roles, manage aria-selected and hidden, and implement keyboard navigation.
  • Performance: Lazy load heavy assets and deliver modern formats. See the WebP format guide for context.
  • Cloudinary can optimize and deliver images in tabs with f_auto,q_auto and responsive sizing.

Ready to optimize images and media inside your tabs while keeping performance high? Create your free Cloudinary account and start transforming and delivering assets on the fly.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free