Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 05 Dec 2024 16:20:51 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Multi-State Buttons https://frontendmasters.com/blog/multi-state-buttons/ https://frontendmasters.com/blog/multi-state-buttons/#comments Thu, 05 Dec 2024 16:20:50 +0000 https://frontendmasters.com/blog/?p=4677 There are traditional ways for a user to pick one-option-from-many. The classics beeing a <select> or a group of <input type="radio"> elements.

But it’s nice to have more options. Sometimes when a user must choose one option from many, it’s 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 three different states:

  1. Disregarded in search results (default state)
  2. Search results must include tag
  3. Search results must exclude tag

Here’s an image example of such a UI:

The Plan

We’ll be coding such a control using a set of stacked HTML radio buttons.

The UI’s functionality — jumping through different states on each click — is implemented by a bit of CSS-only trickery. We’ll be changing the value of the CSS property pointer-events in the radio buttons when one is selected.

The pointer-events property when applied to HTML elements determines whether a pointer event, such as a click or hover — through mouse pointer, touch event, stylus usage, etc — occurs on an element or not. By default, the events do occur in the elements, which is equivalent to setting pointer-events: auto;.

If pointer-events: none; is set, that element won’t 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.

The same will be used to create a multi-state control in this article.

Basic Demo

Below is a basic control we’ll be coding towards to demonstrate the technique. I’ll also include a Pen for the movie tags demo, shown before, at the end.

<div class="control">
  <label class="three">
    <input type="radio" name="radio" />
    Third state
  </label>

  <label class="two">
    <input type="radio" name="radio" />
    Second state
  </label>

  <label class="one">
    <input type="radio" name="radio" checked />
    First state
  </label>
</div>
.control {
    width: 100px;
    line-height: 100px;
    label {
        width: inherit;
        position: absolute; 
        text-align: center;
        border: 2px solid;
        border-radius: 10px;
        cursor: pointer;
        input {
            appearance: none;
            margin: 0;
        }
    }
    .one {
        pointer-events: none;
        background: rgb(247 248 251);
        border-color: rgb(199 203 211); 
    }
    .two {
        background: rgb(228 236 248);
        border-color: rgb(40 68 212); 
    }
    .three {
        background: rgb(250 230 229);
        border-color: rgb(231 83 61);
    }
}

In HTML shown above, there are three <input> radio buttons (for three states), which are nested within their respective <label> elements.

The label elements are stacked over each other within the parent <div> element (.control), 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.

Each label is colored differently in CSS. By default, the topmost label (.one) is checked on page load for having the checked HTML attribute. In CSS, its pointer-events property is set to none.

Which means when we click the control, the topmost label isn’t 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’s. Consequently, the control transitions from its first to second state.

That’s the basis of how we’re coding a multi-state control. Here’s how it’s programmed in the CSS for all the labels and, consequently, their radio buttons:

label:has(:checked) {
    ~ label {
        opacity: 0;
    }
    &:is(:not(:first-child)) {
        pointer-events: none;
        ~ label { pointer-events: none; }
    }
    &:is(:first-child) {
        ~ label { pointer-events: auto; }
    }
}

When a label’s radio button is checked, the following labels in the source code are hidden with opacity: 0 so that it alone is visible to the user.

If a checked radio button’s label isn’t the first one in the source code (bottom-most on screen), it and the labels after it get pointer-events: none. This means the label underneath it on the screen becomes the target of any following pointer events.

If the checked radio button’s label is the first one in the source code (bottom-most on screen), all the labels after it get the pointer-events value auto, allowing them to receive future pointer events. This resets the control.

In a nutshell, when a user selects a state, the following state becomes selectable next by giving the current and all previously selected states pointer-events: none.

Usage Warning

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’s a fun game where the user repeatedly clicks the same box and sees something different each time. Additionally, it’s 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.

Advanced Demo

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’re 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.

Further Reading

]]>
https://frontendmasters.com/blog/multi-state-buttons/feed/ 3 4677
The HTML, CSS, and SVG for a Classic Search Form https://frontendmasters.com/blog/the-html-css-and-svg-for-a-classic-search-form/ https://frontendmasters.com/blog/the-html-css-and-svg-for-a-classic-search-form/#respond Thu, 25 Apr 2024 23:38:05 +0000 https://frontendmasters.com/blog/?p=1821 Let’s build a search form that looks like this:

That feels like the absolute bowl-it-down-the-middle search form right now. Looks good but nothing fancy. And yet, coding it in HTML and CSS I don’t think is perfectly intuitive and makes use of a handful of decently modern and slightly lesser used features.

The Label-Wrapping HTML

At a glance, this looks like an <input> all by itself. Perhaps the placeholder text is pushed in with some text-indent or something and an <svg> icon is plopped on top. But no, that’s actually harder than what we’ve done here. Instead we’re going to wrap the input in a label like this:

<label class="searchLabelWrap">
  Search
  <input type="search" placeholder="Search" class="searchInput" name="s">
</label>

This wrapping means the label and input are automatically tied to each other (e.g. clicking the label will focus the input) without having to use the for attribute and a matching id.

We’re also using the search type here on the input, which is semantically correct, but also gives us the free UX of having a ✖️ “clear search” icon in the input for free.

Wrapping All That in a Search and Form element

HTML now has a <search> element, so again that’s a semantically smart choice, and we’ll also use a <form> element so that submitting the search can be done with the Enter key. Gotta think UX! If you don’t like the extra wrapper, you could put role="search" on the <form>, but I like it:

<search>
  <form action="/search" method="GET" id="searchForm">
    <label class="searchLabelWrap">
       ...
    </label>
  </form>
</search> 

The GET method means it will append our search term as a search parameter which is usually a desirable trait of a search form. The name attribute of the input will be the search param key. That’s looking pretty solid right there.

Hiding the Label, Adding an Icon

We definitely need there to be a text label for the input for screen readers, but since we’ll be visually marking the input with both a visual icon and placeholder text, I think it’s OK to hide the text label while leaving it accessible.

<label class="searchLabelWrap">
  <span class="visually-hidden">Search</span>
  <svg viewBox="0 0 512 512" aria-hidden="true" class="icon">
    <path d="..." />
  </svg>
  <input type="search" placeholder="Search" class="searchInput">
</label>
.visually-hidden {
  position: absolute;
  left: -9999px;
}

Label Wrapping Styling

Without any CSS, we’re in this sort of situation:

Perfectly functional, but we’ve got work to do. Visually, we want the icon to appear inside the “input” area. So we’ll actually apply the background to the searchLabelWrap instead here, and wipe out all the styling on the input itself. While we’re at it, let’s think about Dark Mode/Light Mode and use the newfangled light-dark() function. This is very new so, ya know, do what you gotta do. We’ll keep things aligned with flexbox and apply very chill other styles:

.searchLabelWrap {
  display: flex;
  gap: 0.5rem;
  background: light-dark(var(--gray-light), var(--gray-dark));
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
}

.searchInput {
  border: 0;
  outline: 0; /* focus style on parent */
  background: transparent;
  font: inherit;
}

We’re doing the sin of removing the focus style on the input, which isn’t a very accessible thing to do. So we gotta bring that back!

Focus Within FTW

It’s actually the input itself which receives the :focus, but we can target the parent here instead. Maybe use :has() you say? Like :has(input:focus) could work, but there is actually a cleaner way here:

.searchLabelWrap {
  ...

  &:focus-within {
    outline: 2px solid var(--focus-blue);
    outline-offset: 2px;
  }
}

I love :focus-within it’s so cool. It was kinda the OG has and it was theorized when it came out that it could be a gateway to :has() and that’s totally what happened.

Also notice the outline-offset there. I think that’s a nice touch to push the somewhat beefy outline away a smidge.

The Icon

I’m a fan of just using inline SVG for icons. No network request and easy to style my friends. Here’s one:

<svg viewBox="0 0 512 512" aria-hidden="true" class="icon" width="
20">
  <path d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" />
</svg>

I like tossing a width on there because an <svg> with a viewBox before CSS loads can load super wide on the screen and it’s akward. CSS will come in and make it right here:

.icon {
  width: 1rem;
  aspect-ratio: 1;
  fill: currentColor;
}

You could apply different styling, but here I’m making the icon text follow the text color so it’s easier to update. The icon is also essentially in a square area hence the simple aspect-ratio.

Colors

I used a few --custom-properties as we went. I’ll define them here at the root, as well as ensure our page knows we’re intending to support both modes:

html {
  color-scheme: light dark;

  --gray-light: #eee;
  --gray-dark: #333;
  --focus-blue: #1976d2;
}

body {
  background: light-dark(white, black);
  color: light-dark(black, white);
  font: 100%/1.5 system-ui, sans-serif;
}

That’ll do use nicely, finishing things off.

Demo

]]>
https://frontendmasters.com/blog/the-html-css-and-svg-for-a-classic-search-form/feed/ 0 1821