Open demo

CSS code

HTML
<div class="wrap">
  <span class="lbl">Filter navigation</span>
  <nav class="fp-nav" aria-label="Categories">
    <ul class="fp">
      <li><a href="#" aria-current="page">All <span>248</span></a></li>
      <li><a href="#">Templates <span>96</span></a></li>
      <li><a href="#">Components <span>74</span></a></li>
      <li><a href="#">Dashboards <span>41</span></a></li>
      <li><a href="#">Landing pages <span>28</span></a></li>
      <li><a href="#">Emails <span>9</span></a></li>
    </ul>
  </nav>
</div>

<div class="wrap">
  <span class="lbl" id="chips-lbl">Active filters</span>
  <ul class="chips" aria-labelledby="chips-lbl">
    <li>Free <button type="button" aria-label="Remove filter: Free"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
    <li>Bootstrap 5 <button type="button" aria-label="Remove filter: Bootstrap 5"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
    <li>Dark mode <button type="button" aria-label="Remove filter: Dark mode"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
  </ul>
</div>
CSS
/* Filter pills: a scrollable row of links, the current one marked with aria-current="page". A gradient fades the overflowing edge. */
.fp-nav{--fp-bg:#f4f5f9;--fp-ink:#101828;--fp-focus:#4f46e5;position:relative}
.fp-nav::after{content:"";position:absolute;top:0;bottom:0;right:0;width:52px;pointer-events:none;
  background:linear-gradient(90deg,rgba(244,245,249,0),var(--fp-bg) 78%)}
.fp{display:flex;gap:9px;margin:-4px;padding:4px 4px 8px;list-style:none;overflow-x:auto;scrollbar-width:none}
.fp::-webkit-scrollbar{display:none}
.fp li{flex-shrink:0;display:flex}
.fp a{display:inline-flex;align-items:center;gap:8px;padding:9px 16px;border-radius:999px;font-size:.87rem;font-weight:600;
  color:#475467;background:#fff;border:1px solid #e4e7ec;text-decoration:none;white-space:nowrap;
  transition:background-color .15s,border-color .15s,color .15s}
.fp a:hover{border-color:#cbd2e0;color:var(--fp-ink)}
.fp a:focus-visible{outline:2px solid var(--fp-focus);outline-offset:2px}
.fp a[aria-current]{background:var(--fp-ink);border-color:var(--fp-ink);color:#fff}
.fp a span{font-size:.76rem;background:#f2f4f7;color:#475467;padding:1px 7px;border-radius:999px}
.fp a[aria-current] span{background:rgba(255,255,255,.2);color:#fff}
/* Removable chips: each chip's button removes it and moves focus to the next chip. */
.chips{--chip-ink:#3730a3;display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}
.chips:focus{outline:none}
.chips li{display:inline-flex;align-items:center;gap:8px;padding:7px 8px 7px 14px;border-radius:999px;font-size:.85rem;font-weight:600;
  background:#eef2ff;color:var(--chip-ink)}
.chips button{display:grid;place-items:center;width:19px;height:19px;padding:0;border:0;border-radius:50%;cursor:pointer;
  background:rgba(55,48,163,.12);color:var(--chip-ink);font:inherit}
.chips button:hover{background:rgba(55,48,163,.25)}
.chips button:focus-visible{outline:2px solid var(--fp-focus,#4f46e5);outline-offset:2px}
.chips svg{display:block;width:9px;height:9px;stroke:currentColor;fill:none;stroke-width:3}
@media (prefers-reduced-motion:reduce){.fp a{transition:none}}
JavaScript
// Remove a chip, then put focus on the next chip's button (or the previous one, or the list when none are left)
// so keyboard users don't lose their place.
document.querySelectorAll('.chips').forEach(list => list.addEventListener('click', e => {
  const btn = e.target.closest('button');
  if (!btn) return;
  const chip = btn.closest('li');
  const next = (chip.nextElementSibling || chip.previousElementSibling)?.querySelector('button');
  chip.remove();
  if (next) next.focus();
  else { list.tabIndex = -1; list.focus(); }
}));

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="wrap">
  <span class="lbl">Filter navigation</span>
  <nav class="fp-nav" aria-label="Categories">
    <ul class="nav nav-pills flex-nowrap overflow-x-auto fp">
      <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">All <span class="badge rounded-pill">248</span></a></li>
      <li class="nav-item"><a class="nav-link" href="#">Templates <span class="badge rounded-pill">96</span></a></li>
      <li class="nav-item"><a class="nav-link" href="#">Components <span class="badge rounded-pill">74</span></a></li>
      <li class="nav-item"><a class="nav-link" href="#">Dashboards <span class="badge rounded-pill">41</span></a></li>
      <li class="nav-item"><a class="nav-link" href="#">Landing pages <span class="badge rounded-pill">28</span></a></li>
      <li class="nav-item"><a class="nav-link" href="#">Emails <span class="badge rounded-pill">9</span></a></li>
    </ul>
  </nav>
</div>

<div class="wrap">
  <span class="lbl" id="chips-lbl">Active filters</span>
  <ul class="list-unstyled d-flex flex-wrap m-0 chips" aria-labelledby="chips-lbl">
    <li class="badge rounded-pill">Free <button type="button" class="btn-close" aria-label="Remove filter: Free"></button></li>
    <li class="badge rounded-pill">Bootstrap 5 <button type="button" class="btn-close" aria-label="Remove filter: Bootstrap 5"></button></li>
    <li class="badge rounded-pill">Dark mode <button type="button" class="btn-close" aria-label="Remove filter: Dark mode"></button></li>
  </ul>
</div>
CSS
/* Bootstrap .nav-pills in a scrollable row (.flex-nowrap + .overflow-x-auto), with .badge counts.
   The current link has .active and aria-current="page". A gradient fades the overflowing edge. */
.fp-nav{--fp-bg:#f4f5f9;--fp-ink:#101828;--fp-focus:#4f46e5;position:relative}
.fp-nav::after{content:"";position:absolute;top:0;bottom:0;right:0;width:52px;pointer-events:none;
  background:linear-gradient(90deg,rgba(244,245,249,0),var(--fp-bg) 78%)}
.fp{--bs-nav-pills-border-radius:999px;--bs-nav-pills-link-active-bg:var(--fp-ink);--bs-nav-pills-link-active-color:#fff;
  --bs-nav-link-padding-x:16px;--bs-nav-link-padding-y:9px;--bs-nav-link-font-size:.87rem;--bs-nav-link-font-weight:600;
  --bs-nav-link-color:#475467;--bs-nav-link-hover-color:var(--fp-ink);
  gap:9px;margin:-4px;padding:4px 4px 8px;scrollbar-width:none}
.fp::-webkit-scrollbar{display:none}
.fp .nav-item{flex-shrink:0}
.fp .nav-link{display:inline-flex;align-items:center;gap:8px;white-space:nowrap;line-height:normal;background:#fff;border:1px solid #e4e7ec}
.fp .nav-link:hover{border-color:#cbd2e0}
.fp .nav-link.active{border-color:var(--fp-ink)}
.fp .nav-link:focus-visible{outline:2px solid var(--fp-focus);outline-offset:2px;box-shadow:none}
.fp .badge{--bs-badge-font-size:.76rem;--bs-badge-font-weight:600;--bs-badge-padding-x:7px;--bs-badge-padding-y:1px;--bs-badge-color:#475467;
  line-height:normal;background:#f2f4f7}
.fp .active .badge{--bs-badge-color:#fff;background:rgba(255,255,255,.2)}
/* Removable chips: .badge.rounded-pill with a Bootstrap .btn-close, recoloured through --bs-btn-close-* variables. */
.chips{--chip-ink:#3730a3;gap:8px}
.chips:focus{outline:none}
.chips .badge{display:inline-flex;align-items:center;gap:8px;padding:7px 8px 7px 14px;font-size:.85rem;font-weight:600;line-height:normal;
  color:var(--chip-ink);background:#eef2ff}
.chips .btn-close{--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233730a3' stroke-width='3'%3e%3cpath d='M6 6l12 12M18 6L6 18'/%3e%3c/svg%3e");
  --bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:none;
  width:19px;height:19px;padding:0;border-radius:50%;background-color:rgba(55,48,163,.12);background-size:9px}
.chips .btn-close:hover{background-color:rgba(55,48,163,.25)}
.chips .btn-close:focus-visible{outline:2px solid var(--fp-focus,#4f46e5);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.fp .nav-link{transition:none}}
JavaScript
// Remove a chip, then put focus on the next chip's button (or the previous one, or the list when none are left)
// so keyboard users don't lose their place.
document.querySelectorAll('.chips').forEach(list => list.addEventListener('click', e => {
  const btn = e.target.closest('.btn-close');
  if (!btn) return;
  const chip = btn.closest('li');
  const next = (chip.nextElementSibling || chip.previousElementSibling)?.querySelector('.btn-close');
  chip.remove();
  if (next) next.focus();
  else { list.tabIndex = -1; list.focus(); }
}));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="mx-auto w-full max-w-[660px]">
  <span class="mb-3 block text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase">Filter navigation</span>
  <nav class="relative after:pointer-events-none after:absolute after:inset-y-0 after:right-0 after:w-[52px] after:bg-linear-to-r after:from-[#f4f5f9]/0 after:to-[#f4f5f9] after:to-78%" aria-label="Categories">
    <ul class="-m-1 flex gap-[9px] overflow-x-auto px-1 pt-1 pb-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
      <li class="flex shrink-0"><a href="#" aria-current="page" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">All <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">248</span></a></li>
      <li class="flex shrink-0"><a href="#" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">Templates <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">96</span></a></li>
      <li class="flex shrink-0"><a href="#" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">Components <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">74</span></a></li>
      <li class="flex shrink-0"><a href="#" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">Dashboards <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">41</span></a></li>
      <li class="flex shrink-0"><a href="#" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">Landing pages <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">28</span></a></li>
      <li class="flex shrink-0"><a href="#" class="group inline-flex items-center gap-2 rounded-full border border-[#e4e7ec] bg-white px-4 py-[9px] text-[.87rem] font-semibold whitespace-nowrap text-[#475467] transition-[color,background-color,border-color] duration-150 hover:border-[#cbd2e0] hover:text-[#101828] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] aria-[current=page]:border-[#101828] aria-[current=page]:bg-[#101828] aria-[current=page]:text-white motion-reduce:transition-none">Emails <span class="rounded-full bg-[#f2f4f7] px-[7px] py-px text-[.76rem] text-[#475467] group-aria-[current=page]:bg-white/20 group-aria-[current=page]:text-white">9</span></a></li>
    </ul>
  </nav>
</div>

<div class="mx-auto w-full max-w-[660px]">
  <span class="mb-3 block text-[.72rem] font-bold tracking-[.08em] text-[#5d6679] uppercase" id="chips-lbl">Active filters</span>
  <ul data-chips class="flex flex-wrap gap-2 focus:outline-none" aria-labelledby="chips-lbl">
    <li class="inline-flex items-center gap-2 rounded-full bg-[#eef2ff] py-[7px] pr-2 pl-3.5 text-[.85rem] font-semibold text-[#3730a3]">Free <button type="button" class="grid size-[19px] cursor-pointer place-items-center rounded-full bg-[#3730a3]/12 hover:bg-[#3730a3]/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" aria-label="Remove filter: Free"><svg class="size-[9px] fill-none stroke-current stroke-3" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
    <li class="inline-flex items-center gap-2 rounded-full bg-[#eef2ff] py-[7px] pr-2 pl-3.5 text-[.85rem] font-semibold text-[#3730a3]">Bootstrap 5 <button type="button" class="grid size-[19px] cursor-pointer place-items-center rounded-full bg-[#3730a3]/12 hover:bg-[#3730a3]/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" aria-label="Remove filter: Bootstrap 5"><svg class="size-[9px] fill-none stroke-current stroke-3" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
    <li class="inline-flex items-center gap-2 rounded-full bg-[#eef2ff] py-[7px] pr-2 pl-3.5 text-[.85rem] font-semibold text-[#3730a3]">Dark mode <button type="button" class="grid size-[19px] cursor-pointer place-items-center rounded-full bg-[#3730a3]/12 hover:bg-[#3730a3]/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" aria-label="Remove filter: Dark mode"><svg class="size-[9px] fill-none stroke-current stroke-3" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></li>
  </ul>
</div>
JavaScript
// Remove a chip, then put focus on the next chip's button (or the previous one, or the list when none are left)
// so keyboard users don't lose their place.
document.querySelectorAll('[data-chips]').forEach(list => list.addEventListener('click', e => {
  const btn = e.target.closest('button');
  if (!btn) return;
  const chip = btn.closest('li');
  const next = (chip.nextElementSibling || chip.previousElementSibling)?.querySelector('button');
  chip.remove();
  if (next) next.focus();
  else { list.tabIndex = -1; list.focus(); }
}));

About this set

Two filter patterns for listing pages. Filter navigation is a single row of pill links, each with a count badge (All 248, Templates 96, Components 74 and so on); the current category is a solid dark pill marked with aria-current=”page”. The row scrolls sideways when it runs out of room, with the scrollbar hidden and a gradient fading the right edge to hint that there is more. Active filters is a wrapping list of indigo chips, each with a small round button that removes it. Use the pills for top-level categories on a gallery, shop or blog archive, and the chips to show which filters are applied to a search. The pills need no JavaScript: they are plain links, so each category can have its own URL. The chips use a few lines of script that remove the chip and move keyboard focus to the next chip’s button, or the previous one, or the list itself when the last chip is gone, so keyboard users keep their place. The scroller has inner padding so focus rings are not clipped at its edges.

What’s included

  • Scrollable pill row with a hidden scrollbar and a fading edge
  • Count badges inside each pill
  • Current category marked with aria-current="page"
  • Removable chips that hand focus to the next chip
  • Bootstrap version uses .nav-pills, .badge and .btn-close

Accessibility

The pills are links in a list inside a nav labelled Categories, with aria-current="page" on the current one. Each remove button is a real button named "Remove filter: …", and after a removal focus moves to a neighbouring chip instead of being lost. Pills and buttons show an indigo :focus-visible outline, and the colour transitions switch off under prefers-reduced-motion.

Customise it

In plain CSS set --fp-ink (the current pill), --fp-bg (match the page so the fade blends) and --chip-ink on .chips. In Bootstrap the pill colours are the --bs-nav-pills-* and --bs-nav-link-* variables on .fp; in Tailwind edit the #101828, #f4f5f9 and #3730a3 values in the utilities.