24 CSS Radio Buttons19 / 24

Pure CSSMIT licensed

Image Clip Mask Option Selector

A destination picker where photography is the control: each option is a real photo clipped into a tall arch; unselected images rest desaturated and dim, and picking one blooms it to full colour while the arch relaxes into a softer, wider mask and the caption slides up. Editorial, gallery-grade selection.

Published Updated

Live Demo
Try it

The code

<div class="crb-19-group">
<section class="crb-19a" aria-label="Image clip mask option selector">
  <fieldset class="crb-19a__group">
    <legend class="crb-19a__legend">Where to next?</legend>
    <div class="crb-19a__row">
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="alps" checked>
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=480&q=80&auto=format&fit=crop" alt="Snow-capped alpine peaks at dawn" loading="lazy"><span class="crb-19a__cap">The Alps</span></span>
      </label>
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="coast">
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=480&q=80&auto=format&fit=crop" alt="Turquoise water on a white sand beach" loading="lazy"><span class="crb-19a__cap">The Coast</span></span>
      </label>
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="forest">
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=480&q=80&auto=format&fit=crop" alt="Sunlight through a tall green forest" loading="lazy"><span class="crb-19a__cap">The Forest</span></span>
      </label>
    </div>
  </fieldset>
</section>
</div>
.crb-19-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

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

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

.crb-19a {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #141210;
}

.crb-19a__group {
  border: none;
}

.crb-19a__legend {
  font-size: 26px;
  font-weight: 750;
  color: #f4efe8;
  letter-spacing: -.02em;
  text-align: center;
}

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

.crb-19a__opt {
  position: relative;
  cursor: pointer;
}

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

.crb-19a__arch {
  position: relative;
  overflow: hidden;
  display: block;
  width: 170px;
  height: 250px;
  border-radius: 85px 85px 18px 18px;
  transition: border-radius .5s cubic-bezier(.22,1,.36,1),transform .4s,box-shadow .4s;
}

.crb-19a__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) brightness(.65);
  transform: scale(1.08);
  transition: filter .55s ease,transform .8s cubic-bezier(.22,1,.36,1);
}

.crb-19a__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 26px 14px 12px;
  font-size: 14.5px;
  font-weight: 700;
  letter-spacing: .02em;
  color: #fff;
  text-align: center;
  background: linear-gradient(180deg,transparent,rgba(10,8,5,.75));
  transform: translateY(10px);
  opacity: .75;
  transition: transform .4s,opacity .4s;
}

.crb-19a__opt:hover .crb-19a__img {
  filter: grayscale(.4) brightness(.85);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__arch {
  border-radius: 26px;
  transform: translateY(-6px);
  box-shadow: 0 26px 50px -22px rgba(0,0,0,.85);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__img {
  filter: none;
  transform: scale(1);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__cap {
  transform: translateY(0);
  opacity: 1;
}

.crb-19a__opt:has(.crb-19a__input:focus-visible) .crb-19a__arch {
  outline: 3px solid #f4efe8;
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .crb-19a__arch,
    .crb-19a__img,
    .crb-19a__cap {
    transition: filter .3s,opacity .3s;
  }

  .crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__arch {
    transform: none;
  }
}
/* No JavaScript — border-radius morphs the arch mask (50%→26px interpolates natively, no clip-path needed) while the photo blooms from grayscale via :has(input:checked). */
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: Image Clip Mask Option Selector
Source: https://codefronts.com/components/css-radio-buttons/clip-mask/

A destination picker where photography is the control: each option is a real photo clipped into a tall arch; unselected images rest desaturated and dim, and picking one blooms it to full colour while the arch relaxes into a softer, wider mask and the caption slides up. Editorial, gallery-grade selection.
## HTML
```html
<div class="crb-19-group">
<section class="crb-19a" aria-label="Image clip mask option selector">
  <fieldset class="crb-19a__group">
    <legend class="crb-19a__legend">Where to next?</legend>
    <div class="crb-19a__row">
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="alps" checked>
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=480&q=80&auto=format&fit=crop" alt="Snow-capped alpine peaks at dawn" loading="lazy"><span class="crb-19a__cap">The Alps</span></span>
      </label>
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="coast">
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=480&q=80&auto=format&fit=crop" alt="Turquoise water on a white sand beach" loading="lazy"><span class="crb-19a__cap">The Coast</span></span>
      </label>
      <label class="crb-19a__opt">
        <input class="crb-19a__input" type="radio" name="crb-19a" value="forest">
        <span class="crb-19a__arch"><img class="crb-19a__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=480&q=80&auto=format&fit=crop" alt="Sunlight through a tall green forest" loading="lazy"><span class="crb-19a__cap">The Forest</span></span>
      </label>
    </div>
  </fieldset>
</section>
</div>
```
## CSS
```css
.crb-19-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

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

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

.crb-19a {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #141210;
}

.crb-19a__group {
  border: none;
}

.crb-19a__legend {
  font-size: 26px;
  font-weight: 750;
  color: #f4efe8;
  letter-spacing: -.02em;
  text-align: center;
}

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

.crb-19a__opt {
  position: relative;
  cursor: pointer;
}

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

.crb-19a__arch {
  position: relative;
  overflow: hidden;
  display: block;
  width: 170px;
  height: 250px;
  border-radius: 85px 85px 18px 18px;
  transition: border-radius .5s cubic-bezier(.22,1,.36,1),transform .4s,box-shadow .4s;
}

.crb-19a__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) brightness(.65);
  transform: scale(1.08);
  transition: filter .55s ease,transform .8s cubic-bezier(.22,1,.36,1);
}

.crb-19a__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 26px 14px 12px;
  font-size: 14.5px;
  font-weight: 700;
  letter-spacing: .02em;
  color: #fff;
  text-align: center;
  background: linear-gradient(180deg,transparent,rgba(10,8,5,.75));
  transform: translateY(10px);
  opacity: .75;
  transition: transform .4s,opacity .4s;
}

.crb-19a__opt:hover .crb-19a__img {
  filter: grayscale(.4) brightness(.85);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__arch {
  border-radius: 26px;
  transform: translateY(-6px);
  box-shadow: 0 26px 50px -22px rgba(0,0,0,.85);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__img {
  filter: none;
  transform: scale(1);
}

.crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__cap {
  transform: translateY(0);
  opacity: 1;
}

.crb-19a__opt:has(.crb-19a__input:focus-visible) .crb-19a__arch {
  outline: 3px solid #f4efe8;
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .crb-19a__arch,
    .crb-19a__img,
    .crb-19a__cap {
    transition: filter .3s,opacity .3s;
  }

  .crb-19a__opt:has(.crb-19a__input:checked) .crb-19a__arch {
    transform: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — border-radius morphs the arch mask (50%→26px interpolates natively, no clip-path needed) while the photo blooms from grayscale via :has(input:checked). */
```

How this works

Each arch is border-radius-morphed (a 50%/50%/16px/16px arch that relaxes when checked) with the photo inside transitioning filter: grayscale(1) brightness(.7) → none plus a slow scale settle. Because border-radius interpolates smoothly, the mask morph needs no clip-path polygon gymnastics — though a comment shows the clip-path equivalent.

The caption bar is positioned inside the label and translates up on selection. Images carry real alt text and the label wraps both image and caption, so screen readers hear "Kyoto — temple pathway, radio button". Full arrow-key navigation between photos comes free from the native inputs.

Make it yours

  • Swap Unsplash URLs for your art direction; any portrait crop works.
  • The checked mask shape is one border-radius line — try full-round or squircle.
  • Colour bloom speed is the filter transition duration.
  • Add a fifth option — the flex row wraps and stays centered.

Gotchas — read before shipping

  • Always give selectable images real alt text; an image radio with empty alt is unusable by screen readers.
  • Keep grayscale on the img, not the label, or the caption grays out too.
  • Use loading="lazy" — four photos above the fold cost LCP otherwise.

Browser support

ChromeSafariFirefoxEdge
105+15.4+121+105+

:has() drives the bloom; border-radius morphing and filters are universal. Photos hotlink from Unsplash's free CDN.

Search CodeFronts

Loading…