Under-Engineered Dependency Questions

A common interface pattern allows users to choose one item from a pre-defined set of choices, while still allowing them to add a custom selection if nothing else fits. Often these are radio buttons with one choice labeled “Other” that makes another field visible.

When new fields appear, things can get confusing for everyone. Developers in particular rely on their scripting libraries to control this. Developers who care about accessibility start to worry what ARIA attributes to use and how to manage focus. Yet all you need is HTML and CSS.

Unlike so many CSS-only solutions, which are usually accessibility nightmares because they do not manage focus or state or appropriate properties, this approach does not fall prey to that. In fact, the riskiest part of this approach is that your HTML is too complicated to use this.

Try this example:

See the Pen Under-Engineered Dependency Questions by Adrian Roselli (@aardrian) on CodePen.

Code

The first group of radio buttons has nothing selected by default. This is common, regardless of your (my) opinion that one must always be selected. There is a text box in the group, but is is not shown until the “Other” radio button is chosen. The text box presence relies on this piece of CSS:

#p5:not(:checked) ~ div {
  display: none;
}

HTML

Note that the HTML is a generally flat structure. Only one <div>, and it holds the content we want to hide. The part of the HTML that matters the most is this:

    <input type="radio" name="pet" id="p5" value="Other">
    <label for="p5">Other</label>
    <div>
      <label for="mypet">Your pet</label><br>
      <input type="text" name="mypet" id="mypet">
    </div>

Flexbox

The reason I can lean on CSS only is because I am using CSS flexbox to control the layout. The <input>s, <label>s, and the hiding <div> are all peers to one another. Flexbox ensures I get the layout I want, where each radio button sits alongside each label.

The following layout CSS does the trick for this demo, but you may need to tweak it for your use:

fieldset {
  display: flex;
  flex: 1 1 1em;
  flex-wrap: wrap;
}

fieldset input {
  flex: 0 1 1em;
}

fieldset label {
  flex: 1 1 calc(100% - 2em);
}

fieldset legend, fieldset > div {
  flex: 1 1 100%;
}

fieldset > div {
  margin-left: 1.5em;
}

The radio buttons are allowed to shrink (but not grow) and are otherwise set at 1em width (flex-basis). The labels can shrink and grow, and the calc() is a very basic calculation for the width of the radio buttons and the fieldset padding. With the flex-wrap: wrap, this means each line should only have a single radio and label.

The <div> and <legend> extend the full width, so they each get their own line. The <div> also gets a left margin to visually signal it is related to the radio button that precedes it.

In your own project you will likely want to tweak the values and units. If you have customized radio buttons, 1em may warrant some tweaking. The calc() may as well, especially if you have your own padding at play. For an internationalized site, replace margin-left with the logical property equivalent margin-inline-start.

Sibling Combinator

With that in place, you can now lean on the workhorse for this pattern, the general sibling combinator, or the ~ in the code. That lets me select the sibling <div> when the radio button is selected, even though there is a <label> sibling between them.

If your checkbox is not a peer to the <div> whose presence you are manipulating, then this pattern will not work. That means the standard <div>-soup common in the output of so many libraries (Bootstrap, Material, etc.) is working against you. If you cannot remove it, you might as well go read something more entertaining.

Pseudo-Class

The :checked pseudo-class allows you to apply styles when a radio button has been selected.

This applies whether you as the author add the checked attribute to the checkbox or the user activates it. Activation can be by keyboard, click, tap, voice, screen reader, and so on. The input modality is irrelevant. This is the benefit of using native HTML controls.

When we apply the :not() pseudo-class to wrap the :checked pseudo-class, we are ensuring only an unselected radio button would match.

ID Selector

Finally, we need to associate this overall selector with a specific radio button. Since the radio button should already have an id attribute, that work is done. In the pet example, #p5 is our ID selector and ensures only the single radio button is used for matching.

When we put together our ID selector with the two pseudo-classes and the general sibling combinator, we get #p5:not(:checked) ~ div.

Display

By hiding the <div> when the appropriate radio button is not checked, it is much easier to only manage one value for the display property. If you end up using CSS grid instead of flex, or some other values, this code will still work. All it does it make the <div> either not appear, or use whatever display it already has from the layout.

Focus

This pattern requires no focus management. Because the new field (or fields) to be displayed come after the radio button in the DOM, a keyboard user can move into and out of it without it confusing the flow. A screen reader user will also encounter it in the normal flow of the page.

Group Name

The new field has a <label>, so its accessible name is set. Because the new control appears within the same <fieldset> as the other radio buttons, it also has the group label context. Even if a screen reader user moves out of the group and comes back to the group via that field, the <legend> will be announced as it normally would.

ARIA

No ARIA is needed. There is no reason to have a live region since it appears next in the page flow. There is no state to be conveyed, beyond the radio button’s state, which is conveyed by using a native radio button. The other field(s) or content is either there or not.

Form Submission

If the user filled out the pet field and then realized they had a dog all along, the now-hidden pet field’s value will still be sent to the server. You can see this in the query string when you submit the form in the debug view: ?pet=Dog&mypet=Hammer

Whether you do your processing on the server or client, if you see that pet has any value other than Other, you can discard mypet and its value.

Caveats

For more complex controls to hide or display additional fields, or complex logic on when to display new fields, this approach may not work.

If your HTML is anything other than basic, meaning your radio buttons are not at the same level as the <div> you want to hide, this entire pattern falls down. I noted it above, but it bears repeating again.

Test this with your users. It may have performed great for me and the context of the screens where I tested it, but your screens may not be a fit because of other patterns or expectations already in play.

A potential feature is that you can use this code with other form fields. Checkboxes can work with this, for example. However, you will need to build and test that pattern to be certain. Don’t use mine as a catch-all.

Updated for :has(), 29 June 2023

I have updated the pattern to account for a less flat structure by leaning on the CSS pseudo-class :has() (MDN, W3C). I waited this long because I wanted better support (sometimes the process shakes out bugs), but I got tired of waiting for Firefox.

Live example (editable version, cruft-free version):

See the Pen Under-Engineered Dependency Questions with :has() by Adrian Roselli (@aardrian) on CodePen.

This example does not use any CSS layout (no flex, no grid, no floats, etc.). Instead it minimally replicates the nested <div> structure common to most libraries and frameworks.

For browsers that support :has(), the CSS would look like this:

:has(#p5:not(:checked)) ~ div {
  display: none;
}

Essentially I wrap the first part of the original selector (up to the general sibling combinator) in :has() and call it a day.

2 Comments

Reply

Was just watching you on Ben Myers’ Youtube channel and you noted that nobody has commented on this post. Came over to make sure it’s not lonely.

Also, you said people have asked you why you don’t do any focus management. Wouldn’t moving the focus on radio button selection be a failure of WCAG 3.2.2 anyway?

(and yes I know you can get around that by warning the user of the behavior… but blah to that)

In response to James Catt. Reply

Because neither the viewport, user agent, focus, nor content that changes the meaning of the page occurs, this is fine under 3.2.2.

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>