SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/radio-buttons

8 CSS Radio Buttons

7 Pure CSS1 with JSPublished 2026-10-04Free · no sign-up

Eight radio button designs for real interfaces: a plain custom radio, plan cards, a segmented control, colour swatches, clothing sizes, payment methods, a settings list and shipping options. Each group sits in a fieldset with a legend, so the question is read together with the choices.

01. Custom Radio Buttons

FormAccessible

Native radio inputs restyled with appearance: none. The inner dot is an inset box-shadow, so it scales cleanly at any zoom level, and the group sits in a fieldset with a legend so the question is announced together with the choices.

View code
<fieldset class="rad">
  <legend>Delivery</legend>
  <label><input type="radio" name="d" checked> Standard (3 to 5 days)</label>
  <label><input type="radio" name="d"> Express (next day)</label>
  <label><input type="radio" name="d"> Collect in store</label>
</fieldset>
.rad { display: grid; gap: 12px; margin: 0; padding: 0; border: 0; color: #1d2433; font: 500 15px Inter, system-ui, sans-serif; }
.rad legend { margin-bottom: 8px; font-weight: 700; }
.rad label { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.rad input { appearance: none; flex: none; width: 20px; height: 20px; margin: 0; border: 2px solid #98a2b3; border-radius: 50%; background: #fff; cursor: pointer; transition: box-shadow .15s, border-color .15s; }
.rad input:checked { border-color: #2563eb; box-shadow: inset 0 0 0 4px #fff, inset 0 0 0 10px #2563eb; }
.rad input:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; }

02. Plan Picker Cards

PricingCards

Radio buttons presented as plan cards with a name, a price and a short line of detail. The whole card is clickable, the selected card is outlined using :has(), and the arrow keys still move between plans because the inputs share one name.

View code
<fieldset class="plans">
  <legend>Choose a plan</legend>
  <label class="plan"><input type="radio" name="plan"><b>Starter</b><span class="p">$9<small>/mo</small></span><span class="s">1 store, 100 orders</span></label>
  <label class="plan"><input type="radio" name="plan" checked><b>Growth</b><span class="p">$29<small>/mo</small></span><span class="s">3 stores, unlimited orders</span></label>
  <label class="plan"><input type="radio" name="plan"><b>Scale</b><span class="p">$79<small>/mo</small></span><span class="s">Unlimited stores, priority help</span></label>
</fieldset>
.plans { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; width: min(600px, 94vw); margin: 0; padding: 0; border: 0; font-family: Inter, system-ui, sans-serif; }
.plans legend { margin-bottom: 10px; color: #aab3c7; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.plan { position: relative; display: grid; gap: 4px; padding: 18px 16px; border: 1.5px solid #2c3346; border-radius: 16px; background: #141925; color: #e7ebf5; cursor: pointer; transition: border-color .2s, transform .2s; }
.plan input { position: absolute; top: 14px; right: 14px; margin: 0; accent-color: #a78bfa; }
.plan b { font-size: 16px; }
.plan .p { font-size: 26px; font-weight: 800; }
.plan small { color: #9aa4b8; font-size: 13px; font-weight: 500; }
.plan .s { color: #9aa4b8; font-size: 13px; }
.plan:has(input:checked) { border-color: #a78bfa; background: linear-gradient(180deg, #221b45, #141925); }
.plan:has(input:focus-visible) { outline: 2px solid #c4b5fd; outline-offset: 3px; }
@media (max-width: 560px) { .plans { grid-template-columns: 1fr; } }

03. Segmented Control

RadioToolbar

A segmented control for switching views, like List, Grid and Map. The radios are hidden but still focusable, the selected segment is raised with a white background and shadow, and a fieldset keeps the group semantics for assistive technology.

View code
<fieldset class="seg">
  <legend class="sr">View</legend>
  <input type="radio" name="v" id="v1" checked><label for="v1">List</label>
  <input type="radio" name="v" id="v2"><label for="v2">Grid</label>
  <input type="radio" name="v" id="v3"><label for="v3">Map</label>
</fieldset>
.seg { display: inline-flex; margin: 0; padding: 4px; border: 0; border-radius: 12px; background: #e4e7ee; font: 600 14px Inter, system-ui, sans-serif; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.seg input { position: absolute; opacity: 0; }
.seg label { padding: 9px 18px; border-radius: 9px; color: #475467; cursor: pointer; transition: background .15s, color .15s; }
.seg input:checked + label { background: #fff; color: #101828; box-shadow: 0 1px 3px rgba(16,24,40,.18); }
.seg input:focus-visible + label { outline: 2px solid #2563eb; outline-offset: 1px; }

04. Colour Swatch Picker

E-commerceColour

The colour picker from a product page. Each swatch is a radio with an accessible colour name, the chosen swatch gets a double ring drawn with box-shadow, and the selected colour name is echoed above the swatches with a small script.

View code
<fieldset class="swatch">
  <legend>Colour: <b id="cname">Ocean</b></legend>
  <input type="radio" name="c" id="c1" value="Ocean" checked><label for="c1" style="background:#2563eb"><span class="sr">Ocean</span></label>
  <input type="radio" name="c" id="c2" value="Forest"><label for="c2" style="background:#15803d"><span class="sr">Forest</span></label>
  <input type="radio" name="c" id="c3" value="Sand"><label for="c3" style="background:#d6b98c"><span class="sr">Sand</span></label>
  <input type="radio" name="c" id="c4" value="Charcoal"><label for="c4" style="background:#374151"><span class="sr">Charcoal</span></label>
</fieldset>
.swatch { display: flex; gap: 14px; margin: 0; padding: 0; border: 0; font: 500 15px Inter, system-ui, sans-serif; color: #101828; }
.swatch legend { margin-bottom: 12px; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.swatch input { position: absolute; opacity: 0; }
.swatch label { width: 34px; height: 34px; border-radius: 50%; cursor: pointer; transition: box-shadow .15s; }
.swatch input:checked + label { box-shadow: 0 0 0 3px #fff, 0 0 0 5px #101828; }
.swatch input:focus-visible + label { outline: 2px dashed #101828; outline-offset: 6px; }
document.querySelectorAll('.swatch input').forEach(r => r.addEventListener('change', () => {
  document.getElementById('cname').textContent = r.value;
}));

05. Size Selector Pills

E-commerceSizes

Clothing sizes as square pills, including a sold-out size that is disabled and crossed through. Disabled radios are skipped by the keyboard automatically, and the sold-out state is spelled out in hidden text rather than shown only with a line.

View code
<fieldset class="size">
  <legend>Size</legend>
  <input type="radio" name="s" id="s1"><label for="s1">XS</label>
  <input type="radio" name="s" id="s2" checked><label for="s2">S</label>
  <input type="radio" name="s" id="s3"><label for="s3">M</label>
  <input type="radio" name="s" id="s4" disabled><label for="s4">L <span class="sr">(sold out)</span></label>
  <input type="radio" name="s" id="s5"><label for="s5">XL</label>
</fieldset>
.size { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; border: 0; font: 700 14px Inter, system-ui, sans-serif; }
.size legend { width: 100%; margin-bottom: 10px; color: #aab3c7; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.size input { position: absolute; opacity: 0; }
.size label { display: grid; place-items: center; min-width: 48px; height: 44px; padding: 0 10px; border: 1.5px solid #39425a; border-radius: 10px; color: #e7ebf5; cursor: pointer; }
.size input:checked + label { border-color: #fff; background: #fff; color: #0e1118; }
.size input:disabled + label { color: #5d6680; cursor: not-allowed; background: linear-gradient(to top right, transparent calc(50% - 1px), #5d6680 50%, transparent calc(50% + 1px)); }
.size input:focus-visible + label { outline: 2px solid #9db8ff; outline-offset: 2px; }

06. Payment Method Selector

CheckoutForm

A checkout step listing card, bank transfer and cash on delivery. Each option is a row with an icon tile and a hint, and the extra hint text only appears for the selected method, keeping the step short until a choice is made.

View code
<fieldset class="pay">
  <legend>Payment method</legend>
  <label class="opt"><input type="radio" name="pay" checked><span class="ic">CARD</span><span><b>Credit or debit card</b><small>Visa, Mastercard and Amex accepted.</small></span></label>
  <label class="opt"><input type="radio" name="pay"><span class="ic">BANK</span><span><b>Bank transfer</b><small>We email the account details after you order.</small></span></label>
  <label class="opt"><input type="radio" name="pay"><span class="ic">COD</span><span><b>Cash on delivery</b><small>Pay the courier when your parcel arrives.</small></span></label>
</fieldset>
.pay { display: grid; gap: 10px; width: min(420px, 92vw); margin: 0; padding: 0; border: 0; font-family: Inter, system-ui, sans-serif; }
.pay legend { margin-bottom: 8px; color: #101828; font-weight: 700; }
.opt { display: flex; align-items: center; gap: 12px; padding: 14px; border: 1.5px solid #e4e7ec; border-radius: 12px; background: #fff; cursor: pointer; }
.opt input { margin: 0; accent-color: #0f766e; }
.ic { display: grid; place-items: center; width: 44px; height: 30px; border-radius: 6px; background: #f2f4f7; color: #344054; font: 800 10px Inter, sans-serif; letter-spacing: .04em; }
.opt b { display: block; color: #101828; font-size: 14px; }
.opt small { display: none; margin-top: 2px; color: #5d6679; font-size: 13px; }
.opt:has(input:checked) { border-color: #0f766e; background: #f0fdfa; }
.opt:has(input:checked) small { display: block; }
.opt:has(input:focus-visible) { outline: 2px solid #0f766e; outline-offset: 2px; }

07. Radio List with Descriptions

FormSettings

A settings-style radio list with a title and an explanation for each choice, separated by thin rules. The description is part of the label, so clicking anywhere on the row selects it, and the selected row is marked with a coloured bar on the left edge.

View code
<fieldset class="vis">
  <legend>Profile visibility</legend>
  <label><input type="radio" name="vis" checked><span><b>Public</b><em>Anyone can see your shop and reviews.</em></span></label>
  <label><input type="radio" name="vis"><span><b>Customers only</b><em>Only people who bought from you.</em></span></label>
  <label><input type="radio" name="vis"><span><b>Private</b><em>Hidden from everyone but you.</em></span></label>
</fieldset>
.vis { width: min(400px, 92vw); margin: 0; padding: 0; border: 0; overflow: hidden; border-radius: 14px; background: #151a26; font-family: Inter, system-ui, sans-serif; }
.vis legend { padding: 14px 16px 8px; color: #e7ebf5; font-weight: 700; }
.vis label { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-top: 1px solid #262d3e; cursor: pointer; box-shadow: inset 3px 0 0 transparent; }
.vis input { margin: 3px 0 0; accent-color: #34d399; }
.vis b { display: block; color: #e7ebf5; font-size: 14px; }
.vis em { color: #9aa4b8; font-style: normal; font-size: 13px; }
.vis label:has(input:checked) { box-shadow: inset 3px 0 0 #34d399; background: #182032; }
.vis label:has(input:focus-visible) { outline: 2px solid #6ee7b7; outline-offset: -2px; }

08. Shipping Speed Options

CheckoutPrice

Shipping choices with a delivery estimate on the left and the price on the right, the way checkouts usually show them. The free option is highlighted with a badge, and prices are kept in a separate column so they line up for easy comparison.

View code
<fieldset class="ship">
  <legend>Shipping</legend>
  <label><input type="radio" name="ship" checked><span class="n">Standard<small>Arrives Mon, 12 Oct</small></span><span class="pr free">Free</span></label>
  <label><input type="radio" name="ship"><span class="n">Priority<small>Arrives Thu, 8 Oct</small></span><span class="pr">$7.50</span></label>
  <label><input type="radio" name="ship"><span class="n">Next day<small>Arrives tomorrow</small></span><span class="pr">$14.00</span></label>
</fieldset>
.ship { display: grid; gap: 8px; width: min(400px, 92vw); margin: 0; padding: 0; border: 0; font-family: Inter, system-ui, sans-serif; }
.ship legend { margin-bottom: 8px; color: #101828; font-weight: 700; }
.ship label { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px; padding: 12px 14px; border: 1.5px solid #e4e7ec; border-radius: 12px; background: #fff; cursor: pointer; }
.ship input { margin: 0; accent-color: #4f46e5; }
.n { color: #101828; font-weight: 600; font-size: 14px; }
.n small { display: block; color: #5d6679; font-weight: 400; font-size: 13px; }
.pr { color: #101828; font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
.pr.free { padding: 3px 9px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; }
.ship label:has(input:checked) { border-color: #4f46e5; }
.ship label:has(input:focus-visible) { outline: 2px solid #4f46e5; outline-offset: 2px; }

About these CSS Radio Buttons

Group radios in a fieldset

Radio buttons only make sense as a group. Put them in a <fieldset> with a <legend> that asks the question, and give every input the same name. Screen readers then announce the question with each option, and the arrow keys move between options automatically.

Hidden but focusable

For swatches, sizes and segmented controls the circle is hidden and the label is styled instead. Hide the input with opacity or a visually-hidden class, never with display: none, so it can still receive focus. Then style input:focus-visible + label so keyboard users can see where they are.

Showing the selection

The selected option needs a cue beyond colour: a thicker border, a filled background, a tick or a left bar. The plan cards and settings list use :has(input:checked) on the label; the swatches use a double ring drawn with two box-shadows.

Disabled options

A sold-out size should be disabled so it cannot be chosen, and the reason should be in text, such as a visually hidden (sold out). Disabled radios are skipped by the keyboard, and a diagonal line gives a visual hint.

Frequently asked questions

Can a radio group have no selection?

Yes until the person picks one, but once chosen it cannot be cleared. Pre-select a sensible default where one exists.

Should I use radios or a select menu?

Radios are better for up to about six options because every choice is visible at once.

Do these need JavaScript?

No, except the swatch demo, which echoes the chosen colour name.