Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Fri, 07 Nov 2025 15:08:19 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Staggered Animation with CSS sibling-* Functions https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/ https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/#respond Fri, 07 Nov 2025 15:08:18 +0000 https://frontendmasters.com/blog/?p=7631 The CSS functions sibling-index() and sibling-count() return an element’s position relative to its siblings and the total number of siblings, including itself. This is useful for styling elements based on their positions.

<div class="parent"> <!-- sibling-count() = 3 -->
  <div class="child"></div> <!-- sibling-index() = 1 -->
  <div class="child"></div> <!-- sibling-index() = 2 -->
  <div class="child"></div> <!-- sibling-index() = 3 -->
</div>

For instance, to create a pyramid chart, we could proportionally increase the widths of elements as they align.

The integers returned by sibling-index() and sibling-count() can be easily computed with other data types like length, angle, and time. An incremental or decremental time sequence is the foundation of any staggered animation where elements animate consecutively. For example:

.el {
  animation-delay: calc(sibling-index() * 0.1s);
}

This post covers a demo where selecting an item causes the preceding and succeeding items to disappear sequentially from the outside.

The Layout

<main class="cards-wrapper">
  <div class="cards"><input type="checkbox" aria-label="movie, only yesterday"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, the wind rises"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, howl's moving castle"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, ponyo"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, the cat returns"></div>
</main>
.cards-wrapper {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.cards {
  width: 15vw;
  aspect-ratio: 3/4.2;
  contain: layout;
  /* etc. */

  input[type="checkbox"] {
    position: absolute;
    inset: 0;
  }
}

This HTML contains a group of .cards inside a .cards-wrapper, arranged horizontally using flexbox. Each card has a checkbox covering its entire size, triggering selection on click.

The Selectors

Before seeing what happens when a card is selected, let’s see the different CSS selectors we’ll be using to target the different parts of the user interface.

.cards:has(:checked) {

  /* Style rules for the chosen card (the one with a checked box).
    Highlight the chosen card. */   

  .cards-wrapper:has(:checked) .cards:not(&) {
    /* Style rules for the remaining cards when one is chosen.  
      Animate the unchosen cards to disappear. */ 
  }

  & ~ .cards {
    /* Style rules for cards to the right-side of the chosen one. 
       Set a decremental delay time for the disappearance. */

    .cards:not(&) {
       /* Style rules for cards that aren't to the right of the chosen one. 
          Set an incremental delay time. */
    }
  } 
}

The Declarations

Highlight The Chosen Card

The selected card gets a grey dashed border:

border: 2px dashed #888;

Animate The Unchosen Cards to Disappear

opacity: 0;
width: 0; 
display: none; 
transition: 0.3s calc(var(--n) * 0.2s) all;
transition-behavior: allow-discrete;

input { 
  pointer-events: none; 
}
  1. Zero opacity and width create a fade out and horizontal shrinking of the card, including the space the card occupies.
  2. transition-behavior: allow-discrete allows display: none to apply at the end of the transition, which is appropriate as we do want to apply that to remove them from the accessibility tree.
  3. The transition time, affecting all properties that change, is 0.3s. The transition delay is a multiple of 0.2s and the CSS variable --n (to be discussed)
  4. To prevent unchosen cards from being clicked and chosen while disappearing, their checkbox input get pointer-events: none

Decremental Delay Time for Cards On The Right

The cards, succeeding the chosen one, need descending transition delay times. Thus, --n is decremented for each element (left to right) to the right of the chosen card.

--n: calc(sibling-count() - sibling-index() + 1);

Let’s say the third card is chosen. The remaining cards (4 to last) have to vanish in reverse order. The math below shows each of those cards’ --n value and delay time (--n × 0.2s).

Our example has five cards, so sibling-count() is 5. Let’s see how --n calculated for each card after the 3rd (chosen) card:

4th card
--------
--n = calc( sibling-count() - sibling-index() + 1 )
--n = calc( 5 - 4 + 1 )
--n = 2

delay time = calc( var(--n) * 0.2s )
delay time = calc( 2 * 0.2s )
delay time = 0.4s


5th card
--------
--n = calc( sibling-count() - sibling-index() + 1 )
--n = calc( 5 - 5 + 1 )
--n = 1

delay time = calc( var(--n) * 0.2s )
delay time = calc( 1 * 0.2s )
delay time = 0.2s

Incremental Delay Time for Cards On The Left

The sibling-index() value alone is enough, since the delay time of the elements (left to right) to the left of the chosen card increases outward in. First card goes first, then the second, and so forth.

 --n: sibling-index();

Here’s now --n calculates and thus makes the delay time for each card before the 3rd chosen card:

1st card
--------
--n = sibling-index()
--n = 1

delay time = calc( var(--n) * 0.2s )
delay time = calc( 1 * 0.2s )
delay time = 0.2s


2nd card
--------
--n = sibling-index()
--n = 2

delay time = calc( var(--n) * 0.2s )
delay time = calc( 2 * 0.2s )
delay time = 0.4s

The Rulesets

All combined, these are the style rules that are applied when a card is chosen:

/* chosen card */
.cards:has(:checked) {

   border: 2px dashed #888;

  /* cards that aren't chosen, when one has been */
  .cards-wrapper:has(:checked) .cards:not(&) {
    opacity: 0;
    width: 0; 
    display: none; 
    transition: .3s calc(var(--n) * .2s) all;
    transition-behavior: allow-discrete;
    input { pointer-events: none; }
  }

  /* cards after the chosen one */
  & ~ .cards{
    --n: calc(sibling-count() - sibling-index() + 1);
    /* cards not after the chosen one */
    .cards:not(&){
      --n: sibling-index();
    }
  } 
}

Note: Among the “cards not after the chosen one” (see above snippet) the chosen card is also included, but since it doesn’t have the transition declaration, there’s no effect. If you want, you can exclude it by adding :not(:has(:checked)) to the selector, but it’s not necessary in our example.

The Fallback

If a browser doesn’t support sibling-* functions, we can calculate --n in JavaScript to determine the elements’ positions among their siblings.

if(!CSS.supports('order', 'sibling-index()')) {

  // Turn the NodeList to an array for easier manipulation
  const CARDS = Array.from(document.querySelectorAll('.cards'));

  document.querySelector('.cards-wrapper').addEventListener('change', (e) => {

    // Index of the card with the checkbox that fired the 'change' event.
    const IDX = CARDS.indexOf(e.target.parentElement);

    // All cards after the chosen one. If there are three,
    // they get --n values of 3, 2, 1
    CARDS.slice(IDX + 1).forEach((card, idx, arr) 
      => card.style.setProperty('--n', `${arr.length - idx}`));

    // All cards up to the chosen one. If there are three,
    // they get --n values of 1, 2, 3
    CARDS.slice(0, IDX).forEach((card, idx) 
       => card.style.setProperty('--n', `${idx + 1}`));
  });
}

Below is another example, where you’ll be able see an accordion sort of animation by using the sibling-index() function to show and hide the items, even without transition delay, but with the delay the staggered effect comes through.

]]>
https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/feed/ 0 7631
CSS offset and animation-composition for Rotating Menus https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/ https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/#respond Wed, 17 Sep 2025 13:49:38 +0000 https://frontendmasters.com/blog/?p=7147 Circular menu design exists as a space-saver or choice, and there’s an easy and efficient way to create and animate it in CSS using offset and animation-composition. Here are some examples (click the button in the center of the choices):

I’ll take you through the second example to cover the basics.

The Layout

Just some semantic HTML here. Since we’re offering a menu of options, a <menu> seems appropriate (yes, <li> is correct as a child!) and each button is focusable.

<main>
  <div class="menu-wrapper">
    <menu>
      <li><button>Poland</button></li>
      <li><button>Brazil</button></li>
      <li><button>Qatar</button></li>
      <!-- etc. -->
    </menu>
    <button class="menu-button" onclick="revolve()">See More</button>
  </div>
</main>

Other important bits:

The menu and the menu button (<button id="menu-button">) are the same size and shape and stacked on top of each other.

Half of the menu is hidden via overflow: clip; and the menu wrapper being pulled upwards.

main { 
  overflow: clip;
}
.menu-wrapper { 
  display: grid;
  place-items: center;
  transform: translateY(-129px);
  menu, .menu-button {
    width: 259px;
    height: 129px;
    grid-area: 1 / 1;
    border-radius: 50%;
  }
}

Set the menu items (<li>s) around the <menu>’s center using offset.

menu {
    padding: 30px;
    --gap: 10%; /* The in-between gap for the 10 items */
}
li {
  offset: padding-box 0deg;
  offset-distance: calc((sibling-index() - 1) * var(--gap)); 
  /* or 
    &:nth-of-type(2) { offset-distance: calc(1 * var(--gap)); }
    &:nth-of-type(3) { offset-distance: calc(2 * var(--gap)); }
    etc...
  */
}

The offset (a longhand property) positions all the <li> elements along the <menu>’s padding-box that has been set as the offset path.

The offset CSS shorthand property sets all the properties required for animating an element along a defined path. The offset properties together help to define an offset transform, a transform that aligns a point in an element (offset-anchor) to an offset position (offset-position) on a path (offset-path) at various points along the path (offset-distance) and optionally rotates the element (offset-rotate) to follow the direction of the path. — MDN Web Docs

The offset-distance is set to spread the menu items along the path based on the given gap between them (--gap: 10%).

ItemsInitial value of offset-distance
10%
210%
320%

The Animation

@keyframes rev1 { 
  to {
    offset-distance: 50%;
  } 
}

@keyframes rev2 { 
  from {
    offset-distance: 50%;
  } 
  to {
    offset-distance: 0%;
  } 
}

Set two @keyframes animations to move the menu items halfway to the left, clockwise, (rev1), and then from that position back to the right (rev2)

li {
  /* ... */
  animation: 1s forwards;
  animation-composition: add; 
}

Set animation-time (1s) and animation-direction (forwards), and animation-composition (add) for the <li> elements

Even though animations can be triggered in CSS — for example, within a :checked state — since we’re using a <button>, the names of the animations will be set in the <button>’s click handler to trigger the animations.

By using animation-composition, the animations are made to add, not replace by default, the offset-distance values inside the @keyframes rulesets to the initial offset-distance values of each of the <li>.

ItemsInitial Valueto
10%(0% + 50%) 50%
210%(10% + 50%) 60%
320%(20% + 50%) 70%
rev1 animation w/ animation-composition: add
Itemsfromback to Initial Value
1(0% + 50%) 50%(0% + 0%) 0%
2(10% + 50%) 60%(10% + 0%) 10%
3(20% + 50%) 70%(20% + 0%) 20%
rev2 animation w/ animation-composition: add

Here’s how it would’ve been without animation-composition: add:

ItemsInitial Valueto
10%50%
210%50%
320%50%

The animation-composition CSS property specifies the composite operation to use when multiple animations affect the same property simultaneously.

MDN Web Docs

The Trigger

const LI = document.querySelectorAll('li');
let flag = true;
function revolve() {
  LI.forEach(li => li.style.animationName = flag ? "rev1" : "rev2");
  flag = !flag;
}

In the menu button’s click handler, revolve(), set the <li> elements’ animationName to rev1 and rev2, alternatively.

Assigning the animation name triggers the corresponding keyframes animation each time the <button> is clicked.

Using the method covered in this post, it’s possible to control how much along a revolution the elements are to move (demo one), and which direction. You can also experiment with different offset path shapes. You can declare (@keyframes) and trigger (:checked, :hover, etc.) the animations in CSS, or using JavaScript’s Web Animations API that includes the animation composition property.

]]>
https://frontendmasters.com/blog/css-offset-and-animation-composition-for-rotating-menus/feed/ 0 7147
Adaptive Alerts (a CSS scroll-state Use Case) https://frontendmasters.com/blog/adaptive-alerts-a-css-scroll-state-use-case/ https://frontendmasters.com/blog/adaptive-alerts-a-css-scroll-state-use-case/#respond Wed, 16 Jul 2025 13:13:36 +0000 https://frontendmasters.com/blog/?p=6397 Sometimes it’s useful to adapt the controls available to users based on whether they’ve scrolled through key positions on a page.

Here’s an example: a user scrolls through a Terms & Conditions page. If they click “agree” without having scrolled down until the end, we could prompt them with a “please confirm you’ve read these terms” before continuing. Whereas if they have scrolled down the whole way, that could imply they have read the terms, so we don’t need the additional prompt.

Implementing something like this is relatively easy with the recent CSS scroll-state queries (browser support).

The following is an example of exactly as described above. If you click the “Sign Up” button without having scrolled down until the end, you’ll see an additional prompt reminding that you might not have read the terms yet and if you’d still like to sign up. And if the “Sign Up” is clicked after the text has been scrolled to the end, the sign-up acknowledgement pops up without any confirmation prompt first.

This is a video version of the demo, because browser support is Chrome-only as this article is published.

Here’s a live demo:

The Layout

We’ll start with this basic layout:

<article>
  <!-- many paragraphs of ToS text goes here -->
  <div class="control">
    <button>Sign Up</button>
  </div>
</article>
article {
  overflow: scroll;
  container-type: scroll-state;
  .control {
    position: sticky;
    bottom: -20px;
  }
}

The sign up button’s container (.control) is a sticky element that sticks to the bottom of its scrollable container (<article>). This is so the user always has access to the sign up button, in case they prefer to drop reading the terms and sign up right away.

The scrollable container (<article>) has container-type: scroll-state. This makes it possible to make changes to its descendants based on their scroll positions.

The Scroll-State Conditional Rule

This is where we code in how the button control’s action adapts to its scroll position inside the article.

@container not scroll-state(scrollable: bottom) {
  button {
    appearance: button;
  }
}

When the container (<article> in our example) can no longer be scrolled further down, i.e. the container has already been scrolled until its bottom edge, we make a subtle change to the button in CSS that won’t visually modify it. In the example above, the button’s appearance is set to button from its default auto, keeping the button’s look the same. 

The Alerts

When the button is clicked, depending on the value of its appearance property, show the relevant alert. 

document.querySelector('button').onclick = (e) => {
  if (getComputedStyle(e.target).appearance == "auto" 
      && !confirm("Hope you've read the terms. Do you wish to complete the sign up?"))
    return;

  alert("Sign up complete");
};

If the <article> has not been scrolled down until the end, the button’s appearance value remains its default auto (getComputedStyle(e.target).appearance == "auto"). The click handler executes a confirm() prompt reminding the user they might not have read the terms fully yet, and if they’d like to continue with the sign up. If the user clicks “OK”, the alert("Sign up complete") message shows up next. 

If the article has been scrolled down to the end, the button will have an appearance value other than auto, and so the click handler executes the alert() only.  


Learn about scroll-state queries (here and/or here) to know the different kinds of scrolling scenarios that you can work with. Based on scroll states and positions, you’ll be able to change the appearance, content, or even functionality (as seen in this article) of an element or module.

]]>
https://frontendmasters.com/blog/adaptive-alerts-a-css-scroll-state-use-case/feed/ 0 6397
Curved Box Cutouts in CSS https://frontendmasters.com/blog/curved-box-cutouts-in-css/ https://frontendmasters.com/blog/curved-box-cutouts-in-css/#respond Thu, 01 May 2025 14:32:05 +0000 https://frontendmasters.com/blog/?p=5733 This post explores a trick to create the illusion of an element appended to another with a gap and curved edges at the corners. It’s useful for visually demarcating supplementary elements or user controls in a card module.

An example:

The Layout

Let’s start with the HTML and code a simple design.

<div class="outer">
  <div class="inner"></div>
</div>

Use a nested element (.inner) or a stacked element to create the small box. The key is for the small box to overlap the larger one.

.outer {
  width: 375px;
  aspect-ratio: 1;
  border-radius: 12px;
  background: dodgerblue;

  .inner {
    width: 160px;
    height: 60px;
    border-radius: inherit;
    background: skyblue;
  }
}

The larger square box (.outer) and the smaller rectangle box (.inner) share the same border-radius value (12px).

The Smaller Box

Add an outline of the same color as the page.

.inner {
  /* etc. */
  outline: 8px solid white;
}

That’s all we need to do with the inner box.

The Bigger Box

Add two small radial-gradient() background images to the larger box’s background.

  1. Position the images where the smaller box’s corners overlap, with a negative offset equal to the outline size (8px).
  2. The border radius (12px) plus the smaller box’s outline (8px) equals the images’ size (20px 20px).
  3. The gradients are transparent circles the same size as the border radius (12px), with the rest white
.outer {
  /* etc. */
  background: 
    -8px 60px / 20px 20px 
        radial-gradient(circle at right bottom, transparent 12px, white 12px),
    160px -8px / 20px 20px 
        radial-gradient(circle at right bottom, transparent 12px, white 12px),
    dodgerblue;
}

The code is complete. You’ll get the final result as is. However, let’s make the code more flexible.

CSS Variables

For ease of update, I’ll move the length values to CSS variables, and for clarity, I’ll list each of the background- properties separately.

.outer {
  width: 375px;
  aspect-ratio: 1;

  /* border radius */
  --r: 12px;

  /* width, height, and outline of smaller box */
  --w: 160px;
  --h: 60px;
  --o: 8px;

  /* offset and size of the radial-gradient images */
  --ofs: calc(-1 * var(--o));
  --sz: calc(var(--r) + var(--o));
  --img: radial-gradient(circle at right bottom, transparent var(--r), white var(--r));

  border-radius: var(--r);
  background-image: var(--img), var(--img);
  background-position: var(--ofs) var(--h), var(--w) var(--ofs);
  background-size: var(--sz) var(--sz);
  background-repeat: no-repeat;
  background-color: dodgerblue;

  .inner {
    width: var(--w);
    height: var(--h);
    outline: var(--o) solid white;
    border-radius: inherit;
    background: skyblue;
  }
}

All Four Corner Placements

Place the smaller box in the desired corner against the bigger one, and update the radial gradient image positions and circles accordingly.

.outer {
  /* etc. */
  background-image:
   radial-gradient(circle at var(--cnr), transparent var(--r), white var(--r)),
   radial-gradient(circle at var(--cnr), transparent var(--r), white var(--r)),
   linear-gradient(45deg, rgb(210, 223, 246), rgb(51, 134, 242));

  &:nth-of-type(1) {
    --cnr: right bottom;
    background-position: 
      var(--ofs) var(--h), 
      var(--w) var(--ofs), 
      0 0;
    /* etc. */
  }

  &:nth-of-type(2) {
    --cnr: left bottom;
    background-position: 
      calc(100% - var(--ofs)) var(--h), 
      calc(100% - var(--w)) calc(var(--ofs)), 
      0 0;
    /* etc. */
  }

  &:nth-of-type(3) {
    --cnr: left top;
    background-position: 
      calc(100% - var(--ofs)) calc(100% - var(--h)), 
      calc(100% - var(--w)) calc( 100% - var(--ofs)), 
      0 0;
    /* etc. */
  }

  &:nth-of-type(4) {
    --cnr: right top;
    background-position: 
      var(--ofs) calc(100% - var(--h)), 
      var(--w) calc(100% - var(--ofs)), 
      0 0;
    /* etc. */
  }
}

The larger box in the example is a square, so 100% is used in calculating the radial gradient images’ positions both vertically and horizontally where needed.

How to Use The Design?

Since the design uses an imitation of a gap, effects like drop shadow that require cutouts won’t work. However, no extra markup or style changes are needed, only the background is affected, making it suitable for simple designs.

This doesn’t have to be limited to gap-like designs, either. The outline can be used in other ways, too. The rounded corners will be a subtle touch up.

.date {
  outline: var(--outline) solid navy;
  /* etc. */
}
]]>
https://frontendmasters.com/blog/curved-box-cutouts-in-css/feed/ 0 5733
Expanding CSS Shadow Effects https://frontendmasters.com/blog/expanding-css-shadow-effects/ https://frontendmasters.com/blog/expanding-css-shadow-effects/#respond Fri, 28 Mar 2025 14:47:04 +0000 https://frontendmasters.com/blog/?p=5467 Design principles tell us a shadow is conveys that light is hiding an object, which casts the shadow behind it, giving us a sense of depth. However, if that’s all a shadow is ever used for, then it has not been utilized to its full potential. 

The Power of Shadows

Shadows in CSS can be multi-directional, layered, and are animate-able. On top of being all that, they don’t affect the layout and computed size of an element even though they can make it appear bigger or smaller, which makes them an efficient tool for making visual changes.

Types of Shadows

There are different types of shadows based on the type of component they affect. 

  • box-shadow
  • filter: drop-shadow()
  • text-shadow
The difference between box-shadow and drop-shadow() is worth knowing!

All of this is proof that you’ll benefit from understanding CSS shadows and learning ways to expand their uses beyond simply creating a proverbial shadow. 

Focus on Inset Box Shadows

In this article, for the purpose of simplicity, my examples will focus on box shadows, specifically, inset box shadows. However, the principles we’ll be working with are same for all types of CSS shadows.

Below is an example of what could be possible with the things I’ll be covering in this article. I’ll show you more examples and design variants as we proceed further.

To warm up, let’s just go ahead and animate a group of inset box shadows, via a transition: box-shadow ... ;, and see what we get.

<div class="pokemon golduck">
  <div class="text">Golduck</div>
</div>
.pokemon {
  box-shadow: 
    0 0 10px #eee, 
    inset 3px 3px 10px white, 
    inset -160px -160px 0 royalblue, 
    inset 160px -160px 0 green,
    inset -160px 160px 0 blue, 
    inset 160px 160px 0 yellow;
  /* etc. */
  &:hover {
    box-shadow: none;
    transition: box-shadow linear 0.6s;
    .text { opacity: 0; }
  }
}

The above shows off the inset keyword that box-shadow can use in a couple of different ways. Inward shadows make for terrific overlays, since it’s painted on top of an element’s background and originates from the edges of the element’s padding box. The other thing we got to confirm is that the box-shadow is indeed animate-able. 

Changing the Shadow’s Shape

CSS shadows, by default, follow the shape of the component they are applied to — a box, text, or the opaque area of an element, depending on which shadow is used. By playing around with the shadows’ vertical and horizontal offsets, you can reshape them to a degree. Here’s an example:

.selected {
  /* etc. */
  box-shadow: 
    inset -30px 30px 0 white, 
    inset 30px -30px 0 white,
    inset 0 0 80px lime;
  transform: rotatez(360deg);
  transition: transform 1s linear;
}

The element we’ll apply this to is already a circle, and this will make the shadow somewhat football (🏈) shaped.

In similar ways you can play around with a shadow’s blur radius, spread distance (in box-shadow), and color.

Tip: Add a thin border matching the page’s background to the element if there’s any inset shadow bleeding outside the box

Animating Only Parts of a Shadow

We can take this a step further. So far we’ve been animating the box-shadow property as a whole, but how about pin-pointing the animation to individual values of a shadow? That will not only produce a different result, but you can also assign different animation times for different aspects of a shadow: 

@property --l {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --c {
  syntax: "<color>";
  inherits: false;
  initial-value: red;
}
.selected {
  /* etc. */
  --l: 160px;
  --c: black;
  box-shadow: inset 0 0 0 var(--l) var(--c);
  transition: --l 1s linear, --c 0.5s linear;
}

You can also use @keyframes, instead of transition, for the animations to keep frame stops. For instance, in the following example, there’s multiple color changes throughout the animation sequence as defined by the @keyframes ruleset, colorChange:

@property --c {
  syntax: "<color>";
  inherits: false;
  initial-value: dodgerblue;
}
@keyframes colorChange {
  40% { --c: yellow }
  80% { --c: red }
}
.selected {
  /* etc. */
  box-shadow: inset 0 0 0 var(--l) var(--c);
  animation: 1s linear colorChange;
}

Here’s a demo of that.

For our main demo, let’s keep transition, and then combine the things we’ve seen so far as well as include a few more colors. 

@property --l {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
.selected {
  /* etc. */
  --l: 100px;
  box-shadow: 
    inset var(--l) calc(-1 * var(--l)) 60px azure,
    inset calc(-1 * var(--l)) var(--l) 60px white,
    inset calc(-1 * var(--l)) calc(-1 * var(--l)) 60px white,
    inset var(--l) var(--l) 60px white,
    inset calc(-1 * var(--l)) calc(-1 * var(--l)) 5px fuchsia,
    inset var(--l) var(--l) 5px lime, inset var(--l) calc(-1 * var(--l)) 5px red,
    inset calc(-1 * var(--l)) var(--l) 5px green;
  transition: --l 1s linear;
}
.selected {
  /* etc. */
  box-shadow: 
    inset 0 0 10px 30px white, 
    inset -40px 0px 0 white,
    inset 40px 0px 0 white, 
    inset 0 0 100px red, 
    inset 0 -60px 0 white, 
    inset 60px 30px 0 white, 
    inset 0 0 100px blue;
  transform: rotatez(360deg);
  transition: box-shadow 1s linear, transform 1s linear;
}

This is where you can really see how layering can be helpful.

Other Examples

Here’s a collection of a few on-hover animation designs using CSS shadows to help you get started:

If you want to keep exploring shadow animations further, I recommend combining them with other possible visual effects from CSS properties like filter and blend modes. Also, make sure to see how the animations work out both when individual values are animated and when the shadow as a whole property is animated.

Further Reference

]]>
https://frontendmasters.com/blog/expanding-css-shadow-effects/feed/ 0 5467
Three Approaches to the “&” (ampersand) Selector in CSS https://frontendmasters.com/blog/three-approaches-to-the-ampersand-selector-in-css/ https://frontendmasters.com/blog/three-approaches-to-the-ampersand-selector-in-css/#respond Fri, 07 Feb 2025 14:50:06 +0000 https://frontendmasters.com/blog/?p=5123 In CSS nesting, the & (ampersand symbol) selector adds style rules based on the relation between nested selectors. For example, a pseudo-class (:hover) nested inside a type selector (div) becomes a compound selector (div:hover) when the nested pseudo-class is prefixed with &.

div {
  &:hover {
    background: green;
  }
}

/*
The above code is equivalent to:
div:hover {
  background: green;
}
*/

A use-case for the & selector is to combine it with the :has() pseudo-class to select and style elements based on the child elements they contain. In the following example, a label is styled for when the checkbox within it is checked.

<label>
  <input type="checkbox">
  Allow access to all files
</label>
label {
  /* etc. */
  &:has(:checked) {
    border: 1px solid lime;
  }
}

/*
  The above code is equivalent to:

  label:has(:checked) {
    border: 1px solid lime;
  }
*/

In a way, the & symbol is a placeholder for the outer level selector in the nesting hierarchy. This enables flexible combinations of selectors customized to suit our code’s modular organization preferences. In this article, we’ll see three kinds of modular possibilities with the & selector in native CSS.

1) Linked Class Names

Starting with the easiest approach, the & selector can be used to combine class names. Elements often have multiple class names to group and style them together. Sometimes, grouping is within a module, and other times style rules can intersect between modules due to shared class names.

By using the & selector to concatenate class names, style rules for elements within a module can be arranged together based on their shared class names.

In the example below, the three card modules, grouped under the class name cards, share most style rules, such as dimensions. However, they have different background images to reflect their content, defined separately within the .cards ruleset by concatenating the & selector with the exclusive class names of each card.

<div class="cards trek">
  <p>Trekking</p>
</div>
<div class="cards wildlife">
  <p>Wildlife spotting</p>
</div>
<div class="cards stargaze">
  <p>Stargazing camp</p>
</div>
.cards {
  background: center/cover var(--bg);

  &.trek {
    --bg: url("trek.jpg");
  }
  &.wildlife {
    --bg: url("wildlife.jpg");
  }
  &.stargaze {
    --bg: url("stargaze.jpg");
  }
}

Class names can also be connected using the attribute selector:

<div class="element-one">text one</div>
<div class="element-two">text two</div>
[class|="element"] {
  &[class$="one"] { color: green; }
  &[class$="two"] { color: blue; }
}

Another example is:

<div class="element_one">text one</div>
<div class="element_two">text two</div>
[class^="element"] {
  &[class$="one"] { color: green; }
  &[class$="two"] { color: blue; }
}

2) Parent and Previous Element Selectors

The conventional method of organizing nested style rulesets involves including the rulesets of child elements within the ruleset of their parent elements.

<p class="error">
  Server down. Try again after thirty minutes. 
  If you still can't access the site, 
  <a href="/errorReport">submit us a report</a>. 
  We'll resolve the issue within 24hrs.
</p>
.error {
  color: red;
  a {
    color: navy;
  }
}

However, the opposite is also possible due to the & selector: nesting a parent element’s style rules within its child element’s ruleset. This can be beneficial when it’s easier to organize an element’s style rules by its purpose rather than its position in a layout.

For instance, styling can be static or dynamic. Dynamic styling happens when user interactions, or scripts, trigger the application of a selector’s ruleset to an element on a page. In such cases, it’s convenient to categorize rulesets into dynamic and static.

In the following example, all rules related to the appearance of the agreement modules upon page load, such as layout, dimensions, and colors, are included in the .agreements ruleset. However, the style rules that modify the appearance of the agreement modules when their checkboxes are checked, i.e when user interaction occurs, are placed within the nesting selector .isAgreed:checked.

<article class="agreements terms">
  <header><!-- ... --></header>
  <section>
    <input class="isAgreed" type="checkbox" />
    <!-- ... -->
  </section>
  <footer><! -- ... --></footer>
</article>
<article class="agreements privacy">
  <header><!-- ... --></header>
  <section>
    <input class="isAgreed" type="checkbox" />
    <!-- ... -->
  </section>
  <footer><! -- ... --></footer>
</article>
<article class="agreements diagnostics">
  <header><!-- ... --></header>
  <section>
    <input class="isAgreed" type="checkbox" />
    <!-- ... -->
  </section>
  <footer><!-- ... --></footer>
</article>
.agreements {
  /* etc. */
  &.terms {  --color: rgb(45 94 227);  }
  &.privacy { --color: rgb(231 51 35); }
  &.diagnostics { --color: rgb(59 135 71); }
  /* etc. */
}

.isAgreed:checked { 
  /* checkbox's background color change */
  background: var(--color);
  /* checkbox's border color change */
  border-color: var(--color);
  /* checkbox shows a checkmark */
  &::before { content: '\2713'; /* checkmark (✓) */ }
  
  /* Agreement section's (checkbox's parent) border color change */
  .agreements:has(&) { 
    /* same as .agreements:has(.isAgreed:checked) */
    border-color: var(--color); 
  }
}

In the above demo, a parent element selection is shown as example, but the same logic applies for previous element selections as well.

<p>some text</p>
<input type="checkbox"/>
/* When the checkbox is checked */
:checked {
  /* 
    rules to style the checkbox
  */

  /* style for <p> when the checkbox is checked */
  p:has(+&) { 
    /* same as p:has(+:checked) */
    color: blue;    
  }
}

3) Recurring Selectors

With IDs, class names, semantic markup, and so forth, we rarely need to repeat selectors within compound selectors to achieve specificity. However, repeating selectors are still useful, particularly when the same type of elements are styled similarly but with slight adjustments based on their positions in the layout and among themselves.

A good example of this is how paragraphs are typically spaced in an article. There’s the spacing between each paragraph, and the spacing between the paragraphs and another kind of element, such as an image, that’s inserted between them.

<article>
  <header><!--...--></header>
  
  <p><!--...--></p>
  
  <figure><!--...--></figure>
  
  <p><!--...--></p>
  <p><!--...--></p>
  <p><!--...--></p>
  
  <blockquote><!--...--></blockquote>
  
  <p><!--...--></p>
  <p><!--...--></p>
  
  <figure><!--...--></figure>
  
  <p><!--...--></p>
  
  <footer><!--...--></footer>
</article>
article {
  /* etc. */
  p {
    margin: 0;
    
    /* <p> that's after/below an element that's not <p> */
    *:not(&) + & { 
      margin-top: 30px; 
    }
    
    /* <p> that's before/above an element that's not <p> */
    &:not(:has(+&)) { 
      margin-bottom: 30px; 
    } 
    
    /* <p> that's after/below another <p> */
    & + & {
      margin-top: 12px; 
    }
  }
  /* etc. */
}

In the above example, the gaps between paragraphs are small compared to the larger gaps between a paragraph and a non-paragraph element.

The selectors can be explained like:

  1. *:not(p) + p — The paragraph below a non-paragraph element has a larger top margin
  2. p:not(:has(+p)) — The paragraph above a non-paragraph element has a larger bottom margin
  3. p + p — The paragraph below another paragraph has a smaller top margin

Besides its flexibility, another compelling reason to use the & selector in organizing our code is that it lacks its own specificity. This means we can rely on the specificity of the usual selectors and the nesting hierarchy to apply the rules as intended.

If you’re looking to level up your skills in CSS nesting, check out Kevin Powell’s new course Build a Website from Scratch and Jen Kramer’s Practical CSS Layouts both of which cover CSS nesting and lots more!

Using & in Vanilla CSS vs. Using & in Frameworks

The & is vanilla CSS always represents the outer level selector, which might not be the case for the & used in CSS frameworks, such as Sass. The & in frameworks could mean the outer level string.

<div class="parent--one">text one</div>
<div class="parent--two">text two</div>

In Sass (SCSS), the style could be:

.parent {
  &--one { color: green; }
  &--two { color: blue; }
}

That will not work in vanilla CSS, but it still can be done! A similar ruleset would be:

[class|="parent"] {
  &[class$="--one"] { color: green; }
  &[class$="--two"] { color: blue; }
}

Note that, in the SCSS code, there is no real .parent selector, as in no element on page matches it, whereas in CSS, [class|="parent"] will match an element. If we add a style rule to the outer level ruleset in both of those code snippets, the SCSS will fail to find an element to apply the style rule, whereas the CSS will apply the style to both the elements that has the class name starting with parent.

.parent {
  font-weight: bold; /* this won’t work, as it doesn't match anything */
  &--one { color: green; }
  &--two { color: blue; }
}
[class|="parent"] {
  font-weight: bold; /* works */
  &[class$="one"] { color: green; }
  &[class$="two"] { color: blue; }
}

Hence, the downside of an & that represents a selector-syntax string rather than a viable selector is that it might mislead us into thinking elements matching the perceived selector exist when they don’t, something that we don’t have to worry with the native & selector.

When using a nested style rule, one must be able to refer to the elements matched by the parent rule; that is, after all, the entire point of nesting. To accomplish that, this specification defines a new selector, the nesting selector, written as & (U+0026 AMPERSAND).

When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. When used in any other context, it represents the same elements as :scope in that context (unless otherwise defined).

— CSS Nesting Module 1, W3C

On the other hand, we can combine strings more freely to produce the selectors we want using the & in frameworks. Which is useful when class names are extensively relied on for modularity.

Either way, grouping style rulesets is crucial for enhancing code readability, maintainability, and to provide a desired order of use among conflicting style rules. The & selector in native CSS can help with that, as well as in specifying selection criteria that might otherwise be challenging to define.

Further Reading

]]>
https://frontendmasters.com/blog/three-approaches-to-the-ampersand-selector-in-css/feed/ 0 5123
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
CSS Fan Out with Grid and @property https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/ https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/#comments Wed, 09 Oct 2024 13:27:37 +0000 https://frontendmasters.com/blog/?p=4128 A “fan out” is an expanding animation where a group of items appear one after another, next to each other, as though they were spread out from a stack. There’s usually a subtle bounciness in the reveal.

The effect is customarily achieved by timing and positioning each of the items individually with very specific hard set values. That can be an awful lot of work though. We can make things a bit easier if we let the items’ parent container do this for us. Here’s a result of doing it this way: 

UPDATE: This article has been updated to now include the animation of the grid items’ height, to produce an overall smoother transition effect. The previous version of this article didn’t cover that. 

For HTML, there’s a group of items (plus an empty one — I will explain later why it’s there), bookended by two radio controls to prompt the opening and closing of the items respectively.

<section class="items-container">
  <p class="items"><!--empty--></p>
  <label class="items close">
    Close the messages<input type="radio" name="radio">
  </label>
  <p class="items">Alert from Project X</p>
  <p class="items">&#x1F429; Willow's appointment at <i>Scrubby's</i></p>
  <p class="items">Message from (-_-)</p>
  <p class="items">NYT Feed: <u>Weather In... (Read more)</u></p>
  <p class="items">6 more items to check in your vacation list!</p>
  <label class="items open">
    Show the messages<input type="radio" name="radio">
  </label>
</section>

We need a grid container for this to work, so let’s turn the <section>, the items’ container element, into one. You could use a list or anything you feel is semantically appropriate.

.items-container {
  display: grid; 
}

Now create an Integer CSS custom property with a value same as the number of items inside the container (including the open and close controls, and the empty item). This is key to implement the revealing and hiding of the items, sequentially, from within the grid container’s style rule.

Also, register another CSS custom property of data type length that’ll be used to animate each item’s height during the opening and closing of the control, for a smoother execution of the overall action. 

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

@property --hgt {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

Use the now created --int and --hgt properties to add that many grid rows of zero height in the grid container. 

.items-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), var(--hgt));  
}

When directly adding --int to repeat() it was producing a blotchy animation in Safari for me, so I fed it through calc() and the animation executed well (we’ll look into the animation in a moment). However, calc() computation kept leaving out one item in the iteration, because of how it computed the value 0. Hence, I added the empty item to compensate the exclusion. 

If Safari did not give me a blotchy result, I would’ve not needed an empty item, --int’s initial-value would’ve been 6, and grid-template-rows’s value would’ve been just repeat(var(--int), 0px). In fact, with this set up, I got good animation results both in Firefox and Chrome. 

In the end though, I went with the one that uses calc(), which provided the desired result in all the major browsers. 

Let’s get to animation now:

@keyframes open { to { --int: 0; --hgt:60px;} }
@keyframes close { to { --int: 6; --hgt:0px;} } 
.item-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), var(--hgt)); 
  &:has(.open :checked) {
  /* open action */
    animation: open .3s ease-in-out forwards;
    .open { display: none; }
  }
  &:has(.close :checked) {
  /* close action */
    --int: 0;
    --hgt: 60px;
    animation: close .3s ease-in-out forwards;
  }
}

When the input is in the checked state, the open keyframe animation is executed, and the control itself is hidden with display: none

The open class changes --int’s value from its initial-value, 7, to the one set within the @keyframes rule (0), over a set period (.3s). This decrement removes the zero height from each of the grid row, one by one, thus sequentially revealing all the items in .3s or 300ms. Simultaneously, --hgt’s value is increased to 60px from its initial 0px value. This expands each item’s height as it appears on the screen. 

When the input to hide all the items is in the checked state, the close keyframe animation is executed, setting --int’s value to 0 and --hgt’s value to 60px.

The close class changes the now 0 value of --int to the value declared in its rule: 7. This increment sets a zero height to each of the grid row, one by one, thus sequentially hiding all the items. Simultaneously, --hgt’s value is decreased to 0px. This shrinks each item’s height as it disappears from the screen. 

To perform the close action, instead of making a unique close animation, I tried using the open animation with animation-direction: reverse. Unfortunately, the result was jerky. So I kept unique animations for the open and close actions separately.

Additionally, to polish the UI, I’m adding transition animations to the row gaps and text colors as well. The row gaps set cubic-bezier() animation timing function to create a low-key springy effect. 

.scroll-container {
  display: grid; 
  grid-template-rows: repeat(calc(var(--int)), 0px); /* serves the open and close actions */
  transition: row-gap .3s .1s cubic-bezier(.8, .5, .2, 1.4);
  &:has(.open :checked) {
    /* open action */
    animation: open .3s ease-in-out forwards;
    .open { display: none; }
    /* styling */
    row-gap: 10px;
    .items { color: rgb(113 124 158); transition: color .3s .1s;}
    .close { color: black }
  }
  &:has(.close :checked) {
    /* close action */
    --int: 0;
    animation: close .3s ease-in-out forwards;
    /* styling */
    row-gap: 0;
    .items { color: transparent; transition: color .2s;}
  }
}

When expanded, the row gaps go up to 10px and the text color comes in. When shrinking, the row gaps go down to 0 and the text color fades out to transparent. With that, the example is complete! Here’s the Pen once more:

Note: You can try this method with any grid compositions — rows, columns, or both.

Further Reading

]]>
https://frontendmasters.com/blog/css-fan-out-with-grid-and-property/feed/ 3 4128
The CSS contain property https://frontendmasters.com/blog/the-css-contain-property/ https://frontendmasters.com/blog/the-css-contain-property/#comments Mon, 19 Aug 2024 15:12:53 +0000 https://frontendmasters.com/blog/?p=3519 The purpose of CSS’ contain property (“CSS containment”) is twofold:

  1. Optimization
  2. Styling possibilities

When we apply contain to an element we’re isolating it (and it’s descendents) from the rest of the page, and this isolation what opens those possibilities. There are different types of containment which all do different things, and we’ll be getting into each of them.

.element {
  contain: size;
  contain: paint;
  contain: layout;
  
  /* they can also be combined, e.g. */
  contain: size layout;
}

First, optimization. When something changes an element after the page has rendered, the browser rethinks the entire page in case that element affects the rest of the page (visible or not). But with a contained element, we let the browser know that the change we made is restricted to only the contained element and its children, and the browser needn’t bother with the rest of the page because there won’t be any impact on that.

Second, relative styling. Meaning positioning, visibility, stacking, sizing, etc, and by “relative” I mean styling an element in comparison to that of the viewport or the parent element — relative positioning and relative sizing are two good examples of this that we’ve had for a long time.

Since the introduction of CSS Containment, a lot more relative styling can be done in the scope the contained element and its descendants. You’ll see examples as we proceed.

In this article, CSS Containment refers to the CSS contain property. Its namesake W3C Standard, however, covers a few more specifications, like container units.

Size Containment

Although “size containment” sounds highly useful, in practice you might not use this much.

When an element’s size is containedthe browser does not allow anything that happens inside the contained element (to the content or the descendant elements) to affect its size. In other words, you have a way to achieve truly fixed sizing.

But fixed sizing is not in trend. These days we have a lot of options for for dynamic sizing. For instance, CSS units relative to the root font size (remrex, etc.), units responsive to the viewport size (vwdvh, etc.), units relative to the current font size (emlh, etc.), you name it. Which means we usually do a pretty good job of sizing the different elements on a page for different screen sizes and content.

What if, for example, a user is interacting with a page and caused new dynamic information to appear on the page? This new information takes up space. Now have options and can make a choice.

  • Is it better to allow the element, and potentially the entire page, to reflow and change?
  • Or is it better to contain the element so that the changes prevent the reflow?

If it feels like the latter, size containment can be the solution (or part of the solution) for you.

Be sure to set the desired dimensions, width and height (or logical properties), or aspect ratios when using size containment, because the browser initially renders a contained element as if it’s empty, and without the right dimensions set, elements might end up having a zero dimension.

.box {
  width: 100px;
  min-height: 100px;
  img {
    width: 200%;
    ...
  }
  &:nth-of-type(2) { /* the second box */
    contain: size; 
    ...
  }
  ...
}

Setting an element’s dimensions yourself may feel like size containment already, and typically that is all you need, which is why I said you may not need this much. But if you’re entirely sure you don’t want this element to change size or affect any other elements, protecting against unforseen changes, size containment may be a good idea.

Paint Containment

If you’re familiar with the overflow property, that’s a good place to start understanding paint containment. However, they are not quite the same.

Outside a paint-contained element’s box, the browser neither displays any content, nor it makes the concealed content scrollable to by any means at all. In this aspect, paint containment’s behavior is similar to overflow: clip rather than overflow: hidden (which allows scrolling to the concealed content or elements through methods like the JavaScript scroll event).

So if you want to trim the overflowed content of an element while also ensuring that content is not at all scrollable to, a paint-containment can be used. A browser might also save computational effort by not rendering an off screen paint-contained element.

I also have another good reason for why you might need paint containment.

As I mentioned at the beginning of the article, containment is the isolation of an element in all factors. That’s worth repeating here. Paint containment is not just about clipping what’s fallen out of a container box, but also isolating that box itself from the rest of the rendered page by creating new stacking and formatting contexts for that element. Paint containment also generates a separate absolute and fixed positioning container box (I’ll explain this in Layout Containment).

Below, there are two examples: the first one is a sample of paint containment (contain: paint), and the second is a comparison between the behavior of overflow: clip and contain: paint. You’ll notice the paint containment’s isolating effect clearly in the second example, where the contained element is unaffected by the CSS blending applied to its sibling element.

.box {
  width: 100px;
  img {
    width: 200%;
    ...
  }
  &:nth-of-type(2) { /* second box */
    contain: paint; 
    ...
  }
  ...
}
section {
  div {
  width: 100px; 
  aspect-ratio: 1/1;
  }
  .box {
    background: red; 
  }   
  &:nth-of-type(2) .box { 
    overflow: clip;
  }
  &:nth-of-type(3) .box { 
    contain: paint;
  }
  .box-sibling {
    background: blue; 
    mix-blend-mode: hue;
    ...
  }
}

Layout Containment

Layout is essentially the flow of elements and content. Over the years, more than sizing, more than painting, the most work we have delegated to the browsers is in the scope of layout. There are CSS properties, like floatvertical-align, and such, for us to communicate to the browsers where we prefer an element to be a little re-positioned to from its natural place on the page.

And then there are full scale layouts, like Grid Layout, Page Layout (used for the print medium), Column Layout, etc., to tell the browser to place everything on the page in a particular fashion we fancy.

The modern browsers do all that with incredible speed and efficiency, however, we can still make it easier on them by adding layout containments to elements, so that any layout changes in the contained elements and its descendants can be handled independently, and if they are off screen, laying out their contents can be put off until needed. If you are considering optimizing your pages, layout containment is a good candidate to consider.

And that’s not all. More often than not, position of all the boxes in a page are based on each other — One box moves, the other follows suit. Which is not we always want to happen. With the help of layout containment, you can have multiple layouts in a page with the assurance that their contents are not going to flow into each other, by forming layout islands that are unaffected by whatever’s happening in the nearby islands.

Just as in the case of paint containment, layout containment also creates a separate formatting, and stacking, contexts for the contained elements. They also generate their own absolute and fixed positioning containing boxes, meaning they become the reference box to position any absolute — or fixed — positioned child elements.

.box {
  width: 100px;
  aspect-ratio: 1/1;
  .fixed-element {
    background: lime;
    position: fixed;
    left: 0px;
    bottom: 0px;
    ...
  }
  &:nth-of-type(2) { /* Box B */
    contain: layout; 
    ...
  }
  ...
}

Inline-Size and Style Containments

Although not yet a W3C recommendation, both contain: inline-size and contain: style are valid values, that are well supported in browsers, and are included in the W3C Working Drafts of CSS Containment Module Level 2 and 3. I didn’t mention them at the top only because they haven’t yet reached that recommendation status.

The functions of a Style Containment is also a bit tentative at the moment, and might grow before it hits the W3C recommendation status.

I’ll briefly explain them both, however. Inline Size Containment is same as the Size Containment, but for inline sizes only (the horizontal/width direction in any left-to-right or right-to-left horizontal writing mode). This gives you a size restraint for elements along their inline axes. This is particularly common for container queries. I haven’t mentioned container queries here, but these things are conceptually linked. When you set a container-type: inline-size as is required for typical container queries, you are effectively also setting contain: inline-size implicitly.

When an element has Style Containment, the tallying of CSS Counters and the quote values of the content property inside the contained element are unaffected by any counter or quote values mentioned outside. Essentially, the counters and quotes are scoped within a style-contained element. Pretty niche!

The strict and content Values

As I mentioned at the top, you can combine the different contain values (by space-separating them). There are also keywords for pre-combined values.

When contain has the value strictall the containment types are applied to an element. Probably not something you’d use unless you are working with an element or module on your page that’s prone to a lot of changes during the page’s existence on screen, like the display of live sports data or the like.

Then there’s the content value, which is a combination of paint, layout, and style. You are more likely to want to use this if your goal is to simply ensure nothing spills outside an element’s boundary at all cost, or to keep the browser from rendering or laying out the contained elements and its children when they are off screen or hidden, for the sake of optimization.

Those are my recommendations for the CSS contain property. It’s a property that’s just as worthwhile to learn about for the sake of programming techniques, as it is for the sake of optimizing web pages.

References

]]>
https://frontendmasters.com/blog/the-css-contain-property/feed/ 1 3519
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