← Blog
28 Sept 2026radio buttonsHTML formsCSS layoutform accessibilityschema-driven forms

Vertical Radio Buttons: Stack Options in HTML and CSS Without Losing Accessibility

Learn to build vertical radio buttons with fieldset, labels, accessible keyboard behavior, responsive CSS, validation, and schema-driven form mapping.

Vertical Radio Buttons: Stack Options in HTML and CSS Without Losing Accessibility

Vertical radio buttons are a single-choice question whose options appear one above another. To build them, give the options one shared name, wrap the question in a <fieldset> with a <legend>, label each radio, and stack the labels with CSS. The vertical layout changes where choices appear, not how the radio group works. W3C’s forms tutorial recommends semantic grouping for related radio buttons; MDN’s radio reference explains how the shared name connects their selection and submitted value.

This guide starts with native HTML, then shows how to preserve that contract when options come from form data. I use one question throughout so you can copy the example, change the text, and check its behavior immediately.

Table of contents

Before you start

You need an HTML form, a stylesheet, and a browser where you can test with both mouse and keyboard. The example asks how a user wants to receive account updates. It has three possible answers and requires one selection. If the decision is optional in your product, remove required from every option and decide how an unanswered question should be handled.

Choose radio buttons only when one answer can be selected. If users can choose several answers, use checkboxes; if the list is too long for the available space, consider a select control. We generally prefer a vertical list when labels are longer or the interface must work at narrow widths. The U.S. Web Design System’s radio guidance recommends vertical lists for readability and clearer association between controls and labels.

1. Define one question and one shared radio name

Write the question first: How should we send account updates? Then give each response a distinct, meaningful submitted value such as email, text, or none. Every radio in this question needs the same name="updates"; a second question needs a different name. Otherwise, choosing an answer in one question can deselect an answer in the other.

Do not confuse name with id. The name groups options for selection and submission. An id uniquely identifies a particular element when you use a separate <label for="...">. The submitted value identifies the selected answer and should not depend on its display wording. If you omit it, the browser submits the unhelpful default value on, as MDN documents.

Check: write down what your application should receive. For the second option here, the expected form entry is updates=text, not three separate boolean values. If two questions interfere with each other, inspect their name attributes before changing the CSS.

2. Put the question in a fieldset and label each choice

Use the first <legend> inside the <fieldset> for the question. Wrap each input and its option text in a <label> so the entire row can activate the radio. Keep the question short and let each option label make sense on its own. The W3C grouping tutorial notes that different screen reader configurations can announce the legend at different times, so the legend and individual labels have different jobs.

<form action="/preferences" method="post">
  <fieldset class="radio-group">
    <legend>How should we send account updates?</legend>
    <p>Choose one option.</p>

    <div class="radio-group__options">
      <label class="radio-option">
        <input type="radio" name="updates" value="email" required>
        <span>Email me</span>
      </label>
      <label class="radio-option">
        <input type="radio" name="updates" value="text" required>
        <span>Send a text message</span>
      </label>
      <label class="radio-option">
        <input type="radio" name="updates" value="none" required>
        <span>Do not send me account updates</span>
      </label>
    </div>
  </fieldset>
  <button type="submit">Continue</button>
</form>

The nested-label pattern does not need id and for attributes. If your component renders labels separately, instead assign every radio a unique id and match it with exactly one for value. Avoid an option that is only a small clickable circle: clicking its words should select it too. MDN explains how associated labels expand the usable target.

Check: click the far end of each label row, not just the circle. Only that option should become checked, and the group question should remain visible above the answers. If clicking the words does nothing, inspect the nested label or its for/id association.

Diagram of grouped vertical radio options, one selected state, and arrow-key navigation

If you want to see the grouping idea explained visually before styling it, this short fieldset-and-radio tutorial covers the semantic relationship between the question and its answers.

3. Stack the rows with CSS, not line breaks

Put the rows in one layout container and let CSS set the spacing. Grid is convenient here because gap creates consistent separation without adding <br> tags or per-option margins. Keep the fieldset’s legend outside the grid so browser-specific fieldset layout behavior does not determine the option spacing.

.radio-group {
  min-inline-size: 0;
  margin: 0;
  padding: 1rem;
  border: 1px solid #8b95a5;
  border-radius: 0.5rem;
}

.radio-group legend { font-weight: 700; }
.radio-group p { margin: 0.5rem 0 0.75rem; }

.radio-group__options {
  display: grid;
  gap: 0.625rem;
}

.radio-option {
  display: grid;
  grid-template-columns: 1.25rem minmax(0, 1fr);
  align-items: start;
  gap: 0.75rem;
  min-inline-size: 0;
  padding: 0.65rem 0.75rem;
  border: 1px solid #8b95a5;
  border-radius: 0.4rem;
  cursor: pointer;
}

.radio-option input {
  margin: 0.2rem 0 0;
  accent-color: #174eaa;
}

.radio-option span { overflow-wrap: anywhere; }
.radio-option:focus-within { outline: 2px solid #174eaa; outline-offset: 2px; }

The option list now has one column at every viewport width; the text column is allowed to shrink rather than push the form sideways. For a simpler interface, display: block on each label and margins between labels can also stack rows. If each row has several visual pieces, use display: flex inside the label while keeping the list itself in a single column. Pick the least complicated layout that still handles long labels.

Check: narrow the browser until “Do not send me account updates” wraps. The radio should stay to the left of the first line and the second line should remain in the text column. If the form scrolls horizontally, look for fixed widths or a missing minmax(0, 1fr), not a missing line break.

4. Distinguish stacking options from aligning a circle with text

“Vertical radio buttons” can mean two different things: putting each choice on its own row, or vertically aligning one radio within a row whose label spans multiple lines. The outer grid solves the first problem. The two-column grid on .radio-option solves the second: the control gets a stable first column, and text wraps inside the second.

For top alignment, keep align-items: start and adjust the input’s small top margin for your actual font and line height. align-items: center may look fine with short labels but place the control halfway down a three-line description. Avoid absolute positioning unless you have a tested reason to remove the control from normal layout.

We recommend checking a long translated label as well as an English one. Remove any fixed height on a choice row, zoom the page, and confirm the text can expand without covering the radio, the focus indicator, or another answer. If an option needs a paragraph of explanation, consider a concise label with supporting description instead of forcing all of that copy into one unbroken line.

Desktop and mobile vertical radio groups showing readable spacing and alignment of a wrapped label

Check: compare the wide and narrow views of the same group. In both, each option has its own row, the selected circle remains visible, and any wrapped text belongs unmistakably to that circle. A layout that only looks correct at desktop width is not finished.

5. Preserve native keyboard selection and a visible focus state

Native <input type="radio"> elements already bring selection, focus, and form submission behavior. Tab into the group; use arrow keys to change the focused and selected option; press Space to select a focused option if needed; then Tab onward. The exact initial focus position when no option is checked can vary by browser and navigation direction. The W3C radio group pattern describes the expected interaction and calls out a native-browser difference for reverse Tab navigation.

Try the form with no mouse. The CSS above adds a group-row outline when a control receives focus, while the browser still draws the native radio’s checked state. Do not remove the input with display: none or replace it with a clickable div merely to achieve vertical spacing. A custom ARIA radio group also has to recreate keyboard and focus management that native controls already provide; W3C’s keyboard guidance makes that distinction explicit.

Check: press Tab until the group receives focus, press Down Arrow, and verify both the selected circle and visible focus move. Then press Tab to reach the Continue button. If every arrow press scrolls the page instead, confirm that focus is on a real radio rather than a decorative wrapper. If a design removes the browser’s focus outline elsewhere, restore a clear :focus-visible or :focus-within indication and check it in high-contrast modes.

6. Make required, disabled, and error states understandable

In the sample markup every option has required, so the group needs one checked answer before a normal form submission can succeed. Applying required consistently to all same-named options also makes the markup easier to maintain. MDN’s required-attribute documentation explains the group rule and recommends consistent use. Do not mark an option checked merely to avoid a validation message if a user should make an explicit choice.

If a single answer is unavailable, disable only that radio and say why in visible nearby text, such as “Text message, unavailable for this account.” A disabled control cannot be chosen through ordinary keyboard interaction. If the entire question is unavailable, a disabled fieldset disables its descendant form controls; check that the visual treatment also makes that state understandable. For a failed required choice, show a specific message such as “Choose how to receive account updates” beside the group. Make sure assistive technology can reach or hear that message, for example by associating it with the radios using aria-describedby when the message is shown, and by moving focus or announcing the error appropriately after submission.

The border color alone should not be the error message. Likewise, a selected-row background is optional, but the checked circle should remain apparent without color. The U.S. Web Design System treats each radio and its label as a distinct choice, which is a useful principle when you design states and feedback.

Check: try to submit with nothing selected and confirm an actionable error appears. Then select an answer and ensure the error clears. Temporarily disable one answer and test that the remaining choices still work. If validation passes with no selection, inspect the shared name and confirm the form has not disabled native validation with novalidate unintentionally.

7. Map the same contract from form data

When a form is generated, keep the semantics independent of the CSS. A definition can store the field name, question, requirement, and option values; the renderer can output a labelled radio group and apply a vertical class. For example, this framework-neutral configuration describes the desired contract, not a DOM Studio schema API:

const updatesQuestion = {
  name: 'updates',
  question: 'How should we send account updates?',
  required: true,
  orientation: 'vertical',
  options: [
    { value: 'email', label: 'Email me' },
    { value: 'text', label: 'Send a text message' },
    { value: 'none', label: 'Do not send me account updates' }
  ]
};

Your renderer should produce the same single group question and individual option labels as the HTML example. Derive stable unique IDs if it emits separate labels, use one native name for this question, keep the current selection as one value, and display group-level errors next to the question. orientation is a presentation choice, not permission to change those relationships. We keep the definition and the submitted values conceptually separate, as described in DOM Studio’s form schema documentation.

If your team uses DOM Studio’s Vue controls, the Radio Group playground lets you try the documented orientation: 'vertical', options, label, required, and invalid states. Treat that page as the component-specific next step; the HTML principles above are the checks to apply to the rendered result. Its orientation setting also describes arrow-key direction for the component, so test that behavior rather than assuming every component uses native browser interaction identically.

Check: render this question from data, choose “Send a text message,” and inspect the result. The state should contain one updates value of text; the visible group question, option labels, focus, and error treatment should still make sense. If the data is correct but the rendered group has no accessible question, fix the renderer rather than adding more spacing rules.

Frequently asked questions

Do vertical radio buttons need a fieldset border?

No. The fieldset and legend provide grouping and a question even when you remove the default border with CSS. Keep another clear visual separation between this question and adjacent form fields, and do not hide the legend simply to remove the border. For more on styling the native container, see our fieldset styling guide.

Should I use vertical-align: middle to make the options vertical?

No. The CSS vertical-align property does not create a one-column option list. Stack the labels with grid, block layout, or a column flex container. Adjust alignment inside each row separately when a long label wraps.

With this pattern in place, you have a single-choice control whose rows stack cleanly, whose question and labels stay connected, and whose selection can be tested with a keyboard. Next, try the completed markup at a narrow width and with a real validation error, then carry those same checks into your generated form renderer.