{"id":2775,"date":"2026-02-06T06:49:26","date_gmt":"2026-02-06T06:49:26","guid":{"rendered":"https:\/\/demo.materiamedica.net\/demo6\/?p=2775"},"modified":"2026-02-06T06:49:26","modified_gmt":"2026-02-06T06:49:26","slug":"chapter-6-tabs","status":"publish","type":"post","link":"https:\/\/demo.materiamedica.net\/demo6\/chapter-6-tabs\/","title":{"rendered":"Chapter 6: Tabs"},"content":{"rendered":"<p dir=\"auto\"><strong>How to create tabs<\/strong> (also called tabbed interfaces, tab navigation, tab panels, etc.) from the very beginning.<\/p>\n<h3 dir=\"auto\">What are Tabs?<\/h3>\n<p dir=\"auto\">Tabs are a very common UI pattern that lets users switch between different sections of content without leaving the page.<\/p>\n<p dir=\"auto\">You see them everywhere:<\/p>\n<ul dir=\"auto\">\n<li>Product pages (Description \/ Reviews \/ Specifications)<\/li>\n<li>Settings pages<\/li>\n<li>Dashboard analytics<\/li>\n<li>FAQ pages<\/li>\n<li>Code editors (multiple files open)<\/li>\n<li>Browser dev tools<\/li>\n<\/ul>\n<h3 dir=\"auto\">Goals of good tabs (keep this in mind)<\/h3>\n<ul dir=\"auto\">\n<li>Clear which tab is active<\/li>\n<li>Easy to click \/ tap (especially on mobile)<\/li>\n<li>Content changes instantly or with smooth transition<\/li>\n<li>Accessible with keyboard (Tab key + Enter\/Space)<\/li>\n<li>Works well on small screens<\/li>\n<li>Only loads\/shows relevant content<\/li>\n<\/ul>\n<p dir=\"auto\">We will build <strong>four progressively better versions<\/strong>:<\/p>\n<ol dir=\"auto\">\n<li>Pure CSS tabs (no JavaScript) \u2013 very simple<\/li>\n<li>Classic JavaScript tabs (most common style)<\/li>\n<li>Modern accessible tabs with &lt;tablist&gt;, &lt;tab&gt;, &lt;tabpanel&gt; roles<\/li>\n<li>Bonus: Tabs with fade \/ slide animation<\/li>\n<\/ol>\n<p dir=\"auto\">Let\u2019s go slowly.<\/p>\n<h3 dir=\"auto\">Version 1 \u2013 Pure CSS Tabs (no JavaScript)<\/h3>\n<p dir=\"auto\">This uses radio buttons + labels (very clever trick).<\/p>\n<h4 dir=\"auto\">HTML<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>&lt;div class=\"tabs-container\"&gt;\r\n\r\n  &lt;!-- Hidden radio buttons control which tab is active --&gt;\r\n  &lt;input type=\"radio\" name=\"tabs\" id=\"tab1\" checked&gt;\r\n  &lt;input type=\"radio\" name=\"tabs\" id=\"tab2\"&gt;\r\n  &lt;input type=\"radio\" name=\"tabs\" id=\"tab3\"&gt;\r\n\r\n  &lt;!-- Tab navigation bar --&gt;\r\n  &lt;div class=\"tab-list\"&gt;\r\n    &lt;label for=\"tab1\" class=\"tab-button\"&gt;Profile&lt;\/label&gt;\r\n    &lt;label for=\"tab2\" class=\"tab-button\"&gt;Settings&lt;\/label&gt;\r\n    &lt;label for=\"tab3\" class=\"tab-button\"&gt;Security&lt;\/label&gt;\r\n  &lt;\/div&gt;\r\n\r\n  &lt;!-- Tab contents --&gt;\r\n  &lt;div class=\"tab-content\"&gt;\r\n    &lt;div class=\"tab-panel\" id=\"content1\"&gt;\r\n      &lt;h2&gt;Your Profile&lt;\/h2&gt;\r\n      &lt;p&gt;Here you can update your name, photo, bio, etc.&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;div class=\"tab-panel\" id=\"content2\"&gt;\r\n      &lt;h2&gt;Account Settings&lt;\/h2&gt;\r\n      &lt;p&gt;Language, notifications, dark mode, email preferences...&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;div class=\"tab-panel\" id=\"content3\"&gt;\r\n      &lt;h2&gt;Security &amp; Privacy&lt;\/h2&gt;\r\n      &lt;p&gt;Change password, two-factor authentication, login history.&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n  &lt;\/div&gt;\r\n\r\n&lt;\/div&gt;<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<h4 dir=\"auto\">CSS<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>.tabs-container {\r\n  max-width: 780px;\r\n  margin: 2rem auto;\r\n  font-family: system-ui, sans-serif;\r\n}\r\n\r\n.tab-list {\r\n  display: flex;\r\n  border-bottom: 2px solid #e0e0e0;\r\n}\r\n\r\n.tab-button {\r\n  flex: 1;\r\n  padding: 1rem 1.5rem;\r\n  text-align: center;\r\n  background: #f8f9fa;\r\n  cursor: pointer;\r\n  font-weight: 500;\r\n  transition: all 0.2s;\r\n  user-select: none;\r\n}\r\n\r\n.tab-button:hover {\r\n  background: #e9ecef;\r\n}\r\n\r\n\/* Active tab styling *\/\r\n#tab1:checked ~ .tab-list label[for=\"tab1\"],\r\n#tab2:checked ~ .tab-list label[for=\"tab2\"],\r\n#tab3:checked ~ .tab-list label[for=\"tab3\"] {\r\n  background: white;\r\n  border-bottom: 3px solid #0066ff;\r\n  color: #0066ff;\r\n  font-weight: 600;\r\n}\r\n\r\n\/* Show only the selected panel *\/\r\n.tab-panel {\r\n  display: none;\r\n  padding: 1.8rem 2rem;\r\n  background: white;\r\n  border: 1px solid #e0e0e0;\r\n  border-top: none;\r\n}\r\n\r\n#tab1:checked ~ .tab-content #content1,\r\n#tab2:checked ~ .tab-content #content2,\r\n#tab3:checked ~ .tab-content #content3 {\r\n  display: block;\r\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<p dir=\"auto\"><strong>Pros<\/strong><\/p>\n<ul dir=\"auto\">\n<li>No JavaScript<\/li>\n<li>Very fast<\/li>\n<li>Works with JS disabled<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Cons<\/strong><\/p>\n<ul dir=\"auto\">\n<li>Only one tab active at a time (by design)<\/li>\n<li>Harder to add dynamic content later<\/li>\n<li>Accessibility is okay but not great<\/li>\n<\/ul>\n<h3 dir=\"auto\">Version 2 \u2013 JavaScript Tabs (most common real-world approach)<\/h3>\n<h4 dir=\"auto\">HTML<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>&lt;div class=\"tabs\"&gt;\r\n\r\n  &lt;div class=\"tab-buttons\"&gt;\r\n    &lt;button class=\"tab-btn active\" data-tab=\"profile\"&gt;Profile&lt;\/button&gt;\r\n    &lt;button class=\"tab-btn\" data-tab=\"settings\"&gt;Settings&lt;\/button&gt;\r\n    &lt;button class=\"tab-btn\" data-tab=\"security\"&gt;Security&lt;\/button&gt;\r\n  &lt;\/div&gt;\r\n\r\n  &lt;div class=\"tab-panels\"&gt;\r\n    &lt;div class=\"tab-panel active\" id=\"profile\"&gt;\r\n      &lt;h2&gt;Profile&lt;\/h2&gt;\r\n      &lt;p&gt;Update your personal information here.&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;div class=\"tab-panel\" id=\"settings\"&gt;\r\n      &lt;h2&gt;Settings&lt;\/h2&gt;\r\n      &lt;p&gt;Customize your experience.&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;div class=\"tab-panel\" id=\"security\"&gt;\r\n      &lt;h2&gt;Security&lt;\/h2&gt;\r\n      &lt;p&gt;Protect your account.&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n  &lt;\/div&gt;\r\n\r\n&lt;\/div&gt;<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<h4 dir=\"auto\">CSS<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>.tab-buttons {\r\n  display: flex;\r\n  border-bottom: 2px solid #ddd;\r\n}\r\n\r\n.tab-btn {\r\n  padding: 0.9rem 1.8rem;\r\n  background: none;\r\n  border: none;\r\n  font-size: 1.05rem;\r\n  cursor: pointer;\r\n  color: #555;\r\n  transition: all 0.2s;\r\n}\r\n\r\n.tab-btn:hover {\r\n  background: #f5f5f5;\r\n}\r\n\r\n.tab-btn.active {\r\n  border-bottom: 3px solid #0077ff;\r\n  color: #0077ff;\r\n  font-weight: 600;\r\n}\r\n\r\n.tab-panels {\r\n  position: relative;\r\n}\r\n\r\n.tab-panel {\r\n  display: none;\r\n  padding: 2rem;\r\n  background: white;\r\n  border: 1px solid #ddd;\r\n  border-top: none;\r\n}\r\n\r\n.tab-panel.active {\r\n  display: block;\r\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<h4 dir=\"auto\">JavaScript<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>document.addEventListener(\"DOMContentLoaded\", () =&gt; {\r\n  const tabs = document.querySelectorAll(\".tab-btn\");\r\n  \r\n  tabs.forEach(tab =&gt; {\r\n    tab.addEventListener(\"click\", () =&gt; {\r\n      \/\/ Remove active from all buttons &amp; panels\r\n      tabs.forEach(t =&gt; t.classList.remove(\"active\"));\r\n      document.querySelectorAll(\".tab-panel\").forEach(p =&gt; p.classList.remove(\"active\"));\r\n\r\n      \/\/ Add active to clicked tab\r\n      tab.classList.add(\"active\");\r\n\r\n      \/\/ Show corresponding panel\r\n      const target = tab.getAttribute(\"data-tab\");\r\n      document.getElementById(target).classList.add(\"active\");\r\n    });\r\n  });\r\n});<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<p dir=\"auto\"><strong>Very important:<\/strong> data-tab attribute links button to panel id.<\/p>\n<hr \/>\n<h3 dir=\"auto\">Version 3 \u2013 Accessible Tabs (ARIA + best practices 2025\u20132026)<\/h3>\n<h4 dir=\"auto\">HTML<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<pre tabindex=\"0\"><code>&lt;div class=\"tabs\"&gt;\r\n  \r\n  &lt;div role=\"tablist\" class=\"tab-buttons\"&gt;\r\n    &lt;button role=\"tab\" aria-selected=\"true\" aria-controls=\"profile-panel\" id=\"profile-tab\" class=\"tab-btn active\"&gt;\r\n      Profile\r\n    &lt;\/button&gt;\r\n    &lt;button role=\"tab\" aria-selected=\"false\" aria-controls=\"settings-panel\" id=\"settings-tab\" class=\"tab-btn\"&gt;\r\n      Settings\r\n    &lt;\/button&gt;\r\n    &lt;button role=\"tab\" aria-selected=\"false\" aria-controls=\"security-panel\" id=\"security-tab\" class=\"tab-btn\"&gt;\r\n      Security\r\n    &lt;\/button&gt;\r\n  &lt;\/div&gt;\r\n\r\n  &lt;div class=\"tab-panels\"&gt;\r\n    &lt;div role=\"tabpanel\" id=\"profile-panel\" tabindex=\"0\" class=\"tab-panel active\"&gt;\r\n      &lt;h2&gt;Profile&lt;\/h2&gt;\r\n      &lt;p&gt;...&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div role=\"tabpanel\" id=\"settings-panel\" tabindex=\"0\" hidden class=\"tab-panel\"&gt;\r\n      &lt;h2&gt;Settings&lt;\/h2&gt;\r\n      &lt;p&gt;...&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div role=\"tabpanel\" id=\"security-panel\" tabindex=\"0\" hidden class=\"tab-panel\"&gt;\r\n      &lt;h2&gt;Security&lt;\/h2&gt;\r\n      &lt;p&gt;...&lt;\/p&gt;\r\n    &lt;\/div&gt;\r\n  &lt;\/div&gt;\r\n\r\n&lt;\/div&gt;<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<h4 dir=\"auto\">JavaScript (with ARIA updates)<\/h4>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<div>JavaScript<\/div>\n<div>\n<pre tabindex=\"0\"><code>document.querySelectorAll('[role=\"tab\"]').forEach(tab =&gt; {\r\n  tab.addEventListener(\"click\", () =&gt; {\r\n    \/\/ Deactivate all\r\n    document.querySelectorAll('[role=\"tab\"]').forEach(t =&gt; {\r\n      t.setAttribute(\"aria-selected\", \"false\");\r\n      t.classList.remove(\"active\");\r\n    });\r\n    document.querySelectorAll('[role=\"tabpanel\"]').forEach(p =&gt; {\r\n      p.setAttribute(\"hidden\", \"\");\r\n      p.classList.remove(\"active\");\r\n    });\r\n\r\n    \/\/ Activate selected\r\n    tab.setAttribute(\"aria-selected\", \"true\");\r\n    tab.classList.add(\"active\");\r\n    \r\n    const panel = document.getElementById(tab.getAttribute(\"aria-controls\"));\r\n    panel.removeAttribute(\"hidden\");\r\n    panel.classList.add(\"active\");\r\n  });\r\n\r\n  \/\/ Keyboard support: Arrow keys to move between tabs\r\n  tab.addEventListener(\"keydown\", e =&gt; {\r\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\r\n      e.preventDefault();\r\n      const tabs = [...document.querySelectorAll('[role=\"tab\"]')];\r\n      const current = tabs.indexOf(tab);\r\n      let next;\r\n\r\n      if (e.key === \"ArrowRight\") {\r\n        next = tabs[(current + 1) % tabs.length];\r\n      } else {\r\n        next = tabs[(current - 1 + tabs.length) % tabs.length];\r\n      }\r\n\r\n      next.focus();\r\n      next.click();\r\n    }\r\n  });\r\n});<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<h3 dir=\"auto\">Quick comparison \u2013 which method to choose?<\/h3>\n<div>\n<div dir=\"auto\">\n<table dir=\"auto\">\n<thead>\n<tr>\n<th data-col-size=\"lg\">Goal<\/th>\n<th data-col-size=\"xl\">Best choice<\/th>\n<th data-col-size=\"xs\">Needs JS?<\/th>\n<th data-col-size=\"md\">Accessibility<\/th>\n<th data-col-size=\"xs\">Animation possible?<\/th>\n<th data-col-size=\"md\">Mobile friendly<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td data-col-size=\"lg\">Super simple, no JS<\/td>\n<td data-col-size=\"xl\">Pure CSS (radio)<\/td>\n<td data-col-size=\"xs\">No<\/td>\n<td data-col-size=\"md\">Medium<\/td>\n<td data-col-size=\"xs\">Limited<\/td>\n<td data-col-size=\"md\">Good<\/td>\n<\/tr>\n<tr>\n<td data-col-size=\"lg\">Classic &amp; flexible<\/td>\n<td data-col-size=\"xl\">JS + data attributes<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Good<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Very good<\/td>\n<\/tr>\n<tr>\n<td data-col-size=\"lg\">Professional \/ production<\/td>\n<td data-col-size=\"xl\">ARIA roles + keyboard support<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Excellent<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Excellent<\/td>\n<\/tr>\n<tr>\n<td data-col-size=\"lg\">Very modern look<\/td>\n<td data-col-size=\"xl\">Tailwind \/ Headless UI \/ Radix UI<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Excellent<\/td>\n<td data-col-size=\"xs\">Yes<\/td>\n<td data-col-size=\"md\">Perfect<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<div><\/div>\n<\/div>\n<\/div>\n<h3 dir=\"auto\">Bonus: Adding a nice fade transition<\/h3>\n<p dir=\"auto\">Add this CSS:<\/p>\n<div dir=\"auto\">\n<div data-testid=\"code-block\">\n<div>\n<div>CSS<\/div>\n<div>\n<pre tabindex=\"0\"><code>.tab-panel {\r\n  opacity: 0;\r\n  transition: opacity 0.25s ease;\r\n  pointer-events: none;\r\n}\r\n\r\n.tab-panel.active {\r\n  opacity: 1;\r\n  pointer-events: auto;\r\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<p dir=\"auto\">Now panels fade in\/out smoothly.<\/p>\n<p dir=\"auto\">Would you like to go deeper into any of these topics?<\/p>\n<p dir=\"auto\">Examples:<\/p>\n<ul dir=\"auto\">\n<li>How to make vertical tabs (left sidebar style)<\/li>\n<li>How to make tabs scroll horizontally on mobile<\/li>\n<li>How to lazy-load tab content (only load when opened)<\/li>\n<li>How to make tabs with icons<\/li>\n<li>How to do tabs in React \/ Vue \/ Tailwind<\/li>\n<li>Common accessibility mistakes<\/li>\n<li>How to animate with slide instead of fade<\/li>\n<li>How to make tabs look like browser tabs<\/li>\n<\/ul>\n<p dir=\"auto\">Just tell me which part you want explained slowly with full examples \u2014 I\u2019m here for you. \ud83d\ude0a<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How to create tabs (also called tabbed interfaces, tab navigation, tab panels, etc.) from the very beginning. What are Tabs? Tabs are a very common UI pattern that lets users switch between different sections&#46;&#46;&#46;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[77],"tags":[],"class_list":["post-2775","post","type-post","status-publish","format-standard","hentry","category-how-to"],"_links":{"self":[{"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/posts\/2775","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/comments?post=2775"}],"version-history":[{"count":1,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/posts\/2775\/revisions"}],"predecessor-version":[{"id":2776,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/posts\/2775\/revisions\/2776"}],"wp:attachment":[{"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/media?parent=2775"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/categories?post=2775"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demo.materiamedica.net\/demo6\/wp-json\/wp\/v2\/tags?post=2775"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}