Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 11 Apr 2024 14:46:13 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.2 225069128 A CSS-Powered Add/Remove Tags UI https://frontendmasters.com/blog/a-css-powered-add-remove-tags-ui/ https://frontendmasters.com/blog/a-css-powered-add-remove-tags-ui/#respond Thu, 11 Apr 2024 14:46:11 +0000 https://frontendmasters.com/blog/?p=1650 Checkboxes and labels used to have to be right next to each other to be a potent UI duo. You could do trickery like this:

<label for="toggle">Toggle</label>
<input type="checkbox" id="toggle">
<div></div>
#toggle:checked + div {
  /* Do something fancy in here with a toggleable on/off state */
}

But now, thanks to :has() in CSS, we’re not beholden to that structure anymore. We can :has() it all, as it is said. Now that these HTML elements have some autonomy, without losing their connection to one another, a lot can be achieved

Using this as a base concept, we can build a tag management component operated entirely in HTML & CSS.

A Tag Component with Interactive HTML

<label> is an interactive element that can trigger its controlled element. For instance, when we click a <label> paired with an <input type="checkbox"> the checkbox’s :checked state toggles.

A combo with a <label> allows us to design a toggle UI that can be operated from two different locations on a page (both the label and the checkbox). The controlled element is in one location and the label is in the other.

How far and how independent these two locations have to be from each other for the duo to work used to be limited, since labels can’t directly inform us which state their controlled element is currently in. For that, we have to ask the controlled element itself each time, and keep the label close to the element, so the label can be accessed in CSS during the element’s state change.

That was the case before.

Because of modern CSS standards like Grid, :has() selector and such, there’s much more freedom now between the source code arrangement in the HTML and our ability to reach any element in CSS.

<div>
  <label for="one">One</label>
  <label for="two">Two</label>
  <label for="three">Three</label>
</div>

<p>Arbitrary DOM</p>

<div>
  <input type="checkbox" id="one">
  <input type="checkbox" id="two">
  <input type="checkbox" id="three">
</div>
body:has(#one:checked) p {
  background: pink;
}
body:has(#two:checked) p {
  background: lightgreen;
}
body:has(#three:checked) p {
  background: lightblue;
}

In this article, I’ll be using checkboxes, labelsand :has() selectors to design a UI where you can add and remove “tags”. The UI will have a set of tags to select from, and a set of tags that have been selected. Clicking a tag in one set removes it from one area and makes it appear in the other set. It’s a functionality that’s perfect for checkboxes and labels to take on. Using the :has() selector means, I can keep the two set of tags in as much of a distance or depth from each other as I want, which in turn provides a lot flexibility.

Although the :has() selector can be used in many ways, in this article we’ll focus on its ability to target an element containing a specific child element. The parent is mentioned before the colon (:) and the child is mentioned inside the parentheses of has(). For example, p:has(> mark) selects all elements that have at least one direct descendant that’s a <mark>. Another example, div:has(:checked) selects all <div> elements that have at least one descendant (direct or not) element that’s in a checked state, like a radio or checkbox.

Now that we’ve got the basics covered. Here’s the final demo we’ll be working towards. We’re going to use movie genres as our tags.

Let’s get started.

HTML Construction

There are two parts:

  1. One part nests the checkboxes
  2. The other, their labels

Because we’ll be designing a cluster of tags of movie genres, a script is set up to add the checkboxes and labels for each genre to the HTML.

The script uses HTML <template> to build the new elements off of. This is to prove that you could build all of this dynamically with arbitrary tags from a data source. You can use any method you prefer or not use script at all and directly build the HTML. You’ll see the full source code in a moment.

<div>
  <!-- Plus tags (tags to be included) will render here -->
</div>

<ul>
  <!-- Minus tags (tags already included) will render here -->
</ul>

<template>
  <!-- The tags' templates -->
  <span class="plus"><input type="checkbox" /></span>
  <li><label class="minus"></label></li>
</template>
const template = document.querySelector('template').content;

const div = document.querySelector('div');
const ul = document.querySelector('ul');

const genres = ["Adventure", "Comedy", "Thriller", "Horror"];

for (let i = 0; i < genres.length; i++) {
  /* get a clone of the template's content */
  let clone = template.cloneNode(true);

  let checkbox = clone.querySelector('input[type="checkbox"]');
  /* add id to the checkbox */
  checkbox.setAttribute("id", `c${i + 1}`);
  /* add genre text to the plus tag */
  checkbox.parentElement.innerHTML += genres[i];
  /* add plus tag to the div on page */
  div.appendChild(clone.querySelector(':has(input[type="checkbox"])'));

  let label = clone.querySelector("label");
  /* add "for" attr. to the label */
  label.setAttribute("for", `c${i + 1}`);
  /* add text to the minus tag */
  label.innerText = genres[i];
  /* add minus tag to the ul on page */
  ul.appendChild(clone.querySelector(":has(label)"));
}

In the script:

  1. A set of genres (tag values) is stored as an array
  2. For each item in the genres array, a new clone is created from the template that has an empty plus (checkbox) and minus (label) tag, as seen inside the <template> in HTML
  3. The empty tags’ text and attributes are filled using the genres item’s value and index
  4. Finally, the filled tags are added to their respective containers on the page — <div> and <ul>

Here’s how the HTML source code will look like once the page renders:

<div>
  <span class="plus"><input type="checkbox" id="c1">Adventure</span>
  <span class="plus"><input type="checkbox" id="c2">Comedy</span>
  <span class="plus"><input type="checkbox" id="c3">Thriller</span>
  <span class="plus"><input type="checkbox" id="c4">Horror</span>
</div>

<ul>
  <li><label class="minus" for="c1">Adventure</label></li>
  <li><label class="minus" for="c2">Comedy</label></li>
  <li><label class="minus" for="c3">Thriller</label></li>
  <li><label class="minus" for="c4">Horror</label></li><
</ul>
  1. The parent of each checkbox is .plus. They are meant to be the tags that are yet to be included. It’s sectioned off inside a <div>
  2. Each label is .minus — the tags that are included. These are arranged in a list inside a <ul>

CSS Tag Management

Let’s look at the the key CSS rules first, then we’ll simplify it. This provides the core functionality of the tag management.

/* Remove all minus tags initially */
li { display: none; } 

/* Remove a plus tag, if it's checked */
.plus:has(input:checked){
    display: none; 
}

/* Remove a minus tag, if its plus tag is checked */
:has(#c1:checked) li:has([for='c1']),
:has(#c2:checked) li:has([for='c2']),
:has(#c3:checked) li:has([for='c3']),
:has(#c4:checked) li:has([for='c4']) {
    display: revert; 
}
  1. Initially, the list items (li) with the .minus tags are not displayed. It means the user hasn’t selected any tag to be included yet
  2. When user selects a tag — i.e. checks a checkbox (:has(input:checked)) — its parent element, .plus, is removed with display: “`none`
  3. And the corresponding .minus label’s parent (ex. li:has([for='c1']) is made visible with display: revert

Note: The CSS keyword revert changes a property value to its browser default

To automate the selector listing at the end of the above CSS snippet, I’ll add those rules in the script itself, so that it doesn’t have to be hard-coded in CSS. Again, the whole point here is proving this can all be dynamically generated for your own set of tags if you wanted. If you don’t prefer script, you can leave it as it is in CSS or use a CSS framework, whichever works for you. The following is a continuation of the previous JavaScript snippet, where only the code added now is shown.

const style = document.createElement('style');
document.head.appendChild(style);

for (let i = 0; i < genres.length; i++) {
  /* ... */
  style.sheet.insertRule(`:has(#c${i+1}:checked) li:has([for='c${i+1}']) { display: revert; }`);
}

/* a clear view of the CSS rule string from the above snippet */
`:has(#c${i+1}:checked) li:has([for='c${i+1}']) { display: revert; }`

In the above script: A new style is added to the page, and to this style a css rule for each genres items is added. The css rule is same as in the css snippet from before. Here the id values are dynamically generated.

CSS Tag Styling

Let’s style the tags’ appearance:

.plus,
.minus {
  display: inline-block;
  height: 1lh;
  padding-inline-end: 1.5em;
  border-radius: 4px;
  border: 1px solid currentColor;
  text-indent: 10%;
  font-weight: bold;
  &::after {
    display: block;
    width: 100%;
    margin-block-start: -1lh;
    margin-inline-start: 1.2em;
    text-align: right;
  }
  &:hover {
    box-shadow: 0 0 10px white, 0 0 6px currentColor;
  }
}
.plus {
  position: relative;
  color: rgb(118, 201, 140);
  margin-inline: 0.5em;
  &::after {
    content: "+";
  }
}
.minus {
  color: rgb(95, 163, 228);
  &::after {
    content: "\02212";
  }
}
input[type="checkbox"] {
  width: 100%;
  height: inherit;
  border-radius: inherit;
  appearance: none;
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
}
input[type="checkbox"],
label {
  cursor: pointer;
}
  1. The .plus and .minus tags are colored and bordered. Each has a pseudo-element (::after) to add the “+” and “-“ icon next to it, respectively
  2. The checkbox’s default appearance is removed and is made to fill the area of its container element, so the entire container is clickable

CSS Dynamic Notification

Notification messages can be displayed to the users in certain circumstances, and this can also be done entirely in CSS:

  1. When no tags are selected
  2. When at least one tag is selected
  3. When all tags have been selected
ul {
  &::before {
    display: block;
    text-align: center;
    margin-block-start: 1lh;
  }
  :not(:has(input:checked)) &::before {
    /* has no checked boxes */
    content: "No tags included yet";
  }
  :has(input:checked):has(input:not(:checked)) &::before {
    /* has atleast one checked and unchecked boxes */
    content: "Following tags are included";
  }
  :not(:has(input:not(:checked))) &::before {
    /* has no unchecked boxes */
    content: "All tags are included";
  }
}

In the above CSS nested code snippet:

  1. & represents <ul>. Hence &::before means ul:before
  2. :has() and :not(:has()) represent when the root element (the page) contains a given selector (mentioned inside the parentheses) and when it doesn’t
  3. input:checked is a checked box
  4. input:not(:checked) is an unchecked box

And here’s what’s happening in the code:

  1. ::before pseudo-element is added to the <ul>. This serves as the notification area
  2. When the page has no checked element, 'No tags included yet' is displayed
  3. When the page has at least one checked and one unchecked box, 'Following tags are included' is shown
  4. When the page doesn’t have any unchecked box, 'All tags are included' appears

Tip: Instead of the root element you can scope the code to a common parent, too. Ex. using main:has() instead of :has(), when <main> is a common ancestor of the two group of tags

Here’s the final demo:

Conclusion

Because there’s so much independence between the two set of tags, it frees you up in styling the tags however you like. You can embed the tags in sentences if you want, or you could have as many elements in between them as you want without worrying. For as long as the user is well informed by the design the purpose of the tags, and which ones have been selected, and which ones remain unselected, design them however you feel like.

Bonus: You can even use CSS counters to add a running count of selected and unselected tags, if you want. Read on CSS counters for a similar use case here.

]]>
https://frontendmasters.com/blog/a-css-powered-add-remove-tags-ui/feed/ 0 1650
CSS :has() Interactive Guide https://frontendmasters.com/blog/css-has-interactive-guide/ https://frontendmasters.com/blog/css-has-interactive-guide/#comments Fri, 08 Mar 2024 00:11:18 +0000 https://frontendmasters.com/blog/?p=1156 have a <figcaption>? Yes? OK then style it differently.” And that can be leveled up […]]]> You know I’m a little obsessed with :has() in CSS and how useful it is. So I’m chuffed that Ahmad Shadeed made a killer interactive guide with loads of great examples. The basics are so satisfying like “Does this <figure> have a <figcaption>? Yes? OK then style it differently.” And that can be leveled up to the entire page, like “Is the fixed-position footer on the page right now? Yes? Then move that icon in the lower right corner up a bit.” My favorite on this page is: “Are there two buttons? OK center them. Are there three buttons? Split them up with some flush left and some flush right.” Quantity query!

]]>
https://frontendmasters.com/blog/css-has-interactive-guide/feed/ 1 1156
Scroll-Locked Dialogs https://frontendmasters.com/blog/scroll-locked-dialogs/ https://frontendmasters.com/blog/scroll-locked-dialogs/#comments Mon, 19 Feb 2024 19:01:09 +0000 https://frontendmasters.com/blog/?p=938 I just wrote about the <dialog> element, with some basic usage and things to watch for. It’s a great addition to the web platform.

Here’s another interesting thing we can do, connecting it to another one of my favorite new things on the web platform: :has(). (You can see I’ve been pretty into it lately.) I was reading Locking scroll with :has() from Robb Owen, and he gets into how you might want to prevent the page from scrolling when a modal is open.

To prevent from losing the user’s place in the page whilst that modal is open – particularly on mobile devices – it’s good practice to prevent the page behind it from scrolling. That’s a scroll lock.

I like that. The user can’t interact with what’s behind the modal anyway, as focus is trapped into a modal (that’s what a modal is). So, might as well make sure they don’t inadvertently scroll away. Without doing anything, scrolling is definitely not locked.

My first thought was actually… I wonder if overscroll-behavior on the dialog (and maybe the ::backdrop too?) would prevent that, but some quick testing didn’t seem to work.

So to scroll lock the page, as Robb did in his article, we can do by hiding the overflow on the body. Robb did it like this:

body:has(.lock-scroll) {
  overflow: hidden;
}

Which would then lock if the body had a <dialog class="lock-scroll"> in it. That’s fine, but what I like about <dialog> is that it can sorta always be in the DOM, waiting to open, if you like. If you did that here, it would mean the scroll is always locked, which you certainly don’t want.

Instead, I had a play like:

body {
  ...

  &:has(dialog[open]) {
    overflow: hidden;
  }
}

So this just locks scrolling only when the dialog is open. When you call the API like showModal, it toggles that open attribute, so it’s safe to use.

This works, see:

But… check out that content shift above. When we hide the overflow on the body, there is a possibility (depending on the browser/platform/version/settings and “overlay” scrollbars) that the scrollbars are taking up horizontal space, and the removal of them causes content to reflow.

Fortunately, there is yet another modern CSS feature to save us here, if we determine this to be a problem for the site we’re working on: scrollbar-gutter. If we set:

html {
  scrollbar-gutter: stable;
}

Then the page will reserve space for that scrollbar whether there is scrolling or not, thus there will be no reflow when the scrollbar pops in and out. Now we’re cookin’ — see:

I don’t think scrollbar-gutter is a home run, sadly. It leaves a blank strip down the side of the page (no inherited background) when the scrollbar isn’t there, which can look awkward. Plus, “centered” content can look less centered because of the reserved space. Just one of those situations where you have to pick which is less annoying to you 🫠.

Demo:

]]>
https://frontendmasters.com/blog/scroll-locked-dialogs/feed/ 2 938
We can :has it all https://frontendmasters.com/blog/we-can-has-it-all/ https://frontendmasters.com/blog/we-can-has-it-all/#respond Wed, 10 Jan 2024 20:22:41 +0000 https://frontendmasters.com/blog/?p=442 I’m still obsessed with how awesome and powerful :has() is in CSS. Ryan Mulligan really drives it home in We can :has it all with a single Pen that offers simple, realistic UI controls for:

  1. Changing color theme
  2. Offering alternate layouts
  3. Filtering by category

These are things you can easily imagine on any website, and now handled here entirely without JavaScript at all.

Declarations like this bring me joy:

body:has([name="filter"][value="bakery"]:checked)
  .card:not([data-category="bakery"]) {
  display: none;
}
]]>
https://frontendmasters.com/blog/we-can-has-it-all/feed/ 0 442
Date-based styles with :has() https://frontendmasters.com/blog/date-based-styles-with-has/ https://frontendmasters.com/blog/date-based-styles-with-has/#respond Sun, 17 Dec 2023 18:16:46 +0000 https://frontendmasters.com/blog/?p=273 or <meta> tag that has this information on […]]]> We just looked at how :has() in CSS makes quantity queries so easy, so this post by Terence Eden caught my eye, showing some trickery where you can style an entire page based on when it was published. That is, if there is something like a <time> or <meta> tag that has this information on the page. For example:

<time datetime="2017-01-05">...

Now you can look for that and style the whole page.

body:has([datetime|="2017"] )  {
  /* Style page based on this year. */
}

Fun. You don’t see that “dash separated attribute selector” every day.

]]>
https://frontendmasters.com/blog/date-based-styles-with-has/feed/ 0 273
Quantity Queries are Very Easy with CSS :has() https://frontendmasters.com/blog/quantity-queries-are-very-easy-with-css-has/ https://frontendmasters.com/blog/quantity-queries-are-very-easy-with-css-has/#comments Mon, 11 Dec 2023 22:21:34 +0000 https://frontendmasters.com/blog/?p=239 What is a quantity query? It’s a bit of CSS that allows you to style an element (and its descendants) based on how many children the element has.

Example of a Quantity Query Situation

Imagine a homepage currently showing 20 articles, more than you normally show. Maybe it’s a slow news day, and the lead editor is shooting for variety. So because that’s a lot, you want CSS to scale them down a bit or re-arrange them to make them more equally browsable.

But then you have a really big news day with a lead story, so you decide only to show five articles, the first of which you want to make very large.

Quantity queries (originally coined by Heydon Pickering, best I can tell) may be a solution for this. Here’s some pseudo code to explain:

main {
  
  /* if (articles > 20) */
  display: grid;
  grid-template-columns: repeat(5, 1fr);

  /* if (articles > 5) */
  display: flex;
  flex-wrap: flex;
  article:first-of-type { width: 100% }

  /* default */
  display: block;

}

Old School Quantity Query

Believe it or not, this has been possible in CSS for a while! It just involves some trickery. Here’s Heydon’s first example:

button {
  font-size: 2em;
}

button:not(:only-of-type) {
  font-size: 1.25em;
}

See what that’s doing? That second selector is saying: if this button isn’t totally alone in its parent element, scale down the font-size. Essentially: if one, do this, if more, do this. A simple quantity query. With increasingly complicated selectors like that, you can pull of quantity math. You didn’t see it very often though, as it was pretty weird and complicated.

Quantity Queries with :has()

Now we have :has() in CSS, supported across all the major browsers as of just this month, and we can use it to make quantity queries a lot easier.

Here’s how it works.

You check if an element has an element at all at an :nth-child() position. For example, to check if an element has 10 or more elements (a quantity query), you can do:

.element {
  &:has(> :nth-child(10)) {

    /* Style things knowing that 
       `.element` has at least 10 
       children */

  }
}

You can keep going if you like. Just know that for each of them that matches, all the styles will be applied, so it’s rather additive.

.element {

  &:has(> :nth-child(10)) { }
  &:has(> :nth-child(20)) { }
  &:has(> :nth-child(30)) { }

}

(Note the > child selector is a bit safer than not using it, as it protects against some descendant element having this many children, which is probably not what you mean.)


You could also make sure you’re checking for a particular element type. Like perhaps doing some special styling for a menu that has 10 options within it (or more), knowing that select menus can contain <hr /> element seperators now, which aren’t actually menu options.

selectlist {

  &:has(option:nth-of-type(10)) { }

}

(Wondering what a <selectlist> is? We can’t use it quite yet, but it’s looking to be a 100% CSS styleable <select>, which is awesome).

Example

Here’s a demo where a range input changes the number of children elements are within a parent, then changes the styling of the children depending on how many there are.


What other things can you think of where quantity queries would benefit? Perhaps you would style an article differently if it contains 5 or more header elements. Perhaps you could style table rows to look “collapsed” if there are over a certain threshold of them. Perhaps you would style a comment thread differently if a certain comment has more than 10 replies.

Wanna keep digging into more? Jen Kramer’s course Intermediate HTML & CSS gets into this in the video Level 4 Pseudo-Class Selectors.

]]>
https://frontendmasters.com/blog/quantity-queries-are-very-easy-with-css-has/feed/ 5 239