Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Wed, 26 Jun 2024 13:40:10 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 A Text-Reveal Effect Using conic-gradient() in CSS https://frontendmasters.com/blog/text-reveal-with-conic-gradient/ https://frontendmasters.com/blog/text-reveal-with-conic-gradient/#comments Wed, 26 Jun 2024 13:38:28 +0000 https://frontendmasters.com/blog/?p=2828 Part of the appeal of the web as a design medium is the movement. The animations, transitions, and interactivity. Text can be done in any medium, but text that reveals itself over time in an interesting way? That’s great for the web. Think posters, galleries, banners, advertisements, or even something as small as spoiler-reveal effects, “reveal” animations can be entirely delightful. In this article, we’ll look at one fairly simple method for doing this with CSS.

Here’s the outcome, incorporating a play button to show you it can be done via user interaction:

This is achieved with the help of “masking” and perfectly placed conic-gradient(). The browser support for conic gradients is fine, but we’re also going to be using CSS’ @property here which isn’t in stable Firefox yet, but is in Firefox Nightly.

Note: Masking is a graphic technique where portion of a graphic or image is hidden based on the graphic or image layered over and/or under it, based on either the alpha channel or the light/darkness of masking image.

In our example, the masking is done using CSS Blend Mode. I’ll mention later why this is the method I’d chosen instead of CSS Mask (the mask-image property).

Let’s get started with the HTML. Some black text on a white background is a good place to start. This is technically our “mask”.

<p>Marasmius rotula is... </p>
p {
  background: white;
  font-size: 34px;
  line-height: 42px;
  text-align: justify;
}

A container element is added around the text to serve as the graphic to be shown through the mask/text. Also, a CSS variable is used in the container element to assign the line-height. This variable will later be used in the gradient.

<section class="text">
  <p>Marasmius rotula...</p>
</section>
section.text {
  width: 420px;
  --lH: 42px; /* line height */ 
  
  p {
    background: white;
    font-size: 34px;
    line-height: var(--lH);
    text-align: justify;
  }
}

Now, we write up a conic-gradient() as the background for the <section> container, with the gradient’s height same as the para’s line-height, and set to repeat for each line of text. The gradient should look like an arrow (triangular at the tip) passing through the text.

section.text {
  width: 420px;
  --lH: 42px; /* line height */ 
  background: repeat-y left/100% var(--lH) conic-gradient(white 265deg, red 269deg 271deg, white 275deg), white;
  
  p {
    background: white;
    font-size: 34px;
    line-height: var(--lH);
    text-align: justify;
  }
}

We won’t see anything yet since the “black text with white background”, <p>, is blocking the gradient behind it. However, the gradient looks like this:

We’ll now turn the gradient into an animation, where it grows from zero width to however much width is needed for it to cover the entire text. For the moment, let’s make this transition animation to take place when we hover the cursor on the text.

@property --n {
  syntax: "<length-percentage>";
  inherits: false;
  initial-value: 0%;
}

section.text {
  width: 420px;
  --lH: 42px; /* line height */ 
  background: 
  repeat-y left/var(--n) var(--lH) conic-gradient(white 265deg, red 269deg 271deg, white 275deg), white;
  
  p {
    background: white;
    font-size: 34px;
    line-height: var(--lH);
    text-align: justify;
  }
}

section.text:hover {
  --n: 340%;
  transition: --n linear 2s;
}

The --n custom property is used to assign the size of the gradient. Its initial value is 0%, which increases with a transition effect when the text is hovered, and hence the gradient grows in width.

We still haven’t masked our example. So, once again, only the text will be visible. Let me show you how the gradient animates, separately, below:

Note@property creates a custom property that has a known type, hence the property value can be animated. The custom property may not have been able to be animated otherwise.

Let’s now drop the blend mode (the mix-blend-mode property) into the <p> element to finally see the effect.

@property --n {
  syntax: "";
  inherits: false;
  initial-value: 0%;
}

section.text {
  width: 420px;
  --lH: 42px; /* line height */ 
  background: 
  repeat-y left/var(--n) var(--lH) conic-gradient(white 265deg, red 269deg 271deg, white 275deg), white;
  
  p {
    mix-blend-mode: screen;
    background: white;
    font-size: 34px;
    line-height: var(--lH);
    text-align: justify;
  }
}

section.text:hover {
  --n: 340%;
  transition: --n linear 2s;
}

For the sake of operability, instead of on text hover, I’ll move the animation to take place with user controls, Play and Reset. Here’s the final output:

The reason we didn’t use mask-image, as I mentioned before is because Safari doesn’t render the output if I use multiple gradient images (on top of the conic-gradient()), and also has a blotchy implementation of box-decoration-break during animation, both of which are important to work correctly for the effect I wanted to achieve.

That said, here’s a Pen that uses mask-image and box-decoration-break, in case you want to learn how to go about it and get some ideas on approaching any alternative methods. At the time of writing this article, it’s best to view that in Chrome.

Here’s another example that shows off how this effect might be used in a real-world context, revealing the text of different “tabs” as you navigate between tags.

For design variants, play with different colors, and number and kind of gradients. Let me know what you come up with!

]]>
https://frontendmasters.com/blog/text-reveal-with-conic-gradient/feed/ 1 2828
How to Make a CSS Timer https://frontendmasters.com/blog/how-to-make-a-css-timer/ https://frontendmasters.com/blog/how-to-make-a-css-timer/#comments Wed, 29 May 2024 16:40:25 +0000 https://frontendmasters.com/blog/?p=2424 There are times when we can use a timer on websites. 😉

Perhaps we want to time a quiz or put pressure on a survey. Maybe we are just trying to making a dramatic countdown after a user has done something great like successfully booking a concert ticket. We could be trying to build a micro time management tool (think Pomodoro). Or it could just be an alternative to a spinner UI.

When those situations come up, I have no qualms employing JavaScript which is probably a more powerful tool for this sort of thing generally. And yet! CSS substitutes are just as fun and efficient when the simplest option is the best one. Some email clients these days are highly CSS capable, but would never run JavaScript, so perhaps that situation could be an interesting progressive enhancement.

Let’s take a look at what it takes to cook up a CSS timer. We’ll use some modern CSS tech to do it. The ingredients?

  1. CSS Counters
  2. @property
  3. pseudo elements
  4. @keyframes
  5. A little Celtic sea salt to taste

To get started, fire up a (Code)Pen and keep it warm. Below is the demo we’ll be working towards (later, I’ll show some stylized examples):

There are three main requirements for our CSS Timer:

  1. A number that can decrement from 5 to 0
  2. A way to time five seconds, and decrement the number in each
  3. A way to display the decreasing number on page 

The Number

For our first requirement, the update-able number, we’ll use @property to create a custom property that will hold a value of type <integer>

Note: Integer numbers can be zero, or a positive or negative whole number. If you want numbers with decimal points, use <number>, which holds a real number. 

@property --n {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}

The Counting

For tracking seconds, while decreasing the number, we go to @keyframes animation.

@keyframes count {
  from { --n: 5; }
  to   { --n: 0; }
}

The animation function is put to action with the animation property.

.timer:hover::after {
  animation: 5s linear count;
}

Here’s what’s happening: 

When we register a custom property for a specific value type, <integer>, <percentage>, or <color>, for instances, the browser knows that the property is created to work with that specific type of value. 

With that knowledge the browser confidently updates the custom property’s value in the future, even throughout an animation. 

That’s why our property --n can go from 5 to 0 within an animation, and since the animation is set for five seconds, that’s essentially counting from five to zero over a period of five seconds. Hence, a timer is born. 

But there’s still the matter of printing out the counted numbers onto the page. If you hadn’t noticed earlier, I’d assigned the animation to a pseudo-element, and that should give you a clue for our next move — content

The Display

The property, content, can display contents we have not yet added to the HTML ourselves. We generally use this property for a variety of things, because this accepts a variety of values — images, strings, counters, quotation marks, even attribute values. It doesn’t, however, directly takes a number. So, we’ll feed it our number --n through counter

A counter can be set with either counter-reset or counter-increment. We’ll use counter-reset. This property’s value is a counter name and an integer. Since counter-reset doesn’t correctly process a CSS variable or custom property for an integer yet, but does accept calc(), the calc() function becomes our Trojan Horse, inside of which we’ll send in –n. 

.timer:hover::after {
  animation: 5s linear count;
  animation-fill-mode: forwards; 
  counter-reset: n calc(0 + var(--n));
  content: counter(n);
}

That is:

  1. Our animate-able number, --n, is first fed to calc() 
  2. calc() is then fed to counter()
  3. The counter() in turn is given to content, finally rendering --n on the page. 

The rest is taken care of by the browser. It knows --n is an integer. The browser keeps up with animation changing this integer from 5 to 0 in five seconds. Then, because the integer is used in a content value, the browser displays the integer on the page as it updates.

At the end of the animation, the animation-fill-mode: forwards; style rule prevents the timer from reverting back to the initial --n value, zero, right away. 

Once again, here’s the final demo:

For design variants you can count up or down, or play with its appearance, or you can combine this with other typical loader or progress designs, like a circular animation

At the time of writing, Firefox is the only missing browser support for @property, but they have announced an intent to ship, so shouldn’t be long now. For support reference, here’s the caniuse.com page for @property

CSS Custom Properties can also be set and updated in JavaScript. So, if at some point you would like to be able to update the property in JavaScript, just about with any other CSS property, you can do it using the setProperty() function. And if you wish to create a new custom property in JavaScript, that can be done with registerProperty(). The other direction, if you wanted to let JavaScript know a CSS animation has completed, you could listen for the animationend end event.

If you’re really into this sort of thing, also check out Yuan Chuan’s recent Time-based CSS Animations article.

]]>
https://frontendmasters.com/blog/how-to-make-a-css-timer/feed/ 1 2424
Using CSS Scroll-Driven Animations for Section-Based Scroll Progress Indicators https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/ https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/#comments Fri, 10 May 2024 12:50:18 +0000 https://frontendmasters.com/blog/?p=2134 Scroll-Driven Animations allow you to control animations based on the scroll progress of any particular element (often the whole document), or, a particular element’s visibility progress within the document. These are view() and scroll() animations, respectively. Both useful! It can be useful to apply the animation directly to the element itself, for instance, a <section> sliding into place as it enters the viewport. That kind of thing is cool and useful, but have you thought about extending the effects of these animations beyond the elements triggering them?

In CSS, the scroll-driven animations are effectuated using a couple of animation-timeline functions: scroll() and view(). You can learn more about them here.

In this article, we’ll use a view() animation, combined with a CSS custom property declared with @property to create a “currently-viewing” and section-based progress indicator for each section of a page. This kind of thing can be useful, for example, for a long documentation page so a user can see where they are in it, and how far through their current section they are. Kind of like a reading-progress bar, but smarter, as it is aware of individual page sections.

Here’s a demo:

The view() timeline will end up keeping track of each section’s position throughout scrolling, and @property helps pass down an animate-able result of each section’s scroll progress to its indicator element.

The HTML Foundation

To get started, let’s begin by laying down the HTML elements. We’re going to need:

  1. Page <section>s
  2. Scroll progress indicator bars

Here are both:

<section id="one">
    Section number one
    <span>First</span>
</section>

<section id="two">
    Second section
    <span>Second</span>
</section>

<section id="three">
    Third section
    <span>Third</span>
</section>

<section id="four">
    Final section
    <span>Fourth</span>
</section>

The <span>s above are the indicator elements, soon to be moved to the top-right corner of the viewport where they will remain fixed as a user scrolls through the page.

The CSS for the Sections and Indicators

section {
    width: 400px;
    aspect-ratio: 1 / 2;
    /* ... */
}
span {
    position: fixed;
    height: 1lh;
    line-height: 40px;
    width: 100px;
    right: 60px;
    --t: 60px; /* top variable */
    --h: calc(1lh + 10px); /* for the gap between spans */
    section:nth-of-type(1) &{
        top: var(--t);
    }
    section:nth-of-type(2) &{
        top: calc(var(--t) + var(--h));
    }
    section:nth-of-type(3) &{
        top: calc(var(--t) + 2 * var(--h));
    }
    section:nth-of-type(4) &{
        top: calc(var(--t) + 3 * var(--h));
    }
    &::before { /* the blue bar */
        display: block;
        position: absolute;
        content: '';
        width: 4px;
        height: inherit;
        background: rgb(55,126,245);
        /* ... */
    }
}

The spans are given position: fixed, and a right value, to fix them to the side of the screen. The top value of each span is measured (using calc()) based on their height and the gap in-between them. You may want to consider logical property alternatives to these values if it’s reasonable the page you’re working on could be translated.

A pseudo-element on the span is used as the actual progress indicator bar, a darker blue line that grows/shrinks on each indicator box.

Here’s a demo of that (with no animations yet, we’ll get there!)

Let’s proceed to the fun part — the animation!

The Scroll-Driven Animation

We’ll show the scroll progress of each section by animating the height of each bar indicator as the user scrolls through it. Well, perhaps not the height, the CSS property itself, but the visual height. We’ll actually use scaleY() on the bar, as that’s generally considered more performant to animate. This scaleY() function takes a number data type value, hence our need to declare that with @property. So, let’s register a custom property that takes a number value and set up a @keyframes animation that actually does the work:

@property --n {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}
@keyframes slide {
    from { --n: 0; }
    to { --n: 1; }
}

It’s important that the inherits attribute is true when declaring the custom property. This ensures the value is accessible by the spans even though the animation- properties are added to the sections. The span inherits the value, as it were.

section {
    animation-timeline: view(block 98% 2%);
    animation-name: slide;
    animation-fill-mode: both;
    /* remaining code from the 1st css snippet goes here */
}
span {
    /* remaining code from the 1st css snippet goes here */
    &::before { /* the blue bar */
        /* remaining code from the 1st css snippet goes here */
        transform: scaleY(var(--n)); /* animation takes place here */
        transform-origin: top;
    }
}

I prefer the scroll progress be measured against a (conceptual) horizontal line that’s close to the bottom of the screen. Each time a section passes through this line, its animation timeline moves forward or backward based on the scrolling direction (up or down). The area I’d chosen for this is 2% from the bottom of the screen. Here’s the breakdown of the view() function’s values.

  1. block — the area is defined across the block axis. The block axis is the vertical axis for left to right text direction
  2. 98% — the area being defined begins at 98% from the top of the screen (or 98% from the start of the block axis)
  3. 2% — the area ends at 2% from the bottom of the screen (or 2% from the end of the block axis)

Since I wanted the defined area to be a line, I didn’t leave any space between the beginning and end of the area. You can, however, broaden the area if you wish. For instance, 70% 20% gives you a 10% long area on the screen for the scroll progress to be measured against. Or you can even move the area to the top of the screen. 0 100% will assign the very top of the viewport as the marker to help track the scroll progress.

The progress timeline then moves along the keyframe animation that we named slide, updating the custom variable --n with a corresponding value between 0 and 1. We’re using 0 and 1 here because 0 means “scale this bar all the way down to 0% tall” and 1 means “scale this bar all the way up to 100% tall”.

The transform-origin: top sets up the scaling to take place from the top of the bar element, meaning the bar will look like it “grows” from top to bottom which mimics how scrolling happens.

Here’s the final outcome:

A Variation

If you prefer that there be no bar for the first section initially, at least until after you’ve scrolled down a bit, limit the first section‘s animation range to a desirable potion. This might feel better to you (and/or users) because if they haven’t scrolled at all it might be weird to visually show a section as partially complete. Here’s that aleration:

section:nth-of-type(1) {
  animation-range: contain 70%;
}

The animation won’t begin until the 70% mark of the first section crosses the area on the screen defined by view(). The effect is observed when the viewport is shorter than the first section. Adjust as needed.

Conclusion

That’s it! Hopefully the article gives you an idea on how we can cascade the scroll position value and animation progress beyond a directly-assigned element, allowing us to build more dynamic scroll-based designs with just CSS. Knowing that you can pass a custom property value down to other elements, you might think about how style queries could play into that idea 🤔.

]]>
https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/feed/ 1 2134
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