Open demo

CSS code

HTML
<div class="w">
  <div class="rail" role="radiogroup" aria-label="Section">
    <input type="radio" name="r" id="r1" checked><label for="r1"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="7" rx="2"/><rect x="13" y="4" width="7" height="7" rx="2"/><rect x="4" y="13" width="7" height="7" rx="2"/><rect x="13" y="13" width="7" height="7" rx="2"/></svg><span><i>Dashboard</i></span></label>
    <input type="radio" name="r" id="r2"><label for="r2"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h10"/></svg><span><i>Projects</i></span></label>
    <input type="radio" name="r" id="r3"><label for="r3"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5 20c0-3.4 3-5.6 7-5.6s7 2.2 7 5.6"/></svg><span><i>Team</i></span></label>
    <input type="radio" name="r" id="r4"><label for="r4"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5M4 19h16M8 15l3-4 3 3 4-6"/></svg><span><i>Analytics</i></span></label>
  </div>
  <div class="body">
    <div class="p p1"><h3>Dashboard</h3><p>Hover the rail — it widens and the labels unroll from a grid column going <code>0fr</code> to <code>1fr</code>.</p></div>
    <div class="p p2"><h3>Projects</h3><p>Every board in the workspace, with the ones you follow first.</p></div>
    <div class="p p3"><h3>Team</h3><p>Twelve people. Tabbing into the rail expands it too, so this is reachable without a mouse.</p></div>
    <div class="p p4"><h3>Analytics</h3><p>Weekly actives, retention and the funnel from signup to first project.</p></div>
  </div>
</div>
CSS
/* Icon rail tabs, CSS only: a vertical radio group that shows icons and widens on hover or keyboard focus,
   unrolling each label from a 0fr to a 1fr grid column. :has() shows the panel for the checked radio. */
.w{width:100%;max-width:580px;display:flex;overflow:hidden;min-height:230px;background:#fff;border:1px solid #e3e8f2;
  border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.rail{background:#151b2e;padding:14px 12px;display:flex;flex-direction:column;gap:4px;position:relative;
  transition:width .25s cubic-bezier(.4,0,.2,1);width:60px;overflow:hidden;flex-shrink:0}
.w:hover .rail,.rail:focus-within{width:184px}
.rail input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.rail label{display:flex;align-items:center;padding:10px;border-radius:9px;cursor:pointer;
  color:#9aa5c0;white-space:nowrap;transition:background-color .15s,color .15s}
.rail label:hover{background:rgba(255,255,255,.08);color:#fff}
.rail input:checked+label{background:rgba(99,102,241,.28);color:#fff}
.rail input:focus-visible+label{outline:2px solid #a5b4fc;outline-offset:-2px}
.rail svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;flex-shrink:0}
.rail label span{display:grid;grid-template-columns:0fr;font-size:.86rem;font-weight:600;
  transition:grid-template-columns .25s cubic-bezier(.4,0,.2,1),padding-left .25s}
.rail label span i{overflow:hidden;font-style:normal}
.w:hover .rail label span,.rail:focus-within label span{grid-template-columns:1fr;padding-left:12px}
.body{flex:1;padding:24px 26px}
.body .p{display:none;font-size:.9rem;line-height:1.7;color:#4a5670}
.body h3{font-size:.96rem;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.w:has(#r1:checked) .p1,.w:has(#r2:checked) .p2,.w:has(#r3:checked) .p3,.w:has(#r4:checked) .p4{display:block}
@media (prefers-reduced-motion:reduce){.rail,.rail label,.rail label span{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="w">
  <div class="nav nav-pills flex-column rail" role="tablist" aria-label="Section" aria-orientation="vertical">
    <button class="nav-link active" id="rail-dashboard-tab" data-bs-toggle="pill" data-bs-target="#rail-dashboard" type="button" role="tab" aria-controls="rail-dashboard" aria-selected="true"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="7" rx="2"/><rect x="13" y="4" width="7" height="7" rx="2"/><rect x="4" y="13" width="7" height="7" rx="2"/><rect x="13" y="13" width="7" height="7" rx="2"/></svg><span><i>Dashboard</i></span></button>
    <button class="nav-link" id="rail-projects-tab" data-bs-toggle="pill" data-bs-target="#rail-projects" type="button" role="tab" aria-controls="rail-projects" aria-selected="false" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h10"/></svg><span><i>Projects</i></span></button>
    <button class="nav-link" id="rail-team-tab" data-bs-toggle="pill" data-bs-target="#rail-team" type="button" role="tab" aria-controls="rail-team" aria-selected="false" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5 20c0-3.4 3-5.6 7-5.6s7 2.2 7 5.6"/></svg><span><i>Team</i></span></button>
    <button class="nav-link" id="rail-analytics-tab" data-bs-toggle="pill" data-bs-target="#rail-analytics" type="button" role="tab" aria-controls="rail-analytics" aria-selected="false" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5M4 19h16M8 15l3-4 3 3 4-6"/></svg><span><i>Analytics</i></span></button>
  </div>
  <div class="tab-content body">
    <div class="tab-pane active" id="rail-dashboard" role="tabpanel" aria-labelledby="rail-dashboard-tab" tabindex="0"><h3>Dashboard</h3><p>Hover the rail — it widens and the labels unroll from a grid column going <code>0fr</code> to <code>1fr</code>.</p></div>
    <div class="tab-pane" id="rail-projects" role="tabpanel" aria-labelledby="rail-projects-tab" tabindex="0"><h3>Projects</h3><p>Every board in the workspace, with the ones you follow first.</p></div>
    <div class="tab-pane" id="rail-team" role="tabpanel" aria-labelledby="rail-team-tab" tabindex="0"><h3>Team</h3><p>Twelve people. Tabbing into the rail expands it too, so this is reachable without a mouse.</p></div>
    <div class="tab-pane" id="rail-analytics" role="tabpanel" aria-labelledby="rail-analytics-tab" tabindex="0"><h3>Analytics</h3><p>Weekly actives, retention and the funnel from signup to first project.</p></div>
  </div>
</div>
CSS
/* Icon rail tabs: a vertical Bootstrap .nav-pills (.flex-column) driven by the Tab plugin. The rail shows icons
   and widens on hover or keyboard focus, unrolling each label from a 0fr to a 1fr grid column. */
.w{width:100%;max-width:580px;display:flex;overflow:hidden;min-height:230px;background:#fff;border:1px solid #e3e8f2;
  border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.rail{--bs-nav-link-padding-x:10px;--bs-nav-link-padding-y:10px;--bs-nav-link-color:#9aa5c0;--bs-nav-link-hover-color:#fff;
  --bs-nav-pills-border-radius:9px;--bs-nav-pills-link-active-bg:rgba(99,102,241,.28);--bs-nav-pills-link-active-color:#fff;
  background:#151b2e;padding:14px 12px;gap:4px;flex-wrap:nowrap;flex-shrink:0;width:60px;overflow:hidden;
  transition:width .25s cubic-bezier(.4,0,.2,1)}
.w:hover .rail,.rail:focus-within{width:184px}
.rail .nav-link{display:flex;align-items:center;white-space:nowrap;text-align:left}
.rail .nav-link:not(.active):hover{background:rgba(255,255,255,.08)}
.rail .nav-link:focus-visible{box-shadow:none;outline:2px solid #a5b4fc;outline-offset:-2px}
.rail svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;flex-shrink:0}
.rail .nav-link span{display:grid;grid-template-columns:0fr;font-size:.86rem;font-weight:600;
  transition:grid-template-columns .25s cubic-bezier(.4,0,.2,1),padding-left .25s}
.rail .nav-link span i{overflow:hidden;font-style:normal}
.w:hover .rail .nav-link span,.rail:focus-within .nav-link span{grid-template-columns:1fr;padding-left:12px}
.body{flex:1;padding:24px 26px}
.body .tab-pane{font-size:.9rem;line-height:1.7;color:#4a5670}
.body .tab-pane:focus-visible{outline:2px solid #4f46e5;outline-offset:4px;border-radius:4px}
.body h3{font-size:.96rem;font-weight:700;line-height:inherit;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.body code{color:inherit;font-size:.9em}
@media (prefers-reduced-motion:reduce){.rail,.rail .nav-link,.rail .nav-link span{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="group/card flex min-h-[230px] w-full max-w-[580px] overflow-hidden rounded-2xl border border-tab-line bg-white shadow-[0_16px_38px_-24px_rgba(16,24,48,.24)]">
  <div class="group/rail flex w-[60px] shrink-0 flex-col gap-1 overflow-hidden bg-rail px-3 py-3.5 transition-[width] duration-[250ms] ease-[cubic-bezier(.4,0,.2,1)] group-hover/card:w-[184px] focus-within:w-[184px] motion-reduce:transition-none" role="tablist" aria-label="Section" aria-orientation="vertical">
    <button type="button" role="tab" id="rail-dashboard-tab" aria-controls="rail-dashboard" aria-selected="true" class="flex cursor-pointer items-center rounded-[9px] p-2.5 whitespace-nowrap text-rail-muted transition-[background-color,color] duration-150 hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rail-focus aria-selected:bg-[rgba(99,102,241,.28)] aria-selected:text-white motion-reduce:transition-none"><svg class="size-[18px] shrink-0 fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="7" rx="2"/><rect x="13" y="4" width="7" height="7" rx="2"/><rect x="4" y="13" width="7" height="7" rx="2"/><rect x="13" y="13" width="7" height="7" rx="2"/></svg><span class="grid grid-cols-[0fr] text-[.86rem] font-semibold transition-[grid-template-columns,padding-left] duration-[250ms] ease-[cubic-bezier(.4,0,.2,1)] group-hover/card:grid-cols-[1fr] group-hover/card:pl-3 group-focus-within/rail:grid-cols-[1fr] group-focus-within/rail:pl-3 motion-reduce:transition-none"><i class="overflow-hidden not-italic">Dashboard</i></span></button>
    <button type="button" role="tab" id="rail-projects-tab" aria-controls="rail-projects" aria-selected="false" tabindex="-1" class="flex cursor-pointer items-center rounded-[9px] p-2.5 whitespace-nowrap text-rail-muted transition-[background-color,color] duration-150 hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rail-focus aria-selected:bg-[rgba(99,102,241,.28)] aria-selected:text-white motion-reduce:transition-none"><svg class="size-[18px] shrink-0 fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h10"/></svg><span class="grid grid-cols-[0fr] text-[.86rem] font-semibold transition-[grid-template-columns,padding-left] duration-[250ms] ease-[cubic-bezier(.4,0,.2,1)] group-hover/card:grid-cols-[1fr] group-hover/card:pl-3 group-focus-within/rail:grid-cols-[1fr] group-focus-within/rail:pl-3 motion-reduce:transition-none"><i class="overflow-hidden not-italic">Projects</i></span></button>
    <button type="button" role="tab" id="rail-team-tab" aria-controls="rail-team" aria-selected="false" tabindex="-1" class="flex cursor-pointer items-center rounded-[9px] p-2.5 whitespace-nowrap text-rail-muted transition-[background-color,color] duration-150 hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rail-focus aria-selected:bg-[rgba(99,102,241,.28)] aria-selected:text-white motion-reduce:transition-none"><svg class="size-[18px] shrink-0 fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5 20c0-3.4 3-5.6 7-5.6s7 2.2 7 5.6"/></svg><span class="grid grid-cols-[0fr] text-[.86rem] font-semibold transition-[grid-template-columns,padding-left] duration-[250ms] ease-[cubic-bezier(.4,0,.2,1)] group-hover/card:grid-cols-[1fr] group-hover/card:pl-3 group-focus-within/rail:grid-cols-[1fr] group-focus-within/rail:pl-3 motion-reduce:transition-none"><i class="overflow-hidden not-italic">Team</i></span></button>
    <button type="button" role="tab" id="rail-analytics-tab" aria-controls="rail-analytics" aria-selected="false" tabindex="-1" class="flex cursor-pointer items-center rounded-[9px] p-2.5 whitespace-nowrap text-rail-muted transition-[background-color,color] duration-150 hover:bg-white/8 hover:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rail-focus aria-selected:bg-[rgba(99,102,241,.28)] aria-selected:text-white motion-reduce:transition-none"><svg class="size-[18px] shrink-0 fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5M4 19h16M8 15l3-4 3 3 4-6"/></svg><span class="grid grid-cols-[0fr] text-[.86rem] font-semibold transition-[grid-template-columns,padding-left] duration-[250ms] ease-[cubic-bezier(.4,0,.2,1)] group-hover/card:grid-cols-[1fr] group-hover/card:pl-3 group-focus-within/rail:grid-cols-[1fr] group-focus-within/rail:pl-3 motion-reduce:transition-none"><i class="overflow-hidden not-italic">Analytics</i></span></button>
  </div>
  <div class="flex-1 px-[26px] py-6 text-[.9rem] leading-[1.7] text-tab-body">
    <div id="rail-dashboard" role="tabpanel" aria-labelledby="rail-dashboard-tab" tabindex="0" class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.96rem] font-bold text-tab-ink">Dashboard</h3><p>Hover the rail — it widens and the labels unroll from a grid column going <code>0fr</code> to <code>1fr</code>.</p></div>
    <div id="rail-projects" role="tabpanel" aria-labelledby="rail-projects-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.96rem] font-bold text-tab-ink">Projects</h3><p>Every board in the workspace, with the ones you follow first.</p></div>
    <div id="rail-team" role="tabpanel" aria-labelledby="rail-team-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.96rem] font-bold text-tab-ink">Team</h3><p>Twelve people. Tabbing into the rail expands it too, so this is reachable without a mouse.</p></div>
    <div id="rail-analytics" role="tabpanel" aria-labelledby="rail-analytics-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.96rem] font-bold text-tab-ink">Analytics</h3><p>Weekly actives, retention and the funnel from signup to first project.</p></div>
  </div>
</div>
CSS
@theme {
  --color-tab-ink: #16203a;
  --color-tab-body: #4a5670;
  --color-tab-line: #e3e8f2;
  --color-tab-accent: #4f46e5;
  --color-rail: #151b2e;
  --color-rail-muted: #9aa5c0;
  --color-rail-focus: #a5b4fc;
}
JavaScript
// WAI-ARIA tabs: click or arrow keys select a tab, Home/End jump to the ends, only the selected tab is in the Tab order.
for (const list of document.querySelectorAll('[role=tablist]')) {
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = tab => tabs.forEach(t => {
    const on = t === tab;
    t.setAttribute('aria-selected', on);
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  });
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t && !t.disabled) select(t); });
  list.addEventListener('keydown', e => {
    const live = tabs.filter(t => !t.disabled), i = live.indexOf(e.target);
    const n = { ArrowLeft: i - 1, ArrowUp: i - 1, ArrowRight: i + 1, ArrowDown: i + 1, Home: 0, End: live.length - 1 }[e.key];
    if (n === undefined || i < 0) return;
    e.preventDefault();
    const t = live[(n + live.length) % live.length];
    t.focus();
    select(t);
  });
}

About this set

A dark vertical rail of icon tabs beside a content panel, for app shells, dashboards and settings screens where the navigation should take little room. It has four sections, Dashboard, Projects, Team and Analytics, each with a line icon. At rest the rail is 60 pixels wide and shows only icons; on hover, or as soon as keyboard focus enters it, it widens to 184 pixels and the labels unroll. The unrolling works by transitioning a one-column grid from 0fr to 1fr, so each label animates to its natural width without a fixed max-width. Because the rail also opens on focus-within, keyboard users see the labels as soon as they reach it. The label text is always in the page, only clipped, so screen readers get the names even when the rail is narrow. The plain CSS version is a radio group with :has(); the Bootstrap version is a vertical .nav-pills with .flex-column and aria-orientation set; the Tailwind version uses named groups and a small WAI-ARIA tabs script.

What’s included

  • 60px icon rail that widens to 184px on hover and focus
  • Labels unroll with a 0fr to 1fr grid transition
  • Labels stay available to screen readers while clipped
  • Light indigo focus outline that stands out on the dark rail
  • Bootstrap vertical .nav-pills with aria-orientation

Accessibility

In the plain CSS version the tabs are a radio group, not ARIA tabs: the radios sit inside an element with role="radiogroup" and an aria-label, each takes its name from its visible label, Tab enters the group and the arrow keys move the selection (radios have no Home or End). In the Bootstrap and Tailwind versions they follow the WAI-ARIA tabs pattern: role tablist, tab and tabpanel, aria-selected, aria-controls, a roving tabindex, arrow keys plus Home and End. The Bootstrap and Tailwind tablists set aria-orientation="vertical". Keyboard focus shows a 2px light indigo outline inside the dark rail, and the widening and unrolling happen instantly under prefers-reduced-motion.

Customise it

Change the rail colour #151b2e and the widths 60px and 184px in .rail (plain CSS and Bootstrap) or --color-rail and the w-[60px] and w-[184px] utilities (Tailwind). The selected background is rgba(99,102,241,.28).