25 CSS Button Groups02 / 25

Pure CSSMIT licensed

CSS Segmented Control with Motion-Path Comet Thumb (offset-path Arc Travel)

A segmented control whose selection thumb doesn't slide in a straight line — it arcs along a curved CSS motion path like a comet, trailing an aurora glow that lags behind and catches up. A never-seen 'CSS offset-path segmented control' that swaps the tired linear thumb for real motion-path travel, on accessible radio inputs.

Published

Live Demo
Try it

The code

<section class="btn-02" aria-label="Motion-path comet segmented control demo">
  <div class="btn-02__wrap">
    <p class="btn-02__label">Signal mode</p>
    <div class="btn-02__seg" role="radiogroup" aria-label="Signal mode">
      <input type="radio" name="btn-02" id="btn-02-1" class="btn-02__in" checked>
      <label for="btn-02-1" class="btn-02__opt">Live</label>
      <input type="radio" name="btn-02" id="btn-02-2" class="btn-02__in">
      <label for="btn-02-2" class="btn-02__opt">Pulse</label>
      <input type="radio" name="btn-02" id="btn-02-3" class="btn-02__in">
      <label for="btn-02-3" class="btn-02__opt">Wave</label>
      <input type="radio" name="btn-02" id="btn-02-4" class="btn-02__in">
      <label for="btn-02-4" class="btn-02__opt">Idle</label>
      <span class="btn-02__tail" aria-hidden="true"></span>
      <span class="btn-02__thumb" aria-hidden="true"></span>
    </div>
  </div>
</section>
.btn-02,
.btn-02 *,
.btn-02 *::before,
.btn-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-02 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%,#0d1024,#06070f);
  padding: 44px 20px;
}

.btn-02__wrap {
  width: min(420px,100%);
}

.btn-02__label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #7fe3c4;
  margin: 0 0 14px 4px;
}

.btn-02__seg {
  --d: 0%;
  position: relative;
  display: grid;
  grid-template-columns: repeat(4,80px);
  width: calc(80px*4 + 12px);
  padding: 6px;
  background: #12142a;
  border: 1px solid #222647;
  border-radius: 16px;
  box-shadow: inset 0 2px 8px rgba(0,0,0,.5);
}

.btn-02__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-02__opt {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  font-size: 14.5px;
  font-weight: 700;
  color: #8891bd;
  cursor: pointer;
  transition: color .3s;
}

.btn-02__thumb,
.btn-02__tail {
  position: absolute;
  left: 0;
  top: 0;
  width: 74px;
  height: 44px;
  border-radius: 11px;
  offset-path: path("M43 28 C 123 6, 203 6, 283 28");
  offset-rotate: 0deg;
  offset-distance: var(--d);
}

.btn-02__thumb {
  z-index: 1;
  background: linear-gradient(120deg,#38f2c0,#3aa0ff);
  box-shadow: 0 4px 18px -4px #38f2c0;
  transition: offset-distance .5s cubic-bezier(.5,0,.2,1);
}

.btn-02__tail {
  z-index: 0;
  background: linear-gradient(120deg,#38f2c0,#8a5bff);
  filter: blur(9px);
  opacity: .6;
  transition: offset-distance .8s cubic-bezier(.5,0,.2,1);
}

.btn-02__seg:has(#btn-02-2:checked) {
  --d: 33.3%;
}

.btn-02__seg:has(#btn-02-3:checked) {
  --d: 66.6%;
}

.btn-02__seg:has(#btn-02-4:checked) {
  --d: 100%;
}

.btn-02__in:checked + .btn-02__opt {
  color: #04121c;
}

.btn-02__in:focus-visible + .btn-02__opt {
  outline: 3px solid #7fe3c4;
  outline-offset: 3px;
  border-radius: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-02__thumb,
    .btn-02__tail {
    offset-path: path("M43 28 L 283 28");
    transition: offset-distance .2s;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Button Group from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Segmented Control with Motion-Path Comet Thumb (offset-path Arc Travel)
Source: https://codefronts.com/components/css-button-groups/css-segmented-control-with-motion-path-comet-thumb-offset-path-arc-travel/

A segmented control whose selection thumb doesn't slide in a straight line — it arcs along a curved CSS motion path like a comet, trailing an aurora glow that lags behind and catches up. A never-seen 'CSS offset-path segmented control' that swaps the tired linear thumb for real motion-path travel, on accessible radio inputs.
## HTML
```html
<section class="btn-02" aria-label="Motion-path comet segmented control demo">
  <div class="btn-02__wrap">
    <p class="btn-02__label">Signal mode</p>
    <div class="btn-02__seg" role="radiogroup" aria-label="Signal mode">
      <input type="radio" name="btn-02" id="btn-02-1" class="btn-02__in" checked>
      <label for="btn-02-1" class="btn-02__opt">Live</label>
      <input type="radio" name="btn-02" id="btn-02-2" class="btn-02__in">
      <label for="btn-02-2" class="btn-02__opt">Pulse</label>
      <input type="radio" name="btn-02" id="btn-02-3" class="btn-02__in">
      <label for="btn-02-3" class="btn-02__opt">Wave</label>
      <input type="radio" name="btn-02" id="btn-02-4" class="btn-02__in">
      <label for="btn-02-4" class="btn-02__opt">Idle</label>
      <span class="btn-02__tail" aria-hidden="true"></span>
      <span class="btn-02__thumb" aria-hidden="true"></span>
    </div>
  </div>
</section>
```
## CSS
```css
.btn-02,
.btn-02 *,
.btn-02 *::before,
.btn-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-02 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(120% 120% at 50% 0%,#0d1024,#06070f);
  padding: 44px 20px;
}

.btn-02__wrap {
  width: min(420px,100%);
}

.btn-02__label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #7fe3c4;
  margin: 0 0 14px 4px;
}

.btn-02__seg {
  --d: 0%;
  position: relative;
  display: grid;
  grid-template-columns: repeat(4,80px);
  width: calc(80px*4 + 12px);
  padding: 6px;
  background: #12142a;
  border: 1px solid #222647;
  border-radius: 16px;
  box-shadow: inset 0 2px 8px rgba(0,0,0,.5);
}

.btn-02__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-02__opt {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  font-size: 14.5px;
  font-weight: 700;
  color: #8891bd;
  cursor: pointer;
  transition: color .3s;
}

.btn-02__thumb,
.btn-02__tail {
  position: absolute;
  left: 0;
  top: 0;
  width: 74px;
  height: 44px;
  border-radius: 11px;
  offset-path: path("M43 28 C 123 6, 203 6, 283 28");
  offset-rotate: 0deg;
  offset-distance: var(--d);
}

.btn-02__thumb {
  z-index: 1;
  background: linear-gradient(120deg,#38f2c0,#3aa0ff);
  box-shadow: 0 4px 18px -4px #38f2c0;
  transition: offset-distance .5s cubic-bezier(.5,0,.2,1);
}

.btn-02__tail {
  z-index: 0;
  background: linear-gradient(120deg,#38f2c0,#8a5bff);
  filter: blur(9px);
  opacity: .6;
  transition: offset-distance .8s cubic-bezier(.5,0,.2,1);
}

.btn-02__seg:has(#btn-02-2:checked) {
  --d: 33.3%;
}

.btn-02__seg:has(#btn-02-3:checked) {
  --d: 66.6%;
}

.btn-02__seg:has(#btn-02-4:checked) {
  --d: 100%;
}

.btn-02__in:checked + .btn-02__opt {
  color: #04121c;
}

.btn-02__in:focus-visible + .btn-02__opt {
  outline: 3px solid #7fe3c4;
  outline-offset: 3px;
  border-radius: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-02__thumb,
    .btn-02__tail {
    offset-path: path("M43 28 L 283 28");
    transition: offset-distance .2s;
  }
}
```

How this works

The thumb is placed on a curved offset-path: path(...) that bows gently above the track, and its position is a single animatable value — offset-distance. Each segment maps to a distance stop (0% / 33.3% / 66.6% / 100%); :has() sets the stop and the browser interpolates along the curve, so the thumb swoops between segments instead of sliding flat.

The comet tail is a second element on the same path with a longer transition-duration, so it trails then snaps in — a genuine motion lag, not a fake blur. Because offset-distance transitions natively, the whole effect is declarative and GPU-composited.

.thumb,.tail{ offset-path:path('M50 34 C 138 10,226 10,314 34'); offset-rotate:0deg }
.seg:has(#s3:checked){ --d:66.6% }  .thumb{ offset-distance:var(--d); transition:offset-distance .5s }

Techniques used: CSS Motion Path (offset-path + offset-distance) for curved travel · comet tail via a lagged second element on the same path · :has() distance mapping · offset-rotate control · aurora gradient thumb · reduced-motion straight fallback

Make it yours

  • Arc shape = the path() control points (flatten or exaggerate the bow).
  • Comet lag = the tail's longer transition-duration.
  • Thumb/tail palette = the aurora gradient stops.
  • Add segments by extending the path and adding distance stops.

Gotchas — read before shipping

  • offset-path coordinates are in the element's own box space — keep the control a known width so the stops line up (this demo fixes segment widths).
  • Only offset-distance transitions smoothly along the curve; don't animate left alongside it or the paths fight.
  • Set offset-rotate:0deg unless you want the thumb to bank into the curve.
  • Under prefers-reduced-motion, drop to a flat path/instant move so the swoop doesn't trigger vestibular discomfort.

Browser support

ChromeSafariFirefoxEdge
105+15.4+121+105+

Floor set by :has() as this demo is written. The core technique itself goes back further — Chrome all.

CSS Motion Path (offset-path/offset-distance) is Baseline 2023. The :has() distance mapping needs :has() (Baseline 2023); without it the thumb rests on the first segment.

Techniques used in this demo

Search CodeFronts

Loading…