Thursday, 2 March 2023

Everything You Need to Know About the Gap After the List Marker

I was reading “Creative List Styling” on Google’s web.dev blog and noticed something odd in one of the code examples in the ::marker section of the article. The built-in list markers are bullets, ordinal numbers, and letters. The ::marker pseudo-element allows us to style these markers or replace them with a custom character or image.

::marker {
  content: url('/marker.svg') ' ';
}

The example that caught my attention uses an SVG icon as a custom marker for the list items. But there’s also a single space character (" ") in the CSS value next to the url() function. The purpose of this space seems to be to insert a gap after the custom marker.

When I saw this code, I immediately wondered if there was a better way to create the gap. Appending a space to content feels more like a workaround than the optimal solution. CSS provides margin and padding and other standard ways to space out elements on the page. Could none of these properties be used in this situation?

First, I tried to substitute the space character with a proper margin:

::marker {
  content: url('/marker.svg');
  margin-right: 1ch;
}

This didn’t work. As it turns out, ::marker only supports a small set of mostly text-related CSS properties. For example, you can change the font-size and color of the marker, and define a custom marker by setting content to a string or URL, as shown above. But the margin and padding properties are not supported, so setting them has no effect. What a disappointment.

Could it really be that a space character is the only way to insert a gap after a custom marker? I needed to find out. As I researched this topic, I made a few interesting discoveries that I’d like to share in this article.

Adding padding and margins

First, let’s confirm what margin and padding do on the <ul> and <li> elements. I’ve created a test page for this purpose. Drag the relevant sliders and observe the effect on the spacing on each side of the list marker. Tip: Use the Reset button liberally to reset all controls to their initial values.

Note: Browsers apply a default padding-inline-left of 40px to <ol> and <ul> elements. The logical padding-inline-left property is equivalent to the physical padding-left property in writing systems with a left-to-right inline direction. In this article, I’m going to use physical properties for the sake of simplicity.

As you can see, padding-left on <li> increases the gap after the list marker. The other three properties control the spacing to the left of the marker, in other words, the indentation of the list item.

Notice that even when the list item’s padding-left is 0px, there is still a minimum gap after the marker. This gap cannot be decreased with margin or padding. The exact length of the minimum gap depends on the browser.

First three properties: UL margin-left, UL padding-left, LI margin-left. Fourth property: LI padding-left.
The first three properties push the entire list item (including the marker) to the right. The fourth property pushes only the list item’s content to the right.

To sum up, the list item’s content is positioned at a browser-specific minimum distance from the marker, and this gap can be further increased by adding a padding-left to <li>.

Next, let’s see what happens when we position the marker inside the list item.

Moving the marker inside the list item

The list-style-position property accepts two keywords: outside, which is the default, and inside, which moves the marker inside the list item. The latter is useful for creating designs with full-width list items.

A grocery list. Each item has a thin bottom border that extends from the left to the right edge of the list.
The list marker is positioned inside the list item, so that the list item’s bottom border can extend to the left edge of the list box

If the marker is now inside the list item, does this mean that padding-left on <li> no longer increases the gap after the marker? Let’s find out. On my test page, turn on list-style-position: inside via the checkbox. How are the four padding and margin properties affected by this change?

As you can see, padding-left on <li> now increases the spacing to the left of the marker. This means that we’ve lost the ability to increase the gap after the marker. In this situation, it would be useful to be able to add margin-right to the ::marker itself, but that doesn’t work, as we’ve established above.

The four properties: UL margin-left, UL padding-left, LI margin-left, LI padding-left.
All four properties push the entire list item to the right. The minimum gap cannot be increased by standard means.

Additionally, there’s a bug in Chromium that causes the gap after the marker to triple after switching to inside positioning. By default, the length of the gap is about one-third of the text size. So at a default font-size of 16px, the gap is about 5.5px. After switching to inside, the gap grows to the full 16px in Chrome. This bug affects the disc, circle, and square markers, but not ordinal number markers.

The following image shows the default rendering of outside and inside-positioned list markers across three major browsers on macOS. For your convenience, I’ve horizontally aligned all list items on their markers to make it easier to compare the differences in gap sizes.

Six list items with varying gaps between the marker and text.
Only Firefox maintains the same gap size between the two marker positioning modes. This can be considered a browser interoperability (interop) issue.

To sum up, switching to list-style-position: inside introduces two problems. We can no longer increase the gap via padding-left on <li>, and the gap size is inconsistent between browsers.

Finally, let’s see what happens when we replace the default list marker with a custom marker.

Switching to a custom marker

There are two ways to define a custom marker:

  • list-style-type and list-style-image properties
  • content property on the ::marker pseudo-element

The content property is more powerful. For example, it allows us to use the counter() function to access the list item’s ordinal number (the implicit list-item counter) and decorate it with custom strings.

Unfortunately, Safari doesn’t support the content property on ::marker yet (WebKit bug). For this reason, I’m going to use the list-style-type property to define the custom marker. You can still use the ::marker selector to style the custom marker declared via list-style-type. That aspect of ::marker is supported in Safari.

Any Unicode character can potentially serve as a custom list marker, but only a small set of characters actually have “Bullet” in their official name, so I thought I’d compile them here for reference.

Character Name Code point CSS keyword
Bullet U+2022 disc
Triangular Bullet U+2023
Hyphen Bullet U+2043
Black Leftwards Bullet U+204C
Black Rightwards Bullet U+204D
Inverse Bullet U+25D8
White Bullet U+25E6 circle
Reversed Rotated Floral Heart Bullet U+2619
Rotated Heavy Black Heart Bullet U+2765
Rotated Floral Heart Bullet U+2767
Circled White Bullet U+29BE
⦿ Circled Bullet U+29BF

Note: The CSS square keyword does not have a corresponding “Bullet” character in Unicode. The character that comes closest is the Black Small Square (▪️) emoji (U+25AA).

Now let’s see what happens when we replace the default list marker with list-style-type: "•" (U+2022 Bullet). This is the same character as the default bullet, so there shouldn’t be any major rendering differences. On my test page, turn on the list-style-type option and observe any changes to the marker.

As you can see, there are two significant changes:

  1. There is no longer a minimum gap after the marker.
  2. The bullet has become smaller, as if it were rendered at a smaller font-size.

According to CSS Counter Styles Level 3, the default list marker (disc) should be “similar to • U+2022 BULLET”. It seems that browsers increase the size of the default bullet to make it more legible. Firefox even uses a special font, -moz-bullet-font, for the marker.

:marker selected in the inspector. Fonts used: -moz-bullet-font.
The “Fonts” pane in Firefox’s DOM inspector reveals the special font.

Can the small size problem be fixed with CSS? On my test page, turn on marker styling and observe what happens when you change the font-size, line-height, and font-family of the marker.

As you can see, increasing the font-size causes the custom marker to become vertically misaligned, and this cannot be corrected by decreasing the line-height. The vertical-align property, which could easily fix this problem, is not supported on ::marker.

But did you notice that changing the font-family can cause the marker to become bigger? Try setting it to Tahoma. This could potentially be a good-enough workaround for the small-size problem, although I haven’t tested which font works best across the major browsers and operating systems.

You may also have noticed that the Chromium bug doesn’t occur anymore when you position the marker inside the list item. This means that a custom marker can serve as a workaround for this bug. And this leads me to the main problem, and the reason why I started researching this topic. If you define a custom marker and position it inside the list item, there is no gap after the marker and no way to insert a gap by standard means.

  1. There is no minimum gap after custom markers.
  2. ::marker doesn’t support padding or margin.
  3. padding-left on <li> doesn’t increase the gap, since the marker is positioned inside.

Summary

Here’s a summary of all the key facts that I’ve mentioned in the article:

  1. Browsers apply a default padding-inline-start of 40px to <ul> and <ol> elements.
  2. There is a minimum gap after built-in list markers (disc, decimal, etc.). There is no minimum gap after custom markers (string or URL).
  3. The length of the gap can be increased by adding a padding-left to <ul>, but only if the marker is positioned outside the list item (the default mode).
  4. Custom string markers have a smaller default size than built-in markers. Changing the font-family on ::marker can increase their size.

Conclusion

Looking back at the code example from the beginning of the article, I think I understand now why there’s a space character in the content value. There is just no better way to insert a gap after the SVG marker. It’s a workaround that is needed because no amount of margin and padding can create a gap after a custom marker that is positioned inside the list item. A margin-right on ::marker could easily do it, but that is not supported.

Until ::marker adds support for more properties, web developers will often have no choice but to hide the marker and emulate it with a ::before pseudo-element. I had to do that myself recently because I couldn’t change the marker’s background-color. Hopefully, we won’t have to wait too long for a more powerful ::marker pseudo-element.


Everything You Need to Know About the Gap After the List Marker originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/JWwYtSr
via IFTTT

Monday, 13 February 2023

An Approach to Lazy Loading Custom Elements

We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.

Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.

For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    this.discover(scope);
  }
}
customElements.define("ce-autoloader", AutoLoader);

Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.

Of course, we still have to implement that discover method (as part of the AutoLoader class above):

discover(scope) {
  let candidates = [scope, ...scope.querySelectorAll("*")];
  for(let el of candidates) {
    let tag = el.localName;
    if(tag.includes("-") && !customElements.get(tag)) {
      this.load(tag);
    }
  }
}

Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:

connectedCallback() {
  let scope = this.parentNode;
  requestIdleCallback(() => {
    this.discover(scope);
  });
}

requestIdleCallback is not universally supported yet, but we can use requestAnimationFrame as a fallback:

let defer = window.requestIdleCallback || requestAnimationFrame;

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    defer(() => {
      this.discover(scope);
    });
  }
  // ...
}

Now we can move on to implementing the missing load method to dynamically inject a <script> element:

load(tag) {
  let el = document.createElement("script");
  let res = new Promise((resolve, reject) => {
    el.addEventListener("load", ev => {
      resolve(null);
    });
    el.addEventListener("error", ev => {
      reject(new Error("failed to locate custom-element definition"));
    });
  });
  el.src = this.elementURL(tag);
  document.head.appendChild(el);
  return res;
}

elementURL(tag) {
  return `${this.rootDir}/${tag}.js`;
}

Note the hard-coded convention in elementURL. The src attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget>/components/my-widget.js). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:

class FancyLoader extends AutoLoader {
  elementURL(tag) {
    // fancy logic
  }
}

Either way, note that we’re relying on this.rootDir. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:

get rootDir() {
  let uri = this.getAttribute("root-dir");
  if(!uri) {
    throw new Error("cannot auto-load custom elements: missing `root-dir`");
  }
  if(uri.endsWith("/")) { // remove trailing slash
    return uri.substring(0, uri.length - 1);
  }
  return uri;
}

You might be thinking of observedAttributes now, but that doesn’t really make things easier. Plus updating root-dir at runtime seems like something we’re never going to need.

Now we can — and must — configure our elements directory: <ce-autoloader root-dir="/components">.

With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver comes into play:

connectedCallback() {
  let scope = this.parentNode;
  defer(() => {
    this.discover(scope);
  });
  let observer = this._observer = new MutationObserver(mutations => {
    for(let { addedNodes } of mutations) {
      for(let node of addedNodes) {
        defer(() => {
          this.discover(node);
        });
      }
    }
  });
  observer.observe(scope, { subtree: true, childList: true });
}

disconnectedCallback() {
  this._observer.disconnect();
}

This way, the browser notifies us whenever a new element appears in the DOM — or rather, our respective subtree — which we then use to restart the discovery process. (You might argue we’re re-inventing custom elements here, and you’d be kind of correct.)

Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/A69TK0h
via IFTTT

Friday, 10 February 2023

Different Ways to Get CSS Gradient Shadows

It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about it is basically a lot of CSS tricks to approximate a gradient. We’ll actually cover some of those as we go.

But first… another article about gradient shadows? Really?

Yes, this is yet another post on the topic, but it is different. Together, we’re going to push the limits to get a solution that covers something I haven’t seen anywhere else: transparency. Most of the tricks work if the element has a non-transparent background but what if we have a transparent background? We will explore this case here!

Before we start, let me introduce my gradient shadows generator. All you have to do is to adjust the configuration, and get the code. But follow along because I’m going to help you understand all the logic behind the generated code.

Non-transparent solution

Let’s start with the solution that’ll work for 80% of most cases. The most typical case: you are using an element with a background, and you need to add a gradient shadow to it. No transparency issues to consider there.

The solution is to rely on a pseudo-element where the gradient is defined. You place it behind the actual element and apply a blur filter to it.

.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* control the spread */
  transform: translate(10px, 8px); /* control the offsets */
  z-index: -1; /* place the element behind */
  background: /* your gradient here */;
  filter: blur(10px); /* control the blur */
}

It looks like a lot of code, and that’s because it is. Here’s how we could have done it with a box-shadow instead if we were using a solid color instead of a gradient.

box-shadow: 10px 8px 10px 5px orange;

That should give you a good idea of what the values in the first snippet are doing. We have X and Y offsets, the blur radius, and the spread distance. Note that we need a negative value for the spread distance that comes from the inset property.

Here’s a demo showing the gradient shadow next to a classic box-shadow:

If you look closely you will notice that both shadows are a little different, especially the blur part. It’s not a surprise because I am pretty sure the filter property’s algorithm works differently than the one for box-shadow. That’s not a big deal since the result is, in the end, quite similar.

This solution is good, but still has a few drawbacks related to the z-index: -1 declaration. Yes, there is “stacking context” happening there!

I applied a transform to the main element, and boom! The shadow is no longer below the element. This is not a bug but the logical result of a stacking context. Don’t worry, I will not start a boring explanation about stacking context (I already did that in a Stack Overflow thread), but I’ll still show you how to work around it.

The first solution that I recommend is to use a 3D transform:

.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}

Instead of using z-index: -1, we will use a negative translation along the Z-axis. We will put everything inside translate3d(). Don’t forget to use transform-style: preserve-3d on the main element; otherwise, the 3D transform won’t take effect.

As far as I know, there is no side effect to this solution… but maybe you see one. If that’s the case, share it in the comment section, and let’s try to find a fix for it!

If for some reason you are unable to use a 3D transform, the other solution is to rely on two pseudo-elements — ::before and ::after. One creates the gradient shadow, and the other reproduces the main background (and other styles you might need). That way, we can easily control the stacking order of both pseudo-elements.

.box {
  position: relative;
  z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -5px;
  transform: translate(10px, 8px);
  background: /* .. */;
  filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
  content: """;
  position: absolute;
  z-index: -1;
  inset: 0;
  /* Inherit all the decorations defined on the main element */
  background: inherit;
  border: inherit;
  box-shadow: inherit;
}

It’s important to note that we are forcing the main element to create a stacking context by declaring z-index: 0, or any other property that do the same, on it. Also, don’t forget that pseudo-elements consider the padding box of the main element as a reference. So, if the main element has a border, you need to take that into account when defining the pseudo-element styles. You will notice that I am using inset: -2px on ::after to account for the border defined on the main element.

As I said, this solution is probably good enough in a majority of cases where you want a gradient shadow, as long as you don’t need to support transparency. But we are here for the challenge and to push the limits, so even if you don’t need what is coming next, stay with me. You will probably learn new CSS tricks that you can use elsewhere.

Transparent solution

Let’s pick up where we left off on the 3D transform and remove the background from the main element. I will start with a shadow that has both offsets and spread distance equal to 0.

The idea is to find a way to cut or hide everything inside the area of the element (inside the green border) while keeping what is outside. We are going to use clip-path for that. But you might wonder how clip-path can make a cut inside an element.

Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:

clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)

Tada! We have a gradient shadow that supports transparency. All we did is add a clip-path to the previous code. Here is a figure to illustrate the polygon part.

Showing the clip-path coordinates for the element.

The blue area is the visible part after applying the clip-path. I am only using the blue color to illustrate the concept, but in reality, we will only see the shadow inside that area. As you can see, we have four points defined with a big value (B). My big value is 100vmax, but it can be any big value you want. The idea is to ensure we have enough space for the shadow. We also have four points that are the corners of the pseudo-element.

The arrows illustrate the path that defines the polygon. We start from (-B, -B) until we reach (0,0). In total, we need 10 points. Not eight points because two points are repeated twice in the path ((-B,-B) and (0,0)).

There’s still one more thing left for us to do, and it’s to account for the spread distance and the offsets. The only reason the demo above works is because it is a particular case where the offsets and spread distance are equal to 0.

Let’s define the spread and see what happens. Remember that we use inset with a negative value to do this:

The pseudo-element is now bigger than the main element, so the clip-path cuts more than we need it to. Remember, we always need to cut the part inside the main element (the area inside the green border of the example). We need to adjust the position of the four points inside of clip-path.

.box {
  --s: 10px; /* the spread  */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(0px  + var(--s)),
    calc(0px  + var(--s)) calc(0px  + var(--s))
  );
}

We’ve defined a CSS variable, --s, for the spread distance and updated the polygon points. I didn’t touch the points where I am using the big value. I only update the points that define the corners of the pseudo-element. I increase all the zero values by --s and decrease the 100% values by --s.

It’s the same logic with the offsets. When we translate the pseudo-element, the shadow is out of alignment, and we need to rectify the polygon again and move the points in the opposite direction.

.box {
  --s: 10px; /* the spread */
  --x: 10px; /* X offset */
  --y: 8px;  /* Y offset */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  transform: translate3d(var(--x), var(--y), -1px);
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(0px  + var(--s) - var(--y)),
    calc(0px  + var(--s) - var(--x)) calc(0px  + var(--s) - var(--y))
  );
}

There are two more variables for the offsets: --x and --y. We use them inside of transform and we also update the clip-path values. We still don’t touch the polygon points with big values, but we offset all the others — we reduce --x from the X coordinates, and --y from the Y coordinates.

Now all we have to do is to update a few variables to control the gradient shadow. And while we are at it, let’s also make the blur radius a variable as well:

Do we still need the 3D transform trick?

It all depends on the border. Don’t forget that the reference for a pseudo-element is the padding box, so if you apply a border to your main element, you will have an overlap. You either keep the 3D transform trick or update the inset value to account for the border.

Here is the previous demo with an updated inset value in place of the 3D transform:

I‘d say this is a more suitable way to go because the spread distance will be more accurate, as it starts from the border-box instead of the padding-box. But you will need to adjust the inset value according to the main element’s border. Sometimes, the border of the element is unknown and you have to use the previous solution.

With the earlier non-transparent solution, it’s possible you will face a stacking context issue. And with the transparent solution, it’s possible you face a border issue instead. Now you have options and ways to work around those issues. The 3D transform trick is my favorite solution because it fixes all the issues (The online generator will consider it as well)

Adding a border radius

If you try adding border-radius to the element when using the non-transparent solution we started with, it is a fairly trivial task. All you need to do is to inherit the same value from the main element, and you are done.

Even if you don’t have a border radius, it’s a good idea to define border-radius: inherit. That accounts for any potential border-radius you might want to add later or a border radius that comes from somewhere else.

It’s a different story when dealing with the transparent solution. Unfortunately, it means finding another solution because clip-path cannot deal with curvatures. That means we won’t be able to cut the area inside the main element.

We will introduce the mask property to the mix.

This part was very tedious, and I struggled to find a general solution that doesn’t rely on magic numbers. I ended up with a very complex solution that uses only one pseudo-element, but the code was a lump of spaghetti that covers only a few particular cases. I don’t think it is worth exploring that route.

I decided to insert an extra element for the sake of simpler code. Here’s the markup:

<div class="box">
  <sh></sh>
</div>

I am using a custom element, <sh>, to avoid any potential conflict with external CSS. I could have used a <div>, but since it’s a common element, it can easily be targeted by another CSS rule coming from somewhere else that can break our code.

The first step is to position the <sh> element and purposely create an overflow:

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

The code may look a bit strange, but we’ll get to the logic behind it as we go. Next, we create the gradient shadow using a pseudo-element of <sh>.

.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
  transform-style: preserve-3d;
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
  transform: translateZ(-1px)
}
.box sh::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: var(--r);
  background: /* Your gradient */;
  filter: blur(10px);
  transform: translate(10px,8px);
}

As you can see, the pseudo-element uses the same code as all the previous examples. The only difference is the 3D transform defined on the <sh> element instead of the pseudo-element. For the moment, we have a gradient shadow without the transparency feature:

Note that the area of the <sh> element is defined with the black outline. Why I am doing this? Because that way, I am able to apply a mask on it to hide the part inside the green area and keep the overflowing part where we need to see the shadow.

I know it’s a bit tricky, but unlike clip-path, the mask property doesn’t account for the area outside an element to show and hide things. That’s why I was obligated to introduce the extra element — to simulate the “outside” area.

Also, note that I am using a combination of border and inset to define that area. This allows me to keep the padding-box of that extra element the same as the main element so that the pseudo-element won’t need additional calculations.

Another useful thing we get from using an extra element is that the element is fixed, and only the pseudo-element is moving (using translate). This will allow me to easily define the mask, which is the last step of this trick.

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

It’s done! We have our gradient shadow, and it supports border-radius! You probably expected a complex mask value with oodles of gradients, but no! We only need two simple gradients and a mask-composite to complete the magic.

Let’s isolate the <sh> element to understand what is happening there:

.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid red;
  background: lightblue;
  border-radius: calc(150px + var(--r));
}

Here’s what we get:

Note how the inner radius matches the main element’s border-radius. I have defined a big border (150px) and a border-radius equal to the big border plus the main element’s radius. On the outside, I have a radius equal to 150px + R. On the inside, I have 150px + R - 150px = R.

We must hide the inner (blue) part and make sure the border (red) part is still visible. To do that, I’ve defined two mask layers —One that covers only the content-box area and another that covers the border-box area (the default value). Then I excluded one from another to reveal the border.

mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;

I used the same technique to create a border that supports gradients and border-radius. Ana Tudor has also a good article about masking composite that I invite you to read.

Are there any drawbacks to this method?

Yes, this definitely not perfect. The first issue you may face is related to using a border on the main element. This may create a small misalignment in the radii if you don’t account for it. We have this issue in our example, but perhaps you can hardly notice it.

The fix is relatively easy: Add the border’s width for the <sh> element’s inset.

.box {
  --r: 50px;
  border-radius: var(--r);
  border: 2px solid;
}
.box sh {
  position: absolute;
  inset: -152px; /* 150px + 2px */
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}

Another drawback is the big value we’re using for the border (150px in the example). This value should be big enough to contain the shadow but not too big to avoid overflow and scrollbar issues. Luckily, the online generator will calculate the optimal value considering all the parameters.

The last drawback I am aware of is when you’re working with a complex border-radius. For example, if you want a different radius applied to each corner, you must define a variable for each side. It’s not really a drawback, I suppose, but it can make your code a bit tougher to maintain.

.box {
  --r-top: 10px;
  --r-right: 40px;
  --r-bottom: 30px;
  --r-left: 20px;
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
  border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}

The online generator only considers a uniform radius for the sake of simplicity, but you now know how to modify the code if you want to consider a complex radius configuration.

Wrapping up

We’ve reached the end! The magic behind gradient shadows is no longer a mystery. I tried to cover all the possibilities and any possible issues you might face. If I missed something or you discover any issue, please feel free to report it in the comment section, and I’ll check it out.

Again, a lot of this is likely overkill considering that the de facto solution will cover most of your use cases. Nevertheless, it’s good to know the “why” and “how” behind the trick, and how to overcome its limitations. Plus, we got good exercise playing with CSS clipping and masking.

And, of course, you have the online generator you can reach for anytime you want to avoid the hassle.


Different Ways to Get CSS Gradient Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/g3vzina
via IFTTT

Thursday, 9 February 2023

Healthcare, Selling Lemons, and the Price of Developer Experience

Every now and then, a one blog post is published and it spurs a reaction or response in others that are, in turn, published as blogs posts, and a theme starts to emerge. That’s what happened this past week and the theme developed around the cost of JavaScript frameworks — a cost that, in this case, reveals just how darn important it is to use JavaScript responsibly.

Eric Bailey: Modern Health, frameworks, performance, and harm

This is where the story begins. Eric goes to a health service provider website to book an appointment and gets… a blank screen.

In addition to a terrifying amount of telemetry, Modern Health’s customer-facing experience is delivered using React and Webpack.

If you are familiar with how the web is built, what happened is pretty obvious: A website that over-relies on JavaScript to power its experience had its logic collide with one or more other errant pieces of logic that it summons. This created a deadlock.

If you do not make digital experiences for a living, what happened is not obvious at all. All you see is a tiny fake loading spinner that never stops.

D’oh. This might be mere nuisance — or even laughable — in some situations, but not when someone’s health is on the line:

A person seeking help in a time of crisis does not care about TypeScript, tree shaking, hot module replacement, A/B tests, burndown charts, NPS, OKRs, KPIs, or other startup jargon. Developer experience does not count for shit if the person using the thing they built can’t actually get what they need.

This is the big smack of reality. What happens when our tooling and reporting — the very things that are supposed to make our work more effective — get in the way of the user experience? These are tools that provide insights that can help us anticipate a user’s needs, especially in a time of need.

I realize that pointing the finger at JavaScript frameworks is already divisive. But this goes beyond whether you use React or framework d’jour. It’s about business priorities and developer experience conflicting with user experiences.

Alex Russell: The Market for Lemons

Partisans for slow, complex frameworks have successfully marketed lemons as the hot new thing, despite the pervasive failures in their wake, crowding out higher-quality options in the process.

These technologies were initially pitched on the back of “better user experiences”, but have utterly failed to deliver on that promise outside of the high-management-maturity organisations in which they were born. Transplanted into the wider web, these new stacks have proven to be expensive duds.

There’s the rub. Alex ain’t mincing words, but notice that the onus is on the way frameworks haved been marketed to developers than developers themselves. The sales pitch?

Once the lemon sellers embed the data-light idea that improved “Developer Experience” (“DX”) leads to better user outcomes, improving “DX” became and end unto itself, and many who knew better felt forced to play along. The long lead times in falsifying trickle-down UX was a feature, not a bug; they don’t need you to succeed, only to keep buying.

As marketing goes, the “DX” bait-and-switch is brilliant, but the tech isn’t delivering for anyone but developers.

Tough to stomach, right? No one wants to be duped, and it’s tough to admit a sunken cost when there is one. It gets downright personal if you’ve invested time in a specific piece of tech and effort integrating it into your stack. Development workflows are hard and settling into one is sorta like settling into a house you plan on living in a little while. But you’d want to know if your house was built on what Alex calls a “sandy foundation”.

I’d just like to pause here a moment to say I have no skin in this debate. As a web generalist, I tend to adopt new tools early for familiarity then drop them fast, relegating them to my toolshed until I find a good use for them. In other words, my knowledge is wide but not very deep in one area or thing. HTML, CSS, and JavaScript is my go-to cocktail, but I do care a great deal about user experience and know when to reach for a tool to solve a particular thing.

And let’s acknowledge that not everyone has a say in the matter. Many of us work on managed teams that are prescribed the tools we use. Alex says as much, which I think is important to call out because it’s clear this isn’t meant to be personal. It’s a statement on our priorities and making sure they along to user expectations.

Let’s alow Chris to steer us back to the story…

Chris Coyier: End-To-End Tests with Content Blockers?

So, maybe your app is built on React and it doesn’t matter why it’s that way. There’s still work to do to ensure the app is reliable and accessible.

Just blocking a file shouldn’t totally wreck a website, but it often does! In JavaScript, that may be because the developers have written first-party JavaScript (which I’ll generally allow) that depends on third-party JavaScript (which I’ll generally block).

[…]

If I block resources from tracking-website.com, now my first-party JavaScript is going to throw an error. JavaScript isn’t chill. If an error is thrown, it doesn’t execute more JavaScript further down in the file. If further down in that file is transitionToOnboarding();— that ain’t gonna work.

Maybe it’s worth revisiting your workflow and tweaking it to account to identify more points of failure.

So here’s an idea: Run your end-to-end tests in browsers that have popular content blockers with default configs installed. 

Doing so may uncover problems like this that stop your customers, and indeed people in need, from being stopped in their tracks.

Good idea! Hey, anything that helps paint a more realistic picture of how the app is used. That sort of clarity could happen a lot earlier in the process, perhaps before settling on development decisions. Know your users. Why are they using the app? How do they browse the web? Where are they phsically located? What problems could get in their way? Chris has a great talk on that, too.


Healthcare, Selling Lemons, and the Price of Developer Experience originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/RDGuQVO
via IFTTT

Moving Backgrounds

We often think of background images as texture or something that provides contrast for legible content — in other words, not really content. If it was content, you’d probably reach for an <img> anyway, accessibility and whatnot.

But there are times when the position or scale of a background image might sit somewhere between the poles of content and decoration. Context is king, right? If we change the background image’s position, it may convey a bit more context or experience.

How so? Let’s look at a few examples I’ve seen floating around.

As we get started, I’ll caution that there’s a fine line in these demos between images used for decoration and images used as content. The difference has accessibility implications where backgrounds are not announced to screen readers. If your image is really an image, then maybe consider an <img> tag with proper alt text. And while we’re talking accessibility, it’s a good idea to consider a user’s motion preference’s as well.

Show me more!

Chris Coyier has this neat little demo from several years back.

The demo is super practical in lots of ways because it’s a neat approach for displaying ads in content. You have the sales pitch and an enticing image to supplement it.

The big limitation for most ads, I’d wager, is the limited real estate. I don’t know if you’ve ever had to drop an ad onto a page, but I have and typically ask the advertiser for an image that meets exact pixel dimensions, so the asset fits the space.

But Chris’s demo alleviates the space issue. Hover the image and watch it both move and scale. The user actually gets more context for the product than they would have when the image was in its original position. That’s a win-win, right? The advertiser gets to create an eye-catching image without compromising context. Meanwhile, the user gets a little extra value from the newly revealed portions of the image.

If you peek at the demo’s markup, you’ll notice it’s pretty much what you’d expect. Here’s an abridged version:

<div class="ad-container">
  <a href="#" target="_blank" rel="noopener">
    <!-- Background image container  -->
    <div class="ad-image"></div>
  </a> 
  <div class="ad-content">
    <!-- Content -->
  </div>
</div>

We could probably quibble over the semantics a bit, but that’s not the point. We have a container with a linked-up <div> for the background image and another <div> to hold the content.

As far as styling goes, the important pieces are here:

.container {
  background-image: url("/path/to/some/image.png");
  background-repeat: no-repeat;
  background-position: 0 0;
  height: 400px;
  width: 350px;
}

Not bad, right? We give the container some dimensions and set a background image on it that doesn’t repeat and is positioned by its bottom-left edge.

The real trick is with JavaScript. We will use that to get the mouse position and the container’s offset, then convert that value to an appropriate scale to set the background-position. First, let’s listen for mouse movements on the .container element:

let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
    // Our function
  }
);

From here, we can use the container’s offsetX and offsetY properties. But we won’t use these values directly, as the value for the X coordinate is smaller than what we need, and the Y coordinate is larger. We will have to play around a bit to find a constant that we can use as a multiplier.

It’s a bit touch-and-feel, but I’ve found that 1.32 and 0.455 work perfectly for the X and Y coordinates, respectively. We multiply the offsets by those values, append a px unit on the result, then apply it to the background-position values.

let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
    container.style.backgroundPositionX = -e.offsetX * 1.32 + "px";
    container.style.backgroundPositionY = -e.offsetY * 0.455 + "px";
  }
);

Lastly, we can also reset the background positions back to the original if the user leaves the image container.

container.addEventListener("mouseleave", function() {
    container.style.backgroundPosition = "0px 0px";
  }
);

Since we’re on CSS-Tricks, I’ll offer that we could have done a much cheaper version of this with a little hover transition in vanilla CSS:

Paint a bigger picture

No doubt you’ve been to some online clothing store or whatever and encountered the ol’ zoom-on-hover feature.

This pattern has been around for what feels like forever (Dylan Winn-Brown shared his approach back in 2016), but that’s just a testament (I hope) to its usefulness. The user gets more context as they zoom in and get a better idea of a sweater’s stitching or what have you.

There’s two pieces to this: the container and the magnifier. The container is the only thing we need in the markup, as we’ll inject the magnifier element during the user’s interaction. So, behold our HTML!

<div class="container"></div>

​​In the CSS, we will create width and height variables to store the dimensions of the of the magnifier glass itself.  Then we’ll give that .container​ some shape and a background-image​:

​​:root {
​​  --magnifer-width: 85;
​​  --magnifer-height: 85;
​​}

.container {
  width: 500px;
  height: 400px;
  background-size: cover;
  background-image: url("/path/to/image.png");
  background-repeat: no-repeat;
  position: relative;
}

There are some things we already know about the magnifier before we even see it, and we can define those styles up-front, specifically the previously defined variables for the .maginifier‘s width and height:

.magnifier {
  position: absolute;
  width: calc(var(--magnifer-width) * 1px);
​​  height: calc(var(--magnifer-height) * 1px);
​​  border: 3px solid #000;
​​  cursor: none;
​​  background-image: url("/path/to/image.png");
​​  background-repeat: no-repeat;
}

It’s an absolutely-positioned little square that uses the same background image file as the .container. Do note that the calc function is solely used here to convert the unit-less value in the variable to pixels. Feel free to arrange that however you see fit as far as eliminating repetition in your code.

Now, let’s turn to the JavaScript that pulls this all together. First we need to access the CSS variable defined earlier. We will use this in multiple places later on. Then we need get the mouse position within the container because that’s the value we’ll use for the the magnifier’s background position.

​​// Get the css variables
​​let root = window.getComputedStyle(document.documentElement);
​​let magnifier_width = root.getPropertyValue("--magnifer-width");
​​let magnifier_height = root.getPropertyValue("--magnifer-height");

let container = document.querySelector(".container");
let rect = container.getBoundingClientRect();
let x = (e.pageX - rect.left);
let y = (e.pageY - rect.top);

// Take page scrolling into account
x = x - window.pageXOffset;
y = y - window.pageYOffset;

What we need is basically a mousemove event listener on the .container. Then, we will use the event.pageX or event.pageY property to get the X or Y coordinate of the mouse. But to get the exact relative position of the mouse on an element, we need to subtract the position of the parent element from the mouse position we get from the JavaScript above. A “simple” way to do this is to use getBoundingClientRect(), which returns the size of an element and its position relative to the viewport.

Notice how I’m taking scrolling into account. If there is overflow, subtracting the window pageX and pageY offsets will ensure the effect runs as expected.

We will first create the magnifier div. Next, we will create a mousemove function and add it to the image container. In this function, we will give the magnifier a class attribute. We will also calculate the mouse position and give the magnifier the left and top values we calculated earlier.

Let’s go ahead and build the magnifier when we hear a mousemove event on the .container:

// create the magnifier
let magnifier = document.createElement("div");
container.append(magnifier);

Now we need to make sure it has a class name we can scope to the CSS:

// run the function on `mousemove`
container.addEventListener("mousemove", (e) => {
  magnifier.setAttribute("class", "magnifier");
}

The example video I showed earlier positions the magnifier outside of the container. We’re gonna keep this simple and overlay it on top of the container instead as the mouse moves. We will use if statements to set the magnifier’s position only if the X and Y values are greater or equal to zero, and less than the container’s width or height. That should keep it in bounds. Just be sure to subtract the width and height of the magnifier from the X and Y values.

// Run the function on mouse move.
container.addEventListener("mousemove", (e) => {
  magnifier.setAttribute("class", "magnifier");

  // Get mouse position
  let rect = container.getBoundingClientRect();
  let x = (e.pageX - rect.left);
  let y = (e.pageY - rect.top);
  
  // Take page scrolling into account
  x = x - window.pageXOffset;
  y = y - window.pageYOffset;

  // Prevent magnifier from exiting the container
  // Then set top and left values of magnifier
  if (x >= 0 && x <= container.clientWidth - magnifier_width) {
    magnifier.style.left = x + "px";
  }
  if (y >= 0 && y <= container.clientHeight - magnifier_height) {
    magnifier.style.top = y + "px";
  }
});

Last, but certainly not least… we need to play with the magnifier’s background image a bit. The whole point is that the user gets a BIGGER view of the background image based on where the hover is taking place. So, let’s define a magnifier we can use to scale things up. Then we’ll define variables for the background image’s width and height so we have something to base that scale on, and set all of those values on the .magnifier styles:

// Magnifier image configurations
let magnify = 2;
let imgWidth = 500;
let imgHeight = 400;

magnifier.style.backgroundSize = imgWidth * magnify + "px " + imgHeight * magnify + "px";

​​Let’s take the X and Y coordinates of the magnifier’s image and apply them to the .magnifier​ element’s background-position​. As before with the magnifier position, we need to subtract the width and height of the magnifier from the X and Y values using the CSS variables.

// the x and y positions of the magnifier image
let magnify_x = x * magnify + 15;
let magnify_y = y * magnify + 15;

// set backgroundPosition for magnifier if it is within image
if (
  x <= container.clientWidth - magnifier_width &&
  y <= container.clientHeight - magnifier_height
) {
  magnifier.style.backgroundPosition = -magnify_x + "px " + -magnify_y + "px";
}

Tada!

Make it cinematic

Have you seen the Ken Burns effect? It’s classic and timeless thing where an image is bigger than the container it’s in, then sorta slides and scales slow as a slug. Just about every documentary film in the world seems to use it for image stills. If you have an Apple TV, then you’ve certainly seen it on the screen saver.

There are plenty of examples over at CodePen if you wanna get a better idea.

You’ll see that there are a number of ways to approach this. Some use JavaScript. Others are 100% CSS. I’m sure the JavaScript approaches are good for some uses cases, but if the goal is simply to subtly scale the image, CSS is perfectly suitable.

We could spice things up a bit using multiple backgrounds rather than one. Or, better yet, if we expand the rules to use elements instead of background images, we can apply the same animation to all of the backgrounds and use a dash of animation-delay to stagger the effect.

Lots of ways to do this, of course! It can certainly be optimized with Sass and/or CSS variables. Heck, maybe you can pull it off with a single <div> If so, share it in the comments!

Bonus: Make it immersive

I don’t know if anything is cooler than Sarah Drasner’s “Happy Halloween” pen… and that’s from 2016! It is a great example that layers backgrounds and moves them at varying speeds to create an almost cinematic experience. Good gosh is that cool!

GSAP is the main driver there, but I imagine we could make a boiled-down version that simply translates each background layer from left to right at different speeds. Not as cool, of course, but certainly the baseline experience. Gotta make sure the start and end of each background image is consistent so it repeats seamlessly when the animation repeats.


That’s it for now! Pretty neat that we can use backgrounds for much more than texture and contrast. I’m absolutely positive there are tons of other clever interactions we can apply to backgrounds. Temani Afif did exactly that with a bunch of neat hover effects for links. What do you have in mind? Share it with me in the comments!


Moving Backgrounds originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/cishHM1
via IFTTT

Tuesday, 7 February 2023

The truth about CSS selector performance

Geez, leave it to Patrick Brosset to talk CSS performance in the most approachable and practical way possible. Not that CSS is always what’s gunking up the speed, or even the lowest hanging fruit when it comes to improving performance.

But if you’re looking for gains on the CSS side of things, Patrick has a nice way of sniffing out your most expensive selectors using Edge DevTools:

  • Crack open DevTools.
  • Head to the Performance Tab.
  • Make sure you have the “Enable advanced rendering instrumentation” option enabled. This tripped me up in the process.
  • Record a page load.
  • Open up the “Bottom-Up” tab in the report.
  • Check out your the size of your recalculated styles.
DevTools with Performance tab open and a summary of events.

From here, click on one of the Recalculated Style events in the Main waterfall view and you’ll get a new “Selector Stats” tab. Look at all that gooey goodness!

Now you see all of the selectors that were processed and they can be sorted by how long they took, how many times they matched, the number of matching attempts, and something called “fast reject count” which I learned is the number of elements that were easy and quick to eliminate from matching.

A lot of insights here if CSS is really the bottleneck that needs investigating. But read Patrick’s full post over on the Microsoft Edge Blog because he goes much deeper into the why’s and how’s, and walks through an entire case study.

To Shared LinkPermalink on CSS-Tricks


The truth about CSS selector performance originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/GzErRNq
via IFTTT

Passkeys: What the Heck and Why?

These things called  passkeys  sure are making the rounds these days. They were a main attraction at  W3C TPAC 2022 , gained support in  Saf...