Open demo

CSS code

HTML
<div class="demo"><span class="lbl">Growing underline</span>
<nav aria-label="Project">
  <ul class="ind ind-u">
    <li><a href="#" aria-current="page">Overview</a></li>
    <li><a href="#">Activity</a></li>
    <li><a href="#">Settings</a></li>
    <li><a href="#">Members</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Sliding pill</span>
<nav aria-label="Report">
  <ul class="ind ind-pill">
    <li><a href="#" aria-current="page">Overview</a></li>
    <li><a href="#">Activity</a></li>
    <li><a href="#">Settings</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Top bar</span>
<nav aria-label="Workspace">
  <ul class="ind ind-bar">
    <li><a href="#" aria-current="page">Overview</a></li>
    <li><a href="#">Activity</a></li>
    <li><a href="#">Settings</a></li>
    <li><a href="#">Members</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Dot marker</span>
<nav aria-label="Profile">
  <ul class="ind ind-plain ind-dot">
    <li><a href="#" aria-current="page">Overview</a></li>
    <li><a href="#">Activity</a></li>
    <li><a href="#">Settings</a></li>
    <li><a href="#">Members</a></li>
  </ul>
</nav></div>
CSS
/* Link bars with a current-page indicator. The current link carries aria-current="page"; each variant draws its marker from that. */
.ind{--ind-accent:#4f46e5;--ind-ink:#101828;--ind-muted:#667085;--ind-ease:cubic-bezier(.4,0,.2,1);
  position:relative;display:flex;align-items:center;gap:4px;margin:0;padding:6px 10px;list-style:none;
  background:#fff;border:1px solid #e7e9f0;border-radius:13px;font-size:.9rem}
.ind li{display:flex}
.ind a{position:relative;z-index:1;padding:11px 14px;border-radius:9px;font-weight:600;color:var(--ind-muted);
  text-decoration:none;transition:color .18s}
.ind a:hover,.ind a[aria-current]{color:var(--ind-ink)}
.ind a:focus-visible{outline:2px solid var(--ind-accent);outline-offset:2px}
.ind-plain{padding:0;gap:26px;background:none;border:0}
/* growing underline */
.ind-u a::after{content:"";position:absolute;left:14px;right:14px;bottom:4px;height:2px;background:var(--ind-accent);
  transform:scaleX(0);transition:transform .25s var(--ind-ease)}
.ind-u a:hover::after{transform:scaleX(.6)}
.ind-u a[aria-current]::after{transform:scaleX(1)}
/* sliding pill: the list's ::before is moved under the current link by the script (--x, --y, --w, --h) */
.ind-pill::before{content:"";position:absolute;left:0;top:0;width:var(--w,0);height:var(--h,0);border-radius:9px;
  background:var(--ind-accent);transform:translate(var(--x,0),var(--y,0));
  transition:transform .3s var(--ind-ease),width .3s var(--ind-ease),height .3s var(--ind-ease)}
.ind-pill a[aria-current],.ind-pill a[aria-current]:hover{color:#fff}
.ind-pill:not([data-ready]) a[aria-current]{background:var(--ind-accent)} /* until the script has measured */
/* top bar */
.ind-bar a::before{content:"";position:absolute;left:10px;right:10px;top:0;height:3px;border-radius:0 0 3px 3px;
  background:var(--ind-accent);transform:scaleX(0);transition:transform .25s}
.ind-bar a[aria-current]::before{transform:scaleX(1)}
/* dot marker */
.ind-dot a[aria-current]::after{content:"";position:absolute;left:50%;bottom:1px;width:5px;height:5px;margin-left:-2.5px;
  border-radius:50%;background:var(--ind-accent)}
@media (max-width:480px){.ind{flex-wrap:wrap;justify-content:center}}
@media (prefers-reduced-motion:reduce){.ind a,.ind a::before,.ind a::after,.ind-pill::before{transition:none}}
JavaScript
// Sliding pill: measure the link with aria-current and move the list's ::before under it.
// It re-measures when the list resizes (wrapping, fonts) and when aria-current moves (client-side routing).
document.querySelectorAll('.ind-pill').forEach(list => {
  const place = () => {
    const a = list.querySelector('a[aria-current]');
    if (!a) return;
    list.style.setProperty('--x', a.offsetLeft + 'px');
    list.style.setProperty('--y', a.offsetTop + 'px');
    list.style.setProperty('--w', a.offsetWidth + 'px');
    list.style.setProperty('--h', a.offsetHeight + 'px');
    list.dataset.ready = '';
  };
  place();
  new ResizeObserver(place).observe(list);
  new MutationObserver(place).observe(list, { subtree: true, attributeFilter: ['aria-current'] });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="demo"><span class="lbl">Growing underline</span>
<nav aria-label="Project">
  <ul class="nav ind ind-u">
    <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">Overview</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Activity</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Settings</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Members</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Sliding pill</span>
<nav aria-label="Report">
  <ul class="nav nav-pills ind ind-pill">
    <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">Overview</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Activity</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Settings</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Top bar</span>
<nav aria-label="Workspace">
  <ul class="nav ind ind-bar">
    <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">Overview</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Activity</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Settings</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Members</a></li>
  </ul>
</nav></div>

<div class="demo"><span class="lbl">Dot marker</span>
<nav aria-label="Profile">
  <ul class="nav ind ind-plain ind-dot">
    <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">Overview</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Activity</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Settings</a></li>
    <li class="nav-item"><a class="nav-link" href="#">Members</a></li>
  </ul>
</nav></div>
CSS
/* Bootstrap .nav, recoloured through its --bs-nav-* variables. The current link has .active and aria-current="page";
   each variant draws its marker from aria-current. */
.ind{--ind-accent:#4f46e5;--ind-ease:cubic-bezier(.4,0,.2,1);
  --bs-nav-link-padding-x:14px;--bs-nav-link-padding-y:11px;--bs-nav-link-font-size:.9rem;--bs-nav-link-font-weight:600;
  --bs-nav-link-color:#667085;--bs-nav-link-hover-color:#101828;
  position:relative;flex-wrap:nowrap;align-items:center;gap:4px;padding:6px 10px;background:#fff;border:1px solid #e7e9f0;border-radius:13px}
.ind .nav-link{position:relative;z-index:1;border-radius:9px;line-height:normal;transition:color .18s}
.ind .nav-link.active{color:#101828}
.ind-pill .nav-link.active,.ind-pill .nav-link.active:hover{color:#fff}
.ind .nav-link:focus-visible{outline:2px solid var(--ind-accent);outline-offset:2px;box-shadow:none}
.ind-plain{padding:0;gap:26px;background:none;border:0}
/* growing underline */
.ind-u .nav-link::after{content:"";position:absolute;left:14px;right:14px;bottom:4px;height:2px;background:var(--ind-accent);
  transform:scaleX(0);transition:transform .25s var(--ind-ease)}
.ind-u .nav-link:hover::after{transform:scaleX(.6)}
.ind-u .nav-link.active::after{transform:scaleX(1)}
/* sliding pill: .nav-pills paints .active until the script has measured, then the list's ::before slides under it */
.ind-pill{--bs-nav-pills-border-radius:9px;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:var(--ind-accent)}
.ind-pill[data-ready]{--bs-nav-pills-link-active-bg:transparent}
.ind-pill::before{content:"";position:absolute;left:0;top:0;width:var(--w,0);height:var(--h,0);border-radius:9px;
  background:var(--ind-accent);transform:translate(var(--x,0),var(--y,0));
  transition:transform .3s var(--ind-ease),width .3s var(--ind-ease),height .3s var(--ind-ease)}
/* top bar */
.ind-bar .nav-link::before{content:"";position:absolute;left:10px;right:10px;top:0;height:3px;border-radius:0 0 3px 3px;
  background:var(--ind-accent);transform:scaleX(0);transition:transform .25s}
.ind-bar .nav-link.active::before{transform:scaleX(1)}
/* dot marker */
.ind-dot .nav-link.active::after{content:"";position:absolute;left:50%;bottom:1px;width:5px;height:5px;margin-left:-2.5px;
  border-radius:50%;background:var(--ind-accent)}
@media (max-width:480px){.ind{flex-wrap:wrap;justify-content:center}}
@media (prefers-reduced-motion:reduce){.ind .nav-link,.ind .nav-link::before,.ind .nav-link::after,.ind-pill::before{transition:none}}
JavaScript
// Sliding pill: measure the .active link and move the list's ::before under it.
// It re-measures when the list resizes and when .active moves (client-side routing).
document.querySelectorAll('.ind-pill').forEach(list => {
  const place = () => {
    const a = list.querySelector('.nav-link.active');
    if (!a) return;
    list.style.setProperty('--x', a.offsetLeft + 'px');
    list.style.setProperty('--y', a.offsetTop + 'px');
    list.style.setProperty('--w', a.offsetWidth + 'px');
    list.style.setProperty('--h', a.offsetHeight + 'px');
    list.dataset.ready = '';
  };
  place();
  new ResizeObserver(place).observe(list);
  new MutationObserver(place).observe(list, { subtree: true, attributeFilter: ['class', 'aria-current'] });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="flex max-w-full flex-col items-center gap-3"><span class="text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase">Growing underline</span>
<nav aria-label="Project">
  <ul class="flex items-center gap-1 rounded-[13px] border border-[#e7e9f0] bg-white px-2.5 py-1.5 text-[.9rem] max-[480px]:flex-wrap max-[480px]:justify-center">
    <li class="flex"><a href="#" aria-current="page" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] after:absolute after:inset-x-3.5 after:bottom-1 after:h-0.5 after:scale-x-0 after:bg-[#4f46e5] after:transition-transform after:duration-[250ms] hover:text-[#101828] hover:after:scale-x-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:scale-x-100 motion-reduce:transition-none motion-reduce:after:transition-none">Overview</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] after:absolute after:inset-x-3.5 after:bottom-1 after:h-0.5 after:scale-x-0 after:bg-[#4f46e5] after:transition-transform after:duration-[250ms] hover:text-[#101828] hover:after:scale-x-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:scale-x-100 motion-reduce:transition-none motion-reduce:after:transition-none">Activity</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] after:absolute after:inset-x-3.5 after:bottom-1 after:h-0.5 after:scale-x-0 after:bg-[#4f46e5] after:transition-transform after:duration-[250ms] hover:text-[#101828] hover:after:scale-x-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:scale-x-100 motion-reduce:transition-none motion-reduce:after:transition-none">Settings</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] after:absolute after:inset-x-3.5 after:bottom-1 after:h-0.5 after:scale-x-0 after:bg-[#4f46e5] after:transition-transform after:duration-[250ms] hover:text-[#101828] hover:after:scale-x-60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:scale-x-100 motion-reduce:transition-none motion-reduce:after:transition-none">Members</a></li>
  </ul>
</nav></div>

<div class="flex max-w-full flex-col items-center gap-3"><span class="text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase">Sliding pill</span>
<nav aria-label="Report">
  <ul data-pill class="relative flex items-center gap-1 rounded-[13px] border border-[#e7e9f0] bg-white px-2.5 py-1.5 text-[.9rem] before:absolute before:top-0 before:left-0 before:h-[var(--h,0px)] before:w-[var(--w,0px)] before:translate-x-[var(--x,0px)] before:translate-y-[var(--y,0px)] before:rounded-[9px] before:bg-[#4f46e5] before:transition-[translate,width,height] before:duration-300 before:ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:before:transition-none max-[480px]:flex-wrap max-[480px]:justify-center [&:not([data-ready])_a[aria-current]]:bg-[#4f46e5]">
    <li class="flex"><a href="#" aria-current="page" class="relative z-10 rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-white motion-reduce:transition-none">Overview</a></li>
    <li class="flex"><a href="#" class="relative z-10 rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-white motion-reduce:transition-none">Activity</a></li>
    <li class="flex"><a href="#" class="relative z-10 rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-white motion-reduce:transition-none">Settings</a></li>
  </ul>
</nav></div>

<div class="flex max-w-full flex-col items-center gap-3"><span class="text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase">Top bar</span>
<nav aria-label="Workspace">
  <ul class="flex items-center gap-1 rounded-[13px] border border-[#e7e9f0] bg-white px-2.5 py-1.5 text-[.9rem] max-[480px]:flex-wrap max-[480px]:justify-center">
    <li class="flex"><a href="#" aria-current="page" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] before:absolute before:inset-x-2.5 before:top-0 before:h-[3px] before:scale-x-0 before:rounded-b-[3px] before:bg-[#4f46e5] before:transition-transform before:duration-[250ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:before:scale-x-100 motion-reduce:transition-none motion-reduce:before:transition-none">Overview</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] before:absolute before:inset-x-2.5 before:top-0 before:h-[3px] before:scale-x-0 before:rounded-b-[3px] before:bg-[#4f46e5] before:transition-transform before:duration-[250ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:before:scale-x-100 motion-reduce:transition-none motion-reduce:before:transition-none">Activity</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] before:absolute before:inset-x-2.5 before:top-0 before:h-[3px] before:scale-x-0 before:rounded-b-[3px] before:bg-[#4f46e5] before:transition-transform before:duration-[250ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:before:scale-x-100 motion-reduce:transition-none motion-reduce:before:transition-none">Settings</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] before:absolute before:inset-x-2.5 before:top-0 before:h-[3px] before:scale-x-0 before:rounded-b-[3px] before:bg-[#4f46e5] before:transition-transform before:duration-[250ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:before:scale-x-100 motion-reduce:transition-none motion-reduce:before:transition-none">Members</a></li>
  </ul>
</nav></div>

<div class="flex max-w-full flex-col items-center gap-3"><span class="text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase">Dot marker</span>
<nav aria-label="Profile">
  <ul class="flex items-center gap-[26px] text-[.9rem] max-[480px]:flex-wrap max-[480px]:justify-center">
    <li class="flex"><a href="#" aria-current="page" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:absolute aria-[current=page]:after:bottom-px aria-[current=page]:after:left-1/2 aria-[current=page]:after:size-[5px] aria-[current=page]:after:-translate-x-1/2 aria-[current=page]:after:rounded-full aria-[current=page]:after:bg-[#4f46e5] motion-reduce:transition-none">Overview</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:absolute aria-[current=page]:after:bottom-px aria-[current=page]:after:left-1/2 aria-[current=page]:after:size-[5px] aria-[current=page]:after:-translate-x-1/2 aria-[current=page]:after:rounded-full aria-[current=page]:after:bg-[#4f46e5] motion-reduce:transition-none">Activity</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:absolute aria-[current=page]:after:bottom-px aria-[current=page]:after:left-1/2 aria-[current=page]:after:size-[5px] aria-[current=page]:after:-translate-x-1/2 aria-[current=page]:after:rounded-full aria-[current=page]:after:bg-[#4f46e5] motion-reduce:transition-none">Settings</a></li>
    <li class="flex"><a href="#" class="relative rounded-[9px] px-3.5 py-[11px] font-semibold text-[#667085] transition-[color,background-color,border-color] duration-[180ms] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:text-[#101828] aria-[current=page]:after:absolute aria-[current=page]:after:bottom-px aria-[current=page]:after:left-1/2 aria-[current=page]:after:size-[5px] aria-[current=page]:after:-translate-x-1/2 aria-[current=page]:after:rounded-full aria-[current=page]:after:bg-[#4f46e5] motion-reduce:transition-none">Members</a></li>
  </ul>
</nav></div>
JavaScript
// Sliding pill: measure the link with aria-current and move the list's ::before under it.
// It re-measures when the list resizes and when aria-current moves (client-side routing).
document.querySelectorAll('[data-pill]').forEach(list => {
  const place = () => {
    const a = list.querySelector('a[aria-current]');
    if (!a) return;
    list.style.setProperty('--x', a.offsetLeft + 'px');
    list.style.setProperty('--y', a.offsetTop + 'px');
    list.style.setProperty('--w', a.offsetWidth + 'px');
    list.style.setProperty('--h', a.offsetHeight + 'px');
    list.dataset.ready = '';
  };
  place();
  new ResizeObserver(place).observe(list);
  new MutationObserver(place).observe(list, { subtree: true, attributeFilter: ['aria-current'] });
});

About this set

Four horizontal link bars that each mark the current page a different way. Growing underline scales a 2px bar out from the centre of the current link, and to 60% on hover. Sliding pill puts a filled indigo pill behind the current link and slides it to the next one when the current page changes. Top bar hangs a 3px tab from the top edge of the current link. Dot marker is a borderless row with a small dot under the current link. Use the underline or top bar for section tabs on a project or settings page, the pill for a compact app switcher, and the dot for a quiet marketing header. Every indicator is drawn with ::before or ::after from aria-current=”page”, so the state exposed to screen readers is the state people see. The pill is the only variant with script: it measures the current link, writes its offset and size to four custom properties on the list, and re-measures through a ResizeObserver and a MutationObserver when the row wraps or aria-current moves. Until the script runs, the current link gets the pill colour directly. Rows wrap and centre under 480px.

What’s included

  • Four indicators: growing underline, sliding pill, top bar and dot
  • Current page drawn from aria-current="page" in every variant
  • Sliding pill re-measures itself when the row wraps or the current page changes
  • Pill falls back to a highlighted link before the script runs
  • Bootstrap version uses .nav, .nav-link and .nav-pills

Accessibility

Each bar is a list of links inside a nav with its own aria-label, and the current link carries aria-current="page", which is also what draws the indicator. Links show a 2px indigo :focus-visible outline. The indicator transitions switch off under prefers-reduced-motion.

Customise it

Change --ind-accent, --ind-ink and --ind-muted on .ind for colour; the padding on .ind a sets the size. In Bootstrap the link colours and padding are the --bs-nav-link-* variables on .ind; in Tailwind edit the #4f46e5 and #667085 values in the after:, before: and aria-[current=page]: utilities.