{"id":4677,"date":"2024-12-05T11:20:50","date_gmt":"2024-12-05T16:20:50","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=4677"},"modified":"2024-12-05T11:20:51","modified_gmt":"2024-12-05T16:20:51","slug":"multi-state-buttons","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/multi-state-buttons\/","title":{"rendered":"Multi-State Buttons"},"content":{"rendered":"\n<p>There are traditional ways for a user to pick one-option-from-many. The classics beeing a <code>&lt;select><\/code> or a group of <code>&lt;input type=\"radio\"><\/code> elements.<\/p>\n\n\n\n<p>But it&#8217;s nice to have more options. Sometimes when a user must choose one option from many, it\u2019s nice to have a single element that switches between available option on a quick click. A practical example of such a singular UI is a tag control that transitions through various states on each click. Any given tag in an interface like this could be be in <em>three<\/em> different states:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Disregarded in search results (default state)<\/li>\n\n\n\n<li>Search results <strong>must include<\/strong> tag<\/li>\n\n\n\n<li>Search results <strong>must exclude<\/strong> tag<\/li>\n<\/ol>\n\n\n\n<p>Here&#8217;s an image example of such a UI:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"632\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?resize=1024%2C632&#038;ssl=1\" alt=\"\" class=\"wp-image-4679\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?resize=1024%2C632&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?resize=300%2C185&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?resize=768%2C474&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?resize=1536%2C947&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?w=1628&amp;ssl=1 1628w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The Plan<\/h2>\n\n\n\n<p>We\u2019ll be coding such a control using a set of <em>stacked<\/em> HTML radio buttons. <\/p>\n\n\n\n<p>The UI\u2019s functionality \u2014 jumping through different states on each click \u2014 is implemented by a bit of CSS-only trickery. We&#8217;ll be changing the value of the CSS property\u00a0<code>pointer-events<\/code>\u00a0in the radio buttons when one is selected.<\/p>\n\n\n\n<p>The\u00a0<code>pointer-events<\/code>\u00a0property when applied to HTML elements determines whether a pointer event, such as a <code>click<\/code> or <code>hover<\/code> \u2014 through mouse pointer, touch event, stylus usage, etc \u2014 occurs on an element or not. By default, the events do occur in the elements, which is equivalent to setting <code>pointer-events: auto;<\/code>. <\/p>\n\n\n\n<p>If\u00a0<code>pointer-events: none;<\/code> is set, that element won\u2019t receive any pointer events. This is useful for stacked or nested elements, where we might want a top element to ignore pointer events so that elements below it become the target.<\/p>\n\n\n\n<p>The same will be used to create a multi-state control in this article. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Basic Demo<\/h2>\n\n\n\n<p>Below is a basic control we\u2019ll be coding towards to demonstrate the technique. I\u2019ll also include a Pen for the movie tags demo, shown before, at the end.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_yLmwMww\" src=\"\/\/codepen.io\/anon\/embed\/yLmwMww?height=450&amp;theme-id=47434&amp;slug-hash=yLmwMww&amp;default-tab=css,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed yLmwMww\" title=\"CodePen Embed yLmwMww\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"control\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"three\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> \/&gt;<\/span>\n    Third state\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"two\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> \/&gt;<\/span>\n    Second state\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"one\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"radio\"<\/span> <span class=\"hljs-attr\">checked<\/span> \/&gt;<\/span>\n    First state\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.control<\/span> {\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100px<\/span>;\n    <span class=\"hljs-attribute\">line-height<\/span>: <span class=\"hljs-number\">100px<\/span>;\n    label {\n        <span class=\"hljs-attribute\">width<\/span>: inherit;\n        <span class=\"hljs-attribute\">position<\/span>: absolute; \n        <span class=\"hljs-attribute\">text-align<\/span>: center;\n        <span class=\"hljs-attribute\">border<\/span>: <span class=\"hljs-number\">2px<\/span> solid;\n        <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">10px<\/span>;\n        <span class=\"hljs-attribute\">cursor<\/span>: pointer;\n        input {\n            <span class=\"hljs-attribute\">appearance<\/span>: none;\n            <span class=\"hljs-attribute\">margin<\/span>: <span class=\"hljs-number\">0<\/span>;\n        }\n    }\n    <span class=\"hljs-selector-class\">.one<\/span> {\n        <span class=\"hljs-attribute\">pointer-events<\/span>: none;\n        <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">247<\/span> <span class=\"hljs-number\">248<\/span> <span class=\"hljs-number\">251<\/span>);\n        <span class=\"hljs-attribute\">border-color<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">199<\/span> <span class=\"hljs-number\">203<\/span> <span class=\"hljs-number\">211<\/span>); \n    }\n    <span class=\"hljs-selector-class\">.two<\/span> {\n        <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">228<\/span> <span class=\"hljs-number\">236<\/span> <span class=\"hljs-number\">248<\/span>);\n        <span class=\"hljs-attribute\">border-color<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">40<\/span> <span class=\"hljs-number\">68<\/span> <span class=\"hljs-number\">212<\/span>); \n    }\n    <span class=\"hljs-selector-class\">.three<\/span> {\n        <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">250<\/span> <span class=\"hljs-number\">230<\/span> <span class=\"hljs-number\">229<\/span>);\n        <span class=\"hljs-attribute\">border-color<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">231<\/span> <span class=\"hljs-number\">83<\/span> <span class=\"hljs-number\">61<\/span>);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In HTML shown above, there are three\u00a0<code>&lt;input><\/code>\u00a0radio buttons (for three states), which are nested within their respective\u00a0<code>&lt;label><\/code>\u00a0elements.<\/p>\n\n\n\n<p>The label elements are stacked over each other within the parent&nbsp;<code>&lt;div&gt;<\/code>&nbsp;element (<code>.control<\/code>), sharing the same dimensions and style. The default appearance of the radio buttons is removed. Naturally, the label elements will trigger the check\/uncheck of the radio buttons within them.<\/p>\n\n\n\n<p>Each label is colored differently in CSS. By default, the topmost label (<code>.one<\/code>) is checked on page load for having the&nbsp;<code>checked<\/code>&nbsp;HTML attribute. In CSS, its&nbsp;<code>pointer-events<\/code>&nbsp;property is set to&nbsp;<code>none<\/code>.<\/p>\n\n\n\n<p>Which means when we click the control, the topmost label isn\u2019t the target anymore. Instead, it clicks the label below it and checks its radio button. Since only one radio button in a group with the same name attribute can be checked at a time, when the bottom label is checked, its radio button unchecks the topmost label\u2019s. Consequently, the control transitions from its first to second state.<\/p>\n\n\n\n<p>That\u2019s the basis of how we\u2019re coding a multi-state control. Here\u2019s how it\u2019s programmed in the CSS for all the labels and, consequently, their radio buttons:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">label<\/span><span class=\"hljs-selector-pseudo\">:has(<\/span><span class=\"hljs-selector-pseudo\">:checked)<\/span> {\n    ~ label {\n        <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n    }\n    &amp;<span class=\"hljs-selector-pseudo\">:is(<\/span><span class=\"hljs-selector-pseudo\">:not(<\/span><span class=\"hljs-selector-pseudo\">:first-child))<\/span> {\n        <span class=\"hljs-attribute\">pointer-events<\/span>: none;\n        ~ label { <span class=\"hljs-attribute\">pointer-events<\/span>: none; }\n    }\n    &amp;<span class=\"hljs-selector-pseudo\">:is(<\/span><span class=\"hljs-selector-pseudo\">:first-child)<\/span> {\n        ~ label { <span class=\"hljs-attribute\">pointer-events<\/span>: auto; }\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When a label\u2019s radio button is checked, the following labels in the source code are hidden with&nbsp;<code>opacity: 0<\/code>&nbsp;so that it alone is visible to the user.<\/p>\n\n\n\n<p>If a checked radio button\u2019s label isn\u2019t the first one in the source code (bottom-most on screen), it and the labels after it get&nbsp;<code>pointer-events: none<\/code>. This means the label underneath it on the screen becomes the target of any following pointer events.<\/p>\n\n\n\n<p>If the checked radio button\u2019s label is the first one in the source code (bottom-most on screen), all the labels after it get the&nbsp;<code>pointer-events<\/code>&nbsp;value&nbsp;<code>auto<\/code>, allowing them to receive future pointer events. This resets the control.<\/p>\n\n\n\n<p>In a nutshell, when a user selects a state, the following state becomes selectable next by giving the current and all previously selected states&nbsp;<code>pointer-events: none<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Usage Warning<\/h2>\n\n\n\n<p>Although this method is applicable to any number of states, I would recommend limiting it to three for typical user controls like tags, unless it\u2019s a fun game where the user repeatedly clicks the same box and sees something different each time. Additionally, it\u2019s apt to consider whether keyboard navigation is to be supported or not. If it is, it would be more practical to adopt a user experience where users can see all reachable options using the tab and navigation keys, rather than showing a single UI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Advanced Demo<\/h2>\n\n\n\n<p>Below is a prototype for a tag cluster composed of three-state tags designed to filter movie search results based on genres. For instance, if a user wants to filter for comedy movies that are not action films, they can simply click on comedy once to include it and on action twice to exclude it. If you\u2019re curious about how the counts of included and excluded tags are calculated in the demo below, refer to the list under the Further Reading section.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_yLmwMZZ\" src=\"\/\/codepen.io\/anon\/embed\/yLmwMZZ?height=650&amp;theme-id=47434&amp;slug-hash=yLmwMZZ&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed yLmwMZZ\" title=\"CodePen Embed yLmwMZZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"further-reading\">Further Reading<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/pointer-events\">MDN on <code>pointer-events<\/code><\/a><\/li>\n\n\n\n<li>MDN on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTML\/Element\/input\/radio#defining_a_radio_group\">defining a radio group<\/a><\/li>\n\n\n\n<li>CSS-Tricks on <a href=\"https:\/\/css-tricks.com\/counting-css-counters-css-grid\/\">Counting With CSS Counters and CSS Grid<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Typically, buttons are either pressed or they aren&#8217;t. But as long as you handle it accessibly, you can make a group of radio inputs look like a multi-state button with some CSS trickery.<\/p>\n","protected":false},"author":20,"featured_media":4679,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[93,7,264,166,265],"class_list":["post-4677","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-buttons","tag-css","tag-inputs","tag-search","tag-tags"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/12\/s_2B6564E11D1E51AC46777C57EB87ECE5CDB861925A4A306B5E22E667A57CB6A7_1732966272424_tagcloud.png?fit=1628%2C1004&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4677","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=4677"}],"version-history":[{"count":7,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4677\/revisions"}],"predecessor-version":[{"id":4689,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4677\/revisions\/4689"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/4679"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=4677"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=4677"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=4677"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}