24 CSS Radio Buttons20 / 24
Gradient Border Card Radio
Dark glass cards wrapped in a living gradient border: at rest a faint ring, on selection a vivid violet-to-cyan gradient that slowly rotates around the card's perimeter like circulating energy. The premium checked-state treatment for AI products, crypto dashboards and feature pickers.
Published Updated
The code
<div class="crb-20-group">
<section class="crb-20a" aria-label="Gradient border card radio">
<fieldset class="crb-20a__group">
<legend class="crb-20a__legend">Choose your model</legend>
<div class="crb-20a__row">
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="lite">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Lite</span><span class="crb-20a__meta">Fast drafts & chat</span><span class="crb-20a__stat">128k context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="pro" checked>
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Pro</span><span class="crb-20a__meta">Deep reasoning</span><span class="crb-20a__stat">1M context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="vision">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Vision</span><span class="crb-20a__meta">Image + video in</span><span class="crb-20a__stat">Multimodal</span></span>
</label>
</div>
</fieldset>
</section>
</div><div class="crb-20-group">
<section class="crb-20a" aria-label="Gradient border card radio">
<fieldset class="crb-20a__group">
<legend class="crb-20a__legend">Choose your model</legend>
<div class="crb-20a__row">
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="lite">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Lite</span><span class="crb-20a__meta">Fast drafts & chat</span><span class="crb-20a__stat">128k context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="pro" checked>
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Pro</span><span class="crb-20a__meta">Deep reasoning</span><span class="crb-20a__stat">1M context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="vision">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Vision</span><span class="crb-20a__meta">Image + video in</span><span class="crb-20a__stat">Multimodal</span></span>
</label>
</div>
</fieldset>
</section>
</div>.crb-20-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
}
.crb-20-group > section {
flex: 1 1 340px;
min-width: 300px;
}
@property --crb20a-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.crb-20a,
.crb-20a *,
.crb-20a *::before,
.crb-20a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.crb-20a {
--g1: oklch(0.6 0.24 300);
--g2: oklch(0.75 0.14 195);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0b0c12;
}
.crb-20a__group {
border: none;
}
.crb-20a__legend {
font-size: 24px;
font-weight: 750;
color: #edeffa;
letter-spacing: -.02em;
text-align: center;
}
.crb-20a__row {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 28px;
}
.crb-20a__card {
position: relative;
border-radius: 18px;
padding: 1.5px;
background: conic-gradient(from var(--crb20a-angle),rgba(255,255,255,.14),rgba(255,255,255,.05),rgba(255,255,255,.14));
cursor: pointer;
transition: transform .3s;
}
.crb-20a__input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.crb-20a__panel {
display: grid;
gap: 4px;
width: 196px;
padding: 24px 20px;
border-radius: 16.5px;
background: linear-gradient(170deg,#171927,#101119);
}
.crb-20a__name {
font-size: 17px;
font-weight: 750;
color: #fff;
letter-spacing: -.01em;
}
.crb-20a__meta {
font-size: 13px;
color: #8b91ad;
}
.crb-20a__stat {
margin-top: 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: #6a7194;
}
.crb-20a__card:hover {
transform: translateY(-2px);
}
.crb-20a__card:has(.crb-20a__input:checked) {
background: conic-gradient(from var(--crb20a-angle),var(--g1),var(--g2),var(--g1));
animation: crb-20a-rotate 5s linear infinite;
}
.crb-20a__card:has(.crb-20a__input:checked) .crb-20a__stat {
color: var(--g2);
}
.crb-20a__card:has(.crb-20a__input:focus-visible) {
outline: 3px solid #fff;
outline-offset: 3px;
}
@keyframes crb-20a-rotate {
to {
--crb20a-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.crb-20a__card:has(.crb-20a__input:checked) {
animation: none;
}
.crb-20a__card:hover {
transform: none;
}
}.crb-20-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
}
.crb-20-group > section {
flex: 1 1 340px;
min-width: 300px;
}
@property --crb20a-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.crb-20a,
.crb-20a *,
.crb-20a *::before,
.crb-20a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.crb-20a {
--g1: oklch(0.6 0.24 300);
--g2: oklch(0.75 0.14 195);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0b0c12;
}
.crb-20a__group {
border: none;
}
.crb-20a__legend {
font-size: 24px;
font-weight: 750;
color: #edeffa;
letter-spacing: -.02em;
text-align: center;
}
.crb-20a__row {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 28px;
}
.crb-20a__card {
position: relative;
border-radius: 18px;
padding: 1.5px;
background: conic-gradient(from var(--crb20a-angle),rgba(255,255,255,.14),rgba(255,255,255,.05),rgba(255,255,255,.14));
cursor: pointer;
transition: transform .3s;
}
.crb-20a__input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.crb-20a__panel {
display: grid;
gap: 4px;
width: 196px;
padding: 24px 20px;
border-radius: 16.5px;
background: linear-gradient(170deg,#171927,#101119);
}
.crb-20a__name {
font-size: 17px;
font-weight: 750;
color: #fff;
letter-spacing: -.01em;
}
.crb-20a__meta {
font-size: 13px;
color: #8b91ad;
}
.crb-20a__stat {
margin-top: 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: #6a7194;
}
.crb-20a__card:hover {
transform: translateY(-2px);
}
.crb-20a__card:has(.crb-20a__input:checked) {
background: conic-gradient(from var(--crb20a-angle),var(--g1),var(--g2),var(--g1));
animation: crb-20a-rotate 5s linear infinite;
}
.crb-20a__card:has(.crb-20a__input:checked) .crb-20a__stat {
color: var(--g2);
}
.crb-20a__card:has(.crb-20a__input:focus-visible) {
outline: 3px solid #fff;
outline-offset: 3px;
}
@keyframes crb-20a-rotate {
to {
--crb20a-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.crb-20a__card:has(.crb-20a__input:checked) {
animation: none;
}
.crb-20a__card:hover {
transform: none;
}
}/* No JavaScript — the border is a conic-gradient underlayer showing through a 1.5px padding rim; @property lets the gradient's from-angle animate, so the checked rim circulates. */
/* No JavaScript — the border is a conic-gradient underlayer showing through a 1.5px padding rim; @property lets the gradient's from-angle animate, so the checked rim circulates. */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: Gradient Border Card Radio
Source: https://codefronts.com/components/css-radio-buttons/gradient-border/
Dark glass cards wrapped in a living gradient border: at rest a faint ring, on selection a vivid violet-to-cyan gradient that slowly rotates around the card's perimeter like circulating energy. The premium checked-state treatment for AI products, crypto dashboards and feature pickers.
## HTML
```html
<div class="crb-20-group">
<section class="crb-20a" aria-label="Gradient border card radio">
<fieldset class="crb-20a__group">
<legend class="crb-20a__legend">Choose your model</legend>
<div class="crb-20a__row">
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="lite">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Lite</span><span class="crb-20a__meta">Fast drafts & chat</span><span class="crb-20a__stat">128k context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="pro" checked>
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Pro</span><span class="crb-20a__meta">Deep reasoning</span><span class="crb-20a__stat">1M context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="vision">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Vision</span><span class="crb-20a__meta">Image + video in</span><span class="crb-20a__stat">Multimodal</span></span>
</label>
</div>
</fieldset>
</section>
</div>
```
## CSS
```css
.crb-20-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
}
.crb-20-group > section {
flex: 1 1 340px;
min-width: 300px;
}
@property --crb20a-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.crb-20a,
.crb-20a *,
.crb-20a *::before,
.crb-20a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.crb-20a {
--g1: oklch(0.6 0.24 300);
--g2: oklch(0.75 0.14 195);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0b0c12;
}
.crb-20a__group {
border: none;
}
.crb-20a__legend {
font-size: 24px;
font-weight: 750;
color: #edeffa;
letter-spacing: -.02em;
text-align: center;
}
.crb-20a__row {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 28px;
}
.crb-20a__card {
position: relative;
border-radius: 18px;
padding: 1.5px;
background: conic-gradient(from var(--crb20a-angle),rgba(255,255,255,.14),rgba(255,255,255,.05),rgba(255,255,255,.14));
cursor: pointer;
transition: transform .3s;
}
.crb-20a__input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.crb-20a__panel {
display: grid;
gap: 4px;
width: 196px;
padding: 24px 20px;
border-radius: 16.5px;
background: linear-gradient(170deg,#171927,#101119);
}
.crb-20a__name {
font-size: 17px;
font-weight: 750;
color: #fff;
letter-spacing: -.01em;
}
.crb-20a__meta {
font-size: 13px;
color: #8b91ad;
}
.crb-20a__stat {
margin-top: 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: #6a7194;
}
.crb-20a__card:hover {
transform: translateY(-2px);
}
.crb-20a__card:has(.crb-20a__input:checked) {
background: conic-gradient(from var(--crb20a-angle),var(--g1),var(--g2),var(--g1));
animation: crb-20a-rotate 5s linear infinite;
}
.crb-20a__card:has(.crb-20a__input:checked) .crb-20a__stat {
color: var(--g2);
}
.crb-20a__card:has(.crb-20a__input:focus-visible) {
outline: 3px solid #fff;
outline-offset: 3px;
}
@keyframes crb-20a-rotate {
to {
--crb20a-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.crb-20a__card:has(.crb-20a__input:checked) {
animation: none;
}
.crb-20a__card:hover {
transform: none;
}
}
```
## JavaScript
```js
/* No JavaScript — the border is a conic-gradient underlayer showing through a 1.5px padding rim; @property lets the gradient's from-angle animate, so the checked rim circulates. */
```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: Gradient Border Card Radio
Source: https://codefronts.com/components/css-radio-buttons/gradient-border/
Dark glass cards wrapped in a living gradient border: at rest a faint ring, on selection a vivid violet-to-cyan gradient that slowly rotates around the card's perimeter like circulating energy. The premium checked-state treatment for AI products, crypto dashboards and feature pickers.
## HTML
```html
<div class="crb-20-group">
<section class="crb-20a" aria-label="Gradient border card radio">
<fieldset class="crb-20a__group">
<legend class="crb-20a__legend">Choose your model</legend>
<div class="crb-20a__row">
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="lite">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Lite</span><span class="crb-20a__meta">Fast drafts & chat</span><span class="crb-20a__stat">128k context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="pro" checked>
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Pro</span><span class="crb-20a__meta">Deep reasoning</span><span class="crb-20a__stat">1M context</span></span>
</label>
<label class="crb-20a__card">
<input class="crb-20a__input" type="radio" name="crb-20a" value="vision">
<span class="crb-20a__panel"><span class="crb-20a__name">Nova Vision</span><span class="crb-20a__meta">Image + video in</span><span class="crb-20a__stat">Multimodal</span></span>
</label>
</div>
</fieldset>
</section>
</div>
```
## CSS
```css
.crb-20-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 0;
}
.crb-20-group > section {
flex: 1 1 340px;
min-width: 300px;
}
@property --crb20a-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.crb-20a,
.crb-20a *,
.crb-20a *::before,
.crb-20a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.crb-20a {
--g1: oklch(0.6 0.24 300);
--g2: oklch(0.75 0.14 195);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0b0c12;
}
.crb-20a__group {
border: none;
}
.crb-20a__legend {
font-size: 24px;
font-weight: 750;
color: #edeffa;
letter-spacing: -.02em;
text-align: center;
}
.crb-20a__row {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
margin-top: 28px;
}
.crb-20a__card {
position: relative;
border-radius: 18px;
padding: 1.5px;
background: conic-gradient(from var(--crb20a-angle),rgba(255,255,255,.14),rgba(255,255,255,.05),rgba(255,255,255,.14));
cursor: pointer;
transition: transform .3s;
}
.crb-20a__input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.crb-20a__panel {
display: grid;
gap: 4px;
width: 196px;
padding: 24px 20px;
border-radius: 16.5px;
background: linear-gradient(170deg,#171927,#101119);
}
.crb-20a__name {
font-size: 17px;
font-weight: 750;
color: #fff;
letter-spacing: -.01em;
}
.crb-20a__meta {
font-size: 13px;
color: #8b91ad;
}
.crb-20a__stat {
margin-top: 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
color: #6a7194;
}
.crb-20a__card:hover {
transform: translateY(-2px);
}
.crb-20a__card:has(.crb-20a__input:checked) {
background: conic-gradient(from var(--crb20a-angle),var(--g1),var(--g2),var(--g1));
animation: crb-20a-rotate 5s linear infinite;
}
.crb-20a__card:has(.crb-20a__input:checked) .crb-20a__stat {
color: var(--g2);
}
.crb-20a__card:has(.crb-20a__input:focus-visible) {
outline: 3px solid #fff;
outline-offset: 3px;
}
@keyframes crb-20a-rotate {
to {
--crb20a-angle: 360deg;
}
}
@media (prefers-reduced-motion: reduce) {
.crb-20a__card:has(.crb-20a__input:checked) {
animation: none;
}
.crb-20a__card:hover {
transform: none;
}
}
```
## JavaScript
```js
/* No JavaScript — the border is a conic-gradient underlayer showing through a 1.5px padding rim; @property lets the gradient's from-angle animate, so the checked rim circulates. */
```How this works
The border is the padding trick: the card's backdrop is a conic-gradient(from var(--angle)) underlayer; an inset panel of the card's own background sits 1.5px smaller on top, so only a gradient rim shows. @property registers --angle as an <angle> so a keyframe rotates the gradient smoothly — the classic 'animated gradient border' that's impossible with border-image alone.
Checked state raises the rim's saturation and starts the rotation; unchecked cards keep a static 12%-alpha rim. Radios and fieldset semantics are standard underneath — this is a pure paint-layer effect with no impact on the form model.
Make it yours
- The gradient stops are in one conic-gradient — reorder or recolour freely.
- Rim thickness is the inset value on the inner panel (1.5px).
- Rotation speed = the
crb-20a-rotateduration (8s calm, 3s energetic). - Works on light themes: flip the panel colour and raise rim alpha.
Gotchas — read before shipping
- Without @property support the angle can't interpolate — the fallback (static gradient rim) must look intentional, and does.
- The inner panel must repaint the exact card background or the rim looks like a halo, not a border.
- Long rotations on many cards multiply compositor layers — gate the animation to the checked card only, as here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Floor set by :has(), oklch(), @property as this demo is written. The core technique itself goes back further — Chrome 85+.
@property floors; below them the checked rim is static but present. :has() handles state.