About

Tuesday, November 30, 2021

Recreating the Apple Music Hits Playlist Animation in CSS

Apple Music has this “Spatial Audio” feature where the direction of the music in your headphones is based on the location of the device. It’s tough to explain just how neat it is. But that’s not what I’m here to talk about.

I opened up the Apple Music app and saw a featured playlist of hit songs that support Spatial Audio. The cover for it is this brightly-colored pink container that holds a bunch of boxes stacked one on top of another. The boxes animate in one at a time, fading in at the center of the container, then fading out as it scales to the size of the container. Like an infinite loop.

Animated GIF showing the Apple Music UI we are recreating. It's brightly colored shades of pink against a dark gray background with information about the playlist to the right of the pattern, and options to play and shuffle the sings in orange buttons.

Cool! I knew I had to re-create it in CSS. So I did.

Here’s how it works…

The markup

I started with the HTML. There’s obviously a container we need to define, plus however many boxes we want to animate. I went with an even 10 boxes in the container.

<div class="container">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <!-- etc. -->
</div>

That’s literally it for HTML. We are free to jump right into the CSS!

Styling the container

Nothing too fancy here. I measured approximate dimensions based on what I saw in Apple Music, which happened to be 315px × 385px. then I took a screenshot of the cover and dropped it into my image editing app to get the lightest possible color, which is around the outside edges of the container. My color picker landed on #eb5bec.

.container {
  background-color: #eb5bec;
  height: 315px;
  width: 385px;
}

As I was doing this, I knew I would probably want this to be a grid container to align the boxes and any other elements in the center. I also figured that the boxes themselves would start from the center of the container and stack on top of one another, meaning there will be some absolute positioning. That also means the container ought to have relative positioning to reign them in.

.container {
  background-color: #eb5bec;
  height: 315px;
  position: relative;
  width: 385px;
}

And since we want the boxes to start from the center, we can reach for grid to help with that:

.container {
  background-color: #eb5bec;
  display: grid;
  height: 315px;
  place-items: center;
  position: relative;
  width: 385px;
}

If the boxes in the container are growing outward, then there’s a chance that they could expand beyond the container. Better hide any possible overflow.

.container {
  background-color: #eb5bec;
  height: 315px;
  overflow: hidden;
  position: relative;
  width: 385px;
}

I also noticed some rounded corners on it, so let’s drop that in while we’re here.

.container {
  background-color: #eb5bec;
  border-radius: 16px;
  height: 315px;
  position: relative;
  width: 385px;
}

So far, so good!

Styling the boxes

We have 10 .box elements in the markup and we want them stacked on top of one another. I started with some absolute positioning, then sized them at 100px square. Then I did the same thing with my image editing app to find the darkest color value of a box, which was #471e45.

.box {
  background: #471e45;
  height: 100px;
  position: absolute;
  width: 100px;
}

The boxes seem to fade out as they grow. That allows one box to be seen through the other, so let’s make them opaque to start.

.box {
  background: #471e45;
  height: 100px;
  opacity: 0.5;
  position: absolute;
  width: 100px;
}

Cool, cool. We’re unable to see all the boxes as they’re stacked on top of one another, but we’re making progress!

Creating the animation

Ready to write some @keyframes? We’re gonna make this super simple, going from 0 to 100% without any steps in between. We don’t even need those percentages!

@keyframes grow {
  from {
    /* do stuff */
  }
  to {
    /* do stuff */
  }
}

Specifically, we want two things to happen from start to finish:

  • The boxes go from our starting opacity value of 0.5 to 0 (fully transparent).
  • The boxes scale up to the edges of the container.
@keyframes grow {
  from {
    opacity: 0.5;
    transform: scale(0);
  }
  to {
    opacity: 0;
    transform: scale(3.85);
  }
}

How’d I land on scaling the boxes up by 3.85? Our boxes are 100px square and the container is 385px tall. A value of 3.85 gets the boxes up to 385px as they fade completely out which makes for a nice linear animation when we get there.

Speaking of which…

Applying the animation

It’s pretty easy to call the animation on our boxes. Just gotta make sure it moves in a liner timing function on an infinite basis so it’s like the Energizer Bunny and keeps going and going and going and going and…

.box {
  animation: grow 10s linear infinite; /* 10s = 10 boxes */
  /* etc. */
}

This gives us the animation we want. But! The boxes are all moving at the same time, so all we see is one giant box growing.

We’ve gotta stagger those little fellers. No loops in vanilla CSS, unfortunately, so we have to delay each box individually. We can start by setting a custom property for the delay, set it to one second, then redefine the custom property on each instance.

.box {
  --delay: 1s;
  
  animation-delay: var(--delay);
  /* same as before */
}
.box:nth-child(2) {
  --delay: 2s;
}
.box:nth-child(3) {
  --delay: 3s;
}
.box:nth-child(4) {
  --delay: 4s;
}
.box:nth-child(5) {
  --delay: 5s;
}
/* five more times... */

Huzzah!

Keep on rockin’

That’s it! We just recreated the same sort of effect used by Apple Music. There are a few finishing touches we could plop in there, like the content and whatnot. Here’s my final version again:


The post Recreating the Apple Music Hits Playlist Animation in CSS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/recreating-the-apple-music-hits-playlist-animation-in-css/

We Analyzed 425,909 Favicons

This is a neat idea for a research project. The big map is fun, but the research had some tidbits in it worth looking at.

The average favicon network request takes 130ms, at least from our speedy cloud instance.

Fast, but not that fast, particularly for a file that nearly every website in the world has. All the more reason to get it right and ensure only one is downloaded (ideally SVG).

I would have guessed most favicons are ICO, but no:

The vast majority of the favicons offered up by websites are PNG. 71.6% of <link rel=”icon”> images are PNG.

And lol:

21.1% of /favicon.ico files are secretly PNGs

One of the reasons that file extensions, to browsers, are rather meaningless. It’s all about that content-type.

Also, they question the accuracy of the method, but the dominant color in the analysis so far is purple. I hope it ends up true as it kinda makes sense. Unless you’re offering different favicons for dark mode, using white or black seems too dangerous these days.

To Shared LinkPermalink on CSS-Tricks


The post We Analyzed 425,909 Favicons appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/we-analyzed-425909-favicons/

Saturday, November 27, 2021

When is it “Right” to Reach for contain and will-change in CSS?

I’ve got some blind spots in CSS-related performance things. One example is the will-change property. It’s a good name. You’re telling the browser some particular property (or the scroll-position or content) uh, will, change:

.el {
  will-change: opacity;
}
.el.additional-hard-to-know-state {
  opacity: 0;
}

But is that important to do? I don’t know. The point, as I understand it, is that it will kick .el into processing/rendering/painting on the GPU rather than CPU, which is a speed boost. Sort of like the classic transform: translate3d(0, 0, 0); hack. In the exact case above, it doesn’t seem to my brain like it would matter. I have in my head that opacity is one of the “cheapest” things to animate, so there is no particular benefit to will-change. Or maybe it matters noticeably on some browsers or devices, but not others? This is front-end development after all.

There was a spurt of articles about will-change around 2014/2015 that warn about weird behavior, like unexpected changes in stacking contexts and being careful not to use it “too much.” There was also advice spreading around that you should never use this property directly in CSS stylesheets; you should only apply it in JavaScript before the state change, then remove it after you no longer need it.

I have no idea if any of those things are still true. Sorry! I’d love to read a 2022 deep dive on will-change. We’re capable of that kind of testing, so I’ll put it in the idea pile. But my point is that there are things in CSS that are designed explicitly for performance that are confusing to me, and I wish I had a more full understanding of them because they seem like Very Big Deals.

Take “How I made Google’s data grid scroll 10x faster with one line of CSS” by Johan Isaksson. A 10✕ scrolling performance improvement is a massive deal! Know how they fixed it?

[…] as I was browsing the “Top linking sites” page I noticed major scroll lag. This happens when choosing to display a larger dataset (500 rows) instead of the default 10 results.

[…]

So, what did I do? I simply added a single line of CSS to the <table> on the Elements panel, specifying that it will not affect the layout or style of other elements on the page

table {
  contain: strict; 
}

The contain property is another that I sort of get, but I’d still call it a blind spot because my brain doesn’t just automatically think of when I could (or should?) use it. But that’s a bummer, because clearly I’m not building interfaces as performant as I could be if I did understand contain better.

There’s another! The content-visibility property. The closest I came to understanding it was after watching Jake and Surma’s video on it where they used it (along with contain-intrinsic-size and some odd magic numbers) to dramatically speed up a long page. What hasn’t stuck with me is when I should use it on my pages.

Are all three of these features “there if you need them” features? Is it OK to ignore them until you notice poor performance on something (like a massive page) and then reach for them to attempt to solve it? Almost “don’t use these until you need them,” otherwise you’re in premature optimization territory. The trouble with that is the classic situation where you won’t actually notice the poor performance unless you are very actively testing on the lowest-specced devices out there.

Or are these features “this is what modern CSS is and you should be thinking of them like you think of padding” territory? I kind of suspect it’s more like that. If you’re building an element you know won’t change in certain ways, it’s probably worth “containing” it. If you’re building an element you know will change in certain ways, it’s probably worth providing that info to browsers. If you’re building a part of page you know is always below the fold, it’s probably worth avoiding the paint on it. But personally, I just don’t have enough of this fully grokked to offer any solid advice.


The post When is it “Right” to Reach for contain and will-change in CSS? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/when-is-it-right-to-reach-for-contain-and-will-change-in-css/

A Handy Little System for Animated Entrances in CSS

I love little touches that make a website feel like more than just a static document. What if web content wouldn’t just “appear” when a page loaded, but instead popped, slid, faded, or spun into place? It might be a stretch to say that movements like this are always useful, though in some cases they can draw attention to certain elements, reinforce which elements are distinct from one another, or even indicate a changed state. So, they’re not totally useless, either.

So, I put together a set of CSS utilities for animating elements as they enter into view. And, yes, this pure CSS. It not only has a nice variety of animations and variations, but supports staggering those animations as well, almost like a way of creating scenes.

You know, stuff like this:

Which is really just a fancier version of this:

We’ll go over the foundation I used to create the animations first, then get into the little flourishes I added, how to stagger animations, then how to apply them to HTML elements before we also take a look at how to do all of this while respecting a user’s reduced motion preferences.

The basics

The core idea involves adding a simple CSS @keyframes animation that’s applied to anything we want to animate on page load. Let’s make it so that an element fades in, going from opacity: 0 to opacity: 1 in a half second:

.animate {
  animation-duration: 0.5s;
  animation-name: animate-fade;
  animation-delay: 0.5s;
  animation-fill-mode: backwards;
}

@keyframes animate-fade {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Notice, too, that there’s an animation-delay of a half second in there, allowing the rest of the site a little time to load first. The animation-fill-mode: backwards is there to make sure that our initial animation state is active on page load. Without this, our animated element pops into view before we want it to.

If we’re lazy, we can call it a day and just go with this. But, CSS-Tricks readers aren’t lazy, of course, so let’s look at how we can make this sort of thing even better with a system.

Fancier animations

It’s much more fun to have a variety of animations to work with than just one or two. We don’t even need to create a bunch of new @keyframes to make more animations. It’s simple enough to create new classes where all we change is which frames the animation uses while keeping all the timing the same.

There’s nearly an infinite number of CSS animations out there. (See animate.style for a huge collection.) CSS filters, like blur(), brightness() and saturate() and of course CSS transforms can also be used to create even more variations.

But for now, let’s start with a new animation class that uses a CSS transform to make an element “pop” into place.

.animate.pop {
  animation-duration: 0.5s;
  animation-name: animate-pop;
  animation-timing-function: cubic-bezier(.26, .53, .74, 1.48);
}

@keyframes animate-pop {
  0% {
    opacity: 0;
    transform: scale(0.5, 0.5);
  }

  100% {
    opacity: 1;
    transform: scale(1, 1);
  }
}

I threw in a little cubic-bezier() timing curve, courtesy of Lea Verou’s indispensable cubic-bezier.com for a springy bounce.

Adding delays

We can do better! For example, we can animate elements so that they enter at different times. This creates a stagger that makes for complex-looking motion without a complex amount of code.

This animation on three page elements using a CSS filter, CSS transform, and staggered by about a tenth of a second each, feels really nice:

All we did there was create a new class for each element that spaces when the elements start animating, using animation-delay values that are just a tenth of a second apart.

.delay-1 { animation-delay: 0.6s; }  
.delay-2 { animation-delay: 0.7s; }
.delay-3 { animation-delay: 0.8s; }

Everything else is exactly the same. And remember that our base delay is 0.5s, so these helper classes count up from there.

Respecting accessibility preferences

Let’s be good web citizens and remove our animations for users who have enabled their reduced motion preference setting:

@media screen and (prefers-reduced-motion: reduce) {
  .animate { animation: none !important; }
}

This way, the animation never loads and elements enter into view like normal. It’s here, though, that is worth a reminder that “reduced” motion doesn’t always mean “remove” motion.

Applying animations to HTML elements

So far, we’ve looked at a base animation as well as a slightly fancier one that we were able to make even fancier with staggered animation delays that are contained in new classes. We also saw how we can respect user motion preferences at the same time.

Even though there are live demos that show off the concepts, we haven’t actually walked though how to apply our work to HTML. And what’s cool is that we can use this on just about any element, whether its a div, span, article, header, section, table, form… you get the idea.

Here’s what we’re going to do. We want to use our animation system on three HTML elements where each element gets three classes. We could hard-code all the animation code to the element itself, but splitting it up gives us a little animation system we can reuse.

  • .animate: This is the base class that contains our core animation declaration and timing.
  • The animation type: We’ll use our “pop” animation from before, but we could use the one that fades in as well. This class is technically optional but is a good way to apply distinct movements.
  • .delay-<number>: As we saw earlier, we can create distinct classes that are used to stagger when the animation starts on each element, making for a neat effect. This class is also optional.

So our animated elements might now look like:

<h2 class="animate pop">One!</h2>
<h2 class="animate pop delay-1">Two!</h2>
<h2 class="animate pop delay-2">Three!</h2>

Let’s count them in!

Conclusion

Check that out: we went from a seemingly basic set of @keyframes and turned it into a full-fledged system for applying interesting animations for elements entering into view.

This is ridiculously fun, of course. But the big takeaway for me is how the examples we looked at form a complete system that can be used to create a baseline, different types of animations, staggered delays, and an approach for respecting user motion preferences. These, to me, are all the ingredients for a flexible system that easy to use, while giving us a lot with a little and without a bunch of extra cruft.

What we covered could indeed be a full animation library. But, of course, I did’t stop there and have my entire CSS file of animations in all its glory for you. There are several more types of animations in there, including 15 classes of different delays that can be used for staggering things. I’ve been using these on my own projects, but it’s still an early draft and I love feedback on it—so please enjoy and let me know what you think in the comments!

/* ==========================================================================
Animation System by Neale Van Fleet from Rogue Amoeba
========================================================================== */
.animate {
  animation-duration: 0.75s;
  animation-delay: 0.5s;
  animation-name: animate-fade;
  animation-timing-function: cubic-bezier(.26, .53, .74, 1.48);
  animation-fill-mode: backwards;
}

/* Fade In */
.animate.fade {
  animation-name: animate-fade;
  animation-timing-function: ease;
}

@keyframes animate-fade {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

/* Pop In */
.animate.pop { animation-name: animate-pop; }

@keyframes animate-pop {
  0% {
    opacity: 0;
    transform: scale(0.5, 0.5);
  }
  100% {
    opacity: 1;
    transform: scale(1, 1);
  }
}

/* Blur In */
.animate.blur {
  animation-name: animate-blur;
  animation-timing-function: ease;
}

@keyframes animate-blur {
  0% {
    opacity: 0;
    filter: blur(15px);
  }
  100% {
    opacity: 1;
    filter: blur(0px);
  }
}

/* Glow In */
.animate.glow {
  animation-name: animate-glow;
  animation-timing-function: ease;
}

@keyframes animate-glow {
  0% {
    opacity: 0;
    filter: brightness(3) saturate(3);
    transform: scale(0.8, 0.8);
  }
  100% {
    opacity: 1;
    filter: brightness(1) saturate(1);
    transform: scale(1, 1);
  }
}

/* Grow In */
.animate.grow { animation-name: animate-grow; }

@keyframes animate-grow {
  0% {
    opacity: 0;
    transform: scale(1, 0);
    visibility: hidden;
  }
  100% {
    opacity: 1;
    transform: scale(1, 1);
  }
}

/* Splat In */
.animate.splat { animation-name: animate-splat; }

@keyframes animate-splat {
  0% {
    opacity: 0;
    transform: scale(0, 0) rotate(20deg) translate(0, -30px);
    }
  70% {
    opacity: 1;
    transform: scale(1.1, 1.1) rotate(15deg));
  }
  85% {
    opacity: 1;
    transform: scale(1.1, 1.1) rotate(15deg) translate(0, -10px);
  }

  100% {
    opacity: 1;
    transform: scale(1, 1) rotate(0) translate(0, 0);
  }
}

/* Roll In */
.animate.roll { animation-name: animate-roll; }

@keyframes animate-roll {
  0% {
    opacity: 0;
    transform: scale(0, 0) rotate(360deg);
  }
  100% {
    opacity: 1;
    transform: scale(1, 1) rotate(0deg);
  }
}

/* Flip In */
.animate.flip {
  animation-name: animate-flip;
  transform-style: preserve-3d;
  perspective: 1000px;
}

@keyframes animate-flip {
  0% {
    opacity: 0;
    transform: rotateX(-120deg) scale(0.9, 0.9);
  }
  100% {
    opacity: 1;
    transform: rotateX(0deg) scale(1, 1);
  }
}

/* Spin In */
.animate.spin {
  animation-name: animate-spin;
  transform-style: preserve-3d;
  perspective: 1000px;
}

@keyframes animate-spin {
  0% {
    opacity: 0;
    transform: rotateY(-120deg) scale(0.9, .9);
  }
  100% {
    opacity: 1;
    transform: rotateY(0deg) scale(1, 1);
  }
}

/* Slide In */
.animate.slide { animation-name: animate-slide; }

@keyframes animate-slide {
  0% {
    opacity: 0;
    transform: translate(0, 20px);
  }
  100% {
    opacity: 1;
    transform: translate(0, 0);
  }
}

/* Drop In */
.animate.drop { 
  animation-name: animate-drop; 
  animation-timing-function: cubic-bezier(.77, .14, .91, 1.25);
}

@keyframes animate-drop {
0% {
  opacity: 0;
  transform: translate(0,-300px) scale(0.9, 1.1);
}
95% {
  opacity: 1;
  transform: translate(0, 0) scale(0.9, 1.1);
}
96% {
  opacity: 1;
  transform: translate(10px, 0) scale(1.2, 0.9);
}
97% {
  opacity: 1;
  transform: translate(-10px, 0) scale(1.2, 0.9);
}
98% {
  opacity: 1;
  transform: translate(5px, 0) scale(1.1, 0.9);
}
99% {
  opacity: 1;
  transform: translate(-5px, 0) scale(1.1, 0.9);
}
100% {
  opacity: 1;
  transform: translate(0, 0) scale(1, 1);
  }
}

/* Animation Delays */
.delay-1 {
  animation-delay: 0.6s;
}
.delay-2 {
  animation-delay: 0.7s;
}
.delay-3 {
  animation-delay: 0.8s;
}
.delay-4 {
  animation-delay: 0.9s;
}
.delay-5 {
  animation-delay: 1s;
}
.delay-6 {
  animation-delay: 1.1s;
}
.delay-7 {
  animation-delay: 1.2s;
}
.delay-8 {
  animation-delay: 1.3s;
}
.delay-9 {
  animation-delay: 1.4s;
}
.delay-10 {
  animation-delay: 1.5s;
}
.delay-11 {
  animation-delay: 1.6s;
}
.delay-12 {
  animation-delay: 1.7s;
}
.delay-13 {
  animation-delay: 1.8s;
}
.delay-14 {
  animation-delay: 1.9s;
}
.delay-15 {
  animation-delay: 2s;
}

@media screen and (prefers-reduced-motion: reduce) {
  .animate {
    animation: none !important;
  }
}

The post A Handy Little System for Animated Entrances in CSS appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/a-handy-little-system-for-animated-entrances-in-css/

Thursday, November 25, 2021

Creating Generative Patterns with The CSS Paint API

The browser has long been a medium for art and design. From Lynn Fisher’s joyful A Single Div creations to Diana Smith’s staggeringly detailed CSS paintings, wildly creative, highly skilled developers have — over the years — continuously pushed web technologies to their limits and crafted innovative, inspiring visuals.

CSS, however, has never really had an API dedicated to… well, just drawing stuff! As demonstrated by the talented folks above, it certainly can render most things, but it’s not always easy, and it’s not always practical for production sites/applications.

Recently, though, CSS was gifted an exciting new set of APIs known as Houdini, and one of them — the Paint API — is specifically designed for rendering 2D graphics. For us web folk, this is incredibly exciting. For the first time, we have a section of CSS that exists for the sole purpose of programmatically creating images. The doors to a mystical new world are well and truly open!

In this tutorial, we will be using the Paint API to create three (hopefully!) beautiful, generative patterns that could be used to add a delicious spoonful of character to a range of websites/applications.

Spellbooks/text editors at the ready, friends, let’s do some magic!

Intended audience

This tutorial is perfect for folks who are comfortable writing HTML, CSS, and JavaScript. A little familiarity with generative art and some knowledge of the Paint API/HTML canvas will be handy but not essential. We will do a quick overview before we get started. Speaking of which…

Before we start

For a comprehensive introduction to both the Paint API and generative art/design, I recommend popping over to the first entry in this series. If you are new to either subject, this will be a great place to start. If you don’t feel like navigating another article, however, here are a couple of key concepts to be familiar with before moving on.

If you are already familiar with the CSS Paint API and generative art/design, feel free to skip ahead to the next section.

What is generative art/design?

Generative art/design is any work created with an element of chance. We define some rules and allow a source of randomness to guide us to an outcome. For example, a rule could be “if a random number is greater than 50, render a red square, if it is less than 50, render a blue square*,”* and, in the browser, a source of randomness could be Math.random().

By taking a generative approach to creating patterns, we can generate near-infinite variations of a single idea — this is both an inspiring addition to the creative process and a fantastic opportunity to delight our users. Instead of showing people the same imagery every time they visit a page, we can display something special and unique for them!

What is the CSS Paint API?

The Paint API gives us low-level access to CSS rendering. Through “paint worklets” (JavaScript classes with a special paint() function), it allows us to dynamically create images using a syntax almost identical to HTML canvas. Worklets can render an image wherever CSS expects one. For example:

.worklet-canvas {
  background-image: paint(workletName);
}

Paint API worklets are fast, responsive, and play ever so well with existing CSS-based design systems. In short, they are the coolest thing ever. The only thing they are lacking right now is widespread browser support. Here’s a table:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
65 No No 79 No

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
95 No 95 No

A little thin on the ground! That’s OK, though. As the Paint API is almost inherently decorative, we can use it as a progressive enhancement if it’s available and provide a simple, dependable fallback if not.

What we are making

In this tutorial, we will be learning how to create three unique generative patterns. These patterns are quite simple, but will act as a wonderful springboard for further experimentation. Here they are in all their glory!

The demos in this tutorial currently only work in Chrome and Edge.

“Tiny Specks”

“Bauhaus”

“Voronoi Arcs”

Before moving on, take a moment to explore the examples above. Try changing the custom properties and resizing the browser window — watch how the patterns react. Can you guess how they might work without peeking at the JavaScript?

Getting set up

To save time and eliminate the need for any custom build processes, we will be working entirely in CodePen throughout this tutorial. I have even created a “starter Pen” that we can use as a base for each pattern!

I know, it’s not much to look at… yet.

In the starter Pen, we are using the JavaScript section to write the worklet itself. Then, in the HTML section, we load the JavaScript directly using an internal <script> tag. As Paint API worklets are special workers (code that runs on a separate browser thread), their origin must1 exist in a standalone .js file.

Let’s break down the key pieces of code here.

If you have written Paint API worklets before, and are familiar with CodePen, you can skip ahead to the next section.

Defining the worklet class

First things first: Let’s check out the JavaScript tab. Here we define a worklet class with a simple paint() function:

class Worklet {
  paint(ctx, geometry, props) {
    const { width, height } = geometry;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, width, height);
  }
}

I like to think of a worklet’s paint() function as a callback. When the worklet’s target element updates (changes dimensions, modifies custom properties), it re-runs. A worklet’s paint() function automatically has a few parameters passed when it executes. In this tutorial, we are interested in the first three:

  • ctx — a 2D drawing context very similar to that of HTML canvas
  • geometry — an object containing the width/height dimensions of the worklet’s target element
  • props — an array of CSS custom properties that we can “watch” for changes and re-render when they do. These are a great way of passing values to paint worklets.

Our starter worklet renders a black square that covers the entire width/height of its target element. We will completely rewrite this paint() function for each example, but it’s nice to have something defined to check things are working.

Registering the worklet

Once a worklet class is defined, it needs to be registered before we can use it. To do so, we call registerPaint in the worklet file itself:

if (typeof registerPaint !== "undefined") {
  registerPaint("workletName", Worklet);
}

Followed by CSS.paintWorklet.addModule() in our “main” JavaScript/HTML:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/bGrMXxm.js');
  }
</script>

We are checking registerPaint is defined before running it here, as our pen’s JavaScript will always run once on the main browser thread — registerPaint only becomes available once the JavaScript file is loaded into a worklet using CSS.paintWorklet.addModule(...).

Applying the worklet

Once registered, we can use our worklet to generate an image for any CSS property that expects one. In this tutorial, we will focus on background-image:

.worklet-canvas {
  background-image: paint(workletName);
}

Package imports

You may notice a couple of package imports dangling at the top of the starter pen’s worklet file:

import random from "https://cdn.skypack.dev/random";
import seedrandom from "https://cdn.skypack.dev/seedrandom";
Can you guess what they are?

Random number generators!

All three of the patterns we are creating in this tutorial rely heavily on randomness. Paint API worklets should, however, (almost) always be deterministic. Given the same input properties and dimensions, a worklet’s paint() function should always render the same thing.

Why?

  1. The Paint API may want to use a cached version of a worklet’s paint() output for better performance. Introducing an unpredictable element to a worklet renders this impossible!
  2. A worklet’s paint() function re-runs whenever the element it applies to changes dimensions. When coupled with “pure” randomness, this can result in significant flashes of content — a potential accessibility issue for some folks.

For us, all this renders Math.random() a little useless, as it is entirely unpredictable. As an alternative, we are pulling in random (an excellent library for working with random numbers) and seedrandom (a pseudo-random number generator to use as its base algorithm).

As a quick example, here’s a “random circles” worklet using a pseudo-random number generator:

And here’s a similar worklet using Math.random(). Warning: Resizing the element results in flashing imagery.

There’s a little resize handle in the bottom-right of both of the above patterns. Try resizing both elements. Notice the difference?

Setting up each pattern

Before beginning each of the following patterns, navigate to the starter Pen and click the “Fork” button in the footer. Forking a Pen creates a copy of the original the moment you click the button. From this point, it is yours to do whatever you like.

Once you have forked the starter Pen, there is a critical extra step to complete. The URL passed to CSS.paintWorklet.addModule must be updated to point to the new fork’s JavaScript file. To find the path for your fork’s JavaScript, take a peek at the URL shown in your browser. You want to grab your fork’s URL with all query parameters removed, and append .js — something like this:

Lovely. That’s the ticket! Once you have the URL for your JavaScript, make sure you update it here:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    // ⚠️ hey friend! update the URL below each time you fork this pen! ⚠️
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/QWMVdPG.js');
  }
</script>

When working with this setup, you may occasionally need to manually refresh the Pen in order to see your changes. To do so, hit CMD/CTRL + Shift + 7.

Pattern #1 (Tiny Specks)

OK, we are ready to make our first pattern. Fork the starter Pen, update the .js file reference, and settle in for some generative fun!

As a quick reminder, here’s the finished pattern:

Updating the worklet’s name

Once again, first things first: Let’s update the starter worklet’s name and relevant references:

class TinySpecksPattern {
  // ...
}
if (typeof registerPaint !== "undefined") {
  registerPaint("tinySpecksPattern", TinySpecksPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(tinySpecksPattern);
}

Defining the worklet’s input properties

Our “Tiny Specks” worklet will accept the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each speck
  • --pattern-speck-count — how many individual specks the worklet should render
  • --pattern-speck-min-size — the minimum size for each speck
  • --pattern-speck-max-size — the maximum size for each speck

As our next step, let’s define the inputProperties our worklet can receive. To do so, we can add a getter to our TinySpecksPattern class:

class TinySpecksPattern {
  static get inputProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-speck-count",
      "--pattern-speck-min-size",
      "--pattern-speck-max-size"
    ];
  }
  // ...
}

Alongside some custom property definitions in our CSS:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 1000;
  inherits: true;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #161511, #dd6d45, #f2f2f2;
  inherits: true;
}

@property --pattern-speck-count {
  syntax: "<number>";
  initial-value: 3000;
  inherits: true;
}

@property --pattern-speck-min-size {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

@property --pattern-speck-max-size {
  syntax: "<number>";
  initial-value: 3;
  inherits: true;
}

We are using the Properties and Values API here (another member of the Houdini family) to define our custom properties. Doing so affords us two valuable benefits. First, we can define sensible defaults for the input properties our worklet expects. A tasty sprinkle of developer experience! Second, by including a syntax definition for each custom property, our worklet can interpret them intelligently.

For example, we define the syntax <color># for --pattern-colors. In turn, this allows us to pass an array of comma-separated colors to the worklet in any valid CSS color format. When our worklet receives these values, they have been converted to RGB and placed in a neat little array. Without a syntax definition, worklets interpret all props as simple strings.

Like the Paint API, the Properties and Values API also has limited browser support.

The paint() function

Awesome! Here’s the fun bit. We have created our “Tiny Speck” worklet class, registered it, and defined what input properties it can expect to receive. Now, let’s make it do something!

As a first step, let’s clear out the starter Pen’s paint() function, keeping only the width and height definitions:

paint(ctx, geometry, props) {
  const { width, height } = geometry;
}

Next, let’s store our input properties in some variables:

const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());
const count = props.get("--pattern-speck-count").value;
const minSize = props.get("--pattern-speck-min-size").value;
const maxSize = props.get("--pattern-speck-max-size").value;

Next, we should initialize our pseudo-random number generator:

random.use(seedrandom(seed));

Ahhh, predictable randomness! We are re-seeding seedrandom with the same seed value every time paint() runs, resulting in a consistent stream of random numbers across renders.

Finally, let’s paint our specks!

First off, we create a for-loop that iterates count times. In every iteration of this loop, we are creating one individual speck:

for (let i = 0; i < count; i++) {
}

As the first action in our for-loop, we define an x and y position for the speck. Somewhere between 0 and the width/height of the worklet’s target element is perfect:

const x = random.float(0, width);
const y = random.float(0, height);

Next, we choose a random size (for the radius):

const radius = random.float(minSize, maxSize);

So, we have a position and a size defined for the speck. Let’s choose a random color from our colors to fill it with:

ctx.fillStyle = colors[random.int(0, colors.length - 1)];

Alright. We are all set. Let’s use ctx to render something!

The first thing we need to do at this point is save() the state of our drawing context. Why? We want to rotate each speck, but when working with a 2D drawing context like this, we cannot rotate individual items. To rotate an object, we have to spin the entire drawing space. If we don’t save() and restore() the context, the rotation/translation in every iteration will stack, leaving us with a very messy (or empty) canvas!

ctx.save();

Now that we have saved the drawing context’s state, we can translate to the speck’s center point (defined by our x/y variables) and apply a rotation. Translating to the center point of an object before rotating ensures the object rotates around its center axis:

ctx.translate(x, y);
ctx.rotate(((random.float(0, 360) * 180) / Math.PI) * 2);
ctx.translate(-x, -y);

After applying our rotation, we translate back to the top-left corner of the drawing space.

We choose a random value between 0 and 360 (degrees) here, then convert it into radians (the rotation format ctx understands).

Awesome! Finally, let’s render an ellipse — this is the shape that defines our specks:

ctx.beginPath();
ctx.ellipse(x, y, radius, radius / 2, 0, Math.PI * 2, 0);
ctx.fill();

Here’s a simple pen showing the form of our random specks, a little closer up:

Perfect. Now, all we need to do is restore the drawing context:

ctx.restore();

That’s it! Our first pattern is complete. Let’s also apply a background-color to our worklet canvas to finish off the effect:

.worklet-canvas {
  background-color: #90c3a5;
  background-image: paint(tinySpecksPattern);
}

Next steps

From here, try changing the colors, shapes, and distribution of the specks. There are hundreds of directions you could take this pattern! Here’s an example using little triangles rather than ellipses:

Onwards!

Pattern #2 (Bauhaus)

Nice work! That’s one pattern down. Onto the next one. Once again, fork the starter Pen and update the worklet’s JavaScript reference to get started.

As a quick refresher, here’s the finished pattern we are working toward:

Updating the worklet’s name

Just like we did last time, let’s kick things off by updating the worklet’s name and relevant references:

class BauhausPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("bauhausPattern", BauhausPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(bauhausPattern);
}

Lovely.

Defining the worklet’s input properties

Our “Bauhaus Pattern” worklet expects the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each shape in the pattern
  • --pattern-size — the value used to define both the width and height of a square pattern area
  • --pattern-detail — the number of columns/rows to divide the square pattern into

Let’s add these input properties to our worklet:

class BahausPattern {
  static get inputProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-size",
      "--pattern-detail"
    ];
  }
  // ...
}

…and define them in our CSS, again, using the Properties and Values API:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 1000;
  inherits: true;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #2d58b5, #f43914, #f9c50e, #ffecdc;
  inherits: true;
}

@property --pattern-size {
  syntax: "<number>";
  initial-value: 1024;
  inherits: true;
}

@property --pattern-detail {
  syntax: "<number>";
  initial-value: 12;
  inherits: true;
}

Excellent. Let’s paint!

The paint() function

Again, let’s clear out the starter worklet’s paint function, leaving only the width and height definition:

paint(ctx, geometry, props) {
  const { width, height } = geometry;
}

Next, let’s store our input properties in some variables:

const patternSize = props.get("--pattern-size").value;
const patternDetail = props.get("--pattern-detail").value;
const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());

Now, we can seed our pseudo-random number generator just like before:

random.use(seedrandom(seed));

Awesome! As you might have noticed, the setup for Paint API worklets is always somewhat similar. It’s not the most exciting process, but it’s an excellent opportunity to reflect on the architecture of your worklet and how other developers may use it.

So, with this worklet, we create a fixed-dimension square pattern filled with shapes. This fixed-dimension pattern is then scaled up or down to cover the worklet’s target element. Think of this behavior a bit like background-size: cover in CSS!

Here’s a diagram:

To achieve this behavior in our code, let’s add a scaleContext function to our worklet class:

scaleCtx(ctx, width, height, elementWidth, elementHeight) {
  const ratio = Math.max(elementWidth / width, elementHeight / height);
  const centerShiftX = (elementWidth - width * ratio) / 2;
  const centerShiftY = (elementHeight - height * ratio) / 2;
  ctx.setTransform(ratio, 0, 0, ratio, centerShiftX, centerShiftY);
}

And call it in our paint() function:

this.scaleCtx(ctx, patternSize, patternSize, width, height);

Now, we can work to a set of fixed dimensions and have our worklet’s drawing context automatically scale everything for us — a handy function for lots of use cases.

Next up, we are going to create a 2D grid of cells. To do so, we define a cellSize variable (the size of the pattern area divided by the number of columns/rows we would like):

const cellSize = patternSize / patternDetail;

Then, we can use the cellSize variable to “step-through” the grid, creating equally-spaced, equally-sized cells to add random shapes to:

for (let x = 0; x < patternSize; x += cellSize) {
  for (let y = 0; y < patternSize; y += cellSize) {
  }
}

Within the second nested loop, we can begin to render stuff!

First off, let’s choose a random color for the current shape:

const color = colors[random.int(0, colors.length - 1)];

ctx.fillStyle = color;

Next, let’s store a reference to the current cell’s center x and y position:

const cx = x + cellSize / 2;
const cy = y + cellSize / 2;

In this worklet, we are positioning all of our shapes relative to their center point. While we are here, let’s add some utility functions to our worklet file to help us quickly render center-aligned shape objects. These can live outside of the Worklet class:

function circle(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 2);
  ctx.closePath();
}

function arc(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 1);
  ctx.closePath();
}

function rectangle(ctx, cx, cy, size) {
  ctx.beginPath();
  ctx.rect(cx - size / 2, cy - size / 2, size, size);
  ctx.closePath();
}

function triangle(ctx, cx, cy, size) {
  const originX = cx - size / 2;
  const originY = cy - size / 2;
  ctx.beginPath();
  ctx.moveTo(originX, originY);
  ctx.lineTo(originX + size, originY + size);
  ctx.lineTo(originX, originY + size);
  ctx.closePath();
}

I won’t go into too much detail here, but here’s a diagram visualizing how each of these functions work:

If you get stuck on the graphics rendering part of any of the worklets in this tutorial, look at the MDN docs on HTML canvas. The syntax/usage is almost identical to the 2D graphics context available in Paint API worklets.

Cool! Let’s head back over to our paint() function’s nested loop. The next thing we need to do is choose what shape to render. To do so, we can pick a random string from an array of possibilities:

const shapeChoice = ["circle", "arc", "rectangle", "triangle"][
  random.int(0, 3)
];

We can also pick a random rotation amount in a very similar way:

const rotationDegrees = [0, 90, 180][random.int(0, 2)];

Perfect. We are ready to render!

To start, let’s save our drawing context’s state, just like in the previous worklet:

ctx.save();

Next, we can translate to the center point of the current cell and rotate the canvas using the random value we just chose:

ctx.translate(cx, cy);
ctx.rotate((rotationDegrees * Math.PI) / 180);
ctx.translate(-cx, -cy);

Now we can render the shape itself! Let’s pass our shapeChoice variable to a switch statement and use it to decide which shape rendering function to run:

switch (shapeChoice) {
  case "circle":
    circle(ctx, cx, cy, cellSize / 2);
    break;
  case "arc":
    arc(ctx, cx, cy, cellSize / 2);
    break;
  case "rectangle":
    rectangle(ctx, cx, cy, cellSize);
    break;
  case "triangle":
    triangle(ctx, cx, cy, cellSize);
    break;
}

ctx.fill();

Finally, all we need to do is restore() our drawing context ready for the next shape:

ctx.restore();

With that, our Bauhaus Grids worklet is complete!

Next steps

There are so many directions you could take this worklet. How could you parameterize it further? Could you add a “bias” for specific shapes/colors? Could you add more shape types?

Always experiment — following along with the examples we are creating together is an excellent start, but the best way to learn is to make your own stuff! If you are stuck for inspiration, take a peek at some patterns on Dribbble, look to your favorite artists, the architecture around you, nature, you name it!

As a simple example, here’s the same worklet, in an entirely different color scheme:

Pattern #3 (Voronoi Arcs)

So far, we have created both a chaotic pattern and one that aligns strictly to a grid. For our last example, let’s build one that sits somewhere between the two.

As one last reminder, here’s the finished pattern:

Before we jump in and write any code, let’s take a look at how this worklet… works.

A brief introduction to Voronoi tessellations

As suggested by the name, this worklet uses something called a Voronoi tessellation to calculate its layout. A Voronoi tessellation (or diagram) is, in short, a way to partition a space into non-overlapping polygons.

We add a collection of points to a 2D space. Then for each point, calculate a polygon that contains only it and no other points. Once calculated, the polygons can be used as a kind of “grid” to position anything.

Here’s an animated example:

The fascinating thing about Voronoi-based layouts is that they are responsive in a rather unusual way. As the points in a Voronoi tessellation move around, the polygons automatically re-arrange themselves to fill the space!

Try resizing the element below and watch what happens!

Cool, right?

If you would like to learn more about all things Voronoi, I have an article that goes in-depth. For now, though, this is all we need.

Updating the worklet’s name

Alright, folks, we know the deal here. Let’s fork the starter Pen, update the JavaScript import, and change the worklet’s name and references:

class VoronoiPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("voronoiPattern", VoronoiPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(voronoiPattern);
}

Defining the worklet’s input properties

Our VoronoiPattern worklet expects the following input properties:

  • --pattern-seed — a seed value for the pseudo-random number generator
  • --pattern-colors — the available colors for each arc/circle in the pattern
  • --pattern-background — the pattern’s background color

Let’s add these input properties to our worklet:

class VoronoiPattern {
  static get inputProperties() {
    return ["--pattern-seed", "--pattern-colors", "--pattern-background"];
  }
  // ...
}

…and register them in our CSS:

@property --pattern-seed {
  syntax: "<number>";
  initial-value: 123456;
  inherits: true;
}

@property --pattern-background {
  syntax: "<color>";
  inherits: false;
  initial-value: #141b3d;
}

@property --pattern-colors {
  syntax: "<color>#";
  initial-value: #e9edeb, #66aac6, #e63890;
  inherits: true;
}

Nice! We are all set. Overalls on, friends — let us paint.

The paint() function

First, let’s clear out the starter worklet’s paint() function, retaining only the width and height definitions. We can then create some variables using our input properties, and seed our pseudo-random number generator, too. Just like in our previous examples:

paint(ctx, geometry, props) {
  const { width, height } = geometry;

  const seed = props.get("--pattern-seed").value;
  const background = props.get("--pattern-background").toString();
  const colors = props.getAll("--pattern-colors").map((c) => c.toString());

  random.use(seedrandom(seed));
}

Before we do anything else, let’s paint a quick background color:

ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);

Next, let’s import a helper function that will allow us to quickly cook up a Voronoi tessellation:

import { createVoronoiTessellation } from "https://cdn.skypack.dev/@georgedoescode/generative-utils";

This function is essentially a wrapper around d3-delaunay and is part of my generative-utils repository. You can view the source code on GitHub. With “classic” data structures/algorithms such as Voronoi tessellations, there is no need to reinvent the wheel — unless you want to, of course!

Now that we have our createVoronoiTessellation function available, let’s add it to paint():

const { cells } = createVoronoiTessellation({
  width,
  height,
  points: [...Array(24)].map(() => ({
    x: random.float(0, width),
    y: random.float(0, height)
  }))
});

Here, we create a Voronoi Tessellation at the width and height of the worklet’s target element, with 24 controlling points.

Awesome. Time to render our shapes! Lots of this code should be familiar to us, thanks to the previous two examples.

First, we loop through each cell in the tessellation:

cells.forEach((cell) => {
});

For each cell, the first thing we do is choose a color:

ctx.fillStyle = colors[random.int(0, colors.length - 1)];

Next, we store a reference to the center x and y values of the cell:

const cx = cell.centroid.x;
const cy = cell.centroid.y;

Next, we save the context’s current state and rotate the canvas around the cell’s center point:

ctx.save();

ctx.translate(cx, cy);
ctx.rotate((random.float(0, 360) / 180) * Math.PI);
ctx.translate(-cx, -cy);

Cool! Now, we can render something. Let’s draw an arc with an end angle of either PI or PI * 2. To me and you, a semi-circle or a circle:

ctx.beginPath();
ctx.arc(
  cell.centroid.x,
  cell.centroid.y,
  cell.innerCircleRadius * 0.75,
  0,
  Math.PI * random.int(1, 2)
);
ctx.fill();

Our createVoronoiTessellation function attaches a special innerCircleRadius to each cell — this is the largest possible circle that can fit at its center without touching any edges. Think of it as a handy guide for scaling objects to the bounds of a cell. In the snippet above, we are using innerCircleRadius to determine the size of our arcs.

Here’s a simple pen highlighting what’s happening here:

Now that we have added a “primary” arc to each cell, let’s add another one, 25% of the time. This time, however, we can set the arc’s fill color to our worklets background color. Doing so gives us the effect of a little hole in the middle of some of the shapes!

if (random.float(0, 1) > 0.25) {
  ctx.fillStyle = background;
  ctx.beginPath();
  ctx.arc(
    cell.centroid.x,
    cell.centroid.y,
    (cell.innerCircleRadius * 0.75) / 2,
    0,
    Math.PI * 2
  );
  ctx.fill();
}

Great! All we need to do now is restore the drawing context:

ctx.restore();

And, that’s it!

Next steps

The beautiful thing about Voronoi tessellations is that you can use them to position anything at all. In our example, we used arcs, but you could render rectangles, lines, triangles, whatever! Perhaps you could even render the outlines of the cells themselves?

Here’s a version of our VoronoiPattern worklet that renders lots of small lines, rather than circles and semicircles:

Randomizing patterns

You may have noticed that up until this point, all of our patterns have received a static --pattern-seed value. This is fine, but what if we would like our patterns to be random each time they display? Well, lucky for us, all we need to do is set the --pattern-seed variable when the page loads to be a random number. Something like this:

document.documentElement.style.setProperty('--pattern-seed', Math.random() * 10000);

We touched on this briefly earlier, but this is a lovely way to make sure a webpage is a tiny bit different for everyone that sees it.

Until next time

Well, friends, what a trip!

We have created three beautiful patterns together, learned lots of handy Paint API tricks, and (hopefully!) had some fun, too. From here, I hope you feel inspired to make some more generative art/design with CSS Houdini! I’m not sure about you, but I feel like my portfolio site needs a new coat of paint…

Until next time, fellow CSS magicians!

Oh! Before you go, I have a challenge for you. There is a generative Paint API worklet running on this very page! Can you spot it?

  1. There are certainly ways around this rule, but they can be complex and not entirely suitable for this tutorial.

The post Creating Generative Patterns with The CSS Paint API appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/creating-generative-patterns-with-the-css-paint-api/

Wednesday, November 24, 2021

Which SVG technique performs best for way too many icons?

Tyler Sticka digs in here in the best possible way: by making a test page and literally measuring performance. Maybe 1,000 icons is a little bit of an edge case, but hey, 250 rows of data with four icons in each gets you there. Tyler covers the nuances carefully in the post. The different techniques tested: inline <svg>, same-document sprite <symbol>s, external-document sprite <symbol>s, <img> with an external source, <img> with a data URL, <img> with a filter, <div> with a background-image of an external source, <div> with a background-image of a data URL, and a <div> with a mask. Phew! That’s a lot — and they are all useful techniques in their own right.

Which technique won? Inline <svg>, unless the SVGs are rather complex, then <img> is better. That’s what I would have put my money on. I’ve been on that train for a while now.

To Shared LinkPermalink on CSS-Tricks


The post Which SVG technique performs best for way too many icons? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/which-svg-technique-performs-best-for-way-too-many-icons/

Control Layout in a Multi-Directional Website

Many business websites need a multilingual setup. As with anything development-related, implementing one in an easy, efficient, and maintainable way is desirable. Designing and developing to be ready for multiple languages, whether it happens right at launch or is expected to happen at any point in the future, is smart.

Changing the language and content is the easy part. But when you do that, sometimes the language you are changing to has a different direction. For example, text (and thus layout) in English flows left-to-right while text (and thus layout) in Arabic goes right-to-left.

In this article, I want to build a multilingual landing page and share some CSS techniques that make this process easier. Hopefully the next time you’ll need to do the same thing, you’ll have some implementation techniques to draw from.

We’ll cover six major points. I believe that the first five are straightforward. The sixth includes multiple options that you need to think about first.

1. Start with the HTML markup

The lang and dir attributes will define the page’s language and direction.

<!-- English, left-to-right -->
<html lang="en" dir="ltr">

<!-- Arabic, right-to-left -->
<html lang="ar" dir="rtl">

Then we can use these attributes in selectors to do the the styling. lang and dir attributes are on the HTML tag or a specific element in which the language varies from the rest of the page. Those attributes help improve the website’s SEO by showing the website in the right language for users who search for it in case that each language has a separate HTML document.

Also, we need to ensure that the charset meta tag is included and its value is UTF-8 since it’s the only valid encoding for HTML documents which also supports all languages.

<meta charset="utf-8">

I’ve prepared a landing page in three different languages for demonstration purposes. It includes the HTML, CSS, and JavaScript we need.

2. CSS Custom Properties are your friend

Changing the direction may lead to inverting some properties. So, if you used the CSS property left in a left-to-right layout, you probably need right in the right-to-left layout, and so on. And changing the language may lead to changing font families, font sizes, etc.

These multiple changes may cause unclean and difficult to maintain code. Instead, we can assign the value to a custom property, then change the value when needed. This is also great for responsiveness and other things that might need a toggle, like dark mode. We can change the font-size, margin, padding, colors, etc., in the blink of an eye, where the values then cascade to wherever needed.

Here are some of the CSS custom properties that we are using in this example:

html {
  /* colors */
  --dark-color: #161616;
  --light-color: #eee;
  --primary-text-color: var(--dark-color);
  --primary-bg-color: #fff;
  --shadow-color: var(--light-color);
  --hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);

  /* font sizes */
  --logo-font-size: 2rem;
  --lang-switcher-font-size: 1.02em;
  --offers-item-after-font-size: 1.5rem;

  /* margin and padding */
  --btn-padding: 7px;
  --sec-padding-block: 120px;

  /* height and width */
  --hero-height: 500px;
  --cta-img-width: 45.75%;
}

While styling our page, we may add/change some of these custom properties, and that is entirely natural. Although this article is about multi-directional websites, here’s a quick example that shows how we can re-assign custom property values by having one set of values on the <body>, then another set when the <body> contains a .dark class:

body {
  background-color: var(--primary-bg-color);
  color: var(--primary-text-color);
}
body.dark {
  --primary-bg-color: #0f0f0f;
  --primary-text-color: var(--light-color);

  /* other changes */
  --shadow-color: #13151a;
  --hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313);
}

That’s the general idea. We’re going to use custom properties in the same sort of way, though for changing language directions.

3) CSS pseudo-classes and selectors

CSS has a few features that help with writing directions. The following two pseudo-classes and attribute are good examples that we can put to use in this example.

The :lang() pseudo-class

We can use :lang() pseudo-class to target specific languages and apply CSS property values to them individually, or together. For example, in this example, we can change the font size when the :lang pseudo-class switches to either Arabic or Japanese:

html:lang(ar),
html:lang(jp){
  --offers-item-after-font-size: 1.2rem; 
}

Once we do that, we also need to change the writing-mode property from its horizontal left-to-right default direction to vertical right-to-left direction account:

html:lang(jp) .about__text {
  writing-mode: vertical-rl;
}

The :attr() pseudo-class

The :attr() pseudo-class helps makes the “content” of the pseudo-elements like ::before or ::after “dynamic” in a sense, where we can drop the dir HTML attribute into the CSS content property using the attr() function. That way, the value of dir determines what we’re selecting and styling.

<div dir="ltr"></div>
<div dir="rtl"></div>
div::after {
  content: attr(dir);
}

The power is the ability to use any custom data attribute. Here, we’re using a custom data-name attribute whose value is used in the CSS:

<div data-name="English content" dir="ltr"></div>
<div data-name="محتوى عربي" dir="rtl"></div>
div::after {
  content: attr(data-name);
}

This makes it relatively easy to change the content after switching that language without changing the style. But, back to our design. The three-up grid of cards has a yellow “special” or “best” off mark beside an image.

This is the HTML for each card:

<div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">
  <figure class="offers__item_img">
    <img src="./assets/images/offer1.png" data-attr="alt" data-i18n_attr="image_alt" alt="" class="w-100">
  </figure>
  <div class="offer-content_item-text">
    <p class="para" data-i18n="offer_item_text"></p>
    <span class="price bolder" data-i18n="offer_item_price"></span>
  </div>
</div>

JavaScript’s role is to:

  1. Set an attribute called data-offer on each card.
  2. Assign a “special offer” or “best offer” value to it.

Finally, we can use the data-offer attribute in our style:

.offers__item::after {
  content: attr(data-offer);
  /* etc. */
}

Select by the dir attribute

Many languages are left-to-right, but some are not. We can specify what should be different in the [dir='rtl']. This attribute must be on the element itself or we can use nesting to reach the wanted element. Since we’ve already added the dir attribute to our HTML tag, then we can use it in nesting. We will use it later on our sample page.

4. Prepare the web fonts

In a multilingual website, we may also want to change the font family between languages because perhaps a particular font is more legible for a particular language.

Fallback fonts

We can benefit from the fallback by writing the right-to-left font after the default one.

font-family: 'Roboto', 'Tajawal', sans-serif;

This helps in cases where the default font doesn’t support right-to-left. That snippet above is using the Roboto font, which doesn’t support Arabic letters. If the default font supports right-to-left (like the Cairo font), and the design needs it to be changed, then this is not a perfect solution.

font-family: 'Cairo', 'Tajawal', sans-serif; /* won't work as expected */

Let’s look at another way.

Using CSS variables and the :lang() pseudo-class

We can mix the previous two technique where we change the font-family property value using custom properties that are re-assigned by the :lang pseudo class.

html {
  --font-family: 'Roboto', sans-serif;
}

html:lang(ar){
  --font-family: 'Tajawal', sans-serif;
}

html:lang(jp){
  --font-family: 'Noto Sans JP', sans-serif;
}

5. CSS Logical Properties

In CSS days past, we used to use left and right to define offsets along the x-axis, and the top and bottom properties to to define offsets along the y-axis. That makes direction switching a headache. Fortunately, CSS supports logical properties that define direction‐relative equivalents of the older physical properties. They support things like positioning, alignment, margin, padding, border, etc.

If the writing mode is horizontal (like English), then the logical inline direction is along the x-axis and the block direction refers to the y-axis. Those directions are flipped in a vertical writing mode, where inline travels the y-axis and and block flows along the x-axis.

Writing Mode x-axis y-axis
horizontal inline block
vertical  block inline

In other words, the block dimension is the direction perpendicular to the writing mode and the inline dimension is the direction parallel to the writing mode. Both inline and block levels have start and end values to define a specific direction. For example, we can use margin-inline-start instead of margin-left. This mean the margin direction automatically inverts when the page direction is rtl. It’s like our CSS is direction-aware and adapts when changing contexts.

There is another article on CSS-Tricks, Building Multi-Directional Layouts from Ahmad El-Alfy, that goes into the usefulness of building websites in multiple languages using logical properties.

This is exactly how we can handle margins, padding and borders. We’ll use them in the footer section to change which border gets the rounded edge.

The top-tight edge of the border is rounded in a default ltr writing mode.

As long as we’re using the logical equivalent of border-top-right-radius, CSS will handle that change for us.

.footer {
  border-start-end-radius: 120px;
}

Now, after switching to the rtl direction, it’ll work fine.

The “call to action” section is another great place to apply this:

.cta__text {
  border-start-start-radius: 50%;
  border-end-start-radius: 50px;
}
.cta__img {
  border: 1px dashed var(--secondary-color);
  border-inline-start-color: var(--light-color);
}

Now, in Arabic, we get the correct layout:

You might be wondering exactly how the block and inline dimensions reverse when the writing mode changes. Back to the Japanese version, the text is from vertical, going from top-to-bottom. I added this line to the code:

/* The "About" section when langauge is Japanese */
html:lang(jp) .about__text {
  margin-block-end: auto;
  width: max-content;
}

Although I added margin to the “block” level, it is applied it to the left margin. That’s because the text rotated 90 degrees when the language switched and flows in a vertical direction.

6. Other layout considerations

Even after all this prep, sometimes where elements move to when the direction and language change is way off. There are multiple factors at play, so let’s take a look.

Position

Using an absolute or fixed position to move elements may affect how elements shift when changing directions. Some designs need it. But I’d still recommend asking yourself: do I really need this?

Fro example, the newsletter subscription form in the footer section of our example can be implemented using position. The form itself takes the relative position, while the button takes the absolute position.

<form id="newsletter-form" class="relative">
  <input type="email" data-attr="placeholder" data-i18n_attr="footer_input_placeholder" class="w-100">
  <button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button>
</form>
html[dir="ltr"] .footer__newsletter_btn {
  right: 0;
}
html[dir="rtl"] .footer__newsletter_btn {
  left: 0;
}
This works fine in a rtl writing mode.

In the “hero” section, I made the background using a ::before pseudo-class with an absolute position:

<header class="hero relative">
  <!-- etc. -->
</header>
.hero {
  background-image: linear-gradient(90deg, #30333f, #161616, #161616);
}
.hero::before  {
  content: '';
  display: block;
  height: 100%;
  width: 33.33%;
  background-color: var(--primary-color);
  clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);
  position: absolute;
  top: 0;
  right: 0;
}

Here’s the HTML for our hero element:

<header class="hero relative">
  <!-- etc. -->
  <div class="hero__social absolute">
    <div class="d-flex flex-col">
      <!-- etc. -->
    </div>
  </div>
</header>

Note that an .absolute class is in there that applies position: absolute to the hero section’s social widget. Meanwhile, the hero itself is relatively positioned.

How we move the social widget halfway down the y-axis:

.hero__social {
  left: 0;
  top: 50%;
  transform: translateY(-50%);
}

In the Arabic, we can fix the ::before pseudo-class position that is used in the background using the same technique we use in the footer form. That said, there are multiple issues we need to fix here:

  1. The clip-path direction
  2. The background linear-gradient
  3. The coffee-cup image direction
  4. The social media box’s position

Let’s use a simple flip trick instead. First, we wrap the hero content, and social content in two distinct wrapper elements instead of one:

<header class="hero relative">
  <div class="hero__content">
      <!-- etc. -->
  </div>
  <div class="hero__social absolute">
    <div class="d-flex flex-col">
      <!-- etc. -->
    </div>
  </div>
</header>

Then we rotate both of the hero wrappers—the social box inner wrapper and the image—180 degrees:

html[dir="rtl"] .hero,
html[dir="rtl"] .hero__content,
html[dir="rtl"] .hero__img img,
html[dir="rtl"] .hero__social > div {
  transform: rotateY(180deg);
}

Yeah, that’s all. This simple trick is also helpful if the hero’s background is an image.

transform: translate()

This CSS property and value function helps move the element on one or more axes. The difference between ltr and rtl is that the x-axis is the inverse/negative value of the current value. So, we can store the value in a variable and change it according to the language.

html {
  --about-img-background-move: -20%;
}

html[dir='rtl']{
  --about-img-background-move: 20%;
}

We can do the same thing for the background image in the another section:

<figure class="about__img relative">
  <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">
</figure>
.about__img::after {
  content: '';
  position: absolute;
  z-index: -1;
  transform: translateY(-75%) translateX(var(--about-img-background-move));
  /* etc. */
}

Margins

Margins are used to extend or reduce spaces between elements. It accepts negative values, too. For example, a positive margin-top value (20%) pushes the content down, while a negative value (e.g. -20%) pulls the content up.

If margins values are negative, then the top and left margins move the item up or to the left. However, the right and bottom margins do not. Instead, they pull content that is located in the right of the item towards the left, and the content underneath the item up. For example, if we apply a negative top margin and negative bottom margin together on the same item, the item is moved up and pull the content below it up into the item.

Here’s an example:

<section>
  <div id="d1"></div>
  <div id="d2"></div>
  <div id="d3"></div>
</section>
div {
  width: 100px;
  height: 100px;
  border: 2px solid;
}
#d1 {
  background-color: yellow;
  border-color: red;
}
#d2 {
  background-color: lightblue;
  border-color: blue;
}
#d3 {
  background-color: green;
  border-color: red;
}

The result of the above code should be something like this:

Let’s add these negative margins to the #d2 element:

#d2 {
  margin-top: -40px;
  margin-bottom: -70px;
}

Notice how the second box in the diagram moves up, thanks to a negative margin-top value, and the green box also moves up an overlaps the second box, thanks to a negative margin-bottom value.

The next thing you might be asking: But what is the difference between transform: translate and the margins?

When moving an element with a negative margin, the initial space that is taken by the element is no longer there. But in the case of translating the element using a transform, the opposite is true. In other words, a negative margin leads pulls the element up, but the transform merely changes its position, without losing the space reserved for it.

Let’s stick to using margin in one direction:

#d2 {
  margin-top: -40px;
  /* margin-bottom: -70px; */
}

Now let’s replace the margin with the transform:

#d2 {
  /* margin-top: -40px;*/
  transform: translateY(-40px);
}

You can see that, although the element is pulled up, its initial space is still there according to the natural document flow.

Flexbox

The display: flex provides a quick way to control the how the elements are aligned in their container. We can use align-items and justify-content to align child elements at the parent level.

In our example, we can use flexbox in almost every section to make the column layout. Plus, we can use it in the “offers” section to center the set of those yellow “special” and “best” marks:

.offers__item::after {
  content: attr(data-offer);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

The same can be applied to the hero section to center text vertically.

<div class="d-lg-flex align-items-center">
  <div class="hero__text d-xl-flex align-items-center">
    <div>
      <!-- text -->
    </div>
  </div>
  <figure class="hero__img relative">
    <img src="/image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">
  </figure>
</div>

If the flex-direction value is row, then we can benefit from controlling the width for each element. In the “hero” section, we need to set the image on the angled slope of the background where the color transitions from dark gray to yellow.

.hero__text {
  width: 56.5%;
}
.hero__img {
  width: 33.33%;
}

Both elements take up a a total of 89.83% of the parent container’s width. Since we didn’t specify justify-content on the parent, it defaults to start, leaving the remaining width at the end.

We can combine the flexbox with any of the previous techniques we’ve seen, like transforms and margins. This can help us to reduce how many position instances are in our code. Let’s use it with a negative margin in the “call to action” section to locate the image.

<section class="cta d-xl-flex align-items-center">
  <div class="cta__text w-100">
    <!-- etc. -->
  </div>
  <figure class="cta__img">
    <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">
  </figure>
</section>

Because we didn’t specify the flex-wrap and flex-basis properties, the image and the text both fit in the parent. However, since we used a negative margin, the image is pulled to the left, along with its width. This saves extra space for the text. We also want to use a logical property, inline-start, instead of left to handle switching to the rtl direction.

Grid

Finally, we can use a grid container to positing the elements. CSS Grid is powerful (and different than flexbox) in that it lays things along both the x-axis and the y-axis as opposed to only one of them.

Suppose that in the “offers” section, the role of the “see all” button is to get extra data that to display on the page. Here’s JavaScript code to repeat the current content:

// offers section ==> "see all" btn functionality
(function(){
  document.querySelector('.offers .btn').addEventListener('click', function(){
    const offersContent = document.querySelector('.offers__content');
    offersContent.innerHTML += offersContent.innerHTML;
    offersContent.classList.remove('offers__content--has-margin');
    this.remove();
  })
})();

Then, let’s use display: grid in the CSS for this section. First, here’s our HTML, with our grid container highlighted.

<div class="offers__content offers__content--has-margin d-grid">
  <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">
    <!-- etc. -->
  </div>
  <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">
    <!-- etc. -->
  </div>
  <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">
    <!-- etc. -->
  </div>
</div>

We implement CSS Grid on the .offers__content element:

html {
  /* custom properties */
  --offers-content-column: repeat(3, 1fr);
  --offers-content-gap: 5vw;
}

.offers__content {
  display: grid;
  grid-template-columns: var(--offers-content-column);
  gap: var(--offers-content-gap);
}
.offers__content--has-margin {
  margin-block-end: 60px;
}

This is the result after clicking the button:

Our page is far from being the best example of how CSS Grid works. While I was browsing the designs online, I found a design that uses the following structure:

Notice how CSS Grid makes the responsive layout without media queries. And as you might expect, it works well for changing writing modes where we adjust where elements go on the grid based on the current writing mode.

Wrapping up

Here is the final version of the page. I ensured to implement the responsiveness with a mobile-first approach to show you the power of the CSS variables. Be sure to open the demo in full page mode as well.

I hope these techniques help make creating multilingual designs easier for you. We looked at a bunch of CSS properties we can use to apply styles to specific languages. And we looked at different approaches to do that, like selecting the :lang pseudo-class and data attributes using the attr() function. As part of this, we covered what logical properties are in CSS and how they adapt to a document’s writing mode—which is so much nicer than having to write additional CSS rulesets to swap out physical property units that otherwise are unaffected by the writing mode.

We also checked out a number of different positioning and layout techniques, looking specifically at how different techniques are more responsive and maintainable than others. For example, CSS Grid and Flexbox are equipped with features that can re-align elements inside of a container based on changing conditions.

Clearly, there are lots of moving pieces when working with a multilingual site. There are probably other requirements you need to consider when optimizing a site for specific languages, but the stuff we covered here together should give you all of the layout-bending superpowers you need to create robust layouts that accommodate any number of languages and writing modes.


The post Control Layout in a Multi-Directional Website appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.



source https://css-tricks.com/control-layout-in-a-multi-directional-website/