Search topics
Custom checkboxes that keyboards and screen readers skip
A sign-up form draws its own rounded checkboxes and hides the real ones with display: none, so a mouse can tick them but Tab, Space and screen readers never find them.
input[type="checkbox"] { display: none; } The problem
Keyboard, switch and screen reader users can't reach or tick the boxes, so they can't agree to the terms or create an account.
A sign-up form is drawn with its own checkboxes: rounded boxes with a white tick on violet, the brand's colour, because the browser's own box looks different in every browser and matches nothing else on the page. There are two: "Email me about offers", and a required "I agree to the terms" above the Create account button. The developer builds what was drawn. Each label holds the real checkbox, hidden with display: none, and a span drawn as the box, which shows the tick when the hidden checkbox is checked. Click either line and the box ticks, because clicking a label ticks its checkbox even when the checkbox can't be seen. With a mouse, the form looks right and works.
Put the mouse aside and press Tab from the email field. Focus goes straight to Create account: both boxes are skipped. There is nothing for Space to tick. A screen reader reads "I agree to the terms" as a line of text, with no checkbox and no state. Press Create account and nothing happens: the terms box is required, so the browser refuses to send the form, but it can't move focus to a box that isn't there. Chrome shows no message at all; its only trace is a line in the developer console: "An invalid form control with name='terms' is not focusable." A keyboard user can't create an account at all.
display: none takes the checkbox out of the page entirely: out of the Tab order, and out of the accessibility tree. What is left on screen is a drawing of a checkbox. A mouse can still reach the function through the label, but the keyboard can't, which fails 2.1.1 Keyboard. And nothing that looks like a checkbox is exposed as one, with a name, a role and a checked state, which fails 4.1.2 Name, Role, Value; that the line is a choice someone can tick is shown only by the drawing, not in the code, which fails 1.3.1 Info and Relationships too.
The same drawing is built in other ways that fail the same way:
- The checkbox hidden with
visibility: hiddenor thehiddenattribute instead: both also remove it from the Tab order and the accessibility tree, and a click on the label still ticks it. - A
divdrawn as a checkbox, with a click handler and no input at all. Nothing can reach it from the keyboard, nothing announces it, and nothing is sent with the form unless a script adds it. - Radio buttons drawn as option cards, such as delivery speeds, with the radios hidden. They lose Tab, and the arrow keys that move the choice within a group as well.
Sign-up, checkout and settings pages are where the required choices are: agreeing to the terms, confirming an age, choosing a delivery option. When only a mouse can tick them, the form ends at that box, and nothing on screen says why.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Limited manipulation Keyboard and switch users can't reach either box: Tab goes from the email field straight to Create account, and Space has nothing to tick. With the terms box required, they can't create an account at all.
- Without vision Screen readers don't find a checkbox. "I agree to the terms" is read as a line of plain text, with no box and no state, and Create account then does nothing and says nothing.
A hidden input is out of reach of most checks: axe tests what is rendered, so it skips the display: none checkbox, and the label beside it is just text with nothing wrong in it. The failure is in what is missing, and a scan of what is there finds nothing. EaseWeb's audit reports a checkbox or radio button that is hidden while its label is shown, as ew:hidden-native-input. When asked to fix it, AI coding tools can add tabindex="0" to the label or the drawn box: Tab then stops there, but Space doesn't tick a label, and nothing announces a checkbox or its state. Swapping display: none for visibility: hidden changes nothing. Put the mouse aside and try the form.
Try it
Press Tab to move through the sign-up form, Space to tick each box, then Enter on Create account.
With a mouse, both boxes tick. The panel shows what a screen reader hears as focus moves. Can you agree to the terms without a mouse?
Broken Can you fix this?
Screen reader says:
(Tab through the form)
The fix
Keep the real input on the page: draw the box on it, put it over the box, or colour it with accent-color.
- 1Keep the real checkbox or radio button on the page, in the Tab order and in the accessibility tree.
- 2Draw the tick and the focus ring from the input's own checked and focus states.
- 3Show checked with a tick or a dot, not by colour alone.
<form action="/sign-up" method="post"> … <label class="check"> <input type="checkbox" name="offers"> <span class="box"></span> Email me about offers </label> <label class="check"> <input type="checkbox" name="terms" required> <span class="box"></span> I agree to the terms </label> <button type="submit">Create account</button></form>The box drawn on the input itself
<form action="/sign-up" method="post"> … <label class="check"> <input type="checkbox" name="offers"> Email me about offers </label> <label class="check"> <input type="checkbox" name="terms" required> I agree to the terms </label> <button type="submit">Create account</button></form>Why this fix: Use native HTML first
A click on the label still ticks the hidden checkbox, so the box is drawn ticked, but display: none keeps the checkbox out of the Tab order and the accessibility tree. The keyboard can't reach it, and a screen reader doesn't know it is there.
appearance: none removes the browser's drawing and leaves the checkbox itself, so the CSS draws the same rounded box and tick on it. Nothing is hidden: Tab stops on the box, Space ticks it, a screen reader announces "I agree to the terms, checkbox, not checked, required", and the browser can move focus to the box to say it is required. The tick and the ring come from the checkbox's own :checked and :focus-visible, so what is drawn can't drift from what is announced.
All three pass. Draw the box on the input itself unless you have a reason not to: it is one element, so nothing is hidden, there is no second element to place over the box or keep beside it, and the browser's focus, its touch position and its form behaviour stay with the box people see. That is the reason to recommend it for nearly every custom checkbox. Keep the drawn box with the input over it when the box has to hold what an input can't, such as an icon or a tick drawn in SVG, or when the component's markup can't change; then the box has to stay right after the checkbox, and the checkbox over the box. Use accent-color when the brand needs only its colour on the box, or as the first step on a large site: it removes the failure everywhere at once, and the drawn box can follow.
Radio buttons take the same three fixes. Draw the circle on the input with appearance: none and border-radius: 50%, with its dot drawn from :checked; or keep a drawn circle or card after a radio hidden only from sight; or colour the browser's radio with accent-color. Put each group in a fieldset with a legend, and give its radios one name: Tab then stops on the group once, and the arrow keys move the choice, which is what a group of hidden radios loses. For option cards, put the whole card inside the label, so a click anywhere on it chooses it, and keep the radio's circle visible in the card.
In a Windows contrast theme, the browser replaces fills with the page's colour and removes box shadows. The tick here is drawn with borders, which the browser redraws in a colour from the user's palette, so a ticked box keeps its tick even though its violet fill is gone. A tick or a dot drawn as a filled shape, with background-color and a clip-path or a mask, disappears with the fill: give it background-color: CanvasText inside @media (forced-colors: active). Draw the focus ring with outline, not box-shadow, for the same reason.
Keep required on the real checkbox. With the checkbox on the page, the browser can move focus to it and say what is missing when the form is sent; with it hidden, there is nowhere to put the message. If the form shows its own error messages, tie them to the checkbox, not to the drawn box.
Component libraries and themes often ship the hidden input. Check the rendered markup, not the component's name: a checkbox with display: none or hidden beside a styled label fails, whatever the component is called. A menu opened by a hidden checkbox and its label, the CSS-only "checkbox hack", fails the same way; build it as a button that says whether it is open, as show and hide buttons that don't say whether they are open describes.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Design ideas
A checkbox can look like anything, as long as the real one is still there.
Six ways sites draw checkboxes and radio buttons. Click them, then put the mouse aside and try them with Tab, Space and the arrow keys: four pass, one is skipped by Tab, and one shows the choice only by a tint.
In the design file
- Name each drawn box, chip, swatch or card a checkbox or a radio button in the design file, so it is built on the real one.
- Draw every option with its focus ring, as it looks when reached with Tab.
- Show checked and chosen with a tick, a dot or a ring as well as colour, and check them in greyscale.
- Write the ratio of the box’s outline against the page beside it, at least 3:1. The example’s grey outline is 4.83:1.
- Draw the options in Windows high-contrast mode, where fills disappear and the tick, the dot and the outline must still show.
Passes
- Filter chips that gain a tick Off, an outlined pill with a #6b7280 edge, 4.83:1; on, a violet edge, a pale tint and a tick before the word, in #3730a3, 8.88:1 on the tint. The tick says on without the colour, and the whole chip is the checkbox, so a click, a tap or Space anywhere on it works.
- Delivery options as cards, the radio showing Each card is one radio button, and a click anywhere on it chooses it. The circle stays in the corner, its 4.83:1 edge filled with a violet dot when chosen, so the choice reads without the tint; the chosen card also takes a violet edge. Tab stops on the group once, and the arrow keys move between the cards.
- Colour swatches with a ring and a tick Round swatches that are radio buttons, the chosen one ringed in #111827, 17.74:1 on white, with a tick inside and its name written after “Colour”. People who can’t tell sage from navy read the name; the arrow keys move between the colours.
- A round to-do check that draws its tick Ticked, the circle fills violet and a white tick draws itself in 0.3 seconds, then stops, and the task is struck through in #6b7280. The tick and the line say done in greyscale; with reduced motion, the tick simply appears.
Rule of thumb: Move to show a change, then stop
Fails
- Size tiles with the radios hiddenWCAG 2.1.1 S, M, L and XL as outlined tiles, the chosen one filled #111827. A click picks a size, but the radios behind them are hidden, so Tab goes straight past all four and a screen reader hears only the letters. Try it with Tab.
- Plan cards chosen by a tint aloneWCAG 1.4.1 The radio buttons are real, so the keyboard works, but nothing is drawn for them: the chosen plan is shown only by a #eef2ff tint, 1.12:1 against the white card beside it. In greyscale, in high-contrast mode or on a washed-out screen, both cards look the same.
Verify the fix
5 checks.
Test with: Keyboard, Screen reader, Mouse, Forced colors, Touch
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared form components or the theme change.
Limits
This is a starting pattern, not a guarantee for every application. The CSS is a sketch: check the size of the box, the contrast of its border and the focus ring against your own palette. A rebuilt checkbox with ARIA and a wrong state is a different failure, covered in invalid ARIA that misleads screen readers; a group of options with no question tied to it is covered in radio buttons and checkboxes without a group name; and a checkbox drawn as a switch in toggle switches that don't say whether they are on. Test the whole form with a keyboard, a screen reader and a contrast theme.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- WCAG Understanding: info-and-relationships (opens in a new tab)
- developer.mozilla.org: appearance (opens in a new tab)
- developer.mozilla.org: accent-color (opens in a new tab)
- developer.mozilla.org: forced-colors (opens in a new tab)
- sarasoueidan.com: inclusively-hiding-and-styling-checkboxes-and-radio-buttons (opens in a new tab)
- ARIA APG: radio (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.