webdevpuneet.com

Vertical Tabs — Side Tab Navigation Snippet

Vertical Tabs · Navigation · Plain HTML, CSS & JS · Live preview

CategoryNavigation

Copy & paste

Source Code

<div class="vtabs">
  <div class="vtab-list" role="tablist" aria-orientation="vertical">
    <button class="vtab active" role="tab" aria-selected="true"  aria-controls="p1" id="t1">
      <svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
      Overview
    </button>
    <button class="vtab" role="tab" aria-selected="false" aria-controls="p2" id="t2">
      <svg viewBox="0 0 24 24"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>
      Analytics
    </button>
    <button class="vtab" role="tab" aria-selected="false" aria-controls="p3" id="t3">
      <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2V22a2 2 0 0 1-4 0v-.1A1.7 1.7 0 0 0 6 20.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0-1.2-2.9H2a2 2 0 0 1 0-4h.1A1.7 1.7 0 0 0 3.7 6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H8.5A1.7 1.7 0 0 0 9.6 2.1V2a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 2.9 1.2l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9v.1a1.7 1.7 0 0 0 1.6 1H22a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
      Settings
    </button>
  </div>

  <div class="vtab-panels">
    <div class="vtab-panel active" role="tabpanel" id="p1" aria-labelledby="t1">
      <h3 class="vtab-title">Overview</h3>
      <p>A snapshot of your workspace — recent activity, quick stats, and shortcuts to everything you use most.</p>
    </div>
    <div class="vtab-panel" role="tabpanel" id="p2" aria-labelledby="t2" hidden>
      <h3 class="vtab-title">Analytics</h3>
      <p>Track growth over time with charts for traffic, conversions, and engagement, broken down by source.</p>
    </div>
    <div class="vtab-panel" role="tabpanel" id="p3" aria-labelledby="t3" hidden>
      <h3 class="vtab-title">Settings</h3>
      <p>Manage your profile, team members, billing, and notification preferences all in one place.</p>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Side rail of icon + label tabs with a flexible content panel area
Animated panel fade-and-slide on tab switch
Full WAI-ARIA tabs: role tablist/tab/tabpanel, aria-selected, aria-controls, aria-labelledby
aria-orientation="vertical" for correct screen-reader expectations
Roving tabindex — Tab in/out as one stop, arrow keys move between tabs
ArrowUp/ArrowDown navigation with wrap-around
Inactive panels use the hidden attribute (removed from a11y tree and tab order)
Responsive: collapses to a scrollable horizontal tab bar on mobile
Index-driven script works with any number of tabs
Export as HTML file, React JSX, or React + Tailwind CSS

About this UI Snippet

Vertical Tabs — Icon Side Rail, Animated Panels & ARIA Keyboard Navigation

Screenshot of the Vertical Tabs snippet rendered live

Vertical tabs are one of the most-searched navigation snippets because a side-by-side "rail of tabs on the left, content on the right" layout is the standard for settings pages, dashboards, account screens, and documentation — anywhere there are several sections and the horizontal tab bar would run out of room. This snippet is a complete, accessible vertical tab component: an icon side rail, animated content panels, full WAI-ARIA roles with arrow-key navigation, and a responsive layout that collapses to a horizontal scroll on mobile.

The two-column layout

The component is a flex row: a fixed-width .vtab-list rail on the left and a flexible .vtab-panels area on the right, wrapped in a bordered, rounded container. The rail holds the tab buttons stacked vertically; the panel area shows one panel at a time. Because the rail is flex-shrink: 0 and the panels are flex: 1, the rail keeps its width while the content fills the rest — the canonical settings-screen layout. Each tab has an icon plus a label, which is why vertical tabs scale better than horizontal ones: you can fit many labeled sections down the side without horizontal overflow.

Switching tabs and panels

A select(i) function is the single source of truth. It loops the tabs, marking the chosen one .active and setting aria-selected, and loops the panels, showing the matching one and hiding the rest with the hidden attribute. Using hidden (not just CSS) means inactive panels are removed from the accessibility tree and the tab order entirely, which is correct — a hidden tab panel should not be reachable. Each panel fades and slides in with a short @keyframes fade animation when it becomes active, so switching sections feels smooth rather than an instant swap.

Full WAI-ARIA tabs semantics

This is built to the ARIA Authoring Practices tabs pattern, which most "vertical tabs" tutorials skip. The rail is role="tablist" with aria-orientation="vertical" (telling assistive tech the tabs are arranged vertically, so it expects up/down keys). Each button is role="tab" with aria-selected and an aria-controls pointing at its panel; each panel is role="tabpanel" with aria-labelledby pointing back at its tab. This bidirectional wiring lets screen readers announce "tab, 1 of 3, selected" and associate each panel with its tab. It is the difference between a div soup that looks like tabs and a component that actually behaves like tabs for assistive tech.

Roving tabindex and arrow-key navigation

Proper tabs use a "roving tabindex": only the active tab is in the page tab order (tabIndex = 0), while the others are tabIndex = -1. So pressing Tab moves *into and out of* the tablist as a single stop, and you move *between* tabs with the arrow keys — exactly how native tab widgets work and what keyboard users expect. The keydown handler intercepts ArrowDown/ArrowUp (the correct keys for vertical orientation), wraps around at the ends, selects the new tab, and moves focus to it. This roving-tabindex-plus-arrow-keys behavior is the hallmark of a correctly implemented tab component and is implemented here in a few lines.

Responsive: vertical on desktop, horizontal on mobile

Vertical tabs do not fit a narrow phone screen, so a @media (max-width: 480px) query flips the layout: the container becomes a column, and the rail becomes a horizontal, scrollable strip along the top (flex-direction: row; overflow-x: auto) with a bottom border instead of a right one. The panels then sit below. This gives you the best of both — a spacious side rail on desktop and a familiar horizontal tab bar on mobile — without duplicating any markup.

Customizing the tabs

Re-theme by changing the active tab color, the hover background, and the rail background. Add or remove tabs by adding a <button role="tab"> and a matching <div role="tabpanel"> with paired id/aria-controls/aria-labelledby values; the script picks up any number of tabs automatically. Swap the icons for your own. Widen the rail for longer labels, or hide the labels and show only icons on smaller screens. To remember the selected tab across reloads, save the active index to localStorage in select() and restore it on load. Because the component is index-driven, all of these are small changes.

Accessibility checklist

Keep the role="tablist"/role="tab"/role="tabpanel" triad and the aria-controls/aria-labelledby links intact — they are what make it a real tab widget. Preserve the roving tabindex so keyboard users Tab into the group once and arrow between tabs. Maintain strong contrast for the active tab so its selected state is clear, and ensure the icons are decorative (the text label provides the name). Because inactive panels use the hidden attribute, they are correctly excluded from screen readers and keyboard focus. With these in place, the component is usable by mouse, keyboard, and assistive tech alike.

Step by step

How to Use

  1. 1
    Copy the structureCopy the .vtabs container with its .vtab-list of role="tab" buttons and the .vtab-panels of role="tabpanel" divs. Keep the id / aria-controls / aria-labelledby links paired.
  2. 2
    Edit tabs and contentChange the tab labels and icons, and the heading and text inside each panel. The script handles any number of tab/panel pairs.
  3. 3
    Keep the ARIA wiringEach tab's aria-controls must match its panel id, and each panel's aria-labelledby must match its tab id. This makes it a real accessible tab widget.
  4. 4
    Re-theme itUpdate the active tab color, hover background, and rail background to match your design.
  5. 5
    Remember the active tab (optional)Save the selected index to localStorage in select() and restore it on load so the last tab reopens.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Settings and account pages
The classic use: a left rail of sections (Profile, Billing, Notifications) with the chosen panel on the right.
Dashboard section switcher
Switch between Overview, Analytics, and Reports in an admin panel without leaving the page or losing context.
Documentation and help centers
Many labeled topics fit down a side rail far better than a crowded horizontal tab bar.
Learn the ARIA tabs pattern
See the full tablist/tab/tabpanel wiring, roving tabindex, and arrow-key navigation that most tab tutorials leave out.
Responsive tab layouts
Get a spacious vertical rail on desktop that automatically becomes a familiar horizontal tab bar on phones.
Keyboard-accessible tabs
Tab into the group once, arrow between tabs, with panels properly hidden — a correctly accessible tab widget.

Got questions?

Frequently Asked Questions

Functionally they are the same WAI-ARIA tabs pattern; only the layout and key bindings change. Vertical tabs stack down a side rail (using aria-orientation="vertical" and Up/Down arrow keys) and suit settings pages with many labeled sections that would overflow a horizontal bar.

Tabs use a roving tabindex: only the active tab has tabIndex 0, the rest -1, so Tab moves into and out of the whole tablist as one stop. You move between tabs with the arrow keys (Up/Down for vertical), which select the tab and move focus to it, wrapping at the ends.

The hidden attribute removes inactive panels from the accessibility tree and the tab order, which is correct — a hidden tab panel should not be reachable by keyboard or announced by screen readers. CSS display:none also hides them, but using the attribute keeps the intent explicit and the ARIA state consistent.

Add a <button role="tab"> to the rail and a matching <div role="tabpanel"> to the panels, with paired id, aria-controls, and aria-labelledby values. The index-driven script automatically includes any number of tab/panel pairs.

A media query at 480px flips the container to a column and turns the side rail into a horizontal, scrollable strip along the top with a bottom border. The panels sit below, giving a familiar horizontal tab bar on small screens with no extra markup.

Yes. Click "JSX" for a React component or "Tailwind" for a React + Tailwind version. In React, hold the active index in useState, render tabs and panels from an array with the ARIA attributes, and replicate the arrow-key handler; the structure maps directly.

Yes. While the Vertical Tabs snippet gives you one pattern to copy, the free Responsive Navbar Builder at webdevpuneet.com/navbar-builder/ designs a whole navigation bar: logo, links and dropdown menus, a mobile hamburger menu (dropdown panel, slide-in drawer or full screen) and a scroll effect (sticky, shrink, hide on scroll or transparent). It has six presets, a live preview that runs the real code, and exports HTML + CSS + vanilla JS, React, Tailwind, Vue 3 or Angular — or you can click Fork & Edit to keep editing it in My Code.