October 14, 2026 - 20:48
Create Tabs with CSS and JavaScript Without Bootstrap Image
HTML5 & CSS3

Create Tabs with CSS and JavaScript Without Bootstrap

Comments

Tabs let related sections share the same area of a page. They work well for short alternatives such as an overview, installation notes and examples. They are less suitable when readers need to compare every section at once; ordinary headings may be better in that case.

Changing display on a panel handles only the visual part. The selected tab, keyboard focus and visible panel must stay in agreement. This example uses manual activation: arrow keys move between tab buttons, while Enter, Space or a click activates a tab.

Keep the tab buttons and panels paired

Save this complete example as tabs.html. Each button names exactly one panel through aria-controls, and each panel refers back to its button through aria-labelledby. IDs must remain unique if you copy the component into a larger page.

HTML
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Manual-activation tabs</title>
<style>
 body { font: 1rem/1.6 system-ui; margin: 2rem; }
 [role="tablist"] { display: flex; flex-wrap: wrap; gap: .4rem; }
 [role="tab"] { padding: .65rem 1rem; border: 1px solid #526071; }
 [role="tab"][aria-selected="true"] { background: #163f70; color: white; }
 :focus-visible { outline: 3px solid #a63b00; outline-offset: 3px; }
 [role="tabpanel"] { padding: 1rem; border: 1px solid #526071; margin-top: .6rem; }
 [hidden] { display: none !important; }
</style>
<h1>Web technologies</h1>
<div id="technology-tabs">
 <div role="tablist" aria-label="Technology">
  <button type="button" role="tab" id="tab-html" aria-controls="panel-html" aria-selected="true" tabindex="0">HTML</button>
  <button type="button" role="tab" id="tab-css" aria-controls="panel-css" aria-selected="false" tabindex="-1">CSS</button>
  <button type="button" role="tab" id="tab-js" aria-controls="panel-js" aria-selected="false" tabindex="-1">JavaScript</button>
 </div>
 <section role="tabpanel" id="panel-html" aria-labelledby="tab-html" tabindex="0"><h2>HTML</h2><p>Describe the document's structure.</p></section>
 <section role="tabpanel" id="panel-css" aria-labelledby="tab-css" tabindex="0" hidden><h2>CSS</h2><p>Style the document.</p></section>
 <section role="tabpanel" id="panel-js" aria-labelledby="tab-js" tabindex="0" hidden><h2>JavaScript</h2><p>Add interactive behavior.</p></section>
</div>
<script>
 const root = document.querySelector('#technology-tabs');
 const tabs = [...root.querySelectorAll('[role="tab"]')];
 function focusTab(tab) {
   tabs.forEach(item => { item.tabIndex = item === tab ? 0 : -1; });
   tab.focus();
 }
 function activate(tab) {
   tabs.forEach(item => {
     const selected = item === tab;
     item.setAttribute('aria-selected', String(selected));
     document.getElementById(item.getAttribute('aria-controls')).hidden = !selected;
   });
   focusTab(tab);
 }
 tabs.forEach((tab, index) => {
   tab.addEventListener('click', () => activate(tab));
   tab.addEventListener('keydown', event => {
     let next;
     if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
     if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
     if (event.key === 'Home') next = 0;
     if (event.key === 'End') next = tabs.length - 1;
     if (next !== undefined) {
       event.preventDefault();
       focusTab(tabs[next]);
     }
   });
 });
</script>
</html>

Focus and selection are different states

In this horizontal tab list, Left and Right move focus, including wrapping at the ends. Home and End jump to the first and last buttons. Native buttons already turn Enter and Space into activation, so the click handler also covers keyboard selection. Only one button participates in the normal Tab order. The visible text panel is itself focusable so a keyboard user can move into its content. This follows the manual-activation approach described in the W3C tabs pattern.

There is deliberately no animation or remote loading here. Add those only after the basic state changes work. If activation starts a network request, preserve a useful panel label and show a loading or error message inside that panel; a blank space gives the reader no explanation.

Check it inside your real page

Press Right without pressing Enter: focus should move, but the old panel should stay visible. Activate the focused tab and check that exactly one panel is shown. Also test clicking a previously unfocused button, wrapping from the last tab to the first, and reading the selected state with your target screen reader.

This is a JavaScript-dependent demonstration. If the page must work without scripts, start with all sections visible and add the tab behavior only after initialization. Do not hide essential content permanently in a script that may never run.

Watch the original walkthrough

The original Netopsiyon video below is narrated in Turkish. Watch the demonstration on our YouTube channel alongside this English guide. The example in this article is an independently updated implementation, not a word-for-word transcript.

Watch this tutorial on YouTube.

Related Articles

Comments ()

No comments yet. Be the first to comment!

Leave a Comment