Open demo

CSS code

HTML
<div><span class="lbl">Two options</span><div class="row">
<div class="sg two" role="radiogroup" aria-label="Layout"><input type="radio" name="layout" id="layout-grid" checked><input type="radio" name="layout" id="layout-list">
<em></em><label for="layout-grid">Grid</label><label for="layout-list">List</label></div></div></div>
<div><span class="lbl">Three options</span><div class="row">
<div class="sg three" role="radiogroup" aria-label="Date range"><input type="radio" name="range" id="range-day"><input type="radio" name="range" id="range-week" checked>
<input type="radio" name="range" id="range-month"><em></em>
<label for="range-day">Day</label><label for="range-week">Week</label><label for="range-month">Month</label></div></div></div>
<div><span class="lbl">Filter pills</span><div class="pills" role="radiogroup" aria-label="Filter">
<input type="radio" name="filter" id="filter-all" checked><label for="filter-all">All</label>
<input type="radio" name="filter" id="filter-free"><label for="filter-free">Free</label>
<input type="radio" name="filter" id="filter-premium"><label for="filter-premium">Premium</label>
<input type="radio" name="filter" id="filter-recent"><label for="filter-recent">Recent</label></div></div>
CSS
/* Segmented controls: visually hidden radio inputs, their <label>s, and an <em> indicator that slides under the checked one. */
.sg{display:inline-flex;background:#e2e8f0;border-radius:11px;padding:4px;position:relative}
.sg input{position:absolute;opacity:0;width:0;height:0;margin:0}
.sg label{position:relative;z-index:2;padding:8px 20px;font-size:.85rem;font-weight:600;color:#475569;
  cursor:pointer;transition:color .2s}
.sg em{position:absolute;top:4px;height:calc(100% - 8px);background:#fff;border-radius:8px;
  box-shadow:0 1px 3px rgba(15,23,42,.18);transition:transform .24s;left:4px}
.sg.two em{width:calc(50% - 4px)}
.sg.three em{width:calc(33.333% - 3px)}
.sg input:nth-of-type(1):checked~em{transform:translateX(0)}
.sg input:nth-of-type(2):checked~em{transform:translateX(100%)}
.sg input:nth-of-type(3):checked~em{transform:translateX(200%)}
.sg input:nth-of-type(1):checked~label:nth-of-type(1),
.sg input:nth-of-type(2):checked~label:nth-of-type(2),
.sg input:nth-of-type(3):checked~label:nth-of-type(3){color:#0f172a}
.sg input:focus-visible~em{outline:2px solid #2563eb;outline-offset:2px}
/* filter pills */
.pills{display:flex;gap:9px;flex-wrap:wrap}
.pills input{position:absolute;opacity:0;width:0;height:0;margin:0}
.pills label{padding:8px 17px;border:1px solid #cbd5e1;border-radius:999px;font-size:.83rem;
  cursor:pointer;color:#475569;transition:background .18s,border-color .18s,color .18s}
.pills input:checked+label{background:#0f172a;border-color:#0f172a;color:#fff}
.pills input:focus-visible+label{outline:2px solid #2563eb;outline-offset:2px}
@media (prefers-reduced-motion:reduce){.sg label,.sg em,.pills label{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Two options</span><div class="demo-row">
<div class="btn-group sg two" role="radiogroup" aria-label="Layout">
<input type="radio" class="btn-check" name="layout" id="layout-grid" checked><label class="btn" for="layout-grid">Grid</label>
<input type="radio" class="btn-check" name="layout" id="layout-list"><label class="btn" for="layout-list">List</label><em></em></div></div></div>
<div><span class="lbl">Three options</span><div class="demo-row">
<div class="btn-group sg three" role="radiogroup" aria-label="Date range">
<input type="radio" class="btn-check" name="range" id="range-day"><label class="btn" for="range-day">Day</label>
<input type="radio" class="btn-check" name="range" id="range-week" checked><label class="btn" for="range-week">Week</label>
<input type="radio" class="btn-check" name="range" id="range-month"><label class="btn" for="range-month">Month</label><em></em></div></div></div>
<div><span class="lbl">Filter pills</span><div class="pills" role="radiogroup" aria-label="Filter">
<input type="radio" class="btn-check" name="filter" id="filter-all" checked><label class="btn rounded-pill" for="filter-all">All</label>
<input type="radio" class="btn-check" name="filter" id="filter-free"><label class="btn rounded-pill" for="filter-free">Free</label>
<input type="radio" class="btn-check" name="filter" id="filter-premium"><label class="btn rounded-pill" for="filter-premium">Premium</label>
<input type="radio" class="btn-check" name="filter" id="filter-recent"><label class="btn rounded-pill" for="filter-recent">Recent</label></div></div>
CSS
/* Segmented control: a Bootstrap .btn-group of .btn-check radios, plus an <em> that slides under the checked .btn. */
.sg{background:#e2e8f0;border-radius:11px;padding:4px}
.sg .btn{--bs-btn-padding-x:20px;--bs-btn-padding-y:8px;--bs-btn-font-size:.85rem;--bs-btn-font-weight:600;
  --bs-btn-line-height:normal;--bs-btn-border-width:0;--bs-btn-bg:transparent;--bs-btn-color:#475569;
  --bs-btn-hover-bg:transparent;--bs-btn-hover-color:#475569;--bs-btn-active-bg:transparent;--bs-btn-active-color:#0f172a;
  --bs-btn-focus-box-shadow:none;transition:color .2s}
.sg.btn-group>.btn-check+.btn{z-index:2;margin-left:0}
.sg em{position:absolute;top:4px;left:4px;height:calc(100% - 8px);background:#fff;border-radius:8px;
  box-shadow:0 1px 3px rgba(15,23,42,.18);transition:transform .24s}
.sg.two em{width:calc(50% - 4px)}
.sg.three em{width:calc(33.333% - 3px)}
.sg .btn-check:nth-of-type(2):checked~em{transform:translateX(100%)}
.sg .btn-check:nth-of-type(3):checked~em{transform:translateX(200%)}
.sg .btn-check:focus-visible~em{outline:2px solid #2563eb;outline-offset:2px}
/* filter pills: .btn-check + .btn.rounded-pill */
.pills{display:flex;gap:9px;flex-wrap:wrap}
.pills .btn{--bs-btn-padding-x:17px;--bs-btn-padding-y:8px;--bs-btn-font-size:.83rem;--bs-btn-line-height:normal;
  --bs-btn-color:#475569;--bs-btn-border-color:#cbd5e1;--bs-btn-hover-color:#475569;--bs-btn-hover-bg:transparent;
  --bs-btn-hover-border-color:#cbd5e1;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0f172a;--bs-btn-active-border-color:#0f172a;
  --bs-btn-focus-box-shadow:0 0 0 2px #f8fafc,0 0 0 4px #2563eb;transition:background-color .18s,border-color .18s,color .18s}
@media (prefers-reduced-motion:reduce){.sg .btn,.sg em,.pills .btn{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Two options</span><div class="flex flex-wrap items-center gap-[18px]">
<div class="relative inline-flex rounded-[11px] bg-seg p-1" role="radiogroup" aria-label="Layout">
<input type="radio" name="layout" id="layout-grid" class="peer peer/grid sr-only" checked><input type="radio" name="layout" id="layout-list" class="peer peer/list sr-only">
<em class="absolute top-1 left-1 h-[calc(100%-8px)] w-[calc(50%-4px)] rounded-lg bg-white shadow-[0_1px_3px_rgba(15,23,42,.18)] transition-transform duration-[240ms] peer-checked/list:translate-x-full peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-seg-focus motion-reduce:transition-none"></em>
<label for="layout-grid" class="relative z-[2] cursor-pointer px-5 py-2 text-[.85rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked/grid:text-seg-ink motion-reduce:transition-none">Grid</label><label for="layout-list" class="relative z-[2] cursor-pointer px-5 py-2 text-[.85rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked/list:text-seg-ink motion-reduce:transition-none">List</label></div></div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Three options</span><div class="flex flex-wrap items-center gap-[18px]">
<div class="relative inline-flex rounded-[11px] bg-seg p-1" role="radiogroup" aria-label="Date range">
<input type="radio" name="range" id="range-day" class="peer peer/day sr-only"><input type="radio" name="range" id="range-week" class="peer peer/week sr-only" checked><input type="radio" name="range" id="range-month" class="peer peer/month sr-only">
<em class="absolute top-1 left-1 h-[calc(100%-8px)] w-[calc(33.333%-3px)] rounded-lg bg-white shadow-[0_1px_3px_rgba(15,23,42,.18)] transition-transform duration-[240ms] peer-checked/week:translate-x-full peer-checked/month:translate-x-[200%] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-seg-focus motion-reduce:transition-none"></em>
<label for="range-day" class="relative z-[2] cursor-pointer px-5 py-2 text-[.85rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked/day:text-seg-ink motion-reduce:transition-none">Day</label><label for="range-week" class="relative z-[2] cursor-pointer px-5 py-2 text-[.85rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked/week:text-seg-ink motion-reduce:transition-none">Week</label><label for="range-month" class="relative z-[2] cursor-pointer px-5 py-2 text-[.85rem] font-semibold text-seg-muted transition-colors duration-200 peer-checked/month:text-seg-ink motion-reduce:transition-none">Month</label></div></div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Filter pills</span><div class="flex flex-wrap gap-[9px]" role="radiogroup" aria-label="Filter">
<input type="radio" name="filter" id="filter-all" class="peer/all sr-only" checked><label for="filter-all" class="cursor-pointer rounded-full border border-seg-line px-[17px] py-2 text-[.83rem] text-seg-muted transition-[background-color,border-color,color] duration-[180ms] peer-checked/all:border-seg-ink peer-checked/all:bg-seg-ink peer-checked/all:text-white peer-focus-visible/all:outline-2 peer-focus-visible/all:outline-offset-2 peer-focus-visible/all:outline-seg-focus motion-reduce:transition-none">All</label>
<input type="radio" name="filter" id="filter-free" class="peer/free sr-only"><label for="filter-free" class="cursor-pointer rounded-full border border-seg-line px-[17px] py-2 text-[.83rem] text-seg-muted transition-[background-color,border-color,color] duration-[180ms] peer-checked/free:border-seg-ink peer-checked/free:bg-seg-ink peer-checked/free:text-white peer-focus-visible/free:outline-2 peer-focus-visible/free:outline-offset-2 peer-focus-visible/free:outline-seg-focus motion-reduce:transition-none">Free</label>
<input type="radio" name="filter" id="filter-premium" class="peer/premium sr-only"><label for="filter-premium" class="cursor-pointer rounded-full border border-seg-line px-[17px] py-2 text-[.83rem] text-seg-muted transition-[background-color,border-color,color] duration-[180ms] peer-checked/premium:border-seg-ink peer-checked/premium:bg-seg-ink peer-checked/premium:text-white peer-focus-visible/premium:outline-2 peer-focus-visible/premium:outline-offset-2 peer-focus-visible/premium:outline-seg-focus motion-reduce:transition-none">Premium</label>
<input type="radio" name="filter" id="filter-recent" class="peer/recent sr-only"><label for="filter-recent" class="cursor-pointer rounded-full border border-seg-line px-[17px] py-2 text-[.83rem] text-seg-muted transition-[background-color,border-color,color] duration-[180ms] peer-checked/recent:border-seg-ink peer-checked/recent:bg-seg-ink peer-checked/recent:text-white peer-focus-visible/recent:outline-2 peer-focus-visible/recent:outline-offset-2 peer-focus-visible/recent:outline-seg-focus motion-reduce:transition-none">Recent</label></div></div>
CSS
@theme {
  --color-seg: #e2e8f0;
  --color-seg-muted: #475569;
  --color-seg-ink: #0f172a;
  --color-seg-line: #cbd5e1;
  --color-seg-focus: #2563eb;
}

About this set

Multi-option switches for when there are more than two states. The two-up control switches between two views, such as Grid and List. The three-up control picks one of three ranges, such as Day, Week and Month, with a white indicator that slides under the chosen option. Filter pills are a wrapping row of rounded buttons, such as All, Free, Premium and Recent, where the chosen pill fills dark. Use the segmented controls for view and range pickers in toolbars and dashboards, and the pills for filters above a list or grid. All three are groups of radio inputs sharing one name, so only one option can be chosen and the value submits with a form. The inputs are visually hidden and their labels are the visible buttons; the sliding indicator is an element positioned with sibling selectors that follow the checked radio. In Bootstrap they are .btn-check radios in a .btn-group, and in Tailwind they use named peer variants.

What’s included

  • Two-option and three-option segmented controls
  • Sliding white indicator under the chosen option
  • Filter pills that wrap onto new lines on narrow screens
  • Built on radio inputs, so arrow keys move the choice
  • Label text darkened to #475569 so it reaches 4.5:1 on the grey track

Accessibility

Each group has role="radiogroup" and an aria-label, and every option is a radio input named by its label. Tab enters the group on the chosen option and the arrow keys move the selection, as with any native radio group. Focus shows a 2px blue outline on the indicator or the pill, and the slide and colour transitions are removed under prefers-reduced-motion.

Customise it

Change the track, text and indicator colours in .sg, .sg label and .sg em, and the pill colours in .pills label (plain CSS), the --bs-btn-* variables on .sg .btn and .pills .btn (Bootstrap) or the --color-seg-* theme colours (Tailwind). For a different number of options, set the indicator width to that fraction and add a matching translateX rule.