24 CSS Radio Buttons18 / 24

Pure CSSMIT licensed

Liquid Wave Fill Radio Button

Circular options that fill with liquid when selected: an aqua wave rises inside the checked circle, its surface rolling with two counter-rotating crests before settling near the brim. A tactile, playful selector for sizes, intensities or hydration-style apps — pure CSS fluid dynamics.

Published Updated

Live Demo
Try it

The code

<div class="crb-18-group">
<section class="crb-18a" aria-label="Liquid wave fill radio buttons">
  <fieldset class="crb-18a__group">
    <legend class="crb-18a__legend">Roast strength</legend>
    <div class="crb-18a__row">
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="mild"><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">S</span></span><span class="crb-18a__name">Smooth</span></label>
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="medium" checked><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">M</span></span><span class="crb-18a__name">Medium</span></label>
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="bold"><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">B</span></span><span class="crb-18a__name">Bold</span></label>
    </div>
  </fieldset>
</section>
</div>
.crb-18-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

.crb-18-group > section {
  flex: 1 1 340px;
  min-width: 300px;
}

.crb-18a,
.crb-18a *,
.crb-18a *::before,
.crb-18a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.crb-18a {
  --aqua: oklch(0.68 0.13 210);
  --ink: #173039;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #eef6f8;
}

.crb-18a__group {
  border: none;
}

.crb-18a__legend {
  font-size: 24px;
  font-weight: 750;
  color: var(--ink);
  letter-spacing: -.02em;
  text-align: center;
}

.crb-18a__row {
  display: flex;
  flex-wrap: wrap;
  gap: 30px;
  justify-content: center;
  margin-top: 28px;
}

.crb-18a__opt {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 12px;
  cursor: pointer;
}

.crb-18a__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.crb-18a__cup {
  position: relative;
  overflow: hidden;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: #fff;
  border: 2.5px solid #cfe0e6;
  display: grid;
  place-items: center;
  transition: border-color .3s,box-shadow .3s;
}

.crb-18a__wave {
  position: absolute;
  left: 50%;
  top: 100%;
  width: 220%;
  aspect-ratio: 1;
  margin-left: -110%;
  border-radius: 42%;
  background: var(--aqua);
  transform: translateY(4%) rotate(0deg);
  transition: transform 1s cubic-bezier(.22,1,.36,1);
  pointer-events: none;
}

.crb-18a__wave--back {
  border-radius: 44%;
  background: color-mix(in oklab,var(--aqua) 65%,#0b3a4a);
  opacity: .5;
}

.crb-18a__letter {
  position: relative;
  z-index: 1;
  font-size: 26px;
  font-weight: 800;
  color: #b9cdd5;
  transition: color .4s .2s;
}

.crb-18a__name {
  font-size: 14px;
  font-weight: 650;
  color: #6d8791;
  transition: color .25s;
}

.crb-18a__opt:hover .crb-18a__cup {
  border-color: var(--aqua);
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__cup {
  border-color: var(--aqua);
  box-shadow: 0 12px 26px -12px color-mix(in oklab,var(--aqua) 60%,transparent);
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave {
  transform: translateY(-64%) rotate(0deg);
  animation: crb-18a-roll 5s linear infinite;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave--back {
  animation: crb-18a-rollr 7s linear infinite;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__letter {
  color: #fff;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__name {
  color: var(--ink);
}

.crb-18a__opt:has(.crb-18a__input:focus-visible) .crb-18a__cup {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}

@keyframes crb-18a-roll {
  from {
    transform: translateY(-64%) rotate(0deg);
  }

  to {
    transform: translateY(-64%) rotate(360deg);
  }
}

@keyframes crb-18a-rollr {
  from {
    transform: translateY(-64%) rotate(360deg);
  }

  to {
    transform: translateY(-64%) rotate(0deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .crb-18a__wave {
    transition: transform .3s;
  }

  .crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave,
    .crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave--back {
    animation: none;
    border-radius: 50%;
  }
}
/* No JavaScript — a rounded-square water body rotates inside the clipped circle (its non-circular rim reads as a rolling wave); :checked translates it up to fill 64%. */
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 Radio Button 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: Liquid Wave Fill Radio Button
Source: https://codefronts.com/components/css-radio-buttons/wave-fill/

Circular options that fill with liquid when selected: an aqua wave rises inside the checked circle, its surface rolling with two counter-rotating crests before settling near the brim. A tactile, playful selector for sizes, intensities or hydration-style apps — pure CSS fluid dynamics.
## HTML
```html
<div class="crb-18-group">
<section class="crb-18a" aria-label="Liquid wave fill radio buttons">
  <fieldset class="crb-18a__group">
    <legend class="crb-18a__legend">Roast strength</legend>
    <div class="crb-18a__row">
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="mild"><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">S</span></span><span class="crb-18a__name">Smooth</span></label>
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="medium" checked><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">M</span></span><span class="crb-18a__name">Medium</span></label>
      <label class="crb-18a__opt"><input class="crb-18a__input" type="radio" name="crb-18a" value="bold"><span class="crb-18a__cup" aria-hidden="true"><span class="crb-18a__wave"></span><span class="crb-18a__wave crb-18a__wave--back"></span><span class="crb-18a__letter">B</span></span><span class="crb-18a__name">Bold</span></label>
    </div>
  </fieldset>
</section>
</div>
```
## CSS
```css
.crb-18-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

.crb-18-group > section {
  flex: 1 1 340px;
  min-width: 300px;
}

.crb-18a,
.crb-18a *,
.crb-18a *::before,
.crb-18a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.crb-18a {
  --aqua: oklch(0.68 0.13 210);
  --ink: #173039;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #eef6f8;
}

.crb-18a__group {
  border: none;
}

.crb-18a__legend {
  font-size: 24px;
  font-weight: 750;
  color: var(--ink);
  letter-spacing: -.02em;
  text-align: center;
}

.crb-18a__row {
  display: flex;
  flex-wrap: wrap;
  gap: 30px;
  justify-content: center;
  margin-top: 28px;
}

.crb-18a__opt {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 12px;
  cursor: pointer;
}

.crb-18a__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.crb-18a__cup {
  position: relative;
  overflow: hidden;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: #fff;
  border: 2.5px solid #cfe0e6;
  display: grid;
  place-items: center;
  transition: border-color .3s,box-shadow .3s;
}

.crb-18a__wave {
  position: absolute;
  left: 50%;
  top: 100%;
  width: 220%;
  aspect-ratio: 1;
  margin-left: -110%;
  border-radius: 42%;
  background: var(--aqua);
  transform: translateY(4%) rotate(0deg);
  transition: transform 1s cubic-bezier(.22,1,.36,1);
  pointer-events: none;
}

.crb-18a__wave--back {
  border-radius: 44%;
  background: color-mix(in oklab,var(--aqua) 65%,#0b3a4a);
  opacity: .5;
}

.crb-18a__letter {
  position: relative;
  z-index: 1;
  font-size: 26px;
  font-weight: 800;
  color: #b9cdd5;
  transition: color .4s .2s;
}

.crb-18a__name {
  font-size: 14px;
  font-weight: 650;
  color: #6d8791;
  transition: color .25s;
}

.crb-18a__opt:hover .crb-18a__cup {
  border-color: var(--aqua);
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__cup {
  border-color: var(--aqua);
  box-shadow: 0 12px 26px -12px color-mix(in oklab,var(--aqua) 60%,transparent);
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave {
  transform: translateY(-64%) rotate(0deg);
  animation: crb-18a-roll 5s linear infinite;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave--back {
  animation: crb-18a-rollr 7s linear infinite;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__letter {
  color: #fff;
}

.crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__name {
  color: var(--ink);
}

.crb-18a__opt:has(.crb-18a__input:focus-visible) .crb-18a__cup {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}

@keyframes crb-18a-roll {
  from {
    transform: translateY(-64%) rotate(0deg);
  }

  to {
    transform: translateY(-64%) rotate(360deg);
  }
}

@keyframes crb-18a-rollr {
  from {
    transform: translateY(-64%) rotate(360deg);
  }

  to {
    transform: translateY(-64%) rotate(0deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .crb-18a__wave {
    transition: transform .3s;
  }

  .crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave,
    .crb-18a__opt:has(.crb-18a__input:checked) .crb-18a__wave--back {
    animation: none;
    border-radius: 50%;
  }
}
```

## JavaScript
```js
/* No JavaScript — a rounded-square water body rotates inside the clipped circle (its non-circular rim reads as a rolling wave); :checked translates it up to fill 64%. */
```

How this works

The liquid is the classic two-ellipse trick: a huge rounded-square ::before (the water body, border-radius:42%) rotates continuously inside the clipped circle; because its silhouette is slightly non-circular, the rim of the shape reads as a rolling wave. A second layer at 40% opacity rotates the opposite way for depth. On :checked the whole assembly translates up from below the circle to ~30% height.

Rotation + translation are both compositor transforms, so the fluid runs at 60fps without paint storms. The label letter sits above the water with a color flip when submerged, and the real radio underneath provides the full keyboard model.

Make it yours

  • Water colour = --aqua; the deep layer derives darker via color-mix.
  • Fill level is the checked translateY (−70% ≈ 70% full).
  • Wave choppiness: lower the border-radius toward 38% for taller crests.
  • Rotation duration sets the swell speed — 6s is calm, 2s is sloshy.

Gotchas — read before shipping

  • The circle must clip (overflow:hidden + border-radius:50%) or the water body shows its square corners.
  • Keep both wave layers pointer-events:none; they sit over the input's hit area.
  • Infinite rotation is exactly what prefers-reduced-motion should remove — swap to a still fill level.

Browser support

ChromeSafariFirefoxEdge
111+16.2+121+111+

Floor set by :has(), oklch(), color-mix() as this demo is written. The core technique itself goes back further — Chrome 105+.

:has() raises the water; the rotating-ellipse wave itself works in every modern engine.

Search CodeFronts

Loading…