25 CSS Button Groups16 / 25

Pure CSSMIT licensed

CSS Format Toggle Button Group with Conic Fill (Radial Sweep Select)

An export-format toggle — PNG / JPG / WebP / AVIF — where selecting an option sweeps a conic-gradient fill around the button like a radar wipe before settling into a solid state. A striking 'CSS format toggle button group conic fill' using an @property-animated angle, with real radios keeping it single-select and accessible.

Published Updated

Live Demo
Try it

The code

<section class="btn-16" aria-label="Conic fill format toggle demo">
  <div class="btn-16__wrap">
    <p class="btn-16__label">Export format</p>
    <div class="btn-16__group" role="radiogroup" aria-label="Export format">
      <input type="radio" name="btn-16" id="btn-16-1" class="btn-16__in" checked>
      <label for="btn-16-1" class="btn-16__cell" style="--c:#ff5d73">PNG</label>
      <input type="radio" name="btn-16" id="btn-16-2" class="btn-16__in">
      <label for="btn-16-2" class="btn-16__cell" style="--c:#ffb44d">JPG</label>
      <input type="radio" name="btn-16" id="btn-16-3" class="btn-16__in">
      <label for="btn-16-3" class="btn-16__cell" style="--c:#4dd9c4">WebP</label>
      <input type="radio" name="btn-16" id="btn-16-4" class="btn-16__in">
      <label for="btn-16-4" class="btn-16__cell" style="--c:#7d9bff">AVIF</label>
    </div>
  </div>
</section>
@property --btn-16-a {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.btn-16,
.btn-16 *,
.btn-16 *::before,
.btn-16 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-16 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(160deg,#14121c,#0c0a12);
  padding: 44px 20px;
}

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

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

.btn-16__group {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  gap: 12px;
}

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

.btn-16__cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 66px;
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .02em;
  color: #8b84a6;
  background: #1a1726;
  border: 1.5px solid #2a2540;
  border-radius: 16px;
  cursor: pointer;
  overflow: hidden;
  transition: color .3s,border-color .3s;
}

.btn-16__cell::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(from -90deg,var(--c) var(--btn-16-a),transparent 0);
  opacity: 0;
  transition: opacity .2s;
}

.btn-16__cell:hover {
  color: #d6cff0;
  border-color: #3a3358;
}

.btn-16__in:checked + .btn-16__cell {
  color: #fff;
  border-color: var(--c);
}

.btn-16__in:checked + .btn-16__cell::before {
  opacity: .9;
  animation: btn-16-sweep .65s cubic-bezier(.3,.7,.3,1) forwards;
}

@keyframes btn-16-sweep {
  from {
    --btn-16-a: 0deg;
  }

  to {
    --btn-16-a: 360deg;
  }
}

.btn-16__cell span,
.btn-16__cell {
  z-index: 0;
}

.btn-16__in:checked + .btn-16__cell {
  z-index: 1;
}

.btn-16__cell {
  isolation: isolate;
}

.btn-16__in:checked + .btn-16__cell {
  text-shadow: 0 1px 6px rgba(0,0,0,.5);
}

.btn-16__in:focus-visible + .btn-16__cell {
  outline: 3px solid var(--c);
  outline-offset: 2px;
}

@supports not (background: conic-gradient(from 0deg,red var(--btn-16-a),transparent 0)) {
  .btn-16__in:checked + .btn-16__cell {
    background: var(--c);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-16__in:checked + .btn-16__cell::before {
    animation: none;
    --btn-16-a: 360deg;
  }
}
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 Format Toggle Button Group with Conic Fill (Radial Sweep Select)
Source: https://codefronts.com/components/css-button-groups/cellular-toggle/

An export-format toggle — PNG / JPG / WebP / AVIF — where selecting an option sweeps a conic-gradient fill around the button like a radar wipe before settling into a solid state. A striking 'CSS format toggle button group conic fill' using an @property-animated angle, with real radios keeping it single-select and accessible.
## HTML
```html
<section class="btn-16" aria-label="Conic fill format toggle demo">
  <div class="btn-16__wrap">
    <p class="btn-16__label">Export format</p>
    <div class="btn-16__group" role="radiogroup" aria-label="Export format">
      <input type="radio" name="btn-16" id="btn-16-1" class="btn-16__in" checked>
      <label for="btn-16-1" class="btn-16__cell" style="--c:#ff5d73">PNG</label>
      <input type="radio" name="btn-16" id="btn-16-2" class="btn-16__in">
      <label for="btn-16-2" class="btn-16__cell" style="--c:#ffb44d">JPG</label>
      <input type="radio" name="btn-16" id="btn-16-3" class="btn-16__in">
      <label for="btn-16-3" class="btn-16__cell" style="--c:#4dd9c4">WebP</label>
      <input type="radio" name="btn-16" id="btn-16-4" class="btn-16__in">
      <label for="btn-16-4" class="btn-16__cell" style="--c:#7d9bff">AVIF</label>
    </div>
  </div>
</section>
```
## CSS
```css
@property --btn-16-a {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.btn-16,
.btn-16 *,
.btn-16 *::before,
.btn-16 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-16 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(160deg,#14121c,#0c0a12);
  padding: 44px 20px;
}

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

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

.btn-16__group {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  gap: 12px;
}

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

.btn-16__cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 66px;
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .02em;
  color: #8b84a6;
  background: #1a1726;
  border: 1.5px solid #2a2540;
  border-radius: 16px;
  cursor: pointer;
  overflow: hidden;
  transition: color .3s,border-color .3s;
}

.btn-16__cell::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(from -90deg,var(--c) var(--btn-16-a),transparent 0);
  opacity: 0;
  transition: opacity .2s;
}

.btn-16__cell:hover {
  color: #d6cff0;
  border-color: #3a3358;
}

.btn-16__in:checked + .btn-16__cell {
  color: #fff;
  border-color: var(--c);
}

.btn-16__in:checked + .btn-16__cell::before {
  opacity: .9;
  animation: btn-16-sweep .65s cubic-bezier(.3,.7,.3,1) forwards;
}

@keyframes btn-16-sweep {
  from {
    --btn-16-a: 0deg;
  }

  to {
    --btn-16-a: 360deg;
  }
}

.btn-16__cell span,
.btn-16__cell {
  z-index: 0;
}

.btn-16__in:checked + .btn-16__cell {
  z-index: 1;
}

.btn-16__cell {
  isolation: isolate;
}

.btn-16__in:checked + .btn-16__cell {
  text-shadow: 0 1px 6px rgba(0,0,0,.5);
}

.btn-16__in:focus-visible + .btn-16__cell {
  outline: 3px solid var(--c);
  outline-offset: 2px;
}

@supports not (background: conic-gradient(from 0deg,red var(--btn-16-a),transparent 0)) {
  .btn-16__in:checked + .btn-16__cell {
    background: var(--c);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-16__in:checked + .btn-16__cell::before {
    animation: none;
    --btn-16-a: 360deg;
  }
}
```

How this works

Four hidden radios drive a single-select format group. When a button becomes :checked, an @property-registered <angle> animates from 0→360°, and the button's background is a conic-gradient whose sweep is masked to that angle — so the fill visibly winds around the button once, then holds as a full disc of color.

Because @property makes the angle a real animatable value, the conic sweep interpolates smoothly (plain custom properties can't animate). Each format keeps its own accent hue, giving the group four distinct colors while sharing one cohesive dark cell. A tiny format glyph anchors the center.

@property --btn-16-a{ syntax:'<angle>'; inherits:false; initial-value:0deg }
.in:checked + .cell{ animation:btn-16-sweep .6s ease forwards }

Techniques used: @property animatable conic angle · conic-gradient radar sweep on select · per-option accent hue · single-select radios · masked fill hold state · reduced-motion instant fill

Make it yours

  • Each cell's sweep hue is an inline --c; mix four distinct colors freely.
  • Sweep speed = the btn-16-sweep duration.
  • Direction: flip the conic from angle to sweep counter-clockwise.
  • Add more formats by copying a cell — the animation is per-cell.

Gotchas — read before shipping

  • Register --btn-16-a with @property or the conic angle won't animate at all.
  • Keep the resting (fully-swept) state high-contrast for the label — the sweep is decoration, not the state signal.
  • Radios keep it single-select; a format picker should never allow two active formats.
  • @property is Chrome/Safari/FF but was late to Firefox (128) — fall back to an instant fill via @supports.

Browser support

ChromeSafariFirefoxEdge
85+16.4+128+85+

@property is Baseline 2024. Without it the button fills instantly on select (no sweep) — still fully usable.

Techniques used in this demo

Search CodeFronts

Loading…