shadcn-html / radio
Built with: CSS
Radio Group
A set of mutually exclusive options. Built on native <input type="radio"> with <fieldset> grouping.
Default
Radio group with fieldset and legend.
<fieldset class="radio-group">
<legend class="label">Choose a plan</legend>
<div class="radio-item">
<input class="radio" type="radio" name="plan" id="free" value="free" checked>
<label for="free">Free</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="plan" id="pro" value="pro">
<label for="pro">Pro</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="plan" id="ent" value="enterprise">
<label for="ent">Enterprise</label>
</div>
</fieldset>
With Description
Radio items with label and description text using .radio-item-block.
<fieldset class="radio-group">
<legend class="label">Spacing</legend>
<div class="radio-item-block">
<input class="radio" type="radio" name="spacing" id="sp-default" value="default" checked>
<label for="sp-default">Default</label>
<span class="radio-description">Standard spacing for most use cases.</span>
</div>
<div class="radio-item-block">
<input class="radio" type="radio" name="spacing" id="sp-comfortable" value="comfortable">
<label for="sp-comfortable">Comfortable</label>
<span class="radio-description">More space between elements.</span>
</div>
</fieldset>
Choice Card
Card-style radios using .radio-card with :has() for checked highlight.
<fieldset class="radio-group">
<legend class="label">Select a plan</legend>
<label class="radio-card" for="c-plus">
<input class="radio" type="radio" name="card" id="c-plus" value="plus">
<label for="c-plus">Plus</label>
<span class="radio-description">For individuals and small teams.</span>
</label>
<label class="radio-card" for="c-pro">
<input class="radio" type="radio" name="card" id="c-pro" value="pro" checked>
<label for="c-pro">Pro</label>
<span class="radio-description">For growing businesses.</span>
</label>
</fieldset>
Horizontal
Horizontal layout with data-orientation="horizontal".
<fieldset class="radio-group" data-orientation="horizontal">
<legend class="label">Alignment</legend>
<div class="radio-item">
<input class="radio" type="radio" name="align" id="left" value="left" checked>
<label for="left">Left</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="align" id="center" value="center">
<label for="center">Center</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="align" id="right" value="right">
<label for="right">Right</label>
</div>
</fieldset>
Disabled
Individual or group-level disabling via disabled attribute.
<fieldset class="radio-group">
<legend class="label">Disabled group</legend>
<div class="radio-item">
<input class="radio" type="radio" name="dis" id="d1" disabled checked>
<label for="d1">Option A</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="dis" id="d2" disabled>
<label for="d2">Option B</label>
</div>
</fieldset>
Invalid
Validation error state with aria-invalid="true".
<fieldset class="radio-group">
<legend class="label">Notification Preferences</legend>
<p class="radio-group-description">Choose how you want to receive notifications.</p>
<div class="radio-item">
<input class="radio" type="radio" name="notif" id="inv1" value="email" aria-invalid="true">
<label for="inv1">Email only</label>
</div>
<div class="radio-item">
<input class="radio" type="radio" name="notif" id="inv2" value="sms" aria-invalid="true">
<label for="inv2">SMS only</label>
</div>
</fieldset>
CSS view file
/* -- Radio Group component -------------------------------------- */
@layer components {
.radio-group {
border: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
& > legend {
margin-bottom: 0.375rem;
}
/* Horizontal layout */
&[data-orientation="horizontal"] {
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
}
.radio-item {
display: flex;
align-items: center;
gap: 0.5rem;
& > label {
font-size: 0.875rem;
font-weight: 400;
cursor: pointer;
}
}
/* -- With-description layout -------------------------------- */
.radio-item-block {
display: grid;
grid-template-columns: auto 1fr;
gap: 0 0.5rem;
& > .radio {
grid-row: 1 / 3;
margin-top: 0.125rem;
}
& > label {
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
line-height: 1.4;
}
& > .radio-description {
grid-column: 2;
font-size: 0.8125rem;
color: var(--muted-foreground);
line-height: 1.5;
}
}
/* -- Card variant ------------------------------------------- */
.radio-card {
position: relative;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
cursor: pointer;
transition: border-color 150ms, background 150ms;
&:hover {
background: var(--accent);
}
&:has(.radio:checked) {
border-color: var(--primary);
}
&:has(.radio:focus-visible) {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
&:has(.radio:disabled) {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
& > .radio {
position: absolute;
top: 1rem;
right: 1rem;
}
& > label {
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
}
& > .radio-description {
font-size: 0.8125rem;
color: var(--muted-foreground);
line-height: 1.5;
}
}
.radio {
appearance: none;
width: 1rem;
height: 1rem;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--background);
cursor: pointer;
flex-shrink: 0;
position: relative;
transition: border-color 150ms;
&:checked {
border-color: var(--primary);
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--primary);
transform: translate(-50%, -50%);
}
}
&:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
& + label { opacity: 0.5; cursor: not-allowed; }
}
&[aria-invalid="true"] {
border-color: var(--destructive);
&:checked {
border-color: var(--destructive);
&::after {
background: var(--destructive);
}
}
}
}
/* Helper text for group-level descriptions */
.radio-group-description {
font-size: 0.8125rem;
color: var(--muted-foreground);
margin: -0.125rem 0 0.25rem;
}
/* -- Accessibility ------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
.radio,
.radio-card {
transition: none;
}
}
@media (forced-colors: active) {
.radio {
border-color: ButtonText;
&:checked {
border-color: Highlight;
&::after {
background: Highlight;
}
}
&:disabled {
border-color: GrayText;
&:checked::after {
background: GrayText;
}
}
}
.radio-card {
border-color: ButtonText;
&:has(.radio:checked) {
border-color: Highlight;
}
}
}
}