skip to content

In CSS, what does the :checked pseudo-class match, and how can it drive styling elsewhere on the page without JavaScript?

level: juniorimportance: should knowfreq 50%

answer

  1. state, not the source attribute
  2. attribute selector matches only the initial value
  3. reach sideways with a sibling combinator
  4. hide the control without removing it
  5. third state exists and is not checked

basics

~20 s

:checked matches checkboxes and radio buttons that are currently checked and option elements that are selected. It tracks live state, not the source attribute, so pairing it with a sibling combinator lets a toggle restyle nearby elements with no script.

solid answer

~40 s

`:checked` matches a checkbox or radio button in the checked state and an `option` that is selected. The key detail is that it reflects the control's **current** state, not the attribute written in the source — toggling the control in the browser changes what matches immediately, even though the markup is untouched. Because CSS can look sideways with the sibling combinators, that state can drive appearance elsewhere: `input:checked + label` styles the label of the checked control, and `input:checked ~ .panel` reveals a panel later in the same parent. That is the basis of the JS-free custom checkbox, where the real control is visually hidden but still focusable and a styled element next to it renders the box. Its companions are `:disabled` and `:enabled` for interactivity state, and `:indeterminate` for the third checkbox state.

code

html · 15 lines
html
<label class="toggle">
  <input type="checkbox">
  <span class="track"></span>
  Enable notifications
</label>
<style>
  .toggle input { position: absolute; opacity: 0; }
  .toggle .track {
    display: inline-block; width: 2.5rem; height: 1.25rem;
    background: #ccc; border-radius: 999px;
  }
  .toggle input:checked + .track { background: #0b5fff; }
  .toggle input:focus-visible + .track { outline: 2px solid #0b5fff; }
  .toggle input:disabled + .track { opacity: 0.5; }
</style>

go deeper

for a junior

Be able to say that :checked reflects the control's current state and to write input:checked + label for a JS-free toggle. Know that the attribute selector [checked] is not the same thing.

for a middle

Explain why the pattern depends on sibling combinators and source order, and why the input must be visually hidden rather than removed from the layout. Name the neighbouring state pseudo-classes :disabled, :indeterminate, and :required.

for a senior

Show where the technique stops: CSS-only state cannot be read back or persisted, and markup ordered for selector reach is fragile. Argue for using it for the presentational half of a real control and nothing more.

for a principal

Own the boundary as policy — decide what belongs in CSS state versus application state across a design system, and make the accessible toggle a shared component so no team re-derives the hidden-input pattern incorrectly.

## What it matches `:checked` is a state pseudo-class from the form family. It matches: - a checkbox input that is currently checked, - a radio input that is currently selected within its group, - an `option` element that is currently selected. The crucial word is *currently*. A pseudo-class describes state, not markup. If the source says the box is checked and the user unchecks it, `:checked` stops matching, even though the attribute in the source is unchanged. This is the same distinction that makes `[checked]` — an attribute selector — the wrong tool: it matches the initial attribute and does not follow the user. ```css input[type="checkbox"]:checked { accent-color: rebeccapurple; } ``` ## Driving styles elsewhere On its own, `:checked` can only style the control. Combined with a sibling combinator it reaches other elements in the same parent: ```css /* the label immediately after the checked control */ input:checked + label { font-weight: 700; } /* any later sibling with .panel, once the control is checked */ input:checked ~ .panel { display: block; } ``` This is the mechanism behind the JS-free toggle. The pattern places the real control and the thing you want to style as siblings, hides the control visually while keeping it focusable and operable, and paints a decorative element from the checked state: ```css .toggle input { position: absolute; opacity: 0; /* still focusable and clickable via the label */ } .toggle .track { background: #ccc; transition: background 120ms; } .toggle input:checked + .track { background: #0b5fff; } .toggle input:focus-visible + .track { outline: 2px solid #0b5fff; } ``` Two cautions come with it. First, hide the control with `opacity: 0` or a clip-based visually-hidden helper rather than `display: none` or `visibility: hidden` — those remove it from the interaction and accessibility trees entirely, which breaks keyboard operation. Second, remember the reach is limited by the combinators available: `+` and `~` only see **later siblings**, so the styled element must come after the control in source order and share its parent. That constraint is why so many of these patterns have a peculiar DOM order. ## The rest of the state family `:checked` sits in a group of pseudo-classes that reflect form-control state, and interviewers often ask for its neighbours: - `:disabled` / `:enabled` — whether the control accepts interaction. `:disabled` is the everyday way to grey out a control and its label without adding a class. - `:indeterminate` — the third checkbox state, set only from script, plus a radio group with nothing selected and a `progress` element with no value. It is neither checked nor unchecked, and `:checked` does not match it. - `:read-only` / `:read-write` — whether the value can be edited. - `:required` / `:optional` — whether a value must be supplied. - `:default` — the control that was the default in its form. ```css input:disabled + label { color: #999; cursor: not-allowed; } input[type="checkbox"]:indeterminate + .box::after { content: "–"; } ``` ## Where the technique stops CSS-only state is charming and genuinely useful for small toggles — a details disclosure, a tab strip, a custom checkbox — but it has a hard ceiling. The state lives in a control, so it is only as expressive as that control: a boolean per checkbox, one-of-N per radio group. Anything that must persist, be read back, or coordinate with data belongs in application state. And because the visual result depends on source order and sibling relationships, the markup becomes fragile to reorder. State a preference clearly in an interview: use `:checked` for the presentational half of a real control, not to reimplement application logic in selectors. ## A related trick worth knowing `:target` matches the element whose id equals the current URL fragment, giving a second source of JS-free state — click a link to `#panel-2` and `#panel-2:target { display: block }` reveals it. Unlike `:checked` it survives a page reload and is shareable as a URL, but it is limited to one active target at a time and it moves the scroll position.

  • Why not select the checked box with input[checked] instead?
    `[checked]` is an attribute selector: it matches the attribute present in the source and does not follow user interaction, so a box the user unchecks still matches and a box they check does not. `:checked` reflects the control's live state, which is what any interactive styling needs.
  • What breaks if you hide the real checkbox with display: none?
    The control leaves the interaction and accessibility trees, so it can no longer be focused by keyboard or exposed to assistive technology — the toggle becomes mouse-only. Hide it with `opacity: 0` plus absolute positioning, or a clip-based visually-hidden utility, so it still receives focus and remains operable.
  • Can :checked style an element that appears before the input in the DOM?
    Not with `+` or `~`, which only reach later siblings, so the classic pattern requires the styled element to follow the control. Reaching backwards or upwards needs the relational selector, which is a different tool with a different support and performance story — the pragmatic answer here is to reorder the markup.
  • What is :indeterminate, and does :checked match it?
    It is the checkbox's third state, set only from script, and it also covers a radio group with nothing selected and a valueless `progress`. `:checked` does not match it — the control is neither checked nor unchecked — so a tri-state checkbox needs both selectors to be styled completely.

saying these in an interview costs you the question

  • Says :checked matches the checked attribute in the markup
  • Hides the input with display: none and breaks keyboard use
  • Thinks :checked can style an earlier sibling with +
  • Believes :indeterminate is a form of checked
  • Uses :checked to model real application state

context