{"id":44,"date":"2020-07-19T16:10:37","date_gmt":"2020-07-19T16:10:37","guid":{"rendered":"\/blog\/?p=44"},"modified":"2025-06-23T20:19:58","modified_gmt":"2025-06-23T20:19:58","slug":"creating-accessible-styled-checkboxes","status":"publish","type":"post","link":"https:\/\/www.evinced.com\/blog\/creating-accessible-styled-checkboxes\/","title":{"rendered":"Creating accessible styled checkboxes"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A good user experience is a combination of an interface that catches the eye and clear, easy-to-operate functionality. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These two principles usually do not contradict each other, but sometimes they do &#8211; especially when it comes to elements where the styling cannot be changed directly, such as checkboxes and radio buttons. In such cases, developers often tend to use alternative elements with easier styling options, but without the required semantics. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The use of non-semantic alternative elements may cause screen readers and keyboard users to have a bad user experience or even prevent them from using it at all.\u00a0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this post, I will show how to style checkboxes while retaining their semantic values. At each step, I will point out the properties and values \u200b\u200bthat allow us to do it.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a bonus, I will show you how to extend the semantics and, based on the accessible checkboxes, how to create an accessible switch button.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What makes an accessible checkbox?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s start by mapping the factors that make checkboxes accessible.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>It has to be focusable so that keyboard-only users will be able to reach it.<\/li>\n\n\n\n<li>It must have a discernible focus indication to indicate the position on the focus sequence for keyboard-only users.<\/li>\n\n\n\n<li>It has to programmatically express its role, so that assistive technologies can read and announce it as a checkbox.<\/li>\n\n\n\n<li>It has to be properly labeled so it is understandable and interactable for assistive technology users.<\/li>\n\n\n\n<li>It has to express its state (checked\/unchecked).\u00a0\u00a0<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Bringing into practice<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The markup:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each checkbox will consist of 3 elements. A wrapping element, the checkbox and the element that will act as the presentation element.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>&lt;div class=\"checkbox-wrapper\"&gt;\n    &lt;input type=\"checkbox\" ...all other attributes \/&gt;\n    &lt;span&gt;&lt;\/span&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Heads up, the order of the elements inside \u201c<code>.checkbox-wrapper<\/code>\u201d matters. We are going to use the CSS pseudo class \u201c<code>:checked<\/code>\u201d to change the appearance of the <code>&lt;span&gt;<\/code> element. Since CSS does not have a selector for previous sibling(s), our only choice is referring to the next sibling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is another thing we should do here. Since the purpose of the span is purely decorative, we want assistive technologies to ignore it, so we give it an <code>aria-hidden<\/code> attribute with the value \u201c<code>true<\/code>\u201d.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>&lt;div class=\"checkbox-wrapper\"&gt;\n    &lt;input type=\"checkbox\"  ...all other attributes  \/&gt;\n    &lt;span aria-hidden=\"true\"&gt;&lt;\/span&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">We have the HTML setting for our checkbox (still not labeled), so now we would like to style it according to the design\u2019s guidelines, while preserving the native semantics and behaviours of the native HTML checkbox in order to keep it accessible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The CSS:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s start by styling the wrapper <code>&lt;div&gt;<\/code> (<code>.checkbox-wrapper<\/code>). In this example the wrapper div is defining the checkbox\u2019s dimensions and icon size; this is only a styling choice. The only property here that\u2019s essential to our purpose is the \u201c<code>position: relative;<\/code>\u201d attribute, it will allow us to position the checkbox input correctly and stack it on top of the span.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>.checkbox-wrapper {\n \tposition: relative;\n \tfont-size: 1rem;\n \theight: 3rem;\n \twidth: 3rem;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Next, we are going to style the <code>[type=\u201dcheckbox\u201d]<\/code> element.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We need the input element to be invisible since we want to present a styled version of it, but we also need it to be available to assistive technologies and to be able to respond to users\u2019 click (change its state).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set the width and height of the input to 100% so that it will take the full size of its parent.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set its position to be absolute so that we can stack it on top of the <code>&lt;span&gt;<\/code>. For the same purpose, set its <code>z-index<\/code> to \u201c<code>0<\/code>\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, set the \u201c<code>opacity<\/code>\u201d property to \u201c<code>0<\/code>\u201d. This will hide the checkbox visually, and still keep it on the accessibility and render trees for keyboards and screen readers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>.checkbox-wrapper &gt; &#91;type=\"checkbox\"] {\n      cursor: pointer;\n      height: 100%;\n \tleft: 0;\n  \tmargin: 0;\n \topacity: 0;\n \tposition: absolute;\n \ttop: 0;\n \twidth: 100%;\n \tz-index: 0;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The checkbox input is all set, now we can start to actually style it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The essentials for our purpose are:&nbsp;<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>position: relative;<\/code><\/strong> this is for 2 reasons &#8211;&nbsp; first, to be able to set its <code>z-index<\/code> to be lower than the checkbox so that we can be sure it won\u2019t block clicking on the checkbox, and, second, to position its \u201c<code>::after<\/code>\u201d pseudo element, which will contain the \u201ccheck\u201d sign.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>z-index: -1;<\/code> <\/strong>as mentioned above, we need the span to be stacked under the checkbox.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These <code>z-index<\/code> values are not binding but it is important to see to it that the <code>z-index<\/code> value of the span is lower than the <code>z-index<\/code> value of the input element.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rest of the properties are purely for appearance.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>.checkbox-wrapper &gt; &#91;type=\"checkbox\"] + span {\n    display: block;\n    position: relative;\n    z-index: -1;\n    height: 100%;\n    width: 100%;\n    border: 0.2rem solid #51c0a8;\n  }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the check sign, I chose to use the <code>&lt;span&gt;<\/code>s \u201c<code>::after<\/code>\u201d pseudo element. We want it to appear only when the checkbox is checked, so that we can use the \u201c<code>:checked<\/code>\u201d pseudo class selector.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>.checkbox-wrapper &gt; &#91;type=\"checkbox\"]:checked + span::after {\n\tcontent: \"\\2714\";\n\ttop: -0.2em;\n\tleft: 0.2em;\n\tcolor: #51c0a8;\n\tfont-size: 3em;\n\tposition: absolute;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">As long as you have added the <code>aria-hidden=\u201dtrue\u201d<\/code> the <code>&lt;span&gt;<\/code>, and as long as it has sufficient contrast from its background, this part will not affect accessibility, and it will only depend on your styling requirements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last thing we need to do is to ensure that we have a visual focus indication. Remember that the input is just hidden but it is still on the render and in the focus sequence, so we can use the \u201c<code>:focus<\/code>\u201d pseudo class in the same way we used \u201c<code>:checked<\/code>\u201d to change the appearance of the <code>&lt;span&gt;<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><button class=\"copy-code-button\" type=\"button\" aria-label=\"Copy code to clipboard\" aria-live=\"polite\">Copy<\/button><code>.checkbox-wrapper &gt; &#91;type=\"checkbox\"]:focus + span {\n\tbox-shadow: 1px 1px 2px 2px #51c0a8;\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">To summarize<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Markup:\n<ol class=\"wp-block-list\">\n<li>Use an input[type=\u201dcheckbox\u201d] for the functionality and a &lt;span> or &lt;div> for the presentation.<\/li>\n\n\n\n<li>Let the accessibility APIs ignore the presentational element by adding aria-hidden=\u201dtrue\u201d to it.<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>Styling:\n<ol class=\"wp-block-list\">\n<li>Let the checkbox be visually hidden, and keep it on the render and accessibility trees (opacity: 0) so that the advantages of its native semantics and accessibility are kept.<\/li>\n\n\n\n<li>Make sure that the checkbox is stacked on top of the styling element so that it catches the mouse\u2019s click events.<\/li>\n\n\n\n<li>Don\u2019t forget to handle the visual focus indication for the benefit of keyboard-only users.<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">See it on Codepen<\/h3>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_wvMqLWy\" src=\"\/\/codepen.io\/anon\/embed\/wvMqLWy?height=250&amp;theme-id=1&amp;slug-hash=wvMqLWy&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed wvMqLWy\" title=\"CodePen Embed wvMqLWy\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"e9af\">Switch Button<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\" id=\"3494\">See how to use the same approach to create accessible switch buttons.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_pogdobK\" src=\"\/\/codepen.io\/anon\/embed\/pogdobK?height=250&amp;theme-id=1&amp;slug-hash=pogdobK&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed pogdobK\" title=\"CodePen Embed pogdobK\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Thank you for reading!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A good user experience is a combination of an interface that catches the eye and clear, easy-to-operate functionality. These two principles usually do not contradict each other, but sometimes they do &#8211; especially when it comes to elements where the styling cannot be changed directly, such as checkboxes and radio buttons. In such cases, developers [&hellip;]<\/p>\n","protected":false},"author":11,"featured_media":1235,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[2],"tags":[],"class_list":["post-44","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-accessibility"],"acf":{"authors_to_show":[926]},"_links":{"self":[{"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/posts\/44","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/users\/11"}],"replies":[{"embeddable":true,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/comments?post=44"}],"version-history":[{"count":4,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/posts\/44\/revisions"}],"predecessor-version":[{"id":1320,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/posts\/44\/revisions\/1320"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/media\/1235"}],"wp:attachment":[{"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/media?parent=44"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/categories?post=44"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.evinced.com\/blog\/wp-json\/wp\/v2\/tags?post=44"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}