Css Tricks

Syndicate content CSS-Tricks
Tips, Tricks, and Techniques on using Cascading Style Sheets.
Updated: 14 hours 52 min ago

An Interactive Starry Backdrop for Content

Wed, 05/18/2022 - 4:16am

I was fortunate last year to get approached by Shawn Wang (swyx) about doing some work for Temporal. The idea was to cast my creative eye over what was on the site and come up with some ideas that would give the site a little “something” extra. This was quite a neat challenge as I consider myself more of a developer than a designer. But I love learning and leveling up the design side of my game.

One of the ideas I came up with was this interactive starry backdrop. You can see it working in this shared demo:

Blockquote concept using a little perspective and CSS custom properties 😎

Been enjoying the creative freedom to come up with things @temporalio 🤓

Adding a dash of whimsy to materials 🎉

⚒️ @reactjs && @tailwindcss (Site is NextJS)
👉 https://t.co/GHtUEcl674 via @CodePen pic.twitter.com/s9xP2tRrOx

— Jhey 🔨🐻✨ (@jh3yy) July 2, 2021

The neat thing about this design is that it’s built as a drop-in React component. And it’s super configurable in the sense that once you’ve put together the foundations for it, you can make it completely your own. Don’t want stars? Put something else in place. Don’t want randomly positioned particles? Place them in a constructed way. You have total control of what to bend it to your will.

So, let’s look at how we can create this drop-in component for your site! Today’s weapons of choice? React, GreenSock and HTML <canvas>. The React part is totally optional, of course, but, having this interactive backdrop as a drop-in component makes it something you can employ on other projects.

Let’s start by scaffolding a basic app import React from 'https://cdn.skypack.dev/react' import ReactDOM from 'https://cdn.skypack.dev/react-dom' import gsap from 'https://cdn.skypack.dev/gsap' const ROOT_NODE = document.querySelector('#app') const Starscape = () => <h1>Cool Thingzzz!</h1> const App = () => <Starscape/> ReactDOM.render(<App/>, ROOT_NODE)

First thing we need to do is render a <canvas> element and grab a reference to it that we can use within React’s useEffect. For those not using React, store a reference to the <canvas> in a variable instead.

const Starscape = () => { const canvasRef = React.useRef(null) return <canvas ref={canvasRef} /> }

Our <canvas> is going to need some styles, too. For starters, we can make it so the canvas takes up the full viewport size and sits behind the content:

canvas { position: fixed; inset: 0; background: #262626; z-index: -1; height: 100vh; width: 100vw; }

Cool! But not much to see yet.

CodePen Embed Fallback We need stars in our sky

We’re going to “cheat” a little here. We aren’t going to draw the “classic” pointy star shape. We’re going to use circles of differing opacities and sizes.

Draw a circle on a <canvas> is a case of grabbing a context from the <canvas> and using the arc function. Let’s render a circle, err star, in the middle. We can do this within a React useEffect:

const Starscape = () => { const canvasRef = React.useRef(null) const contextRef = React.useRef(null) React.useEffect(() => { canvasRef.current.width = window.innerWidth canvasRef.current.height = window.innerHeight contextRef.current = canvasRef.current.getContext('2d') contextRef.current.fillStyle = 'yellow' contextRef.current.beginPath() contextRef.current.arc( window.innerWidth / 2, // X window.innerHeight / 2, // Y 100, // Radius 0, // Start Angle (Radians) Math.PI * 2 // End Angle (Radians) ) contextRef.current.fill() }, []) return <canvas ref={canvasRef} /> }

So what we have is a big yellow circle:

CodePen Embed Fallback

This is a good start! The rest of our code will take place within this useEffect function. That’s why the React part is kinda optional. You can extract this code out and use it in whichever form you like.

We need to think about how we’re going to generate a bunch of “stars” and render them. Let’s create a LOAD function. This function is going to handle generating our stars as well as the general <canvas> setup. We can also move the sizing logic of the <canvas> sizing logic into this function:

const LOAD = () => { const VMIN = Math.min(window.innerHeight, window.innerWidth) const STAR_COUNT = Math.floor(VMIN * densityRatio) canvasRef.current.width = window.innerWidth canvasRef.current.height = window.innerHeight starsRef.current = new Array(STAR_COUNT).fill().map(() => ({ x: gsap.utils.random(0, window.innerWidth, 1), y: gsap.utils.random(0, window.innerHeight, 1), size: gsap.utils.random(1, sizeLimit, 1), scale: 1, alpha: gsap.utils.random(0.1, defaultAlpha, 0.1), })) }

Our stars are now an array of objects. And each star has properties that define their characteristics, including:

  • x: The star’s position on the x-axis
  • y: The star’s position on the y-axis
  • size: The star’s size, in pixels
  • scale: The star’s scale, which will come into play when we interact with the component
  • alpha: The star’s alpha value, or opacity, which will also come into play during interactions

We can use GreenSock’s random() method to generate some of these values. You may also be wondering where sizeLimit, defaultAlpha, and densityRatio came from. These are now props we can pass to the Starscape component. We’ve provided some default values for them:

const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {

A randomly generated star Object might look like this:

{ "x": 1252, "y": 29, "size": 4, "scale": 1, "alpha": 0.5 }

But, we need to see these stars and we do that by rendering them. Let’s create a RENDER function. This function will loop over our stars and render each of them onto the <canvas> using the arc function:

const RENDER = () => { contextRef.current.clearRect( 0, 0, canvasRef.current.width, canvasRef.current.height ) starsRef.current.forEach(star => { contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})` contextRef.current.beginPath() contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2) contextRef.current.fill() }) }

Now, we don’t need that clearRect function for our current implementation as we are only rendering once onto a blank <canvas>. But clearing the <canvas> before rendering anything isn’t a bad habit to get into, And it’s one we’ll need as we make our canvas interactive.

Consider this demo that shows the effect of not clearing between frames.

CodePen Embed Fallback

Our Starscape component is starting to take shape.

See the code const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => { const canvasRef = React.useRef(null) const contextRef = React.useRef(null) const starsRef = React.useRef(null) React.useEffect(() => { contextRef.current = canvasRef.current.getContext('2d') const LOAD = () => { const VMIN = Math.min(window.innerHeight, window.innerWidth) const STAR_COUNT = Math.floor(VMIN * densityRatio) canvasRef.current.width = window.innerWidth canvasRef.current.height = window.innerHeight starsRef.current = new Array(STAR_COUNT).fill().map(() => ({ x: gsap.utils.random(0, window.innerWidth, 1), y: gsap.utils.random(0, window.innerHeight, 1), size: gsap.utils.random(1, sizeLimit, 1), scale: 1, alpha: gsap.utils.random(0.1, defaultAlpha, 0.1), })) } const RENDER = () => { contextRef.current.clearRect( 0, 0, canvasRef.current.width, canvasRef.current.height ) starsRef.current.forEach(star => { contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})` contextRef.current.beginPath() contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2) contextRef.current.fill() }) } LOAD() RENDER() }, []) return <canvas ref={canvasRef} /> } CodePen Embed Fallback

Have a play around with the props in this demo to see how they affect the the way stars are rendered.

CodePen Embed Fallback

Before we go further, you may have noticed a quirk in the demo where resizing the viewport distorts the <canvas>. As a quick win, we can rerun our LOAD and RENDER functions on resize. In most cases, we’ll want to debounce this, too. We can add the following code into our useEffect call. Note how we also remove the event listener in the teardown.

// Naming things is hard... const RUN = () => { LOAD() RENDER() } RUN() // Set up event handling window.addEventListener('resize', RUN) return () => { window.removeEventListener('resize', RUN) }

Cool. Now when we resize the viewport, we get a new generated starry.

CodePen Embed Fallback Interacting with the starry backdrop

Now for the fun part! Let’s make this thing interactive.

The idea is that as we move our pointer around the screen, we detect the proximity of the stars to the mouse cursor. Depending on that proximity, the stars both brighten and scale up.

We’re going to need to add another event listener to pull this off. Let’s call this UPDATE. This will work out the distance between the pointer and each star, then tween each star’s scale and alpha values. To make sure those tweeted values are correct, we can use GreenSock’s mapRange() utility. In fact, inside our LOAD function, we can create references to some mapping functions as well as a size unit then share these between the functions if we need to.

Here’s our new LOAD function. Note the new props for scaleLimit and proximityRatio. They are used to limit the range of how big or small a star can get, plus the proximity at which to base that on.

const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5, scaleLimit = 2, proximityRatio = 0.1 }) => { const canvasRef = React.useRef(null) const contextRef = React.useRef(null) const starsRef = React.useRef(null) const vminRef = React.useRef(null) const scaleMapperRef = React.useRef(null) const alphaMapperRef = React.useRef(null) React.useEffect(() => { contextRef.current = canvasRef.current.getContext('2d') const LOAD = () => { vminRef.current = Math.min(window.innerHeight, window.innerWidth) const STAR_COUNT = Math.floor(vminRef.current * densityRatio) scaleMapperRef.current = gsap.utils.mapRange( 0, vminRef.current * proximityRatio, scaleLimit, 1 ); alphaMapperRef.current = gsap.utils.mapRange( 0, vminRef.current * proximityRatio, 1, defaultAlpha ); canvasRef.current.width = window.innerWidth canvasRef.current.height = window.innerHeight starsRef.current = new Array(STAR_COUNT).fill().map(() => ({ x: gsap.utils.random(0, window.innerWidth, 1), y: gsap.utils.random(0, window.innerHeight, 1), size: gsap.utils.random(1, sizeLimit, 1), scale: 1, alpha: gsap.utils.random(0.1, defaultAlpha, 0.1), })) } }

And here’s our UPDATE function. It calculates the distance and generates an appropriate scale and alpha for a star:

const UPDATE = ({ x, y }) => { starsRef.current.forEach(STAR => { const DISTANCE = Math.sqrt(Math.pow(STAR.x - x, 2) + Math.pow(STAR.y - y, 2)); gsap.to(STAR, { scale: scaleMapperRef.current( Math.min(DISTANCE, vminRef.current * proximityRatio) ), alpha: alphaMapperRef.current( Math.min(DISTANCE, vminRef.current * proximityRatio) ) }); }) };

But wait… it doesn’t do anything?

CodePen Embed Fallback

Well, it does. But, we haven’t set our component up to show updates. We need to render new frames as we interact. We can reach for requestAnimationFrame often. But, because we’re using GreenSock, we can make use of gsap.ticker. This is often referred to as “the heartbeat of the GSAP engine” and it’s is a good substitute for requestAnimationFrame.

To use it, we add the RENDER function to the ticker and make sure we remove it in the teardown. One of the neat things about using the ticker is that we can dictate the number of frames per second (fps). I like to go with a “cinematic” 24fps:

// Remove RUN LOAD() gsap.ticker.add(RENDER) gsap.ticker.fps(24) window.addEventListener('resize', LOAD) document.addEventListener('pointermove', UPDATE) return () => { window.removeEventListener('resize', LOAD) document.removeEventListener('pointermove', UPDATE) gsap.ticker.remove(RENDER) }

Note how we’re now also running LOAD on resize. We also need to make sure our scale is being picked up in that RENDER function when using arc:

const RENDER = () => { contextRef.current.clearRect( 0, 0, canvasRef.current.width, canvasRef.current.height ) starsRef.current.forEach(star => { contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})` contextRef.current.beginPath() contextRef.current.arc( star.x, star.y, (star.size / 2) * star.scale, 0, Math.PI * 2 ) contextRef.current.fill() }) }

It works! &#x1f64c;

CodePen Embed Fallback

It’s a very subtle effect. But, that’s intentional because, while it’s is super neat, we don’t want this sort of thing to distract from the actual content. I’d recommend playing with the props for the component to see different effects. It makes sense to set all the stars to low alpha by default too.

The following demo allows you to play with the different props. I’ve gone for some pretty standout defaults here for the sake of demonstration! But remember, this article is more about showing you the techniques so you can go off and make your own cool backdrops — while being mindful of how it interacts with content.

CodePen Embed Fallback Refinements

There is one issue with our interactive starry backdrop. If the mouse cursor leaves the <canvas>, the stars stay bright and upscaled but we want them to return to their original state. To fix this, we can add an extra handler for pointerleave. When the pointer leaves, this tweens all of the stars down to scale 1 and the original alpha value set by defaultAlpha.

const EXIT = () => { gsap.to(starsRef.current, { scale: 1, alpha: defaultAlpha, }) } // Set up event handling window.addEventListener('resize', LOAD) document.addEventListener('pointermove', UPDATE) document.addEventListener('pointerleave', EXIT) return () => { window.removeEventListener('resize', LOAD) document.removeEventListener('pointermove', UPDATE) document.removeEventListener('pointerleave', EXIT) gsap.ticker.remove(RENDER) }

Neat! Now our stars scale back down and return to their previous alpha when the mouse cursor leaves the scene.

CodePen Embed Fallback Bonus: Adding an Easter egg

Before we wrap up, let’s add a little Easter egg surprise to our interactive starry backdrop. Ever heard of the Konami Code? It’s a famous cheat code and a cool way to add an Easter egg to our component.

We can practically do anything with the backdrop once the code runs. Like, we could make all the stars pulse in a random way for example. Or they could come to life with additional colors? It’s an opportunity to get creative with things!

We’re going listen for keyboard events and detect whether the code gets entered. Let’s start by creating a variable for the code:

const KONAMI_CODE = 'arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya';

Then we create a second effect within our the starry backdrop. This is a good way to maintain a separation of concerns in that one effect handles all the rendering, and the other handles the Easter egg. Specifically, we’re listening for keyup events and check whether our input matches the code.

const codeRef = React.useRef([]) React.useEffect(() => { const handleCode = e => { codeRef.current = [...codeRef.current, e.code] .slice( codeRef.current.length > 9 ? codeRef.current.length - 9 : 0 ) if (codeRef.current.join(',').toLowerCase() === KONAMI_CODE) { // Party in here!!! } } window.addEventListener('keyup', handleCode) return () => { window.removeEventListener('keyup', handleCode) } }, [])

We store the user input in an Array that we store inside a ref. Once we hit the party code, we can clear the Array and do whatever we want. For example, we may create a gsap.timeline that does something to our stars for a given amount of time. If this is the case, we don’t want to allow Konami code to input while the timeline is active. Instead, we can store the timeline in a ref and make another check before running the party code.

const partyRef = React.useRef(null) const isPartying = () => partyRef.current && partyRef.current.progress() !== 0 && partyRef.current.progress() !== 1;

For this example, I’ve created a little timeline that colors each star and moves it to a new position. This requires updating our LOAD and RENDER functions.

First, we need each star to now have its own hue, saturation and lightness:

// Generating stars! ⭐️ starsRef.current = new Array(STAR_COUNT).fill().map(() => ({ hue: 0, saturation: 0, lightness: 100, x: gsap.utils.random(0, window.innerWidth, 1), y: gsap.utils.random(0, window.innerHeight, 1), size: gsap.utils.random(1, sizeLimit, 1), scale: 1, alpha: defaultAlpha }));

Second, we need to take those new values into consideration when rendering takes place:

starsRef.current.forEach((star) => { contextRef.current.fillStyle = `hsla( ${star.hue}, ${star.saturation}%, ${star.lightness}%, ${star.alpha} )`; contextRef.current.beginPath(); contextRef.current.arc( star.x, star.y, (star.size / 2) * star.scale, 0, Math.PI * 2 ); contextRef.current.fill(); });

And here’s the fun bit of code that moves all the stars around:

partyRef.current = gsap.timeline().to(starsRef.current, { scale: 1, alpha: defaultAlpha }); const STAGGER = 0.01; for (let s = 0; s < starsRef.current.length; s++) { partyRef.current .to( starsRef.current[s], { onStart: () => { gsap.set(starsRef.current[s], { hue: gsap.utils.random(0, 360), saturation: 80, lightness: 60, alpha: 1, }) }, onComplete: () => { gsap.set(starsRef.current[s], { saturation: 0, lightness: 100, alpha: defaultAlpha, }) }, x: gsap.utils.random(0, window.innerWidth), y: gsap.utils.random(0, window.innerHeight), duration: 0.3 }, s * STAGGER ); }

From there, we generate a new timeline and tween the values of each star. These new values get picked up by RENDER. We’re adding a stagger by positioning each tween in the timeline using GSAP’s position parameter.

CodePen Embed Fallback That’s it!

That’s one way to make an interactive starry backdrop for your site. We combined GSAP and an HTML <canvas>, and even sprinkled in some React that makes it more configurable and reusable. We even dropped an Easter egg in there!

Where can you take this component from here? How might you use it on a site? The combination of GreenSock and <canvas> is a lot of fun and I’m looking forward to seeing what you make! Here are a couple more ideas to get your creative juices flowing…

CodePen Embed Fallback CodePen Embed Fallback

An Interactive Starry Backdrop for Content originally published on CSS-Tricks. You should get the newsletter.

Improving Icons for UI Elements with Typographic Alignment and Scale

Tue, 05/17/2022 - 4:30am

Utilizing icons in user interface elements is helpful. In addition to element labeling, icons can help reinforce a user element’s intention to users. But I have to say, I notice a bit of icon misalignment while browsing the web. Even if the icon’s alignment is correct, icons often do not respond well when typographic styles for the element change.

I took note of a couple real-world examples and I’d like to share my thoughts on how I improved them. It’s my hope these techniques can help others build user interface elements that better accommodate typographic changes and while upholding the original goals of the design.

Example 1 — Site messaging

I found this messaging example on a popular media website. The icon’s position doesn’t look so bad. But when changing some of the element’s style properties like font-size and line-height, it begins to unravel.

CodePen Embed Fallback Identified issues
  • the icon is absolutely positioned from the left edge using a relative unit (rem)
  • because the icon is taken out of the flow, the parent is given a larger padding-left value to help with overall spacing – ideally, our padding-x is uniform, and everything looks good whether or not an icon is present
  • the icon (it’s an SVG) is also sized in rems – this doesn’t allow for respective resizing if its parent’s font-size changes
Recommendations Indicating the issues with aligning the icon and typography.

We want our icon’s top edge to be at the blue dashed line, but we often find our icon’s top edge at the red dashed line.

Have you ever inserted an icon next to some text and it just won’t align to the top of the text? You may move the icon into place with something like position: relative; top: 0.2em. This works well enough, but if typographic styles change in the future, your icon could look misaligned.

We can position our icon more reliably. Let’s use the element’s baseline distance (the distance from one line’s baseline to the next line’s baseline) to help solve this.

Calculating the baseline distance.

Baseline distance is font-size * line-height.

We’ll store that in a CSS custom property:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

We can then move our icon down using the result of (baseline distance – font size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--fontSize)) / 2);

With a font-size of 1rem (16px) and line-height of 1.5, our icon will be moved 4 pixels.

  • baseline distance = 16px * 1.5 = 24px
  • icon offset = (24px – 16px) / 2 = 4px
Demo: before and after CodePen Embed Fallback Example 2 – unordered lists

The second example I found is an unordered list. It uses a web font (Font Awesome) for its icon via a ::before pseudo-element. There have been plenty of great articles on styling both ordered and unordered lists, so I won’t go into details about the relatively new ::marker pseudo-element and such. Web fonts can generally work pretty well with icon alignment depending on the icon used.

CodePen Embed Fallback Identified issues
  • no absolute positioning used – when using pseudo-elements, we don’t often use flexbox like our first example and absolute positioning shines here
  • the list item uses a combination of padding and negative text-indent to help with layout – I am never able to get this to work well when accounting for multi-line text and icon scalability
Recommendations

Because we’ll also use a pseudo-element in our solution, we’ll leverage absolute positioning. This example’s icon size was a bit larger than its adjacent copy (about 2x). Because of this, we will alter how we calculate the icon’s top position. The center of our icon should align vertically with the center of the first line.

Start with the baseline distance calculation:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

Move the icon down using the result of (baseline distance – icon size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--iconSize)) / 2);

So with a font-size of 1rem (16px), a line-height of 1.6, and an icon sized 2x the copy (32px), our icon will get get a top value of -3.2 pixels.

  • baseline distance = 16px * 1.6 = 25.6px
  • icon offset = (25.6px – 32px) / 2 = -3.2px

With a larger font-size of 2rem (32px), line-height of 1.2, and 64px icon, our icon will get get a top value of -12.8 pixels.

  • baseline distance = 32px * 1.2 = 38.4px
  • icon offset = (38.4px – 64px) / 2 = -12.8px
Demo: before and after CodePen Embed Fallback Conclusion

For user interface icons, we have a lot of options and techniques. We have SVGs, web fonts, static images, ::marker, and list-style-type. One could even use background-colors and clip-paths to achieve some interesting icon results. Performing some simple calculations can help align and scale icons in a more graceful manner, resulting in implementations that are a bit more bulletproof.

See also: Previous discussion on aligning icon to text.

Improving Icons for UI Elements with Typographic Alignment and Scale originally published on CSS-Tricks. You should get the newsletter.

Cool Hover Effects That Use CSS Text Shadow

Fri, 05/13/2022 - 4:43am

In my last article we saw how CSS background properties allow us to create cool hover effects. This time, we will focus on the CSS text-shadow property to explore even more interesting hovers. You are probably wondering how adding shadow to text can possibly give us a cool effect, but here’s the catch: we’re not actually going to make any shadows for these text hover effects.

text-shadow but no text shadows?

Let me clear the confusion by showing the hover effects we are going to build in the following demo:

CodePen Embed Fallback

Without looking at the code many of you will, intuitively, think that for each hover effect we are duplicating the text and then independently animating them. Now, if you check the code you will see that none of the text is actually duplicated in the HTML. And did you notice that there is no use of content: "text" in the CSS?

The text layers are completely made with text-shadow!

Hover effect #1 CodePen Embed Fallback

Let’s pick apart the CSS:

.hover-1 { line-height: 1.2em; color: #0000; text-shadow: 0 0 #000, 0 1.2em #1095c1; overflow: hidden; transition: .3s; } .hover-1:hover { text-shadow: 0 -1.2em #000, 0 0 #1095c1; }

The first thing to notice is that I am making the color of the actual text transparent (using #0000) in order to hide it. After that, I am using text-shadow to create two shadows where I am defining only two length values for each one. That means there’s no blur radius, making for a sharp, crisp shadow that effectively produces a copy of the text with the specified color.

That’s why I was able to claim in the introduction that there are no shadows in here. What we’re doing is less of a “classic” shadow than it is a simple way to duplicate the text.

We have two text layers that we move on hover. If we hide the overflow, then the duplicated text is out of view and the movement makes it appear as though the actual text is being replaced by other text. This is the main trick that that makes all of the examples in this article work.

Let’s optimize our code. I am using the value 1.2em a lot to define the height and the offset of the shadows, making it an ideal candidate for a CSS custom property (which we’re calling --h):

.hover-1 { --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 0 #000, 0 var(--h) #1095c1; overflow: hidden; transition: .3s; } .hover-1:hover { text-shadow: 0 calc(-1 * var(--h)) #000, 0 0 #1095c1; }

We can still go further and apply more calc()-ulations to streamline things to where we only use the text-shadow once. (We did the same in the previous article.)

.hover-1 { --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 calc(-1*var(--_t, 0em)) #000, 0 calc(var(--h) - var(--_t, 0em)) #1095c1; overflow: hidden; transition: .3s; } .hover-1:hover { --_t: var(--h); }

In case you are wondering why I am adding an underscore to the --_t variable, it’s just a naming convention I am using to distinguish between the variables we use to control the effect that the user can update (like --h) and the internal variables that are only used for optimization purposes that we don’t need to change (like --_t ). In other words, the underscore is part of the variable name and has no special meaning.

We can also update the code to get the opposite effect where the duplicated text slides in from the top instead:

CodePen Embed Fallback

All we did is a small update to the text-shadow property — we didn’t touch anything else!

Hover effect #2 CodePen Embed Fallback

For this one, we will animate two properties: text-shadow and background. Concerning the text-shadow, we still have two layers like the previous example, but this time we will move only one of them while making the color of the other one transparent during the swap.

CodePen Embed Fallback .hover-2 { /* the height */ --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 var(--_t, var(--h)) #fff, 0 0 var(--_c, #000); transition: 0.3s; } .hover-2:hover { --_t: 0; --_c: #0000; }

On hover, we move the white text layer to the top while changing the color of the other one to transparent. To this, we add a background-size animation applied to a gradient:

CodePen Embed Fallback

And finally, we add overflow: hidden to keep the animation only visible inside the element’s boundaries:

CodePen Embed Fallback .hover-2 { /* the height */ --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 var(--_t,var(--h)) #fff, 0 0 var(--_c, #000); background: linear-gradient(#1095c1 0 0) bottom/100% var(--_d, 0) no-repeat; overflow: hidden; transition: 0.3s; } .hover-2:hover { --_d: 100%; --_t: 0; --_c: #0000; }

What we’ve done here is combine the CSS text-shadow and background properties to create a cool hover effect. Plus, we were able to use CSS variables to optimize the code.

If the background syntax looks strange to you, I highly recommend reading my previous article. The next hover effect also relies on an animation I detailed in that article. Unless you are comfortable with CSS background trickery, I’d suggest reading that article before continuing this one for more context.

In the previous article, you show us how to use only one variable to create the hover effect — is it possible to do that here?

Yes, absolutely! We can indeed use that same DRY switching technique so that we’re only working with a single CSS custom property that merely switches values on hover:

.hover-2 { /* the height */ --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 var(--_i, var(--h)) #fff, 0 0 rgb(0 0 0 / calc(var(--_i, 1) * 100%) ); background: linear-gradient(#1095c1 0 0) bottom/100% calc(100% - var(--_i, 1) * 100%) no-repeat; overflow: hidden; transition: 0.3s; } .hover-2:hover { --_i: 0; } CodePen Embed Fallback Hover effect #3 CodePen Embed Fallback

This hover effect is nothing but a combination of two effects we’ve already made: the second hover effect of the previous article and the first hover effect in this article.

.hover-3 { /* the color */ --c: #1095c1; /* the height */ --h: 1.2em; /* The first hover effect in this article */ line-height: var(--h); color: #0000; overflow: hidden; text-shadow: 0 calc(-1 * var(--_t, 0em)) var(--c), 0 calc(var(--h) - var(--_t, 0em)) #fff; /* The second hover effect from the previous article */ background: linear-gradient(var(--c) 0 0) no-repeat calc(200% - var(--_p, 0%)) 100% / 200% var(--_p, .08em); transition: .3s var(--_s, 0s), background-position .3s calc(.3s - var(--_s, 0s)); } .hover-3:hover { --_t: var(--h); --_p: 100%; --_s: .3s }

All I did was copy and paste the effects from those other examples and make minor adjustments to the variable names. They make for a neat hover effect when they’re combined! At first glance, such an effect may look complex and difficult but, in the end, it’s merely two relatively easy effects made into one.

Optimizing the code with the DRY switching variable technique should also be an easy task if we consider the previous optimizations we’ve already done:

.hover-3 { /* the color */ --c: #1095c1; /* the height */ --h: 1.2em; line-height: var(--h); color: #0000; overflow: hidden; text-shadow: 0 calc(-1 * var(--h) * var(--_i, 0)) var(--c), 0 calc(var(--h) * (1 - var(--_i, 0))) #fff; background: linear-gradient(var(--c) 0 0) no-repeat calc(200% - var(--_i, 0) * 100%) 100% / 200% calc(100% * var(--_i, 0) + .08em); transition: .3s calc(var(--_i, 0) * .3s), background-position .3s calc(.3s - calc(var(--_i, 0) * .3s)); } .hover-3:hover { --_i: 1; } CodePen Embed Fallback Hover effect #4 CodePen Embed Fallback

This hover effect is an improvement of the second one. First, let’s introduce a clip-path animation to reveal one of the text layers before it moves:

CodePen Embed Fallback

Here’s another illustration to better understand what is happening:

Initially, we use inset(0 0 0 0) which is similar to overflow: hidden in that all we see is the actual text. On hover, we update the the third value (which represent the bottom offset) using a negative value equal to the height to reveal the text layer placed at the bottom.

From there, we can add this to the second hover effect we made in this article, and this is what we get:

CodePen Embed Fallback

We are getting closer! Note that we need to first run the clip-path animation and then everything else. For this reason, we can add a delay to all of the properties on hover, except clip-path:

transition: 0.4s 0.4s, clip-path 0.4s;

And on mouse out, we do the opposite:

transition: 0.4s, clip-path 0.4s 0.4s;

The final touch is to add a box-shadow to create the sliding effect of the blue rectangle. Unfortunately, background is unable to produce the effect since backgrounds are clipped to the content area by default. Meanwhile, box-shadow can go outside the content area.

.hover-4 { /* the color */ --c: #1095c1; /* the height */ --h: 1.2em; line-height: var(--h); color: #0000; text-shadow: 0 var(--_t, var(--h)) #fff, 0 0 var(--_c, #000); box-shadow: 0 var(--_t, var(--h)) var(--c); clip-path: inset(0 0 0 0); background: linear-gradient(var(--c) 0 0) 0 var(--_t, var(--h)) no-repeat; transition: 0.4s, clip-path 0.4s 0.4s; } .hover-4:hover { --_t: 0; --_c: #0000; clip-path: inset(0 0 calc(-1 * var(--h)) 0); transition: 0.4s 0.4s, clip-path 0.4s; }

If you look closely at the box-shadow, you will see it has the same values as the white text layer inside text-shadow. This is logical since both need to move the same way. Both will slide to the top. Then the box-shadow goes behind the element while text-shadow winds up on the top.

Here is a demo with some modified values to visualize how the layers move:

CodePen Embed Fallback

Wait, The background syntax is a bit different from the one used in the second hover effect!

Good catch! Yes, we are using a different technique with background that produces the same effect. Instead of animating the size from 0% to 100%, we are animating the position.

If we don’t specify a size on our gradient, then it take up the full width and height by default. Since we know the height of our element (--h) we can create a sliding effect by updating the position from 0 var(--h) to 0 0.

.hover-4 { /* ... */ background: linear-gradient(var(--c) 0 0) 0 var(--_t, var(--h)) no-repeat; } .hover-4:hover { --_t: 0; }

We could have used the background-size animation to get the same effect, but we just added another trick to our list!

In the demos, you also used inset(0 0 1px 0)… why?

I sometimes add or remove a few pixels or percentages here and there to refine anything that looks off. In this case, a bad line was appearing at the bottom and adding 1px removed it.

What about the DRY switch variable optimization?

I am leaving this task for you! After those four hover effects and the previous article, you should be able to update the code so it only uses one variable. I’d love to see you attempt it in the comments!

Your turn!

Let me share one last hover effect which is another version of the previous one. Can you find out how it’s done without looking at the code? It’s a good exercise, so don’t cheat!

CodePen Embed Fallback Wrapping up

We looked at a bunch of examples that show how one element and few lines of CSS are enough to create some pretty complex-looking hover effects on text elements — no pseudo elements needed! We were even able to combine techniques to achieve even more complex animations with a small amount of effort.

If you’re interested in going deeper than the four text-shadow hover effects in this article, check my collection of 500 hover effects where I am exploring all kinds of different techniques.

Cool Hover Effects That Use CSS Text Shadow originally published on CSS-Tricks. You should get the newsletter.

A CSS Slinky in 3D? Challenge Accepted!

Thu, 05/12/2022 - 4:27am

Braydon Coyer recently launched a monthly CSS art challenge. He actually had reached out to me about donating a copy of my book Move Things with CSS to use as a prize for the winner of the challenge — which I was more than happy to do!

The first month’s challenge? Spring. And when thinking of what to make for the challenge, Slinkys immediately came to mind. You know Slinkys, right? That classic toy you knock down the stairs and it travels with its own momentum.

A slinking Slinky

Can we create a Slinky walking down stairs like that in CSS? That’s exactly the sort of challenge I like, so I thought we could tackle that together in this article. Ready to roll? (Pun intended.)

Setting up the Slinky HTML

Let’s make this flexible. (No pun intended.) What I mean by that is we want to be able to control the Slinky’s behavior through CSS custom properties, giving us the flexibility of swapping values when we need to.

Here’s how I’m setting the scene, written in Pug for brevity:

- const RING_COUNT = 10; .container .scene .plane(style=`--ring-count: ${RING_COUNT}`) - let rings = 0; while rings < RING_COUNT .ring(style=`--index: ${rings};`) - rings++;

Those inline custom properties are an easy way for us to update the number of rings and will come in handy as we get deeper into this challenge. The code above gives us 10 rings with HTML that looks something like this when compiled:

<div class="container"> <div class="scene"> <div class="plane" style="--ring-count: 10"> <div class="ring" style="--index: 0;"></div> <div class="ring" style="--index: 1;"></div> <div class="ring" style="--index: 2;"></div> <div class="ring" style="--index: 3;"></div> <div class="ring" style="--index: 4;"></div> <div class="ring" style="--index: 5;"></div> <div class="ring" style="--index: 6;"></div> <div class="ring" style="--index: 7;"></div> <div class="ring" style="--index: 8;"></div> <div class="ring" style="--index: 9;"></div> </div> </div> </div> The initial Slinky CSS

We’re going to need some styles! What we want is a three-dimensional scene. I’m mindful of some things we may want to do later, so that’s the thinking behind having an extra wrapper component with a .scene class.

Let’s start by defining some properties for our “infini-slinky” scene:

:root { --border-width: 1.2vmin; --depth: 20vmin; --stack-height: 6vmin; --scene-size: 20vmin; --ring-size: calc(var(--scene-size) * 0.6); --plane: radial-gradient(rgb(0 0 0 / 0.1) 50%, transparent 65%); --ring-shadow: rgb(0 0 0 / 0.5); --hue-one: 320; --hue-two: 210; --blur: 10px; --speed: 1.2s; --bg: #fafafa; --ring-filter: brightness(1) drop-shadow(0 0 0 var(--accent)); }

These properties define the characteristics of our Slinky and the scene. With the majority of 3D CSS scenes, we’re going to set transform-style across the board:

* { box-sizing: border-box; transform-style: preserve-3d; }

Now we need styles for our .scene. The trick is to translate the .plane so it looks like our CSS Slinky is moving infinitely down a flight of stairs. I had to play around to get things exactly the way I want, so bear with the magic number for now, as they’ll make sense later.

.container { /* Define the scene's dimensions */ height: var(--scene-size); width: var(--scene-size); /* Add depth to the scene */ transform: translate3d(0, 0, 100vmin) rotateX(-24deg) rotateY(32deg) rotateX(90deg) translateZ(calc((var(--depth) + var(--stack-height)) * -1)) rotate(0deg); } .scene, .plane { /* Ensure our container take up the full .container */ height: 100%; width: 100%; position: relative; } .scene { /* Color is arbitrary */ background: rgb(162 25 230 / 0.25); } .plane { /* Color is arbitrary */ background: rgb(25 161 230 / 0.25); /* Overrides the previous selector */ transform: translateZ(var(--depth)); }

There is a fair bit going on here with the .container transformation. Specifically:

  • translate3d(0, 0, 100vmin): This brings the .container forward and stops our 3D work from getting cut off by the body. We aren’t using perspective at this level, so we can get away with it.
  • rotateX(-24deg) rotateY(32deg): This rotates the scene based on our preferences.
  • rotateX(90deg): This rotates the .container by a quarter turn, which flattens the .scene and .plane by default, Otherwise, the two layers would look like the top and bottom of a 3D cube.
  • translate3d(0, 0, calc((var(--depth) + var(--stack-height)) * -1)): We can use this to move the scene and center it on the y-axis (well, actually the z-axis). This is in the eye of the designer. Here, we are using the --depth and --stack-height to center things.
  • rotate(0deg): Although, not in use at the moment, we may want to rotate the scene or animate the rotation of the scene later.

To visualize what’s happening with the .container, check this demo and tap anywhere to see the transform applied (sorry, Chromium only. &#x1f62d;):

CodePen Embed Fallback

We now have a styled scene! &#x1f4aa;

CodePen Embed Fallback Styling the Slinky’s rings

This is where those CSS custom properties are going to play their part. We have the inlined properties --index and --ring-count from our HTML. We also have the predefined properties in the CSS that we saw earlier on the :root.

The inline properties will play a part in positioning each ring:

.ring { --origin-z: calc( var(--stack-height) - (var(--stack-height) / var(--ring-count)) * var(--index) ); --hue: var(--hue-one); --accent: hsl(var(--hue) 100% 55%); height: var(--ring-size); width: var(--ring-size); border-radius: 50%; border: var(--border-width) solid var(--accent); position: absolute; top: 50%; left: 50%; transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%; transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg); } .ring:nth-of-type(odd) { --hue: var(--hue-two); }

Take note of how we are calculating the --origin-z value as well as how we position each ring with the transform property. That comes after positioning each ring with position: absolute .

It is also worth noting how we’re alternating the color of each ring in that last ruleset. When I first implemented this, I wanted to create a rainbow slinky where the rings went through the hues. But that adds a bit of complexity to the effect.

Now we’ve got some rings on our raised .plane:

CodePen Embed Fallback Transforming the Slinky rings

It’s time to get things moving! You may have noticed that we set a transform-origin on each .ring like this:

.ring { transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%; }

This is based on the .scene size. That 0.2 value is half the remaining available size of the .scene after the .ring is positioned.

We could tidy this up a bit for sure!

:root { --ring-percentage: 0.6; --ring-size: calc(var(--scene-size) * var(--ring-percentage)); --ring-transform: calc( 100% + (var(--scene-size) * ((1 - var(--ring-percentage)) * 0.5)) ) 50%; } .ring { transform-origin: var(--ring-transform); }

Why that transform-origin? Well, we need the ring to look like is moving off-center. Playing with the transform of an individual ring is a good way to work out the transform we want to apply. Move the slider on this demo to see the ring flip:

CodePen Embed Fallback

Add all the rings back and we can flip the whole stack!

CodePen Embed Fallback

Hmm, but they aren’t falling to the next stair. How can we make each ring fall to the right position?

Well, we have a calculated --origin-z, so let’s calculate --destination-z so the depth changes as the rings transform. If we have a ring on top of the stack, it should wind up at the bottom after it falls. We can use our custom properties to scope a destination for each ring:

ring { --destination-z: calc( ( (var(--depth) + var(--origin-z)) - (var(--stack-height) - var(--origin-z)) ) * -1 ); transform-origin: var(--ring-transform); transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(calc(var(--destination-z) * var(--flipped, 0))) rotateY(calc(var(--flipped, 0) * 180deg)); }

Now try moving the stack! We’re getting there. &#x1f64c;

CodePen Embed Fallback Animating the rings

We want our ring to flip and then fall. A first attempt might look something like this:

.ring { animation-name: slink; animation-duration: 2s; animation-fill-mode: both; animation-iteration-count: infinite; } @keyframes slink { 0%, 5% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg); } 25% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(180deg); } 45%, 100% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(var(--destination-z)) rotateY(180deg); } }

Oof, that’s not right at all!

CodePen Embed Fallback

But that’s only because we aren’t using animation-delay. All the rings are, um, slinking at the same time. Let’s introduce an animation-delay based on the --index of the ring so they slink in succession.

.ring { animation-delay: calc(var(--index) * 0.1s); }

OK, that is indeed “better.” But the timing is still off. What sticks out more, though, is the shortcoming of animation-delay. It is only applied on the first animation iteration. After that, we lose the effect.

CodePen Embed Fallback

At this point, let’s color the rings so they progress through the hue wheel. This is going to make it easier to see what’s going on.

.ring { --hue: calc((360 / var(--ring-count)) * var(--index)); }

That’s better! ✨

CodePen Embed Fallback

Back to the issue. Because we are unable to specify a delay that’s applied to every iteration, we are also unable to get the effect we want. For our Slinky, if we were able to have a consistent animation-delay, we might be able to achieve the effect we want. And we could use one keyframe while relying on our scoped custom properties. Even an animation-repeat-delay could be an interesting addition.

This functionality is available in JavaScript animation solutions. For example, GreenSock allows you to specify a delay and a repeatDelay.

But, our Slinky example isn’t the easiest thing to illustrate this problem. Let’s break this down into a basic example. Consider two boxes. And you want them to alternate spinning.

CodePen Embed Fallback

How do we do this with CSS and no “tricks”? One idea is to add a delay to one of the boxes:

.box { animation: spin 1s var(--delay, 0s) infinite; } .box:nth-of-type(2) { --delay: 1s; } @keyframes spin { to { transform: rotate(360deg); } }

But, that won’t work because the red box will keep spinning. And so will the blue one after its initial animation-delay.

CodePen Embed Fallback

With something like GreenSock, though, we can achieve the effect we want with relative ease:

import gsap from 'https://cdn.skypack.dev/gsap' gsap.to('.box', { rotate: 360, /** * A function based value, means that the first box has a delay of 0 and * the second has a delay of 1 */ delay: (index) > index, repeatDelay: 1, repeat: -1, ease: 'power1.inOut', })

And there it is!

CodePen Embed Fallback But how can we do this without JavaScript?

Well, we have to “hack” our @keyframes and completely do away with animation-delay. Instead, we will pad out the @keyframes with empty space. This comes with various quirks, but let’s go ahead and build a new keyframe first. This will fully rotate the element twice:

@keyframes spin { 50%, 100% { transform: rotate(360deg); } }

It’s like we’ve cut the keyframe in half. And now we’ll have to double the animation-duration to get the same speed. Without using animation-delay, we could try setting animation-direction: reverse on the second box:

.box { animation: spin 2s infinite; } .box:nth-of-type(2) { animation-direction: reverse; }

Almost.

CodePen Embed Fallback

The rotation is the wrong way round. We could use a wrapper element and rotate that, but that could get tricky as there are more things to balance. The other approach is to create two keyframes instead of one:

@keyframes box-one { 50%, 100% { transform: rotate(360deg); } } @keyframes box-two { 0%, 50% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

And there we have it:

CodePen Embed Fallback

This would’ve been a lot easier if we had a way to specify the repeat delay with something like this:

/* Hypothetical! */ animation: spin 1s 0s 1s infinite;

Or if the repeated delay matched the initial delay, we could possibly have a combinator for it:

/* Hypothetical! */ animation: spin 1s 1s+ infinite;

It would make for an interesting addition for sure!

So, we need keyframes for all those rings?

Yes, that is, if we want a consistent delay. And we need to do that based on what we are going to use as the animation window. All the rings need to have “slinked” and settled before the keyframes repeat.

This would be horrible to write out by hand. But this is why we have CSS preprocessors, right? Well, at least until we get loops and some extra custom property features on the web. &#x1f609;

Today’s weapon of choice will be Stylus. It’s my favorite CSS preprocessor and has been for some time. Habit means I haven’t moved to Sass. Plus, I like Stylus’s lack of required grammar and flexibility.

Good thing we only need to write this once:

// STYLUS GENERATED KEYFRAMES BE HERE... $ring-count = 10 $animation-window = 50 $animation-step = $animation-window / $ring-count for $ring in (0..$ring-count) // Generate a set of keyframes based on the ring index // index is the ring $start = $animation-step * ($ring + 1) @keyframes slink-{$ring} { // In here is where we need to generate the keyframe steps based on ring count and window. 0%, {$start * 1%} { transform translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg) } // Flip without falling {($start + ($animation-window * 0.75)) * 1%} { transform translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(180deg) } // Fall until the cut-off point {($start + $animation-window) * 1%}, 100% { transform translate3d(-50%, -50%, var(--origin-z)) translateZ(var(--destination-z)) rotateY(180deg) } }

Here’s what those variables mean:

  • $ring-count: The number of rings in our slinky.
  • $animation-window: This is the percentage of the keyframe that we can slink in. In our example, we’re saying we want to slink over 50% of the keyframes. The remaining 50% should get used for delays.
  • $animation-step: This is the calculated stagger for each ring. We can use this to calculate the unique keyframe percentages for each ring.

Here’s how it compiles to CSS, at least for the first couple of iterations:

View full code @keyframes slink-0 { 0%, 4.5% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg); } 38.25% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(180deg); } 49.5%, 100% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(var(--destination-z)) rotateY(180deg); } } @keyframes slink-1 { 0%, 9% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(0deg); } 42.75% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(0) rotateY(180deg); } 54%, 100% { transform: translate3d(-50%, -50%, var(--origin-z)) translateZ(var(--destination-z)) rotateY(180deg); } }

The last thing to do is apply each set of keyframes to each ring. We can do this using our markup if we want by updating it to define both an --index and a --name:

- const RING_COUNT = 10; .container .scene .plane(style=`--ring-count: ${RING_COUNT}`) - let rings = 0; while rings < RING_COUNT .ring(style=`--index: ${rings}; --name: slink-${rings};`) - rings++;

Which gives us this when compiled:

<div class="container"> <div class="scene"> <div class="plane" style="--ring-count: 10"> <div class="ring" style="--index: 0; --name: slink-0;"></div> <div class="ring" style="--index: 1; --name: slink-1;"></div> <div class="ring" style="--index: 2; --name: slink-2;"></div> <div class="ring" style="--index: 3; --name: slink-3;"></div> <div class="ring" style="--index: 4; --name: slink-4;"></div> <div class="ring" style="--index: 5; --name: slink-5;"></div> <div class="ring" style="--index: 6; --name: slink-6;"></div> <div class="ring" style="--index: 7; --name: slink-7;"></div> <div class="ring" style="--index: 8; --name: slink-8;"></div> <div class="ring" style="--index: 9; --name: slink-9;"></div> </div> </div> </div>

And then our styling can be updated accordingly:

.ring { animation: var(--name) var(--speed) both infinite cubic-bezier(0.25, 0, 1, 1); }

Timing is everything. So we’ve ditched the default animation-timing-function and we’re using a cubic-bezier. We’re also making use of the --speed custom property we defined at the start.

Aw yeah. Now we have a slinking CSS Slinky! Have a play with some of the variables in the code and see what different behavior you can yield.

CodePen Embed Fallback Creating an infinite animation

Now that we have the hardest part out of the way, we can make get this to where the animation repeats infinitely. To do this, we’re going to translate the scene as our Slinky slinks so it looks like it is slinking back into its original position.

.scene { animation: step-up var(--speed) infinite linear both; } @keyframes step-up { to { transform: translate3d(-100%, 0, var(--depth)); } }

Wow, that took very little effort!

CodePen Embed Fallback

We can remove the platform colors from .scene and .plane to prevent the animation from being too jarring:

CodePen Embed Fallback

Almost done! The last thing to address is that the stack of rings flips before it slinks again. This is where we mentioned earlier that the use of color would come in handy. Change the number of rings to an odd number, like 11, and switch back to alternating the ring color:

CodePen Embed Fallback

Boom! We have a working CSS slinky! It’s configurable, too!

Fun variations

How about a “flip flop” effect? By that, I mean getting the Slink to slink alternate ways. If we add an extra wrapper element to the scene, we could rotate the scene by 180deg on each slink.

- const RING_COUNT = 11; .container .flipper .scene .plane(style=`--ring-count: ${RING_COUNT}`) - let rings = 0; while rings < RING_COUNT .ring(style=`--index: ${rings}; --name: slink-${rings};`) - rings++;

As far as animation goes, we can make use of the steps() timing function and use twice the --speed:

.flipper { animation: flip-flop calc(var(--speed) * 2) infinite steps(1); height: 100%; width: 100%; } @keyframes flip-flop { 0% { transform: rotate(0deg); } 50% { transform: rotate(180deg); } 100% { transform: rotate(360deg); } }

Last, but not least, let’s change the way the .scene element’s step-up animation works. It no longer needs to move on the x-axis.

@keyframes step-up { 0% { transform: translate3d(-50%, 0, 0); } 100% { transform: translate3d(-50%, 0, var(--depth)); } } CodePen Embed Fallback

Note the animation-timing-function that we use. That use of steps(1) is what makes it possible.

If you want another fun use of steps(), check out this #SpeedyCSSTip!

For an extra touch, we could rotate the whole scene slow:

.container { animation: rotate calc(var(--speed) * 40) infinite linear; } @keyframes rotate { to { transform: translate3d(0, 0, 100vmin) rotateX(-24deg) rotateY(-32deg) rotateX(90deg) translateZ(calc((var(--depth) + var(--stack-height)) * -1)) rotate(360deg); } } CodePen Embed Fallback

I like it! Of course, styling is subjective… so, I made a little app you can use configure your Slinky:

CodePen Embed Fallback

And here are the “Original” and “Flip-Flop” versions I took a little further with shadows and theming.

Final demos CodePen Embed Fallback CodePen Embed Fallback That’s it!

That’s at least one way to make a pure CSS Slinky that’s both 3D and configurable. Sure, you might not reach for something like this every day, but it brings up interesting CSS animation techniques. It also raises the question of whether having a animation-repeat-delay property in CSS would be useful. What do you think? Do you think there would be some good use cases for it? I’d love to know.

Be sure to have a play with the code — all of it is available in this CodePen Collection!

A CSS Slinky in 3D? Challenge Accepted! originally published on CSS-Tricks. You should get the newsletter.

COLRv1 and CSS font-palette: Web Typography Gets Colorful

Wed, 05/11/2022 - 4:12am

According to Toshi Omagari, the author of Arcade Game Typography, the world’s first multi-colored digital font was created in 1982 for a never-released video game called Insector. Multi-colored fonts, sometimes called chromatic type, are still relatively rare on the web, even though the COLR font format has had full cross-browser support since 2018 (even in Internet Explorer!).

The technology opens up an entirely new vein of typographic creativity. While some of the color fonts I’ve seen have been lurid, at best, chromatic fonts are fun, innovative and attention-grabbing. With both the addition of a new CSS features — including the font-palette property and @font-palette-values rule — for controlling the color palette of color fonts and the evolution of the COLR font format, it’s a great time to dive in and experiment with with what modern web typography can do.

COLR support

I last wrote about color fonts in 2018. At the time, there were four different standards for multicolored typefaces: OpenType-SVG, COLR, SBIX, and CBDT/CBLC. You can use ChromaCheck to see what color font formats your own browser supports.

Google Chrome has marked OpenType-SVG as “wontfix” meaning that format will never be supported by Chrome or Edge. SBIX and CBDT/CBLC can mostly be ignored for use on the web as they’re both based on raster images and become blurry at larger font sizes. The large file size of bitmap-based fonts also makes them a bad choice for the web.

Ulrike Rausch is the creator of LiebeHeide, a bitmap color font which uncannily replicates the look of a ballpoint pen. “My biggest goal was always to reproduce handwritten text as authentically as possible,” she told me. “For LiebeHeide, I was finally able to simulate these handmade attributes with a font. The downside? All the PNG images in the font file add up and result in an enormous OTF file size. This might not be a problem for desktop applications, like Adobe InDesign, but for use on the web the font is hardly applicable.”

All browsers support COLR fonts (now generally referred to as COLRv0). Version 98 of Chrome (and Edge), released in February, added support for COLRv1, an evolution of the format.

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

DesktopChromeFirefoxIEEdgeSafari713291211Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari10110010111.0-11.2 COLRv0 and COLRv1 Plakato from Underware foundry

COLRv1 is part of the OpenType 1.9 standard. While the initial COLRv0 lacked many of the creative possibilities of OpenType-SVG, COLRv1 matches those possibilities while avoiding certain drawbacks. COLRv0, for example, could only do solid colors whereas COLRv1 can do linear, radial, and conic gradients. The format also adds compositing and blending and allows for shape reuse to save on file size.

Typography expert Roel Nieskins explains: “I used to say the OpenType-SVG format was the best format as it offered the most versatility — until I realized this is just too complex for a low-level job like text rendering. It implements a basic subset of SVG on the font rendering level. But it doesn’t work well with other font tech (hinting, variable axes, etc.), and it’s a pain to implement. So, I switched sides to COLR. COLR basically re-uses everything that OpenType fonts already have. It ‘only’ adds layering, and the possibility to change the color of each layer. Simple, but effective.”

COLRv1 is entirely compatible with variable font axes, and there are already examples of variable COLR fonts like Merit Badge, Plakato Color and Rocher Color.

Here’s a striking example from Ulrike Rausch of what’s possible with the format, a (currently unreleased) typeface which digitally recreates the look of a neon sign:

Akiem Helmling of the type foundry Underware is enamored with COLRv1, telling me that “[t]he COLRv1 format can potentially have a similar (or even bigger) impact on type design than variable fonts had in recent years.” For Akiem, it’s undoubtedly the superior format. “All former color formats have been bad hacks to add colors to glyphs. While OpenType-SVG is regarded by some people as a good solution, it is, from my point of view, not at all. From a pragmatic point of view, SVG is a ‘locked room’ within the open structure of OpenType. There is no way to re-use or link data or to create a connection between other font tables and the SVG table. And because of this, we can not make a variable font with variable SVG data.”

It’s still early days for the format. Mozilla hasn’t yet shipped COLRv1 but has taken a positive position on the format, stating that it has “the potential to supersede OpenType-SVG fonts in web use.” Apple is reluctant to implement it in Safari.

COLRv1 fonts will still show up and be readable in these browsers but all the letter’s will be a single solid color (which you can set with the CSS color property, just like with a normal font). We’re yet to see many type foundries release COLRv1 typefaces, and some popular design tools like Figma don’t even support COLRv0, but I’m hopeful and believe it will be the future of color typography on the web. In the short time COLRv1 has been around there have already been some beautiful examples of what the technology can do, such as Reem Kufi and Bradley Initials.

COLR and CSS

If you’re using a color font, you probably want to be able to control its colors. Until now, that was impossible to do with CSS. The font-palette property brings the power to override the default color scheme of a typeface and apply your own. This property works on COLRv0 and COLRv1 typefaces. (Apple’s Myles Maxfield explains that SVG fonts can opt-in to using palettes, whereas all the colors of a COLR typeface are automatically overridden by CSS.)

Coming up with a decent color palette is a fine art. Some type designers have done the hard work for us and include alternative palettes inside the font. You can select from these different color schemes using base-palette in CSS.

How do you find out if a font offers an alternate palette? The site for the font might tell you. If not, there’s a handy tool called Wakamai Fondue that will list all the available color schemes (shown in the image below). For this example, I’ll use Rocher Color, a free variable color font from Henrique Beier with a Flintstones vibe. From looking at Wakamai Foundue we can see that this typeface uses four colors and comes with eleven different palette options.

Using base-palette: 0 will select the default color palette (in the case of Rocher, that’s shades of orange and a brown).

Using base-palette: 1 will select the first alternative palette defined by the creator of the typeface, and so on. In the following code example, I’m selecting a color palette that’s different shades of gray:

@font-palette-values --Grays { font-family: Rocher; base-palette: 9; }

Once you’ve selected a palette with the CSS @font-palette-values rule, you can apply it using the font-palette property:

.grays { font-family: 'Rocher'; font-palette: --Grays; } CodePen Embed Fallback

Of course, you might want to create your own palette to match your brand colors or to meet your own design sensibility. If you’re going to override all the colors then you don’t need to specify the base-palette.

Let’s take Bungee from pioneering type designer David Jonathan Ross as an example. It only uses two colors by default, red and white. In the following example, I’m overriding both colors of the font, so the base-palette doesn’t matter and is omitted:

@font-palette-values --PinkAndGray { font-family: bungee; override-colors: 0 #c1cbed, 1 #ff3a92; } @font-palette-values --GrayAndPink { font-family: bungee; override-colors: 0 #ff3a92, 1 #c1cbed; }

Alternatively, you can set the base-palette as a starting point and selectively change just some of the colors. Below I’m using the gray color palette of Rocher, but overriding one color with a minty green:

@font-palette-values --GraysRemix { font-family: Rocher; base-palette: 9; override-colors: 2 rgb(90,290,210); } body { font-family: "Rocher"; font-palette: --GraysRemix; }

When specifying override-colors it’s difficult to know which bit of the font will be overridden by which number — you have to play around and experiment and achieve the desired effect through trial and error.

Should you wish, you can even change the color of emoji fonts, such as Twemoji (shown below) or Noto. Here’s a fun demo from a font engineer at Google.

Current limitations

One regrettable limitation, at least for the time being is that CSS custom properties don’t work in @font-palette-values. That means the following is invalid:

@font-palette-values --PinkAndBlue { font-family: bungee; override-colors: 0 var(--pink), 1 var(--blue); }

Another limitation: animations and transitions from one font-palette to another don’t interpolate — meaning you can switch instantly from one palette to another, but can’t gradually animate between them. My dream of a luridly animated emoji font is sadly unrealized.

Browser support

font-palette and @font-palette-values have been supported in Safari since version 15.4, and landed in Chrome and Edge with the release of version 101.

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

DesktopChromeFirefoxIEEdgeSafari101NoNoNo15.4Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari101No10115.4 Use cases

You can probably already imagine how you might use color fonts in your own projects. There are a couple of specific use cases, though, that are worth calling out.

COLR and icon fonts

Icon fonts may no longer be the most popular method for displaying icons on the web (Chris explains why) but they are still widely used. If you use an icon font with multiple colors, like Duotone from FontAwesome or the two-tone icons from Material Design, font-palette could offer an easier method for customization.

Solving the emoji problem

Nolan Lawson recently wrote about the problems of using emoji on the web. The Chrome developer blog explains one rather complex current solution:

If you support user generated content, your users probably use emojis. Today it’s very common to scan text and replace any emoji encountered with images to ensure consistent cross-platform rendering and the ability to support newer emojis than the OS supports. Those images then have to be switched back to text during clipboard operations.

If it receives greater browser support, COLRv1 emoji fonts will offer a far simpler approach. COLRv1 also comes with the benefit of looking crisp at any size, whereas native browser emojis get blurry and pixelated at larger font sizes.

Wrapping up

Before color fonts, typographic creativity on the web was limited to applying strokes or gradient fills with CSS. You could always do something more custom with a vector image, but that’s not real text — it can’t be selected by the user and copied to the clipboard, it can’t be searched on the page with Command+F, it isn’t read by screen readers or search engines, and you’d need to open Adobe Illustrator just to edit the copy.

Color fonts have the potential to really grab a user’s attention, making them perfect for landing pages and banners. They may not be something you reach for often, but they promise new expressive and creative possibilities for web design that can make your site stand out.

COLRv1 and CSS font-palette: Web Typography Gets Colorful originally published on CSS-Tricks. You should get the newsletter.

Let’s Create a Tiny Programming Language

Tue, 05/10/2022 - 3:56am

By now, you are probably familiar with one or more programming languages. But have you ever wondered how you could create your own programming language? And by that, I mean:

A programming language is any set of rules that convert strings to various kinds of machine code output.

In short, a programming language is just a set of predefined rules. And to make them useful, you need something that understands those rules. And those things are compilers, interpreters, etc. So we can simply define some rules, then, to make it work, we can use any existing programming language to make a program that can understand those rules, which will be our interpreter.

Compiler

A compiler converts codes into machine code that the processor can execute (e.g. C++ compiler).

Interpreter

An interpreter goes through the program line by line and executes each command.

Want to give it a try? Let’s create a super simple programming language together that outputs magenta-colored output in the console. We’ll call it Magenta.

Our simple programming language creates a codes variable that contains text that gets printed to the console… in magenta, of course. Setting up our programming language

I am going to use Node.js but you can use any language to follow along, the concept will remain the same. Let me start by creating an index.js file and set things up.

class Magenta { constructor(codes) { this.codes = codes } run() { console.log(this.codes) } } // For now, we are storing codes in a string variable called `codes` // Later, we will read codes from a file const codes = `print "hello world" print "hello again"` const magenta = new Magenta(codes) magenta.run()

What we’re doing here is declaring a class called Magenta. That class defines and initiates an object that is responsible for logging text to the console with whatever text we provide it via a codes variable. And, for the time being, we’ve defined that codes variable directly in the file with a couple of “hello” messages.

If we were to run this code we would get the text stored in codes logged in the console.

OK, now we need to create a what’s called a Lexer.

What is a Lexer?

OK, let’s talks about the English language for a second. Take the following phrase:

How are you?

Here, “How” is an adverb, “are” is a verb, and “you” is a pronoun. We also have a question mark (“?”) at the end. We can divide any sentence or phrase like this into many grammatical components in JavaScript. Another way we can distinguish these parts is to divide them into small tokens. The program that divides the text into tokens is our Lexer.

Since our language is very tiny, it only has two types of tokens, each with a value:

  1. keyword
  2. string

We could’ve used a regular expression to extract tokes from the codes string but the performance will be very slow. A better approach is to loop through each character of the code string and grab tokens. So, let’s create a tokenize method in our Magenta class — which will be our Lexer.

Full code class Magenta { constructor(codes) { this.codes = codes } tokenize() { const length = this.codes.length // pos keeps track of current position/index let pos = 0 let tokens = [] const BUILT_IN_KEYWORDS = ["print"] // allowed characters for variable/keyword const varChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' while (pos < length) { let currentChar = this.codes[pos] // if current char is space or newline, continue if (currentChar === " " || currentChar === "\n") { pos++ continue } else if (currentChar === '"') { // if current char is " then we have a string let res = "" pos++ // while next char is not " or \n and we are not at the end of the code while (this.codes[pos] !== '"' && this.codes[pos] !== '\n' && pos < length) { // adding the char to the string res += this.codes[pos] pos++ } // if the loop ended because of the end of the code and we didn't find the closing " if (this.codes[pos] !== '"') { return { error: `Unterminated string` } } pos++ // adding the string to the tokens tokens.push({ type: "string", value: res }) } else if (varChars.includes(currentChar)) { let res = currentChar pos++ // while the next char is a valid variable/keyword charater while (varChars.includes(this.codes[pos]) && pos < length) { // adding the char to the string res += this.codes[pos] pos++ } // if the keyword is not a built in keyword if (!BUILT_IN_KEYWORDS.includes(res)) { return { error: `Unexpected token ${res}` } } // adding the keyword to the tokens tokens.push({ type: "keyword", value: res }) } else { // we have a invalid character in our code return { error: `Unexpected character ${this.codes[pos]}` } } } // returning the tokens return { error: false, tokens } } run() { const { tokens, error } = this.tokenize() if (error) { console.log(error) return } console.log(tokens) } }

If we run this in a terminal with node index.js, we should see a list of tokens printed in the console.

Great stuff! Defining rules and syntaxes

We want to see if the order of our codes matches some sort of rule or syntax. But first we need to define what those rules and syntaxes are. Since our language is so tiny, it only has one simple syntax which is a print keyword followed by a string.

keyword:print string

So let’s create a parse method that loops through our tokens and see if we have a valid syntax formed. If so, it will take necessary actions.

class Magenta { constructor(codes) { this.codes = codes } tokenize(){ /* previous codes for tokenizer */ } parse(tokens){ const len = tokens.length let pos = 0 while(pos < len) { const token = tokens[pos] // if token is a print keyword if(token.type === "keyword" && token.value === "print") { // if the next token doesn't exist if(!tokens[pos + 1]) { return console.log("Unexpected end of line, expected string") } // check if the next token is a string let isString = tokens[pos + 1].type === "string" // if the next token is not a string if(!isString) { return console.log(`Unexpected token ${tokens[pos + 1].type}, expected string`) } // if we reach this point, we have valid syntax // so we can print the string console.log('\x1b[35m%s\x1b[0m', tokens[pos + 1].value) // we add 2 because we also check the token after print keyword pos += 2 } else{ // if we didn't match any rules return console.log(`Unexpected token ${token.type}`) } } } run(){ const {tokens, error} = this.tokenize() if(error){ console.log(error) return } this.parse(tokens) } }

And would you look at that — we already have a working language!

Okay but having codes in a string variable is not that fun. So lets put our Magenta codes in a file called code.m. That way we can keep our magenta codes separate from the compiler logic. We are using .m as file extension to indicate that this file contains code for our language.

Let’s read the code from that file:

// importing file system module const fs = require('fs') //importing path module for convenient path joining const path = require('path') class Magenta{ constructor(codes){ this.codes = codes } tokenize(){ /* previous codes for tokenizer */ } parse(tokens){ /* previous codes for parse method */ } run(){ /* previous codes for run method */ } } // Reading code.m file // Some text editors use \r\n for new line instead of \n, so we are removing \r const codes = fs.readFileSync(path.join(__dirname, 'code.m'), 'utf8').toString().replace(/\r/g, &quot;&quot;) const magenta = new Magenta(codes) magenta.run() Go create a programming language!

And with that, we have successfully created a tiny Programming Language from scratch. See, a programming language can be as simple as something that accomplishes one specific thing. Sure, it’s unlikely that a language like Magenta here will ever be useful enough to be part of a popular framework or anything, but now you see what it takes to make one.

The sky is really the limit. If you want dive in a little deeper, try following along with this video I made going over a more advanced example. This is video I have also shown hoe you can add variables to your language also.

Let’s Create a Tiny Programming Language originally published on CSS-Tricks. You should get the newsletter.

Useful Tools for Creating AVIF Images

Mon, 05/09/2022 - 5:00am

AVIF (AV1 Image File Format) is a modern image file format specification for storing images that offer a much more significant file reduction when compared to other formats like JPG, JPEG, PNG, and WebP. Version 1.0.0 of the AVIF specification was finalized in February 2019 and released by Alliance for Open Media to the public.

In this article, you will learn about some browser-based tools and command line tools for creating AVIF images.

Why use AVIF over JPGs, PNGS, WebP, and GIF?
  • Lossless compression and lossy compression
  • JPEG suffers from awful banding
  • WebP is much better, but there’s still noticeable blockiness compared to the AVIF
  • Multiple color space
  • 8, 10, 12-bit color depth
Caveats

Jake Archibald, wrote an article a few years back on this new image format and also helped us to identify some disadvantages to compressing images, normally you should look out for these two when compressing to AVIF:

  1. If a user looks at the image in the context of the page, and it strikes them as ugly due to compression, then that level of compression is not acceptable. But, one tiny notch above that boundary is fine.
  2. It’s okay for the image to lose noticeable detail compared to the original unless that detail is significant to the context of the image.

See also: Addy Osmani at Smashing Magazine goes in-depth on using AVIF and WebP.

Browser Solutions Squoosh Screenshot of Squoosh.

Squoosh is a popular image compression web app that allows you to convert images in numerous formats to other widely used compressed formats, including AVIF.

Features
  • File-size limit: 4MB
  • Image optimization settings (located on the right side)
  • Download controls – this includes seeing the size of the resulting file and the percentage reduction from the original image
  • Free to use
AVIF.io Screenshot of AVIF.io.

AVIF.io is an image tool that doesn’t require any form of code. All you need to do is upload your selected images in PNG, JPG, GIF, etc. and it would return compressed versions of them.

Features
  • Conversion settings (located on the top-right of upload banner)
  • Free to use

You can find answers to your questions on the AVIF.io FAQ page.

Command Line Solutions avif-cli

avif-cli by lovell lets you take your images (PNG, JPEG, etc.) stored in a folder and converts them to AVIF images of your specified reduction size.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install avif

Run the command in your terminal:

npx avif --input="./imgs/*" --output="./output/" --verbose
  • ./imgs/* – represents the location of all your image files
  • ./output/ – represents the location of your output folder
CodePen Embed Fallback Features
  • Free to use
  • Speed of conversion can be set

You can find out about more commands via the avif-cli GitHub page.

sharp

sharp is another useful tool for converting large images in common formats to smaller, web-friendly AVIF images.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install sharp

Create a JavaScript file named sharp-example.js and copy this code:

const sharp = require('sharp') const convertToAVIF = () => { sharp('path_to_image') .toFormat('avif', {palette: true}) .toFile(__dirname + 'path_to_output_image') } convertToAVIF()

Where path_to_image represents the path to your image with its name and extension, i.e.:

./imgs/example.jpg

And path_to_output_image represents the path you want your image to be stored with its name and new extension, i.e.:

/sharp-compressed/compressed-example.avif

Run the command in your terminal:

node sharp-example.js

And there! You should have a compressed AVIF file in your output location!

Features
  • Free to use
  • Images can be rotated, blurred, resized, cropped, scaled, and more using sharp

See also: Stanley Ulili’s article on How To Process Images in Node.js With Sharp.

Conclusion

AVIF is a technology that front-end developers should consider for their projects. These tools allow you to convert your existing JPEG and PNG images to AVIF format. But as with adopting any new tool in your workflow, the benefits and downsides will need to be properly evaluated in accordance with your particular use case.

I hope you enjoyed reading this article as much as I enjoyed writing it. Thank you so much for your time and I hope you have a great day ahead!

Useful Tools for Creating AVIF Images originally published on CSS-Tricks. You should get the newsletter.

How to Serve a Subdomain as a Subdirectory

Fri, 05/06/2022 - 4:19am

Let’s say you have a website built on a platform that excels at design and it’s available at example.com. But that platform falls short at blogging. So you think to yourself, “What if I could use a different blogging platform and make it available at example.com/blog?”

Most people would tell you that goes against how DNS and websites are supposed to work and to use a subdomain instead. But there are benefits to keeping your content on the root domain that we just don’t get with subdomains.

There’s a way to serve two different platforms on the same URL. And I’m going to show you the secret sauce so that, by the end of this article, we’ll make blog.example.com serve as example.com/blog.

Why you’d want to do this

Because you’re here, you probably already know why this is a path to pursue. But I’d like to ensure you are here for the primary reason to do this: SEO. Check out these 14 case studies that show positive results when people move their subdomains over to subdirectories. You want your blog and your domain to share SEO value. Putting it on a subdomain would somewhat disconnect the two.

This was my reason, and wound up merging two platforms, where the main domain was on WordPress and the subdomain was on Drupal. But this tutorial is platform agnostic — it’ll work with just about any platform.

That said, the Cloudflare approach we’re covering in this tutorial is incompatible with Shopify unless you pay for Cloudflare’s Enterprise plan. That’s because Shopify also uses Cloudflare and does not allow us to proxy the traffic on their free pricing tier.

Step 0 (Preview)

Before I jump in, I want to explain the high level of what’s going to happen. In short, we’ll have two websites: our main one (example.com) and the subdomain (blog.example.com). I use “blog” as an example, but in my case, I needed to drop in Drupal with a different type of content. But a blog is the typical use case.

This approach relies on using Cloudflare for DNS and a little extra something that’ll provide the magic. We’re going to tell Cloudflare that when someone visits example.com/blog, it should:

  1. intercept that request (because example.com/blog doesn’t really exist),
  2. request a different domain (blog.example.com/blog) behind the scenes, and
  3. deliver the results from that last step to the visitor masked through example.com/blog.

Okay, let’s dive into it in more detail!

Step 1: Using Cloudflare

Again, we’re using Cloudflare for the DNS. Pointing your domain’s DNS there is the first step to getting started.

The reason for Cloudflare is that it allows us to create Workers that are capable of running a bit of code anytime somebody visits certain URLs (called Routes which we’ll create in step 3). This code will be responsible for switching the websites behind the scenes.

Cloudflare has an excellent guide to getting started. The goal is to point your domain’s — wherever it is registered — to Cloudflare’s nameservers and confirm that Cloudflare is connected in your Cloudflare account.

Step 2: Create the Worker

This code will be responsible for switching the websites behind the scenes. Head over to Workers and click Create a Service.

Note the median CPU time! This process added about .7ms to the request (so basically nothing).

Name your service, then select “HTTP handler”:

Click Create Service and then Quick Edit.

Paste in the following code and replace the domain names with your own on line 16:

// Listen for every request and respond with our function. // Note, this will only run on the routes configured in Cloudflare. addEventListener('fetch', function(event) { event.respondWith(handleRequest(event.request)) }) // Our function to handle the response. async function handleRequest(request) { // Only GET requests work with this proxy. if (request.method !== 'GET') return MethodNotAllowed(request); // The URL that is being requested. const url = new URL(request.url); // Request "origin URL" aka the real blog instead of what was requested. // This switches out the absolute URL leaving the relative path unchanged. const originUrl = url.toString().replace('https://example.com', 'https://blog.example.com'); // The contents of the origin page. const originPage = await fetch(originUrl); // Give the response our origin page. const newResponse = new Response(originPage.body, originPage); return newResponse; } // Hey! GET requests only function MethodNotAllowed(request) { return new Response(`Method ${request.method} not allowed.`, { status: 405, headers: { 'Allow': 'GET' } }) }

Lastly, click Save and Deploy.

Step 3: Add Routes

Now let’s inform Cloudflare which URLs (aka Routes) to run this code on. Head over to the website in Cloudflare, then click Workers.

There is the Workers section on the main screen of Cloudflare, where you edit the code, and then there is the Workers section on each website where you add the routes. They are two different places, and it’s confusing.

First off, click Add Route:

Because we are adding a blog that has many child pages, we’ll use https://example.com/blog*. Note the asterisk acts as a wild card for matching. This code will run on the blog page and every page that begins with blog.

This can have unintended consequences. Say, for example, you have a page that starts with “blog” but isn’t a part of the actual blog, like https://example.com/blogging-services. That would get picked up with this rule.

Then, select the Worker in the Service dropdown.

We have a lot of the work done, but there are more routes we need to add — the CSS, JavaScript, and other file paths that the blog is dependent on (unless all the files are hosted on a different URL, such as on a CDN). A good way to find these is by testing your route and checking the console.

Head over to your https://example.com/blog and make sure something is loading. It’ll look messed up because it’s missing the theme files. That’s fine for now, just as long as it’s not producing a 404 error. The important thing is to open up your browser’s DevTools, fire up the console, and make note of all the red URLs it can’t find or load (usually a 404 or 403) that are a part of your domain.

The resources in the orange boxes are the ones we need to target.

You’ll want to add those as routes… but only do the parent paths. So, if your red URL is https://example.com/wp-content/themes/file1.css, then do https://example.com/wp-content* as your route. You can add a child path, too, if you want to be more specific, but the idea is to use one route to catch most of the files.

Once you add those routes, check out your URL and see if it looks like your subdomain. If it doesn’t, check the previous steps. (Chances are you will need to add more routes.)

It’s best to do a quality check by navigating to multiple pages and seeing if anything is missing. I also recommend opening up DevTools and searching for your subdomain (blog.example.com). If that’s showing up, you either need to add routes to target those resources or do something with your platform to stop outputting those URLs. For example, my platform was outputting a canonical tag with my subdomain, so I found a plugin to modify the canonical URL to be my root domain.

Step 4: The secretest of sauces (noindex)

You might see that we have a problem. Our URLs are available at two different URLs. Yeah, we can use the canonical attribute to inform Google which URL is our “main” one, but let’s not leave it up to Google to pick the right one.

First, set your entire subdomain as noindex (the way to do this will vary by platform). Then, in the Cloudflare Worker, we are going to add the following line of code, which basically says to remove noindex when the current URL is accessed through the proxy.

newResponse.headers.delete("x-robots-tag");

The full code solution is provided at the end of this article.

Step 5: Modify the sitemap

The last thing to do is to modify the subdomain’s sitemap so it doesn’t use the subdomain in it. The way to do this will vary by platform, but the goal is to modify the base/absolute/domain in your sitemap so that it prints example.com/mypost) instead of blog.exmaple.com/mypost. Some plugins and modules will allow this without custom code.

That’s that! The solution should be working!

Limitations

This Cloudflare magic isn’t without its downsides. For example, it only accepts GET requests, meaning we can only get things from the server. We are unable to POST which is what forms use. So, if you need to have your visitors log in or submit forms, there will be more work on top of what we’ve already done. I discussed several solutions for this in another article.

As noted earlier, another limitation is that using this approach on Shopify requires subscribing to Cloudflare’s Enterprise pricing tier. Again, that’s because Shopify also uses Cloudflare and restricts the ability to proxy traffic on their other plans.

You also might get some issues if you’re trying to merge two instances of the same platforms together (e.g. both your top-level domain and subdomain use WordPress). But in a case like that you should be able to consolidate and use one instance of the platform.

Full solution

Here’s the code in all its glory:

// Listen for every request and respond with our function. // Note, this will only run on the routes configured in Cloudflare. addEventListener('fetch', function(event) { event.respondWith(handleRequest(event.request)) }) // Our function to handle the response. async function handleRequest(request) { // Only GET requests work with this proxy. if (request.method !== 'GET') return MethodNotAllowed(request); // The URL that is being requested. const url = new URL(request.url); // Request "origin URL" aka the real blog instead of what was requested. // This switches out the absolute URL leaving the relative path unchanged. const originUrl = url.toString().replace('https://example.com', 'https://blog.example.com'); // The contents of the origin page. const originPage = await fetch(originUrl); // Give the response our origin page. const newResponse = new Response(originPage.body, originPage); // Remove "noindex" from the origin domain. newResponse.headers.delete("x-robots-tag"); // Remove Cloudflare cache as it's meant for WordPress. // If you are using Cloudflare APO and your blog isn't WordPress, (but // your main domain is), then stop APO from running on your origin URL. // newResponse.headers.set("cf-edge-cache", "no-cache"); return newResponse; } // Hey! GET requests only function MethodNotAllowed(request) { return new Response(`Method ${request.method} not allowed.`, { status: 405, headers: { 'Allow': 'GET' } }) }

If you need help along the way, I welcome you to reach out to me through my website CreateToday.io or check out my YouTube for a video demonstration.

How to Serve a Subdomain as a Subdirectory originally published on CSS-Tricks. You should get the newsletter.

Syntax Highlighting (and More!) With Prism on a Static Site

Wed, 05/04/2022 - 4:16am

So, you’ve decided to build a blog with Next.js. Like any dev blogger, you’d like to have code snippets in your posts that are formatted nicely with syntax highlighting. Perhaps you also want to display line numbers in the snippets, and maybe even have the ability to call out certain lines of code.

This post will show you how to get that set up, as well as some tips and tricks for getting these other features working. Some of it is tricker than you might expect.

Prerequisites

We’re using the Next.js blog starter as the base for our project, but the same principles should apply to other frameworks. That repo has clear (and simple) getting started instructions. Scaffold the blog, and let’s go!

Another thing we’re using here is Prism.js, a popular syntax highlighting library that’s even used right here on CSS-Tricks. The Next.js blog starter uses Remark to convert Markdown into markup, so we’ll use the remark-Prism.js plugin for formatting our code snippets.

Basic Prism.js integration

Let’s start by integrating Prism.js into our Next.js starter. Since we already know we’re using the remark-prism plugin, the first thing to do is install it with your favorite package manager:

npm i remark-prism

Now go into the markdownToHtml file, in the /lib folder, and switch on remark-prism:

import remarkPrism from "remark-prism"; // later ... .use(remarkPrism, { plugins: ["line-numbers"] })

Depending on which version of the remark-html you’re using, you might also need to change its usage to .use(html, { sanitize: false }).

The whole module should now look like this:

import { remark } from "remark"; import html from "remark-html"; import remarkPrism from "remark-prism"; export default async function markdownToHtml(markdown) { const result = await remark() .use(html, { sanitize: false }) .use(remarkPrism, { plugins: ["line-numbers"] }) .process(markdown); return result.toString(); } Adding Prism.js styles and theme

Now let’s import the CSS that Prism.js needs to style the code snippets. In the pages/_app.js file, import the main Prism.js stylesheet, and the stylesheet for whichever theme you’d like to use. I’m using Prism.js’s “Tomorrow Night” theme, so my imports look like this:

import "prismjs/themes/prism-tomorrow.css"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; import "../styles/prism-overrides.css";

Notice I’ve also started a prism-overrides.css stylesheet where we can tweak some defaults. This will become useful later. For now, it can remain empty.

And with that, we now have some basic styles. The following code in Markdown:

```js class Shape { draw() { console.log("Uhhh maybe override me"); } } class Circle { draw() { console.log("I'm a circle! :D"); } } ```

…should format nicely:

Adding line numbers

You might have noticed that the code snippet we generated does not display line numbers even though we imported the plugin that supports it when we imported remark-prism. The solution is hidden in plain sight in the remark-prism README:

Don’t forget to include the appropriate css in your stylesheets.

In other words, we need to force a .line-numbers CSS class onto the generated <pre> tag, which we can do like this:

And with that, we now have line numbers!

Note that, based on the version of Prism.js I have and the “Tomorrow Night” theme I chose, I needed to add this to the prism-overrides.css file we started above:

.line-numbers span.line-numbers-rows { margin-top: -1px; }

You may not need that, but there you have it. We have line numbers!

Highlighting lines

Our next feature will be a bit more work. This is where we want the ability to highlight, or call out certain lines of code in the snippet.

There’s a Prism.js line-highlight plugin; unfortunately, it is not integrated with remark-prism. The plugin works by analyzing the formatted code’s position in the DOM, and manually highlights lines based on that information. That’s impossible with the remark-prism plugin since there is no DOM at the time the plugin runs. This is, after all, static site generation. Next.js is running our Markdown through a build step and generating HTML to render the blog. All of this Prism.js code runs during this static site generation, when there is no DOM.

But fear not! There’s a fun workaround that fits right in with CSS-Tricks: we can use plain CSS (and a dash of JavaScript) to highlight lines of code.

Let me be clear that this is a non-trivial amount of work. If you don’t need line highlighting, then feel free to skip to the next section. But if nothing else, it can be a fun demonstration of what’s possible.

Our base CSS

Let’s start by adding the following CSS to our prism-overrides.css stylesheet:

:root { --highlight-background: rgb(0 0 0 / 0); --highlight-width: 0; } .line-numbers span.line-numbers-rows > span { position: relative; } .line-numbers span.line-numbers-rows > span::after { content: " "; background: var(--highlight-background); width: var(--highlight-width); position: absolute; top: 0; }

We’re defining some CSS custom properties up front: a background color and a highlight width. We’re setting them to empty values for now. Later, though, we’ll set meaningful values in JavaScript for the lines we want highlighted.

We’re then setting the line number <span> to position: relative, so that we can add a ::after pseudo element with absolute positioning. It’s this pseudo element that we’ll use to highlight our lines.

Declaring the highlighted lines

Now, let’s manually add a data attribute to the <pre> tag that’s generated, then read that in code, and use JavaScript to tweak the styles above to highlight specific lines of code. We can do this the same way that we added line numbers before:

This will cause our <pre> element to be rendered with a data-line="3,8-10" attribute, where line 3 and lines 8-10 are highlighted in the code snippet. We can comma-separate line numbers, or provide ranges.

Let’s look at how we can parse that in JavaScript, and get highlighting working.

Reading the highlighted lines

Head over to components/post-body.tsx. If this file is JavaScript for you, feel free to either convert it to TypeScript (.tsx), or just ignore all my typings.

First, we’ll need some imports:

import { useEffect, useRef } from "react";

And we need to add a ref to this component:

const rootRef = useRef<HTMLDivElement>(null);

Then, we apply it to the root element:

<div ref={rootRef} className="max-w-2xl mx-auto">

The next piece of code is a little long, but it’s not doing anything crazy. I’ll show it, then walk through it.

useEffect(() => { const allPres = rootRef.current.querySelectorAll("pre"); const cleanup: (() => void)[] = []; for (const pre of allPres) { const code = pre.firstElementChild; if (!code || !/code/i.test(code.tagName)) { continue; } const highlightRanges = pre.dataset.line; const lineNumbersContainer = pre.querySelector(".line-numbers-rows"); if (!highlightRanges || !lineNumbersContainer) { continue; } const runHighlight = () => highlightCode(pre, highlightRanges, lineNumbersContainer); runHighlight(); const ro = new ResizeObserver(runHighlight); ro.observe(pre); cleanup.push(() => ro.disconnect()); } return () => cleanup.forEach(f => f()); }, []);

We’re running an effect once, when the content has all been rendered to the screen. We’re using querySelectorAll to grab all the <pre> elements under this root element; in other words, whatever blog post the user is viewing.

For each one, we make sure there’s a <code> element under it, and we check for both the line numbers container and the data-line attribute. That’s what dataset.line checks. See the docs for more info.

If we make it past the second continue, then highlightRanges is the set of highlights we declared earlier which, in our case, is "3,8-10", where lineNumbersContainer is the container with the .line-numbers-rows CSS class.

Lastly, we declare a runHighlight function that calls a highlightCode function that I’m about to show you. Then, we set up a ResizeObserver to run that same function anytime our blog post changes size, i.e., if the user resizes the browser window.

The highlightCode function

Finally, let’s see our highlightCode function:

function highlightCode(pre, highlightRanges, lineNumberRowsContainer) { const ranges = highlightRanges.split(",").filter(val => val); const preWidth = pre.scrollWidth; for (const range of ranges) { let [start, end] = range.split("-"); if (!start || !end) { start = range; end = range; } for (let i = +start; i <= +end; i++) { const lineNumberSpan: HTMLSpanElement = lineNumberRowsContainer.querySelector( `span:nth-child(${i})` ); lineNumberSpan.style.setProperty( "--highlight-background", "rgb(100 100 100 / 0.5)" ); lineNumberSpan.style.setProperty("--highlight-width", `${preWidth}px`); } } }

We get each range and read the width of the <pre> element. Then we loop through each range, find the relevant line number <span>, and set the CSS custom property values for them. We set whatever highlight color we want, and we set the width to the total scrollWidth value of the <pre> element. I kept it simple and used "rgb(100 100 100 / 0.5)" but feel free to use whatever color you think looks best for your blog.

Here’s what it looks like:

Line highlighting without line numbers

You may have noticed that all of this so far depends on line numbers being present. But what if we want to highlight lines, but without line numbers?

One way to implement this would be to keep everything the same and add a new option to simply hide those line numbers with CSS. First, we’ll add a new CSS class, .hide-numbers:

```js[class="line-numbers"][class="hide-numbers"][data-line="3,8-10"] class Shape { draw() { console.log("Uhhh maybe override me"); } } class Circle { draw() { console.log("I'm a circle! :D"); } } ```

Now let’s add CSS rules to hide the line numbers when the .hide-numbers class is applied:

.line-numbers.hide-numbers { padding: 1em !important; } .hide-numbers .line-numbers-rows { width: 0; } .hide-numbers .line-numbers-rows > span::before { content: " "; } .hide-numbers .line-numbers-rows > span { padding-left: 2.8em; }

The first rule undoes the shift to the right from our base code in order to make room for the line numbers. By default, the padding of the Prism.js theme I chose is 1em. The line-numbers plugin increases it to 3.8em, then inserts the line numbers with absolute positioning. What we did reverts the padding back to the 1em default.

The second rule takes the container of line numbers, and squishes it to have no width. The third rule erases all of the line numbers themselves (they’re generated with ::before pseudo elements).

The last rule simply shifts the now-empty line number <span> elements back to where they would have been so that the highlighting can be positioned how we want it. Again, for my theme, the line numbers normally adds 3.8em worth of left padding, which we reverted back to the default 1em. These new styles add the other 2.8em so things are back to where they should be, but with the line numbers hidden. If you’re using different plugins, you might need slightly different values.

Here’s what the result looks like:

Copy-to-Clipboard feature

Before we wrap up, let’s add one finishing touch: a button allowing our dear reader to copy the code from our snippet. It’s a nice little enhancement that spares people from having to manually select and copy the code snippets.

It’s actually somewhat straightforward. There’s a navigator.clipboard.writeText API for this. We pass that method the text we’d like to copy, and that’s that. We can inject a button next to every one of our <code> elements to send the code’s text to that API call to copy it. We’re already messing with those <code> elements in order to highlight lines, so let’s integrate our copy-to-clipboard button in the same place.

First, from the useEffect code above, let’s add one line:

useEffect(() => { const allPres = rootRef.current.querySelectorAll("pre"); const cleanup: (() => void)[] = []; for (const pre of allPres) { const code = pre.firstElementChild; if (!code || !/code/i.test(code.tagName)) { continue; } pre.appendChild(createCopyButton(code));

Note the last line. We’re going to append our button right into the DOM underneath our <pre> element, which is already position: relative, allowing us to position the button more easily.

Let’s see what the createCopyButton function looks like:

function createCopyButton(codeEl) { const button = document.createElement("button"); button.classList.add("prism-copy-button"); button.textContent = "Copy"; button.addEventListener("click", () => { if (button.textContent === "Copied") { return; } navigator.clipboard.writeText(codeEl.textContent || ""); button.textContent = "Copied"; button.disabled = true; setTimeout(() => { button.textContent = "Copy"; button.disabled = false; }, 3000); }); return button; }

Lots of code, but it’s mostly boilerplate. We create our button then give it a CSS class and some text. And then, of course, we create a click handler to do the copying. After the copy is done, we change the button’s text and disable it for a few seconds to help give the user feedback that it worked.

The real work is on this line:

navigator.clipboard.writeText(codeEl.textContent || "");

We’re passing codeEl.textContent rather than innerHTML since we want only the actual text that’s rendered, rather than all the markup Prism.js adds in order to format our code nicely.

Now let’s see how we might style this button. I’m no designer, but this is what I came up with:

.prism-copy-button { position: absolute; top: 5px; right: 5px; width: 10ch; background-color: rgb(100 100 100 / 0.5); border-width: 0; color: rgb(0, 0, 0); cursor: pointer; } .prism-copy-button[disabled] { cursor: default; }

Which looks like this:

And it works! It copies our code, and even preserves the formatting (i.e. new lines and indentation)!

Wrapping up

I hope this has been useful to you. Prism.js is a wonderful library, but it wasn’t originally written for static sites. This post walked you through some tips and tricks for bridging that gap, and getting it to work well with a Next.js site.

Syntax Highlighting (and More!) With Prism on a Static Site originally published on CSS-Tricks. You should get the newsletter.

Adding Custom GitHub Badges to Your Repo

Tue, 05/03/2022 - 4:28am

If you’ve spent time looking at open-source repos on GitHub, you’ve probably noticed that most of them use badges in their README files. Take the official React repository, for instance. There are GitHub badges all over the README file that communicate important dynamic info, like the latest released version and whether the current build is passing.

Badges like these provide a nice way to highlight key information about a repository. You can even use your own custom assets as badges, like Next.js does in its repo.

But the most useful thing about GitHub badges by far is that they update by themselves. Instead of hardcoding values into your README, badges in GitHub can automatically pick up changes from a remote server.

Let’s discuss how to add dynamic GitHub badges to the README file of your own project. We’ll start by using an online generator called badgen.net to create some basic badges. Then we’ll make our badges dynamic by hooking them up to our own serverless function via Napkin. Finally, we’ll take things one step further by using our own custom SVG files.

First off: How do badges work?

Before we start building some badges in GitHub, let’s quickly go over how they are implemented. It’s actually very simple: badges are just images. README files are written in Markdown, and Markdown supports images like so:

!\[alt text\](path or URL to image)

The fact that we can include a URL to an image means that a Markdown page will request the image data from a server when the page is rendered. So, if we control the server that has the image, we can change what image is sent back using whatever logic we want!

Thankfully, we have a couple options to deploy our own server logic without the whole “setting up the server” part. For basic use cases, we can create our GitHub badge images with badgen.net using its predefined templates. And again, Napkin will let us quickly code a serverless function in our browser and then deploy it as an endpoint that our GitHub badges can talk to.

Making badges with Badgen

Let’s start off with the simplest badge solution: a static badge via badgen.net. The Badgen API uses URL patterns to create templated badges on the fly. The URL pattern is as follows:

https://badgen.net/badge/:subject/:status/:color?icon=github

There’s a full list of the options you have for colors, icons, and more on badgen.net. For this example, let’s use these values:

  • :subject : Hello
  • :status: : World
  • :color: : red
  • :icon: : twitter

Our final URL winds up looking like this:

https://badgen.net/badge/hello/world/red?icon=twitter View the image Adding a GitHub badge to the README file

Now we need to embed this badge in the README file of our GitHub repo. We can do that in Markdown using the syntax we looked at earlier:

!\[my badge\](https://badgen.net/badge/hello/world/red?icon=twitter)

Badgen provides a ton of different options, so I encourage you to check out their site and play around! For instance, one of the templates lets you show the number of times a given GitHub repo has been starred. Here’s a star GitHub badge for the Next.js repo as an example:

https://badgen.net/github/stars/vercel/next.js View the image

Pretty cool! But what if you want your badge to show some information that Badgen doesn’t natively support? Luckily, Badgen has a URL template for using your own HTTPS endpoints to get data:

https://badgen.net/https/url/to/your/endpoint

For example, let’s say we want our badge to show the current price of Bitcoin in USD. All we need is a custom endpoint that returns this data as JSON like this:

{ "color": "blue", "status": "$39,333.7", "subject": "Bitcoin Price USD" }

Assuming our endpoint is available at https://some-endpoint.example.com/bitcoin, we can pass its data to Badgen using the following URL scheme:

https://badgen.net/https/some-endpoint.example.com/bitcoin The data for the cost of Bitcoin is served right to the GitHub badge.

Even cooler now! But we still have to actually create the endpoint that provides the data for the GitHub badge. &#x1f914; Which brings us to…

Badgen + Napkin

There’s plenty of ways to get your own HTTPS endpoint. You could spin up a server with DigitalOcean or AWS EC2, or you could use a serverless option like Google Cloud Functions or AWS Lambda; however, those can all still become a bit complex and tedious for our simple use case. That’s why I’m suggesting Napkin’s in-browser function editor to code and deploy an endpoint without any installs or configuration.

Head over to Napkin’s Bitcoin badge example to see an example endpoint. You can see the code to retrieve the current Bitcoin price and return it as JSON in the editor. You can run the code yourself from the editor or directly use the endpoint.

To use the endpoint with Badgen, work with the same URL scheme from above, only this time with the Napkin endpoint:

https://badgen.net/https/napkin-examples.npkn.net/bitcoin-badge View the image More ways to customize GitHub badges

Next, let’s fork this function so we can add in our own custom code to it. Click the “Fork” button in the top-right to do so. You’ll be prompted to make an account with Napkin if you’re not already signed in.

Once we’ve successfully forked the function, we can add whatever code we want, using any npm modules we want. Let’s add the Moment.js npm package and update the endpoint response to show the time that the price of Bitcoin was last updated directly in our GitHub badge:

import fetch from 'node-fetch' import moment from 'moment' const bitcoinPrice = async () => { const res = await fetch("<https://blockchain.info/ticker>") const json = await res.json() const lastPrice = json.USD.last+"" const [ints, decimals] = lastPrice.split(".") return ints.slice(0, -3) + "," + ints.slice(-3) + "." + decimals } export default async (req, res) => { const btc = await bitcoinPrice() res.json({ icon: 'bitcoin', subject: `Bitcoin Price USD (${moment().format('h:mma')})`, color: 'blue', status: `\\$${btc}` }) } Deploy the function, update your URL, and now we get this.

You might notice that the badge takes some time to refresh the next time you load up the README file over at GitHub. That’s is because GitHub uses a proxy mechanism to serve badge images.

GitHub serves the badge images this way to prevent abuse, like high request volume or JavaScript code injection. We can’t control GitHub’s proxy, but fortunately, it doesn’t cache too aggressively (or else that would kind of defeat the purpose of badges). In my experience, the TTL is around 5-10 minutes.

OK, final boss time.

Custom SVG badges with Napkin

For our final trick, let’s use Napkin to send back a completely new SVG, so we can use custom images like we saw on the Next.js repo.

A common use case for GitHub badges is showing the current status for a website. Let’s do that. Here are the two states our badge will support:

Badgen doesn’t support custom SVGs, so instead, we’ll have our badge talk directly to our Napkin endpoint. Let’s create a new Napkin function for this called site-status-badge.

The code in this function makes a request to example.com. If the request status is 200, it returns the green badge as an SVG file; otherwise, it returns the red badge. You can check out the function, but I’ll also include the code here for reference:

import fetch from 'node-fetch' const site_url = "<https://example.com>" // full SVGs at <https://napkin.io/examples/site-status-badge> const customUpBadge = '' const customDownBadge = '' const isSiteUp = async () => { const res = await fetch(site_url) return res.ok } export default async (req, res) => { const forceFail = req.path?.endsWith('/400') const healthy = await isSiteUp() res.set('content-type', 'image/svg+xml') if (healthy && !forceFail) { res.send(Buffer.from(customUpBadge).toString('base64')) } else { res.send(Buffer.from(customDownBadge).toString('base64')) } }

Odds are pretty low that the example.com site will ever go down, so I added the forceFail case to simulate that scenario. Now we can add a /400 after the Napkin endpoint URL to try it:

!\[status up\](https://napkin-examples.npkn.net/site-status-badge/) !\[status down\](https://napkin-examples.npkn.net/site-status-badge/400) View Status Up Badge View Status Down Badge

Very nice &#x1f60e;

And there we have it! Your GitHub badge training is complete. But the journey is far from over. There’s a million different things where badges like this are super helpful. Have fun experimenting and go make that README sparkle! ✨

Adding Custom GitHub Badges to Your Repo originally published on CSS-Tricks. You should get the newsletter.

Creating Realistic Reflections With CSS

Mon, 05/02/2022 - 4:14am

In design, reflections are stylized mirror images of objects. Even though they are not as popular as shadows, they have their moments — just think about the first time you explored the different font formats in MS Word or PowerPoint: I bet reflection was your second-most-used style, next to shadow, foregoing others like outline and glow. Or perhaps you remember when reflections were all the rage back when Apple used them on just about everything.

Reflections are still cool! And unlike years past, we can actually make reflections with CSS! Here’s what we’ll be making in this article:

CodePen Embed Fallback

There are two steps to a reflection design:

  1. Create a copy of the original design.
  2. Style that copy.

The most authentic and standardized way to get a mirror image in CSS now would be to use the element() property. But it’s still in its experimental phase and is only supported in Firefox, at the time of writing this article. If you’re curious, you can check out this article I wrote that experiments with it.

So, rather than element(), I’m going to add two of the same designs and use one as the reflection in my examples. You can code this part to be dynamic using JavaScript, or use pseudo-elements, but in my demos, I’m using a pair of identical elements per design.

<div class="units"> <div>trinket</div> <div>trinket</div> </div> .units > * { background-image: url('image.jpeg'); background-clip: text; color: transparent; /* etc. */ }

The original design is a knock-out text graphic created from the combination of a background image, transparent text color, and the background-clip property with its text value.

The bottom element of the pair is then turned upside-down and moved closer to the original design using transform. This is the reflection:

.units > :last-child { transform: rotatex(180deg) translatey(15px); }

The now upturned bottom element will take on some styles to create fades and other graphic effects on the reflection. A gradual fading of reflection can be achieved with a linear gradient image used as a mask layer on the upturned element.

.units > :last-child { transform: rotatex(180deg) translatey(15px); mask-image: linear-gradient(transparent 50%, white 90%); } CodePen Embed Fallback

By default, the mask-mode of the mask-image property is alpha. That means the transparent parts of an image, when the image is used as a mask layer for an element, turn their corresponding areas of the element transparent as well. That’s why a linear-gradient with transparent gradation at the top fades out the upside-down reflection at the end.

We can also try other gradient styles, with or without combining them. Take this one with stripes, for example. I added the pattern along with the fade effect from before.

.units > :last-child { /* ... */ mask-image: repeating-linear-gradient(transparent, transparent 3px, white 3px, white 4px), linear-gradient( transparent 50%, white 90%); } CodePen Embed Fallback

Or this one with radial-gradient:

.units > :last-child { /* ... */ mask-image: radial-gradient(circle at center, white, transparent 50%); } CodePen Embed Fallback

Another idea is to morph the mirror image by adding skew() to the transform property. This gives some movement to the reflection.

.units > :last-child { /* ... */ transform: rotatex(180deg) translatey(15px) skew(135deg) translatex(30px); } CodePen Embed Fallback

When you need the reflection to be subtle and more like a shadow, then blurring it out, brightening it, or reducing its opacity, can do the trick.

.units > :last-child { /* ... */ filter: blur(4px) brightness(1.5); } CodePen Embed Fallback

Sometimes a reflection can also be shadowy itself, so, instead of using the background image (from the original design) or a block color for the text, I tried giving the reflection a series of translucent shadows of red, blue and green colors that go well with the original design.

.units > :last-child { /* ... */ text-shadow: 0 0 8px rgb(255 0 0 / .4), -2px -2px 6px rgb(0 255 0 / .4), 2px 2px 4px rgb(0 255 255 / .4); }

Do those rgb()values look weird? That’s a new syntax that’s part of some exciting new CSS color features.

CodePen Embed Fallback

Let’s bring all of these approaches together in one big demo:

CodePen Embed Fallback Wrapping up

The key to a good reflection is to go with effects that are subtler than the main object, but not so subtle that it’s difficult to notice. Then there are other considerations, including the reflection’s color, direction, and shape.

I hope you got some inspirations from this! Sure, all we looked at here was text, but reflections can work well for any striking element in a design that has a sensible enough space around it and can benefit from a reflection to elevate itself on the page.

Creating Realistic Reflections With CSS originally published on CSS-Tricks. You should get the newsletter.

Creating the DigitalOcean Logo in 3D With CSS

Fri, 04/29/2022 - 4:37am

Howdy y’all! Unless you’ve been living under a rock (and maybe even then), you’ve undoubtedly heard the news that CSS-Tricks, was acquired by DigitalOcean. Congratulations to everyone! &#x1f973;

As a little hurrah to commemorate the occasion, I wanted to create the DigitalOcean logo in CSS. I did that, but then took it a little further with some 3D and parallax. This also makes for quite a good article because the way I made the logo uses various pieces from previous articles I’ve written. This cool little demo brings many of those concepts together.

So, let’s dive right in!

Creating the DigitalOcean logo

We are going to “trace” the DigitalOcean logo by grabbing an SVG version of it from simpleicons.org.

<svg role="img" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <title>DigitalOcean</title> <path d="M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z"></path> </svg>

Being mindful that we’re taking this 3D, we can wrap our SVG in a .scene element. Then we can use the tracing technique from my “Advice for Advanced CSS Illustrations” article. We are using Pug so we can leverage its mixins and reduce the amount of markup we need to write for the 3D part.

- const SIZE = 40 .scene svg(role='img' viewbox='0 0 24 24' xmlns='http://www.w3.org/2000/svg') title DigitalOcean path(d='M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z') .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner .logo__arc.logo__arc--outer .logo__square.logo__square--one .logo__square.logo__square--two .logo__square.logo__square--three

The idea is to style these elements so that they overlap our logo. We don’t need to create the “arc” portion of the logo as we’re thinking ahead because we are going to make this logo in 3D and can create the arc with two cylinder shapes. That means for now all we need is the containing elements for each cylinder, the inner arc, and the outer arc.

Check out this demo that lays out the different pieces of the DigitalOcean logo. If you toggle the “Explode” and hover elements, you can what the logo consists of.

CodePen Embed Fallback

If we wanted a flat DigitalOcean logo, we could use a CSS mask with a conic gradient. Then we would only need one “arc” element that uses a solid border.

.logo__arc--outer { border: calc(var(--size) * 0.1925vmin) solid #006aff; mask: conic-gradient(transparent 0deg 90deg, #000 90deg); transform: translate(-50%, -50%) rotate(180deg); }

That would give us the logo. The “reveal” transitions a clip-path that shows the traced SVG image underneath.

CodePen Embed Fallback

Check out my “Advice for Complex CSS Illustrations” article for tips on working with advanced illustrations in CSS.

Extruding for the 3D

We have the blueprint for our DigitalOcean logo, so it’s time to make this 3D. Why didn’t we create 3D blocks from the start? Creating containing elements, makes it easier to create 3D via extrusion.

We covered creating 3D scenes in CSS in my “Learning to Think in Cubes Instead of Boxes” article. We are going to use some of those techniques for what we’re making here. Let’s start with the squares in the logo. Each square is a cuboid. And using Pug, we are going to create and use a cuboid mixin to help generate all of them.

mixin cuboid() .cuboid(class!=attributes.class) if block block - let s = 0 while s < 6 .cuboid__side - s++

Then we can use this in our markup:

.scene .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner .logo__arc.logo__arc--outer .logo__square.logo__square--one +cuboid().square-cuboid.square-cuboid--one .logo__square.logo__square--two +cuboid().square-cuboid.square-cuboid--two .logo__square.logo__square--three +cuboid().square-cuboid.square-cuboid--three

Next, we need the styles to display our cuboids. Note that cuboids have six sides, so we’re styling those with the nth-of-type() pseudo selector while leveraging the vmin length unit to keep things responsive.

.cuboid { width: 100%; height: 100%; position: relative; } .cuboid__side { filter: brightness(var(--b, 1)); position: absolute; } .cuboid__side:nth-of-type(1) { --b: 1.1; height: calc(var(--depth, 20) * 1vmin); width: 100%; top: 0; transform: translate(0, -50%) rotateX(90deg); } .cuboid__side:nth-of-type(2) { --b: 0.9; height: 100%; width: calc(var(--depth, 20) * 1vmin); top: 50%; right: 0; transform: translate(50%, -50%) rotateY(90deg); } .cuboid__side:nth-of-type(3) { --b: 0.5; width: 100%; height: calc(var(--depth, 20) * 1vmin); bottom: 0; transform: translate(0%, 50%) rotateX(90deg); } .cuboid__side:nth-of-type(4) { --b: 1; height: 100%; width: calc(var(--depth, 20) * 1vmin); left: 0; top: 50%; transform: translate(-50%, -50%) rotateY(90deg); } .cuboid__side:nth-of-type(5) { --b: 0.8; height: 100%; width: 100%; transform: translate3d(0, 0, calc(var(--depth, 20) * 0.5vmin)); top: 0; left: 0; } .cuboid__side:nth-of-type(6) { --b: 1.2; height: 100%; width: 100%; transform: translate3d(0, 0, calc(var(--depth, 20) * -0.5vmin)) rotateY(180deg); top: 0; left: 0; }

We are approaching this in a different way from how we have done it in past articles. Instead of applying height, width, and depth to a cuboid, we are only concerned with its depth. And instead of trying to color each side, we can make use of filter: brightness to handle that for us.

If you need to have cuboids or other 3D elements as a child of a side using filter, you may need to shuffle things. A filtered side will flatten any 3D children.

The DigitalOcean logo has three cuboids, so we have a class for each one and are styling them like this:

.square-cuboid .cuboid__side { background: hsl(var(--hue), 100%, 50%); } .square-cuboid--one { /* 0.1925? It's a percentage of the --size for that square */ --depth: calc((var(--size) * 0.1925) * var(--depth-multiplier)); } .square-cuboid--two { --depth: calc((var(--size) * 0.1475) * var(--depth-multiplier)); } .square-cuboid--three { --depth: calc((var(--size) * 0.125) * var(--depth-multiplier)); }

…which gives us something like this:

CodePen Embed Fallback

You can play with the depth slider to extrude the cuboids as you wish! For our demo, we’ve chosen to make the cuboids true cubes with equal height, width, and depth. The depth of the arc will match the largest cuboid.

Now for the cylinders. The idea is to create two ends that use border-radius: 50%. Then, we can use many elements as the sides of the cylinder to create the effect. The trick is positioning all the sides.

CodePen Embed Fallback

There are various approaches we can take to create the cylinders in CSS. But, for me, if this is something I can foresee using many times, I’ll try and future-proof it. That means making a mixin and some styles I can reuse for other demos. And those styles should try and cater to scenarios I could see popping up. For a cylinder, there is some configuration we may want to consider:

  • radius
  • sides
  • how many of those sides are displayed
  • whether to show one or both ends of the cylinder

Putting that together, we can create a Pug mixin that caters to those needs:

mixin cylinder(radius = 10, sides = 10, cut = [5, 10], top = true, bottom = true) - const innerAngle = (((sides - 2) * 180) / sides) * 0.5 - const cosAngle = Math.cos(innerAngle * (Math.PI / 180)) - const side = 2 * radius * Math.cos(innerAngle * (Math.PI / 180)) //- Use the cut to determine how many sides get rendered and from what point .cylinder(style=`--side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class) if top .cylinder__end.cylinder__segment.cylinder__end--top if bottom .cylinder__end.cylinder__segment.cylinder__end--bottom - const [start, end] = cut - let i = start while i < end .cylinder__side.cylinder__segment(style=`--index: ${i};`) - i++

See how //- is prepended to the comment in the code? That tells Pug to ignore the comment and leave it out from the compiled HTML markup.

Why do we need to pass the radius into the cylinder? Well, unfortunately, we can’t quite handle trigonometry with CSS calc() just yet (but it is coming). And we need to work out things like the width of the cylinder sides and how far out from the center they should project. The great thing is that we have a nice way to pass that information to our styles via inline custom properties.

.cylinder( style=` --side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class )

An example use for our mixin would be as follows:

+cylinder(20, 30, [10, 30])

This would create a cylinder with a radius of 20, 30 sides, where only sides 10 to 30 are rendered.

Then we need some styling. Styling the cylinders for the DigitalOcean logo is pretty straightforward, thankfully:

.cylinder { --bg: hsl(var(--hue), 100%, 50%); background: rgba(255,43,0,0.5); height: 100%; width: 100%; position: relative; } .cylinder__segment { filter: brightness(var(--b, 1)); background: var(--bg, #e61919); position: absolute; top: 50%; left: 50%; } .cylinder__end { --b: 1.2; --end-coefficient: 0.5; height: 100%; width: 100%; border-radius: 50%; transform: translate3d(-50%, -50%, calc((var(--depth, 0) * var(--end-coefficient)) * 1vmin)); } .cylinder__end--bottom { --b: 0.8; --end-coefficient: -0.5; } .cylinder__side { --b: 0.9; height: calc(var(--depth, 30) * 1vmin); width: calc(var(--side) * 1vmin); transform: translate(-50%, -50%) rotateX(90deg) rotateY(calc((var(--index, 0) * 360 / var(--sides)) * 1deg)) translate3d(50%, 0, calc(var(--radius) * 1vmin)); }

The idea is that we create all the sides of the cylinder and put them in the middle of the cylinder. Then we rotate them on the Y-axis and project them out by roughly the distance of the radius.

CodePen Embed Fallback

There’s no need to show the ends of the cylinder in the inner part since they’re already obscured. But we do need to show them for the outer portion. Our two-cylinder mixin use look like this:

.logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner .logo__arc.logo__arc--outer +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer

We know the radius from the diameter we used when tracing the logo earlier. Plus, we can use the outer cylinder ends to create the faces of the DigitalOcean logo. A combination of border-width and clip-path comes in handy here.

.cylinder-arc--outer .cylinder__end--top, .cylinder-arc--outer .cylinder__end--bottom { /* Based on the percentage of the size needed to cap the arc */ border-width: calc(var(--size) * 0.1975vmin); border-style: solid; border-color: hsl(var(--hue), 100%, 50%); --clip: polygon(50% 0, 50% 50%, 0 50%, 0 100%, 100% 100%, 100% 0); clip-path: var(--clip); }

We’re pretty close to where we want to be!

CodePen Embed Fallback

There is one thing missing though: capping the arc. We need to create some ends for the arc, which requires two elements that we can position and rotate on the X or Y-axis:

.scene .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner .logo__arc.logo__arc--outer +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer .logo__square.logo__square--one +cuboid().square-cuboid.square-cuboid--one .logo__square.logo__square--two +cuboid().square-cuboid.square-cuboid--two .logo__square.logo__square--three +cuboid().square-cuboid.square-cuboid--three .logo__cap.logo__cap--top .logo__cap.logo__cap--bottom

The arc’s capped ends will assume the height and width based on the end’s border-width value as well as the depth of the arc.

.logo__cap { --hue: 10; position: absolute; height: calc(var(--size) * 0.1925vmin); width: calc(var(--size) * 0.1975vmin); background: hsl(var(--hue), 100%, 50%); } .logo__cap--top { top: 50%; left: 0; transform: translate(0, -50%) rotateX(90deg); } .logo__cap--bottom { bottom: 0; right: 50%; transform: translate(50%, 0) rotateY(90deg); height: calc(var(--size) * 0.1975vmin); width: calc(var(--size) * 0.1925vmin); }

We’ve capped the arc!

CodePen Embed Fallback

Throwing everything together, we have our DigitalOcean logo. This demo allows you to rotate it in different directions.

CodePen Embed Fallback

But there’s still one more trick up our sleeve!

Adding a parallax effect to the logo

We’ve got our 3D DigitalOcean logo but it would be neat if it was interactive in some way. Back in November 2021, we covered how to create a parallax effect with CSS custom properties. Let’s use that same technique here, the idea being that the logo rotates and moves by following a user’s mouse cursor.

We do need a dash of JavaScript so that we can update the custom properties we need for a coefficient that sets the logo’s movement along the X and Y-axes in the CSS. Those coefficients are calculated from a user’s pointer position. I’ll often use GreenSock so I can use gsap.utils.mapRange. But, here is a vanilla JavaScript version of it that implements mapRange:

const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => { const INPUT_RANGE = inputUpper - inputLower const OUTPUT_RANGE = outputUpper - outputLower return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0) } const BOUNDS = 100 const update = ({ x, y }) => { const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x) const POS_Y = mapRange(0, window.innerHeight, -BOUNDS, BOUNDS)(y) document.body.style.setProperty('--coefficient-x', POS_X) document.body.style.setProperty('--coefficient-y', POS_Y) } document.addEventListener('pointermove', update)

The magic happens in CSS-land. This is one of the major benefits of using custom properties this way. JavaScript is telling CSS what’s happening with the interaction. But, it doesn’t care what CSS does with it. That’s a rad decoupling. I use this JavaScript snippet in so many of my demos for this very reason. We can create different experiences simply by updating the CSS.

How do we do that? Use calc() and custom properties that are scoped directly to the .scene element. Consider these updated styles for .scene:

.scene { --rotation-y: 75deg; --rotation-x: -14deg; transform: translate3d(0, 0, 100vmin) rotateX(-16deg) rotateY(28deg) rotateX(calc(var(--coefficient-y, 0) * var(--rotation-x, 0deg))) rotateY(calc(var(--coefficient-x, 0) * var(--rotation-y, 0deg))); }

The makes the scene rotate on the X and Y-axes based on the user’s pointer movement. But we can adjust this behavior by tweaking the values for --rotation-x and --rotation-y.

Each cuboid will move its own way. They are able to move on either the X, Y, or Z-axis. But, we only need to define one transform. Then we can use scoped custom properties to do the rest.

.logo__square { transform: translate3d( calc(min(0, var(--coefficient-x, 0) * var(--offset-x, 0)) * 1%), calc((var(--coefficient-y) * var(--offset-y, 0)) * 1%), calc((var(--coefficient-x) * var(--offset-z, 0)) * 1vmin) ); } .logo__square--one { --offset-x: 50; --offset-y: 10; --offset-z: -2; } .logo__square--two { --offset-x: -35; --offset-y: -20; --offset-z: 4; } .logo__square--three { --offset-x: 25; --offset-y: 30; --offset-z: -6; }

That will give you something like this:

CodePen Embed Fallback

And we can tweak these to our heart’s content until we get something we’re happy with!

CodePen Embed Fallback Adding an intro animation to the mix

OK, I fibbed a bit and have one final (I promise!) way we can enhance our work. What if we had some sort of intro animation? How about a wave or something that washes across and reveals the logo?

We could do this with the pseudo-elements of the body element:

:root { --hue: 215; --initial-delay: 1; --wave-speed: 2; } body:after, body:before { content: ''; position: absolute; height: 100vh; width: 100vw; background: hsl(var(--hue), 100%, calc(var(--lightness, 50) * 1%)); transform: translate(100%, 0); animation-name: wave; animation-duration: calc(var(--wave-speed) * 1s); animation-delay: calc(var(--initial-delay) * 1s); animation-timing-function: ease-in; } body:before { --lightness: 85; animation-timing-function: ease-out; } @keyframes wave { from { transform: translate(-100%, 0); } }

Now, the idea is that the DigitalOcean logo is hidden until the wave washes over the top of it. For this effect, we’re going to animate our 3D elements from an opacity of 0. And we’re going to animate all the sides to our 3D elements from a brightness of 1 to reveal the logo. Because the wave color matches that of the logo, we won’t see it fade in. Also, using animation-fill-mode: both means that our elements will extend the styling of our keyframes in both directions.

This requires some form of animation timeline. And this is where custom properties come into play. We can use the duration of our animations to calculate the delays of others. We looked at this in my “How to Make a Pure CSS 3D Package Toggle” and “Animated Matryoshka Dolls in CSS” articles.

:root { --hue: 215; --initial-delay: 1; --wave-speed: 2; --fade-speed: 0.5; --filter-speed: 1; } .cylinder__segment, .cuboid__side, .logo__cap { animation-name: fade-in, filter-in; animation-duration: calc(var(--fade-speed) * 1s), calc(var(--filter-speed) * 1s); animation-delay: calc((var(--initial-delay) + var(--wave-speed)) * 0.75s), calc((var(--initial-delay) + var(--wave-speed)) * 1.15s); animation-fill-mode: both; } @keyframes filter-in { from { filter: brightness(1); } } @keyframes fade-in { from { opacity: 0; } }

How do we get the timing right? A little tinkering and making use of the “Animations Inspector” in Chrome’s DevTool goes a long ways. Try adjusting the timings in this demo:

CodePen Embed Fallback

You may find that the fade timing is unnecessary if you want the logo to be there once the wave has passed. In that case, try setting the fade to 0. And in particular, experiment with the filter and fade coefficients. They relate to the 0.75s and 1.15s from the code above. It’s worth adjusting things and having a play in Chrome’s Animation Inspector to see how things time in.

That’s it!

Putting it all together, we have this neat intro for our 3D DigitalOcean logo!

CodePen Embed Fallback

And, of course, this only one approach to create the DigitalOcean logo in 3D with CSS. If you see other possibilities or perhaps something that can be optimized further, drop a link to your demo in the comments!

Congratulations, again, to the CSS-Tricks team and DigitalOcean for their new partnership. I’m excited to see where things go with the acquisition. One thing is for sure: CSS-Tricks will continue to inspire and produce fantastic content for the community. &#x1f60e;

Creating the DigitalOcean Logo in 3D With CSS originally published on CSS-Tricks. You should get the newsletter.

Setting Up CloudFront to Host Your Web App

Thu, 04/28/2022 - 4:42am

In my last article, we went over how to set up a web app that serves chunks and bundles of CSS and JavaScript from CloudFront. We integrated it into Vite so that when the app runs in a browser, the assets requested from the app’s root HTML file would pull from CloudFront as the CDN.

While CloudFront’s edge caching does offer benefits, serving your app’s resources from these multiple locations is not without a cost of its own. Let’s take a look at a WebPageTest trace of my own web app, running with the configuration from the last blog post.

Notice the large connection times for lines 2-4. Line 1 is our HTML entry point. That HTML is parsed, the browser sees script and link tags for the JavaScript and CSS assets that reside on the CDN, and requests them. This causes a new connection to be set up which, as you can see, takes time.

This post will show you how to get around this. We’ll walk through how to host the entire web app on CloudFront and have CloudFront forward — or “proxy” — non-cacheable requests for data, auth, etc., onto our underlying web server.

Note that this is substantially more work than what we saw in the last article, and the instructions are likely to be different for you based on the exact needs of your web app, so your mileage may vary. We’ll be changing DNS records and, depending on your web app, you may have to add some cache headers in order to prevent certain assets from ever being cached. We’ll get into all of this!

You may be wondering whether the setup we covered in the last article even offers any benefits because of what we’re doing here in this article. Given the long connection time, would we have been better off forgoing the CDN, and instead serve all our assets from the web server to avoid that longer wait? I measured this with my own web app, and the CDN version, above, was indeed faster, but not by a lot. The initial LCP page load was about 200-300ms faster. And remember, that’s just for the initial load. Once this connection has been set up, edge caching should add much more value for all your subsequent, asynchronously loaded chunks.

Setting up our DNS

Our end goal is to serve our entire web app from CloudFront. That means when we hit our domain, we want the results to come from CloudFront instead of whatever web server it’s currently linked to. That means we’ll have to modify our DNS settings. We’ll use AWS Route 53 for this.

I’m using mydemo.technology as an example, which is a domain I own. I’ll show you all the steps here. But by the time you read this, I’ll have removed this domain from my web app. So, later when I start showing you actual CNAME records, and similar, those will no longer exist.

Go to the Route 53 homepage, and click on hosted zones:

Click Create hosted zone and enter the app’s domain:

Now, take note of the name servers listed in the next screen. They should look something like this.

We haven’t really accomplished anything yet. We told AWS we want it to manage this domain for us, and AWS gave us the name servers it’ll route our traffic through. To put this into effect, we need to go to wherever our domain is registered. There should be a place for you to enter in your own custom name servers.

Note that my domain is registered with GoDaddy and that is reflected in the screenshots throughout this article. The UI, settings, and options may differ from what you see in your registrar.

Warning: I recommend writing down the original name servers as well as any and all DNS records before making changes. That way, should something fail, you have everything you need to roll back to how things were before you started. And even if everything works fine, you’ll still want to re-add any other records into Route 53, ie MX records, etc.

Setting up a CloudFront distribution

Let’s make a CloudFront distribution to host our web app. We covered the basics in the last post, so we’ll get right to it. One big change from last time is what we enter for the origin domain. Do not put the top-level domain, e.g. your-app.net. What you need is the underlying domain where your app is hosted. If that’s Heroku, then enter the URL Heroku provides you.

Next, be sure to change the default protocol if you plan to use this site over a secure HTTPS connection:

This part is crucial. If your web app is running authentication, hosting data, or anything else, be sure to enable other verbs besides GET. If you skip this part, then any POST requests for authentication, mutating data, etc., will be rejected and fail. If your web app is doing nothing but serving assets and all those things are handled by external services, then outstanding! You have a great setup, and you can skip this step.

We have to make quite a few changes to the cache key and origin requests settings compared to last time:

We need to create a cache policy with a minimum TTL of 0, so non-caching headers we send back will are properly respected. You may also want to enable all query strings. I was seeing weird behavior when multiple GraphQL requests went out together with different query strings, which were ignored, causing all these requests to appear identical from CloudFront’s perspective.

My policy wound up looking like this:

For an origin request policy, if needed, we should make sure to send query strings and cookies for things like authentication and data queries to work. To be clear, this determines whether cookies and query strings will be sent from CloudFront down to your web server (e.g. Heroku, or similar).

Mine looks like this:

Lastly, for response headers policy, we can select “CORS With Preflight” from the list. In the end, your first two will have different names depending on how you set them up. But mine looks like this:

Let’s connect our domain, whatever it is, to this CloudFront distribution. Unfortunately, this is more work than you might expect. We need to prove to AWS that we actually own the domain because, for all Amazon knows, we don’t. We created a hosted zone in Route 53. And we took the nameservers it gave us and registered them with GoDaddy (or whoever your domain is registered with). But Amazon doesn’t know this yet. We need to demonstrate to Amazon that we do, in fact, control the DNS for this domain.

First, we’ll request an SSL certificate.

Next, let’s request the certificate link:

Now, we’ll select the option to request a public certificate option:

We need to provide the domain:

And, in my case, the certificate is pending:

So, I’m going to click it:

This proves that we own and control this domain. In a separate tab, go back to Route 53, and open our hosted zone:

Now we need to create the CNAME record. Copy the first part for the Record name. For example, if the CNAME is _xhyqtrajdkrr.mydemo.technology, then put the _xhyqtrajdkrr part. For the Record value, copy the entire value.

Assuming you registered the AWS name servers with your domain host, GoDaddy or whomever, AWS will soon be able to ping the DNS entry it just asked you to create, see the response it expects, and validate your certificate.

It can take time for the name servers you set at the beginning to propagate. In theory, it can take up to 72 hours, but it usually updates within an hour for me.

You’ll see success on the domain:

…as well as the certificate:

Whew! Almost done. Now let’s connect all of this to our CloudFront distribution. We can head back to the CloudFront settings screen. Now, under custom SSL certificate, we should see what we created (and any others you’ve created in the past):

Then, let’s add the app’s top-level domain:

All that’s left is to tell Route 53 to route our domain to this CloudFront distribution. So, let’s go back to Route 53 and create another DNS record.

We need to enter an A record for IPv4, and an AAAA record for IPv6. For both, leave the record name empty since we’re only registering our top-level domain and nothing else.

Select the A record type. Next, specify the record as an alias, then map the alias to the CloudFront distribution. That should open up an option to choose your CloudFront distribution, and since we previously registered the domain with CloudFront, you should see that distribution, and only that distribution when making a selection.

We repeat the exact same steps for the AAAA record type we need for IPv6 support.

Run your web app, and make sure it actually, you know, works. It should!

Things to test and verify

OK, while we’re technically done here, chances are there are still a few things left to do to meet the exact needs of your web app. Different apps have different needs and what I’ve demonstrated so far has walked us through the common steps to route things through CloudFront for better performance. Chances are there are things unique to your app that require more love. So, for that, let me cover a few possible additional items you might encounter during setup.

First off, make sure any POSTs you have are correctly sent to your origin. Assuming CloudFront is correctly configured to forward cookies to your origin, this should already work but there’s no harm in checking.

The bigger concern are all other GET requests that are sent to your web app. By default, any GET requests CloudFront receives, if cached, are served to your web app with the cached response. This can be disastrous. Any data requests to any REST or GraphQL endpoints sent with GET are cached by the CDN. And if you’re shipping a service worker, that will be cached too, instead of the normal behavior, where the current version is sent down in the background and updated if there are changes.

In order to tell CloudFront not to cache certain things, be sure to set the "Cache-Control" header to "no-cache" . If you’re using a framework, like Express, you can set middleware for your data access with something like this:

app.use("/graphql", (req, res, next) => { res.set("Cache-Control", "no-cache"); next(); }); app.use( "/graphql", expressGraphql({ schema: executableSchema, graphiql: true, rootValue: root }) );

For things like service workers, you can put specific rules for those files before your static middleware:

app.get("/service-worker.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") })); app.get("/sw-index-bundle.js", express.static(__dirname + "/react/dist", { setHeaders: resp => resp.set("Cache-Control", "no-cache") })); app.use(express.static(__dirname + "/react/dist", { maxAge: 432000 * 1000 * 10 }));

And so on. Test everything thoroughly because there’s so much that can go wrong. And after each change you make, be sure to run a full invalidation in CloudFront and clear the cache before re-running your web app to test that things are correctly excluded from cache. You can do this from the Invalidations tab in CloudFront. Open that up and put /* in for the value, to clear everything.

A working CloudFront implementation

Now that we have everything running, let’s re-run our trace in WebPageTest:

And just like that, we no longer have setup connections like we saw before for our assets. For my own web app, I was seeing a substantial improvement of 500ms in LCP. That’s a solid win!

Hosting an entire web app on a CDN can offer the best of all worlds. We get edge caching for static resources, but without the connection costs. Unfortunately, this improvement doesn’t come for free. Getting all of the necessary proxying correctly set up isn’t entirely intuitive, and then there’s still the need to set up cache headers in order to avoid non-cacheable requests from winding up in the CDN’s cache.

Setting Up CloudFront to Host Your Web App originally published on CSS-Tricks. You should get the newsletter.

Cool Hover Effects That Use Background Properties

Wed, 04/27/2022 - 4:20am

A while ago, Geoff wrote an article about a cool hover effect. The effect relies on a combination of CSS pseudo-elements, transforms, and transitions. A lot of comments have shown that the same effect can be done using background properties. Geoff mentioned that was his initial thought and that’s what I was thinking as well. I am not saying the pseudo-element he landed on is bad, but knowing different methods to achieve the same effect can only be a good thing.

In this post, we will re-work that hover effect, but also expand it into other types of hover effects that only use CSS background properties.

CodePen Embed Fallback

You can see the background properties at work in that demo, as well as how we can use custom properties and the calc() function to do even more. We are going to learn how to combine all of these so we are left with nicely optimized code!

Hover effect #1 CodePen Embed Fallback

Let’s start with the first effect which is the reproduction of the one detailed by Geoff in his article. The code used to achieve that effect is the following:

.hover-1 { background: linear-gradient(#1095c1 0 0) var(--p, 0) / var(--p, 0) no-repeat; transition: .4s, background-position 0s; } .hover-1:hover { --p: 100%; color: #fff; }

If we omit the color transition (which is optional), we only need three CSS declarations to achieve the effect. You are probably surprised how small the code is, but you will see how we got there.

First, let’s start with a simple background-size transition:

CodePen Embed Fallback

We are animating the size of a linear gradient from 0 100% to 100% 100%. That means the width is going from 0 to 100% while the background itself remains at full height. Nothing complex so far.

Let’s start our optimizations. We first transform our gradient to use the color only once:

background-image: linear-gradient(#1095c1 0 0);

The syntax might look a bit strange, but we are telling the browser that one color is applied to two color stops, and that’s enough to define a gradient in CSS. Both color stops are 0, so the browser automatically makes the last one 100% and fills our gradient with the same color. Shortcuts, FTW!

With background-size, we can omit the height because gradients are full height by default. We can do a transition from background-size: 0 to background-size: 100%.

.hover-1 { background-image: linear-gradient(#1095c1 0 0); background-size: 0; background-repeat: no-repeat; transition: .4s; } .hover-1:hover { background-size: 100%; }

Let’s introduce a custom property to avoid the repetition of background-size:

.hover-1 { background-image: linear-gradient(#1095c1 0 0); background-size: var(--p, 0%); background-repeat: no-repeat; transition: .4s; } .hover-1:hover { --p: 100%; }

We are not defining --p initially, so the fallback value (0% in our case) will be used. On hover, we define a value that replaces the fallback one ( 100%).

Now, let’s combine all the background properties using the shorthand version to get:

.hover-1 { background: linear-gradient(#1095c1 0 0) left / var(--p, 0%) no-repeat; transition: .4s; } .hover-1:hover { --p: 100%; }

We are getting closer! Note that I have introduced a left value (for the background-position) which is mandatory when defining the size in the background shorthand. Plus, we need it anyway to achieve our hover effect.

We need to also update the position on hover. We can do that in two steps:

  1. Increase the size from the right on mouse hover.
  2. Decrease the size from the left on mouse out.

To do this, we need to update the background-position on hover as well:

CodePen Embed Fallback

We added two things to our code:

  • A background-position value of right on hover
  • A transition-duration of 0s on the background-position

This means that, on hover, we instantly change the background-position from left (see, we needed that value!) to right so the background’s size will increase from the right side. Then, when the mouse cursor leaves the link, the transition plays in reverse, from right to left, making it appear that we are decreasing the background’s size from the left side. Our hover effect is done!

But you said we only needed three declarations and there are four.

That’s true, nice catch. The left and right values can be changed to 0 0 and 100% 0, respectively; and since our gradient is already full height by default, we can get by using 0 and 100%.

.hover-1 { background: linear-gradient(#1095c1 0 0) 0 / var(--p, 0%) no-repeat; transition: .4s, background-position 0s; } .hover-1:hover { --p: 100%; background-position: 100%; }

See how background-position and --p are using the same values? Now we can reduce the code down to three declarations:

.hover-1 { background: linear-gradient(#1095c1 0 0) var(--p, 0%) / var(--p,0%) no-repeat; transition: .4s, background-position 0s; } .hover-1:hover { --p: 100%; }

The custom property --p is defining both the background position and size. On hover, It will update both of them as well. This is a perfect use case showing how custom properties can help us reduce redundant code and avoid writing properties more than once. We define our setting using custom properties and we only update the latter on hover.

But the effect Geoff described is doing the opposite, starting from left and ending at right. How do we do that when it seems we cannot rely on the same variable?

We can still use one variable and update our code slightly to achieve the opposite effect. What we want is to go from 100% to 0% instead of 0% to 100%. We have a difference of 100% that we can express using calc(), like this:

.hover-1 { background: linear-gradient(#1095c1 0 0) calc(100% - var(--p,0%)) / var(--p,0%) no-repeat; transition: .4s, background-position 0s; } .hover-1:hover { --p: 100%; }

--p will change from 0% to 100%, but the background’s position will change from 100% to 0%, thanks to calc().

We still have three declarations and one custom property, but a different effect.

CodePen Embed Fallback

Before we move to the next hover effect, I want to highlight something important that you have probably noticed. When dealing with custom properties, I am using 0% (with a unit) instead of a unit-less 0. The unit-less zero may work when the custom property is alone, but will fail inside calc() where we need to explicitly define the unit. I may need another article to explain this quirk but always remember to add the unit when dealing with custom properties. I have two answers on StackOverflow (here and here) that go into more detail.

Hover effect #2 CodePen Embed Fallback

We need a more complex transition for this effect. Let’s take a look at a step-by-step illustration to understand what is happening.

Initially, a fixed-height, full-width gradient is outside of view. Then we move the gradient to the right to cover the bottom side. Finally, we increase the size of the gradient from the fixed height to 100% to cover the whole element.

We first have a background-position transition followed by a background-size one. Let’s translate this into code:

.hover-2 { background-image: linear-gradient(#1095c1 0 0); background-size: 100% .08em; /* .08em is our fixed height; modify as needed. */ background-position: /* ??? */; background-repeat: no-repeat; transition: background-size .3s, background-position .3s .3s; } .hover-2:hover { transition: background-size .3s .3s, background-position .3s; background-size: 100% 100%; background-position: /* ??? */; }

Note the use of two transition values. On hover, we need to first change the position and later the size, which is why we are adding a delay to the size. On mouse out, we do the opposite.

The question now is: what values do we use for background-position? We left those blank above. The background-size values are trivial, but the ones for background-position are not. And if we keep the actual configuration we’re unable to move our gradient.

Our gradient has a width equal to 100%, so we cannot use percentage values on background-position to move it.

Percentage values used with background-position are always a pain especially when you use them for the first time. Their behavior is non-intuitive but well defined and easy to understand if we get the logic behind it. I think it would take another article for a full explanation why it works this way, but here’s another “long” explanation I posted over at Stack Overflow. I recommend taking a few minutes to read that answer and you will thank me later!

The trick is to change the width to something different than 100%. Let’s use 200%. We’re not worried about the background exceeding the element because the overflow is hidden anyway.

.hover-2 { background-image: linear-gradient(#1095c1 0 0); background-size: 200% .08em; background-position: 200% 100%; background-repeat: no-repeat; transition: background-size .3s, background-position .3s .3s; } .hover-2:hover { transition: background-size .3s .3s, background-position .3s; background-size: 200% 100%; background-position: 100% 100%; }

And here’s what we get:

CodePen Embed Fallback

It’s time to optimize our code. If we take the ideas we learned from the first hover effect, we can use shorthand properties and write fewer declarations to make this work:

.hover-2 { background: linear-gradient(#1095c1 0 0) no-repeat var(--p, 200%) 100% / 200% var(--p, .08em); transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s)); } .hover-2:hover { --p: 100%; --t: .3s; }

We add all the background properties together using the shorthand version then we use --p to express our values. The sizes change from .08em to 100% and the position from 200% to 100%

I am also using another variable --t , to optimize the transition property. On mouse hover we have it set to a .3s value, which gives us this:

transition: .3s .3s, background-position .3s 0s;

On mouse out, --t is undefined, so the fallback value will be used:

transition: .3s 0s, background-position .3s .3s;

Shouldn’t we have background-size in the transition?

That is indeed another optimization we can make. If we don’t specify any property it means “all” the properties, so the transition is defined for “all” the properties (including background-size and background-position). Then it’s defined again for background-position which is similar to defining it for background-size, then background-position.

“Similar” is different than saying something is the “same.” You will see a difference if you change more properties on hover, so the last optimization might be unsuitable in some cases.

Can we still optimize the code and use only one custom property?

Yes, we can! Ana Tudor shared a great article explaining how to create DRY switching where one custom property can update multiple properties. I won’t go into the details here, but our code can be revised like this:

.hover-2 { background: linear-gradient(#1095c1 0 0) no-repeat calc(200% - var(--i, 0) * 100%) 100% / 200% calc(100% * var(--i, 0) + .08em); transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - calc(var(--i, 0) * .3s)); } .hover-2:hover { --i: 1; }

The --i custom property is initially undefined, so the fallback value, 0, is used. On hover though, we replace 0 with 1. You can do the math for both cases and get the values for each one. You can see that variable as a “switch” that update all our values at once on hover.

Again, we’re back to only three declarations for a pretty cool hover effect!

CodePen Embed Fallback Hover effect #3

We are going to use two gradients instead of one for this effect. We will see that combining multiple gradients is another way to create fancy hover effects.

Here’s a diagram of what we’re doing:

We initially have two gradients that overflow the element so that they are out of view. Each one has a fixed height and toes up half of the element’s width. Then we slide them into view to make them visible. The first gradient is placed at the bottom-left and the second one at the top-right. Finally, we increase the height to cover the whole element.

Here’s how that looks in CSS:

.hover-3 { background-image: linear-gradient(#1095c1 0 0), linear-gradient(#1095c1 0 0); background-repeat: no-repeat; background-size: 50% .08em; background-position: -100% 100%, 200% 0; transition: background-size .3s, background-position .3s .3s; } .hover-3:hover { background-size: 50% 100%; background-position: 0 100%, 100% 0; transition: background-size .3s .3s, background-position .3s; }

The code is almost the same as the other hover effects we’ve covered. The only difference is that we have two gradients with two different positions. The position values may look strange but, again, that’s related to how percentages work with the background-position property in CSS, so I highly recommend reading my Stack Overflow answer if you want to get into the gritty details.

Now let’s optimize! You get the idea by now — we’re using shorthand properties, custom properties, and calc() to tidy things up.

.hover-3 { --c: no-repeat linear-gradient(#1095c1 0 0); background: var(--c) calc(-100% + var(--p, 0%)) 100% / 50% var(--p, .08em), var(--c) calc( 200% - var(--p, 0%)) 0 / 50% var(--p, .08em); transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s)); } .hover-3:hover { --p: 100%; --t: 0.3s; }

I have added an extra custom property, --c, that defines the gradient since the same gradient is used in both places.

CodePen Embed Fallback

I am using 50.1% in that demo instead of 50% for the background size because it prevents a gap from showing between the gradients. I also added 1% to the positions for similar reasons.

Let’s do the second optimization by using the switch variable:

.hover-3 { --c: no-repeat linear-gradient(#1095c1 0 0); background: var(--c) calc(-100% + var(--i, 0) * 100%) 100% / 50% calc(100% * var(--i, 0) + .08em), var(--c) calc( 200% - var(--i, 0) * 100%) 0 / 50% calc(100% * var(--i, 0) + .08em); transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - var(--i, 0) * .3s); } .hover-3:hover { --i: 1; } CodePen Embed Fallback

Are you started to see the patterns here? It’s not so much that the effects we’re making are difficult. It’s more the “final step” of code optimization. We start by writing verbose code with a lot of properties, then reduce it following simple rules (e.g. using shorthand, removing default values, avoiding redundant values, etc) to simplify things down as much as possible.

Hover effect #4 CodePen Embed Fallback

I will raise the difficulty level for this last effect, but you know enough from the other examples that I doubt you’ll have any issues with this one.

This hover effect relies on two conic gradients and more calculations.

Initially, we have both gradients with zero dimensions in Step 1. We increase the size of each one in Step 2. We keep increasing their widths until they fully cover the element, as shown in Step 3. After that, we slide them to the bottom to update their position. This is the “magic” part of the hover effect. Since both gradients will use the same coloration, changing their position in Step 4 will make no visual difference — but we will see a difference once we reduce the size on mouse out during Step 5.

If you compare Step 2 and Step 5, you can see that we have a different inclination. Let’s translate that into code:

.hover-4 { background-image: conic-gradient(/* ??? */), conic-gradient(/* ??? */); background-position: 0 0, 100% 0; background-size: 0% 200%; background-repeat: no-repeat; transition: background-size .4s, background-position 0s; } .hover-4:hover { background-size: /* ??? */ 200%; background-position: 0 100%, 100% 100%; }

The positions are pretty clear. One gradient starts at top left (0 0) and ends at bottom left (0 100%) while the other starts at top right (100% 0) and ends at bottom right (100% 100%).

We’re using a transition on the background positions and sizes to reveal them. We only need a transition value for the background-size. And like before, background-position needs to change instantly, so we’re assigning a 0s value for the transition’s duration.

For the sizes, both gradient need to have 0 width and twice the element height (0% 200%). We will see later how their sizes change on hover. Let’s first define the gradient configuration.

The diagram below illustrates the configuration of each gradient:

Note that for the second gradient (indicated in green), we need to know the height to use it inside the conic-gradient we’re creating. For this reason, I am going to add a line-height that sets the element’s height and then try that same value for the conic gradient values we left out.

.hover-4 { --c: #1095c1; line-height: 1.2em; background-image: conic-gradient(from -135deg at 100% 50%, var(--c) 90deg, #0000 0), conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0); background-position: 0 0, 100% 0; background-size: 0% 200%; background-repeat: no-repeat; transition: background-size .4s, background-position 0s; } .hover-4:hover { background-size: /* ??? */ 200%; background-position: 0 100%, 100% 100%; }

The last thing we have left is to figure out the background’s size. Intuitively, we may think that each gradient needs to take up half of the element’s width but that’s actually not enough.

We’re left with a large gap if we use 50% as the background-size value for both gradients.

We get a gap equal to the height, so we actually need to do is increase the size of each gradient by half the height on hover for them to cover the whole element.

.hover-4:hover { background-size: calc(50% + .6em) 200%; background-position: 0 100%, 100% 100%; }

Here’s what we get after optimizing them like the previous examples:

.hover-4 { --c: #1095c1; line-height: 1.2em; background: conic-gradient(from -135deg at 100% 50%, var(--c) 90deg, #0000 0) 0 var(--p, 0%) / var(--s, 0%) 200% no-repeat, conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0) 100% var(--p, 0%) / var(--s, 0%) 200% no-repeat; transition: .4s, background-position 0s; } .hover-4:hover { --p: 100%; --s: calc(50% + .6em); } CodePen Embed Fallback

What about the version with only one custom property?

I will leave that for you! After looking at four similar hover effects, you should be able to get the final optimization down to a single custom property. Share your work in the comment section! There’s no prize, but we may end up with different implementations and ideas that benefit everyone!

Before we end, let me share a version of that last hover effect that Ana Tudor cooked up. It’s an improvement! But note that it lacks Firefox supports due to a known bug. Still, it’s a great idea that shows how to combine gradients with blend modes to create even cooler hover effects.

CodePen Embed Fallback Wrapping up

We made four super cool hover effects! And even though they are different effects, they all take the same approach of using CSS background properties, custom properties, and calc(). Different combinations allowed us to make different versions, all using the same techniques that leave us with clean, maintainable code.

If you want to get some ideas, I made a collection of 500 (yes, 500!) hover effects, 400 of which are done without pseudo-elements. The four we covered in this article are just the tip of the iceberg!

Cool Hover Effects That Use Background Properties originally published on CSS-Tricks. You should get the newsletter.

Avoiding the Pitfalls of Nested Components in a Design System

Tue, 04/26/2022 - 4:30am

When creating a component-based, front-end infrastructure, one of the biggest pain points I’ve personally encountered is making components that are both reusable and responsive when there are nested components within components.

Take the following “call to action” (<CTA />) component, for example:

On smaller devices we want it to look like this:

This is simple enough with basic media queries. If we’re using flexbox, a media query can change the flex direction and makes the button go the full width. But we run into a problem when we start nesting other components in there. For example, say we’re using a component for the button and it already has a prop that makes it full-width. We are actually duplicating the button’s styling when applying a media query to the parent component. The nested button is already capable of handling it!

This is a small example and it wouldn’t be that bad of a problem, but for other scenarios it could cause a lot of duplicated code to replicate the styling. What if in the future we wanted to change something about how full-width buttons are styled? We’d need to go through and change it in all these different places. We should be able to change it in the button component and have that update everywhere.

Wouldn’t it be nice if we could move away from media queries and have more control of the styling? We should be using a component’s existing props and be able to pass different values based on the screen width.

Well, I have a way to do that and will show you how I did it.

I am aware that container queries can solve a lot of these issues, but it’s still in early days and doesn’t solve the issue with passing a variety of props based on screen width.

Tracking the window width

First, we need to track the current width of the page and set a breakpoint. This can be done with any front-end framework, but I’m using a Vue composable here as to demonstrate the idea:

// composables/useBreakpoints.js import { readonly, ref } from "vue"; const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }) const currentBreakpoint = ref(bps.xl); export default () => { const updateBreakpoint = () => { const windowWidth = window.innerWidth; if(windowWidth >= 1200) { currentBreakpoint.value = bps.xl } else if(windowWidth >= 992) { currentBreakpoint.value = bps.lg } else if(windowWidth >= 768) { currentBreakpoint.value = bps.md } else if(windowWidth >= 576) { currentBreakpoint.value = bps.sm } else { currentBreakpoint.value = bps.xs } } return { currentBreakpoint: readonly(currentBreakpoint), bps: readonly(bps), updateBreakpoint, }; };

The reason we are using numbers for the currentBreakpoint object will become clear later.

Now we can listen for window resize events and update the current breakpoint using the composable in the main App.vue file:

// App.vue <script> import useBreakpoints from "@/composables/useBreakpoints"; import { onMounted, onUnmounted } from 'vue' export default { name: 'App', setup() { const { updateBreakpoint } = useBreakpoints() onMounted(() => { updateBreakpoint(); window.addEventListener('resize', updateBreakpoint) }) onUnmounted(() => { window.removeEventListener('resize', updateBreakpoint) }) } } </script>

We probably want this to be debounced, but I’m keeping things simple for brevity.

Styling components

We can update the <CTA /> component to accept a new prop for how it should be styled:

// CTA.vue props: { displayMode: { type: String, default: "default" } }

The naming here is totally arbitrary. You can use whatever names you’d like for each of the component modes.

We can then use this prop to change the mode based on the current breakpoint:

<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />

You can see now why we’re using a number to represent the current breakpoint — it’s so the correct mode can be applied to all breakpoints below or above a certain number.

We can then use this in the CTA component to style according to the mode passed through:

// components/CTA.vue <template> <div class="cta" :class="displayMode"> <div class="cta-content"> <h5>title</h5> <p>description</p> </div> <Btn :block="displayMode === 'compact'">Continue</Btn> </div> </template> <script> import Btn from "@/components/ui/Btn"; export default { name: "CTA", components: { Btn }, props: { displayMode: { type: String, default: "default" }, } } </script> <style scoped lang="scss"> .cta { display: flex; align-items: center; .cta-content { margin-right: 2rem; } &.compact { flex-direction: column; .cta-content { margin-right: 0; margin-bottom: 2rem; } } } </style>

Already, we have removed the need for media queries! You can see this in action on a demo page I created.

Admittedly, this may seem like a lengthy process for something so simple. But when applied to multiple components, this approach can massively improve the consistency and stability of the UI while reducing the total amount of code we need to write. This way of using JavaScript and CSS classes to control the responsive styling also has another benefit…

Extensible functionality for nested components

There have been scenarios where I’ve needed to revert back to a previous breakpoint for a component. For example, if it takes up 50% of the screen, I want it displayed in the small mode. But at a certain screen size, it becomes full-width. In other words, the mode should change one way or the other when there’s a resize event.

I’ve also been in situations where the same component is used in different modes on different pages. This isn’t something that frameworks like Bootstrap and Tailwind can do, and using media queries to pull it off would be a nightmare. (You can still use those frameworks using this technique, just without the need for the responsive classes they provide.)

We could use a media query that only applies to middle sized screens, but this doesn’t solve the issue with varying props based on screen width. Thankfully, the approach we’re covering can solve that. We can modify the previous code to allow for a custom mode per breakpoint by passing it through an array, with the first item in the array being the smallest screen size.

<CTA :custom-mode="['compact', 'default', 'compact']" />

First, let’s update the props that the <CTA /> component can accept:

props: { displayMode: { type: String, default: "default" }, customMode: { type: [Boolean, Array], default: false }, }

We can then add the following to generate to correct mode:

import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints"; // ... setup(props) { const { currentBreakpoint } = useBreakpoints() const mode = computed(() => { if(props.customMode) { return props.customMode[currentBreakpoint.value] ?? props.displayMode } return props.displayMode }) return { mode } },

This is taking the mode from the array based on the current breakpoint, and defaults to the displayMode if one isn’t found. Then we can use mode instead to style the component.

Extraction for reusability

Many of these methods can be extracted into additional composables and mixins that can be reuseD with other components.

Extracting computed mode

The logic for returning the correct mode can be extracted into a composable:

// composables/useResponsive.js import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints"; export const useResponsive = (props) => { const { currentBreakpoint } = useBreakpoints() const mode = computed(() => { if(props.customMode) { return props.customMode[currentBreakpoint.value] ?? props.displayMode } return props.displayMode }) return { mode } } Extracting props

In Vue 2, we could repeat props was by using mixins, but there are noticeable drawbacks. Vue 3 allows us to merge these with other props using the same composable. There’s a small caveat with this, as IDEs seem unable to recognize props for autocompletion using this method. If this is too annoying, you can use a mixin instead.

Optionally, we can also pass custom validation to make sure we’re using the modes only available to each component, where the first value passed through to the validator is the default.

// composables/useResponsive.js // ... export const withResponsiveProps = (validation, props) => { return { displayMode: { type: String, default: validation[0], validator: function (value) { return validation.indexOf(value) !== -1 } }, customMode: { type: [Boolean, Array], default: false, validator: function (value) { return value ? value.every(mode => validation.includes(mode)) : true } }, ...props } }

Now let’s move the logic out and import these instead:

// components/CTA.vue import Btn from "@/components/ui/Btn"; import { useResponsive, withResponsiveProps } from "@/composables/useResponsive"; export default { name: "CTA", components: { Btn }, props: withResponsiveProps(['default 'compact'], { extraPropExample: { type: String, }, }), setup(props) { const { mode } = useResponsive(props) return { mode } } } Conclusion

Creating a design system of reusable and responsive components is challenging and prone to inconsistencies. Plus, we saw how easy it is to wind up with a load of duplicated code. There’s a fine balance when it comes to creating components that not only work in many contexts, but play well with other components when they’re combined.

I’m sure you’ve come across this sort of situation in your own work. Using these methods can reduce the problem and hopefully make the UI more stable, reusable, maintainable, and easy to use.

Avoiding the Pitfalls of Nested Components in a Design System originally published on CSS-Tricks. You should get the newsletter.

Writing Strong Front-end Test Element Locators

Fri, 04/22/2022 - 8:45am

Automated front-end tests are awesome. We can write a test with code to visit a page — or load up just a single component — and have that test code click on things or type text like a user would, then make assertions about the state of the application after the interactions. This lets us confirm that everything described in the tests work as expected in the application.

Since this post is about one of the building blocks of any automated UI tests, I don’t assume too much prior knowledge. Feel free to skip the first couple of sections if you’re already familiar with the basics.

Structure of a front-end test

There’s a classic pattern that’s useful to know when writing tests: Arrange, Act, Assert. In front-end tests, this translates to a test file that does the following:

  1. Arrange: Get things ready for the test. Visit a certain page, or mount a certain component with the right props, mock some state, whatever.
  2. Act: Do something to the application. Click a button, fill out a form, etc. Or not, for simple state-checks, we can skip this.
  3. Assert: Check some stuff. Did submitting a form show a thank you message? Did it send the right data to the back end with a POST?

In specifying what to interact with and then later what to check on the page, we can use various element locators to target the parts of the DOM we need to use.

A locator can be something like an element’s ID, the text content of an element, or a CSS selector, like .blog-post or even article > div.container > div > div > p:nth-child(12). Anything about an element that can identify that element to your test runner can be a locator. As you can probably already tell from that last CSS selector, locators come in many varieties.

We often evaluate locators in terms of being brittle or stable. In general, we want the most stable element locators possible so that our test can always find the element it needs, even if the code around the element is changing over time. That said, maximizing stability at all costs can lead to defensive test-writing that actually weakens the tests. We get the most value by having a combination of brittleness and stability that aligns with what we want our tests to care about.

In this way, element locators are like duct tape. They should be really strong in one direction, and tear easily in the other direction. Our tests should hold together and keep passing when unimportant changes are made to the application, but they should readily fail when important changes happen that contradict what we’ve specified in the test.

Beginner’s guide to element locators in front-end testing

First, let’s pretend we are writing instructions for an actual person to do their job. A new gate inspector has just been hired at Gate Inspectors, Inc. You are their boss, and after everybody’s been introduced you are supposed to give them instructions for inspecting their first gate. If you want them to be successful, you probably would not write them a note like this:

Go past the yellow house, keep going ‘til you hit the field where Mike’s mother’s friend’s goat went missing that time, then turn left and tell me if the gate in front of the house across the street from you opens or not.

Those directions are kind of like using a long CSS selector or XPath as a locator. It’s brittle — and it’s the “bad kind of brittle”. If the yellow house gets repainted and you repeat the steps, you can’t find the gate anymore, and might decide to give up (or in this case, the test fails).

Likewise, if you don’t know about Mike’s mother’s friend’s goat situation, you can’t stop at the right reference point to know what gate to check. This is exactly what makes the “bad kind of brittle” bad — the test can break for all kinds of reasons, and none of those reasons have anything to do with the usability of the gate.

So let’s make a different front-end test, one that’s much more stable. After all, legally in this area, all gates on a given road are supposed to have unique serial numbers from the maker:

Go to the gate with serial number 1234 and check if it opens.

This is more like locating an element by its ID. It’s more stable and it’s only one step. All the points of failure from the last test have been removed. This test will only fail if the gate with that ID doesn’t open as expected.

Now, as it turns out, though no two gates should have the same ID on the same road, that’s not actually enforced anywhere And one day, another gate on the road ends up with the same ID.

So the next time the newly hired gate inspector goes to test “Gate 1234,” they find that other one first, and are now visiting the wrong house and checking the wrong thing. The test might fail, or worse: if that gate works as expected, the test still passes but it’s not testing the intended subject. It provides false confidence. It would keep passing even if our original target gate was stolen in the middle of the night, by gate thieves.

After an incident like this, it’s clear that IDs are not as stable as we thought. So, we do some next-level thinking and decide that, on the inside of the gate, we’d like a special ID just for testing. We’ll send out a tech to put the special ID on just this one gate. The new test instructions look like this:

Go to the gate with Test ID “my-favorite-gate” and check if it opens.

This one is like using the popular data-testid attribute. Attributes like this are great because it is obvious in the code that they are intended for use by automated tests and shouldn’t be changed or removed. As long as the gate has that attribute, you will always find the gate. Just like IDs, uniqueness is still not enforced, but it’s a bit more likely.

This is about as far away from brittle as you can get, and it confirms the functionality of the gate. We don’t depend on anything except the attribute we deliberately added for testing. But there’s a bit of problem hiding here…

This is a user interface test for the gate, but the locator is something that no user would ever use to find the gate.

It’s a missed opportunity because, in this imaginary county, it turns out gates are required to have the house number printed on them so that people can see the address. So, all gates should have an unique human-facing label, and if they don’t, it’s a problem in and of itself.

When locating the gate with the test ID, if it turns out that the gate has been repainted and the house number covered up, our test would still pass. But the whole point of the gate is for people to access the house. In other words, a working gate that a user can’t find should still be a test failure, and we want a locator that is capable of revealing this problem.

Here’s another pass at this test instruction for the gate inspector on their first day:

Go to the gate for house number 40 and check if it opens.

This one uses a locator that adds value to the test: it depends on something users also depend on, which is the label for the gate. It adds back a potential reason for the test to fail before it reaches the interaction we want to actually test, which might seem bad at first glance. But in this case, because the locator is meaningful from a user’s perspective, we shouldn’t shrug this off as “brittle.” If the gate can’t be found by its label, it doesn’t matter if it opens or not — this is is the “good kind of brittle”.

The DOM matters

A lot of front-end testing advice tells us to avoid writing locators that depend on DOM structure. This means that developers can refactor components and pages over time and let the tests confirm that user-facing workflows haven’t broken, without having to update tests to catch up to the new structure. This principle is useful, but I would tweak it a bit to say we ought to avoid writing locators that depend on irrelevant DOM structure in our front-end testing.

For an application to function correctly, the DOM should reflect the nature and structure of the content that’s on the screen at any given time. One reason for this is accessibility. A DOM that’s correct in this sense is much easier for assistive technology to parse properly and describe to users who aren’t seeing the contents rendered by the browser. DOM structure and plain old HTML make a huge difference to the independence of users who rely on assistive technology.

Let’s spin up a front-end test to submit something to the contact form of our app. We’ll use Cypress for this, but the principles of choosing locators strategically apply to all front-end testing frameworks that use the DOM for locating elements. Here we find elements, enter some test, submit the form, and verify the “thank you” state is reached:

// &#x1f44e; Not recommended cy.get('#name').type('Mark') cy.get('#comment').type('test comment') cy.get('.submit-btn').click() cy.get('.thank-you').should('be.visible')

There are all kinds of implicit assertions happening in these four lines. cy.get() is checking that the element exists in the DOM. The test will fail if the element doesn’t exist after a certain time, while actions like type and click verify that the elements are visible, enabled, and unobstructed by something else before taking an action.

So, we get a lot “for free” even in a simple test like this, but we’ve also introduced some dependencies upon things we (and our users) don’t really care about. The specific ID and classes that we are checking seem stable enough, especially compared to selectors like div.main > p:nth-child(3) > span.is-a-button or whatever. Those long selectors are so specific that a minor change to the DOM could cause a test to fail because it can’t find the element, not because the functionality is broken.

But even our short selectors, like #name, come with three problems:

  1. The ID could be changed or removed in the code, causing the element to go overlooked, especially if the form might appear more than once on a page. A unique ID might need to be generated for each instance, and that’s not something we can easily pre-fill into a test.
  2. If there is more than one instance of a form on the page and they have the same ID, we need to decide which form to fill out.
  3. We don’t actually care what the ID is from a user perspective, so all the built-in assertions are kind of… not fully leveraged?

For problems one and two, the recommended solution is often to use dedicated data attributes in our HTML that are added exclusively for testing. This is better because our tests no longer depend on the DOM structure, and as a developer modifies the code around a component, the tests will continue to pass without needing an update, as long as they keep the data-test="name-field" attached to the right input element.

This doesn’t address problem three though — we still have a front-end interaction test that depends on something that is meaningless to the user.

Meaningful locators for interactive elements

Element locators are meaningful when they depend on something we actually want to depend on because something about the locator is important to the user experience. In the case of interactive elements, I would argue that the best selector to use is the element’s accessible name. Something like this is ideal:

// &#x1f44d; Recommended cy.getByLabelText('Name').type('Mark')

This example uses the byLabelText helper from Cypress Testing Library. (In fact, if you are using Testing Library in any form, it is probably already helping you write accessible locators like this.)

This is useful because now the built-in checks (that we get for free through the cy.type() command) include the accessibility of the form field. All interactive elements should have an accessible name that is exposed to assistive technology. This is how, for example, a screenreader user would know what the form field they are typing into is called in order to enter the needed information.

The way to provide this accessible name for a form field is usually through a label element associated with the field by an ID. The getByLabelText command from Cypress Testing Library verifies that the field is labeled appropriately, but also that the field itself is an element that’s allowed to have a label. So, for example, the following HTML would correctly fail before the type() command is attempted, because even though a label is present, labeling a div is invalid HTML:

<!-- &#x1f44e; Not recommended --> <label for="my-custom-input">Editable DIV element:</label> <div id="my-custom-input" contenteditable="true" />

Because this is invalid HTML, screenreader software could never associate this label with this field correctly. To fix this, we would update the markup to use a real input element:

<!-- &#x1f44d; Recommended --> <label for="my-real-input">Real input:</label> <input id="my-real-input" type="text" />

This is awesome. Now if the test fails at this point after edits made to the DOM, it’s not because of an irrelevant structure changes to how elements are arranged, but because our edits did something to break a part of DOM that our front-end tests explicitly care about, that would actually matter to users.

Meaningful locators for non-interactive elements

For non-interactive elements, we should put on our thinking caps. Let’s use a little bit of judgement before falling back on the data-cy or data-test attributes that will always be there for us if the DOM doesn’t matter at all.

Before we dip into the generic locators, let’s remember: the state of the DOM is our Whole Thing™ as web developers (at least, I think it is). And the DOM drives the UX for everybody who is not experiencing the page visually. So a lot of the time, there might be some meaningful element that we could or should be using in the code that we can include in a test locator.

And if there’s not, because. say, the application code is all generic containers like div and span, we should consider fixing up the application code first, while adding the test. Otherwise there is a risk of having our tests actually specify that the generic containers are expected and desired, making it a little harder for somebody to modify that component to be more accessible.

This topic opens up a can of worms about how accessibility works in an organization. Often, if nobody is talking about it and it’s not a part of the practice at our companies, we don’t take accessibility seriously as front-end developers. But at the end of the day, we are supposed to be the experts in what is the “right markup” for design, and what to consider in deciding that. I discuss this side of things a lot more in my talk from Connect.Tech 2021, called “Researching and Writing Accessible Vue… Thingies”.

As we saw above, with the elements we traditionally think of as interactive, there is a pretty good rule of thumb that’s easy to build into our front-end tests: interactive elements should have perceivable labels correctly associated to the element. So anything interactive, when we test it, should be selected from the DOM using that required label.

For elements that we don’t think of as interactive — like most content-rendering elements that display pieces of text of whatever, aside from some basic landmarks like main — we wouldn’t trigger any Lighthouse audit failures if we put the bulk of our non-interactive content into generic div or span containers. But the markup won’t be very informative or helpful to assistive technology because it’s not describing the nature and structure of the content to somebody who can’t see it. (To see this taken to an extreme, check out Manuel Matuzovic’s excellent blog post, “Building the most inaccessible site possible with a perfect Lighthouse score.”)

The HTML we render is where we communicate important contextual information to anybody who is not perceiving the content visually. The HTML is used to build the DOM, the DOM is used to create the browser’s accessibility tree, and the accessibility tree is the API that assistive technologies of all kinds can use to express the content and the actions that can be taken to a disabled person using our software. A screenreader is often the first example we think of, but the accessibility tree can also be used by other technology, like displays that turn webpages into Braille, among others.

Automated accessibility checks won’t tell us if we’ve really created the right HTML for the content. The “rightness” of the HTML a judgement call we are making developers about what information we think needs to be communicated in the accessibility tree.

Once we’ve made that call, we can decide how much of that is suitable to bake into the automated front-end testing.

Let’s say that we have decided that a container with the status ARIA role will hold the “thank you” and error messaging for a contact form. This might be nice so that the feedback for the form’s success or failure can be announced by a screenreader. CSS classes of .thank-you and .error could be applied to control the visual state.

If we add this element and want to write a UI test for it, we might write an assertion like this after the test fills out the form and submits it:

// &#x1f44e; Not recommended cy.get('.thank-you').should('be.visible')

Or even a test that uses a non-brittle but still meaningless selector like this:

// &#x1f44e; Not recommended cy.get('[data-testid="thank-you-message"]').should('be.visible')

Both could be rewritten using cy.contains():

// &#x1f44d; Recommended cy.contains('[role="status"]', 'Thank you, we have received your message') .should('be.visible')

This would confirm that the expected text appeared and was inside the right kind of container. Compared to the previous test, this has much more value in terms of verifying actual functionality. If any part of this test fails, we’d want to know, because both the message and the element selector are important to us and shouldn’t be changed trivially.

We have definitely gained some coverage here without a lot of extra code, but we’ve also introduced a different kind of brittleness. We have plain English strings in our tests, and that means if the “thank you” message changes to something like “Thank you for reaching out!” this test suddenly fails. Same with all the other tests. A small change to how a label is written would require updating any test that targets elements using that label.

We can improve this by using the same source of truth for these strings in front-end tests as we do in our code. And if we currently have human-readable sentences embedded right there in the HTML of our components… well now we have a reason to pull that stuff out of there.

Human-readable strings might be the magic numbers of UI code

A magic number (or less-excitingly, an “unnamed numerical constant”) is that super-specific value you sometimes see in code that is important to the end result of a calculation, like a good old 1.023033 or something. But since that number is not unlabeled, its significance is unclear, and so it’s unclear what it’s doing. Maybe it applies a tax. Maybe it compensates for some bug that we don’t know about. Who knows?

Either way, the same is true for front-end testing and the general advice is to avoid magic numbers because of their lack of clarity. Code reviews will often catch them and ask what the number is for. Can we move it into a constant? We also do the same thing if a value is to be reused multiple places. Rather than repeat the value everywhere, we can store it in a variable and use the variable as needed.

Writing user interfaces over the years, I’ve come to see text content in HTML or template files as very similar to magic numbers in other contexts. We’re putting absolute values all through our code, but in reality it might be more useful to store those values elsewhere and bring them in at build time (or even through an API depending on the situation).

There are a few reasons for this:

  1. I used to work with clients who wanted to drive everything from a content management system. Content, even form labels and status messages, that didn’t live in the CMS were to be avoided. Clients wanted full control so that content changes didn’t require editing code and re-deploying the site. That makes sense; code and content are different concepts.
  2. I’ve worked in many multilingual codebases where all the text needs to be pulled in through some internationalization framework, like this:
<label for="name"> <!-- prints "Name" in English but something else in a different language --> {{content[currentLanguage].contactForm.name}} </label>
  1. As far as front-end testing goes, our UI tests are much more robust if, instead of checking for a specific “thank you” message we hardcode into the test, we do something like this:
const text = content.en.contactFrom // we would do this once and all tests in the file can read from it cy.contains(text.nameLabel, '[role="status"]').should('be.visible')

Every situation is different, but having some system of constants for strings is a huge asset when writing robust UI tests, and I would recommend it. Then, if and when translation or dynamic content does become necessary for our situation, we are a lot better prepared for it.

I’ve heard good arguments against importing text strings in tests, too. For example, some find tests are more readable and generally better if the test itself specifies the expected content independently from the content source.

It’s a fair point. I’m less persuaded by this because I think content should be controlled through more of an editorial review/publishing model, and I want the test to check if the expected content from the source got rendered, not some specific strings that were correct when the test was written. But plenty of people disagree with me on this, and I say as long as within a team the tradeoff is understood, either choice is acceptable.

That said, it’s still a good idea to isolate code from content in the front end more generally. And sometimes it might even be valid to mix and match — like importing strings in our component tests and not importing them in our end-to-end tests. This way, we save some duplication and gain confidence that our components display correct content, while still having front-end tests that independently assert the expected text, in the editorial, user-facing sense.

When to use data-test locators

CSS selectors like [data-test="success-message"] are still useful and can be very helpful when used in an intentional way, instead of used all the time. If our judgement is that there’s no meaningful, accessible way to target an element, data-test attributes are still the best option. They are much better than, say, depending on a coincidence like whatever the DOM structure happens to be on the day you are writing the test, and falling back to the “second list item in the third div with a class of card” style of test.

There are also times when content is expected to be dynamic and there’s no way to easily grab strings from some common source of truth to use in our tests. In those situations, a data-test attribute helps us reach the specific element we care about. It can still be combined with an accessibility-friendly assertion, for example:

cy.get('h2[data-test="intro-subheading"]')

Here we want to find what has the data-test attribute of intro-subheading, but still allow our test to assert that it should be a h2 element if that’s what we expect it to be. The data-test attribute is used to make sure we get the specific h2 we are interested in, not some other h2 that might be on the page, if for some reason the content of that h2 can’t be known at the time of the test.

Even in cases where we do know the content, we might still use data attributes to make sure the application renders that content in the right spot:

cy.contains('h2[data-test="intro-subheading"]', 'Welcome to Testing!')

data-test selectors can also be a convenience to get down to a certain part of the page and then make assertions within that. This could look like the following:

cy.get('article[data-test="ablum-card-blur-great-escape"]').within(() => { cy.contains('h2', 'The Great Escape').should('be.visible') cy.contains('p', '1995 Album by Blur').should('be.visible') cy.get('[data-test="stars"]').should('have.length', 5) })

At that point we get into some nuance because there may very well be other good ways to target this content, it’s just an example. But at the end of the day, it’s a good if worrying about details like that is where we are because at least we have some understanding of the accessibility features embedded in the HTML we are testing, and that we want to include those in our tests.

When the DOM matters, test it

Front-end tests bring a lot more value to us if we are thoughtful about how we tell the tests what elements to interact with, and what to contents to expect. We should prefer accessible names to target interactive components, and we should include specific elements names, ARIA roles, etc., for non-interactive content — if those things are relevant to the functionality. These locators, when practical, create the right combination of strength and brittleness.

And of course, for everything else, there’s data-test.

Writing Strong Front-end Test Element Locators originally published on CSS-Tricks. You should get the newsletter.

Adding Tailwind CSS to New and Existing WordPress Themes

Wed, 04/20/2022 - 5:30am

In the 15 or so years since I started making WordPress websites, nothing has had more of an impact on my productivity — and my ability to enjoy front-end development — than adding Tailwind CSS to my workflow (and it isn’t close).

When I began working with Tailwind, there was an up-to-date, first-party repository on GitHub describing how to use Tailwind with WordPress. That repository hasn’t been updated since 2019. But that lack of updates isn’t a statement on Tailwind’s utility to WordPress developers. By allowing Tailwind to do what Tailwind does best while letting WordPress still be WordPress, it’s possible to take advantage of the best parts of both platforms and build modern websites in less time.

The minimal setup example in this article aims to provide an update to that original setup repository, revised to work with the latest versions of both Tailwind and WordPress. This approach can be extended to work with all kinds of WordPress themes, from a forked default theme to something totally custom.

Why WordPress developers should care about Tailwind

Before we talk about setup, it’s worth stepping back and discussing how Tailwind works and what that means in a WordPress context.

Tailwind allows you to style HTML elements using pre-existing utility classes, removing the need for you to write most or all of your site’s CSS yourself. (Think classes like hidden for display: hidden or uppercase for text-transform: uppercase.) If you’ve used frameworks like Bootstrap and Foundation in the past, the biggest difference you’ll find with Tailwind CSS is its blank-slate approach to design combined with the lightness of being CSS-only, with just a CSS reset included by default. These properties allow for highly optimized sites without pushing developers towards an aesthetic built into the framework itself.

Also unlike many other CSS frameworks, it’s infeasible to load a “standard” build of Tailwind CSS from an existing CDN. With all of its utility classes included, the generated CSS file would simply be too large. Tailwind offers a “Play CDN,” but it’s not meant for production, as it significantly reduces Tailwind’s performance benefits. (It does come in handy, though, if you want to do some rapid prototyping or otherwise experiment with Tailwind without actually installing it or setting up a build process.)

This need to use Tailwind’s build process to create a subset of the framework’s utility classes specific to your project makes it important to understand how Tailwind decides which utility classes to include, and how this process affects the use of utility classes in WordPress’s editor.

And, finally, Tailwind’s aggressive Preflight (its version of a CSS reset) means some parts of WordPress are not well-suited to the framework with its default settings.

Let’s begin by looking at where Tailwind works well with WordPress.

Where Tailwind and WordPress work well together

In order for Tailwind to work well without significant customization, it needs to act as the primary CSS for a given page; this eliminates a number of use cases within WordPress.

If you’re building a WordPress plugin and you need to include front-end CSS, for example, Tailwind’s Preflight would be in direct conflict with the active theme. Similarly, if you need to style the WordPress administration area — outside of the editor — the administration area’s own styles may be overridden.

There are ways around both of these issues: You can disable Preflight and add a prefix to all of your utility classes, or you could use PostCSS to add a namespace to all of your selectors. Either way, your configuration and workflow are going to get more complicated.

But if you’re building a theme, Tailwind is an excellent fit right out of the box. I’ve had success creating custom themes using both the classic editor and the block editor, and I’m optimistic that as full-site editing matures, there will be a number of full-site editing features that work well alongside Tailwind.

In her blog post “Gutenberg Full Site Editing does not have to be full,” Tammie Lister describes full-site editing as a set of separate features that can be adopted in part or in full. It’s unlikely full-site editing’s Global Styles functionality will ever work with Tailwind, but many other features probably will.

So: You’re building a theme, Tailwind is installed and configured, and you’re adding utility classes with a smile on your face. But will those utility classes work in the WordPress editor?

With planning, yes! Utility classes will be available to use in the editor so long as you decide which ones you’d like to use in advance. You’re unable to open up the editor and use any and all Tailwind utility classes; baked into Tailwind’s emphasis on performance is the limitation of only including the utility classes your theme uses, so you need to let Tailwind know in advance which ones are required in the editor despite them being absent elsewhere in your code.

There are a number of ways to do this: You can create a safelist within your Tailwind configuration file; you can include comments containing lists of classes alongside the code for custom blocks you’ll want to style in the block editor; you could even just create a file listing all of your editor-specific classes and tell Tailwind to include it as one of the source files it monitors for class names.

The need to commit to editor classes in advance has never held me back in my work, but this remains the aspect of the relationship between Tailwind and WordPress I get asked about the most.

A minimal WordPress theme with a minimal Tailwind CSS integration

Let’s start with the most basic WordPress theme possible. There are only two required files:

  • style.css
  • index.php

We’ll generate style.css using Tailwind. For index.php, let’s start with something simple:

<!doctype html> <html lang="en"> <head> <?php wp_head(); ?> <link rel="stylesheet" href="<?php echo get_stylesheet_uri(); ?>" type="text/css" media="all" /> </head> <body> <?php if ( have_posts() ) { while ( have_posts() ) { the_post(); the_title( '<h1 class="entry-title">', '</h1>' ); ?> <div class="entry-content"> <?php the_content(); ?> </div> <?php } } ?> </body> </html>

There are a lot of things a WordPress theme should do that the above code doesn’t — things like pagination, post thumbnails, enqueuing stylesheets instead of using link elements, and so on — but this will be enough to display a post and test that Tailwind is working as it should.

On the Tailwind side, we need three files:

  • package.json
  • tailwind.config.js
  • An input file for Tailwind

Before we go any further, you’re going to need npm. If you’re uncomfortable working with it, we have a beginner’s guide to npm that is a good place to start!

Since there is no package.json file yet, we’ll create an empty JSON file in the same folder with index.php by running this command in our terminal of choice:

echo {} > ./package.json

With this file in place, we can install Tailwind:

npm install tailwindcss --save-dev

And generate our Tailwind configuration file:

npx tailwindcss init

In our tailwind.config.js file, all we need to do is tell Tailwind to search for utility classes in our PHP files:

module.exports = { content: ["./**/*.php"], theme: { extend: {}, }, plugins: [], }

If our theme used Composer, we’d want to ignore the vendor directory by adding something like "!**/vendor/**" to the content array. But if all of your PHP files are part of your theme, the above will work!

We can name our input file anything we want. Let’s create a file called tailwind.css and add this to it:

/*! Theme Name: WordPress + Tailwind */ @tailwind base; @tailwind components; @tailwind utilities;

The top comment is required by WordPress to recognize the theme; the three @tailwind directives add each of Tailwind’s layers.

And that’s it! We can now run the following command:

npx tailwindcss -i ./tailwind.css -o ./style.css --watch

This tells the Tailwind CLI to generate our style.css file using tailwind.css as the input file. The --watch flag will continuously rebuild the style.css file as utility classes are added or removed from any PHP file in our project repository.

That’s as simple as a Tailwind-powered WordPress theme could conceivably be, but it’s unlikely to be something you’d ever want to deploy to production. So, let’s talk about some pathways to a production-ready theme.

Adding TailwindCSS to an existing theme

There are two reasons why you might want to add Tailwind CSS to an existing theme that already has its own vanilla CSS:

  • To experiment with adding Tailwind components to an already styled theme
  • To transition a theme from vanilla CSS to Tailwind

We’ll demonstrate this by installing Tailwind inside Twenty Twenty-One, the WordPress default theme. (Why not Twenty Twenty-Two? The most recent WordPress default theme is meant to showcase full-site editing and isn’t a good fit for a Tailwind integration.)

To start, you should download and install the theme in your development environment if it isn’t installed there. We only need to follow a handful of steps after that:

  • Navigate to the theme folder in your terminal.
  • Because Twenty Twenty-One already has its own package.json file, install Tailwind without creating a new one:
npm install tailwindcss --save-dev
  • Add your tailwind.config.json file:
npx tailwindcss init
  • Update your tailwind.config.json file to look the same as the one in the previous section.
  • Copy Twenty Twenty-One’s existing style.css file to tailwind.css.

Now we need to add our three @tailwind directives to the tailwind.css file. I suggest structuring your tailwind.css file as follows:

/* The WordPress theme file header goes here. */ @tailwind base; /* All of the existing CSS goes here. */ @tailwind components; @tailwind utilities;

Putting the base layer immediately after the theme header ensures that WordPress continues to detect your theme while also ensuring the Tailwind CSS reset comes as early in the file as possible.

All of the existing CSS follows the base layer, ensuring that these styles take precedence over the reset.

And finally, the components and utilities layers follow so they can take precedence over any CSS declarations with the same specificity.

And now, as with our minimal theme, we’ll run the following command:

npx tailwindcss -i ./tailwind.css -o ./style.css --watch

With your new style.css file now being generated each time you change a PHP file, you should check your revised theme for minor rendering differences from the original. These are caused by Tailwind’s CSS reset, which resets things a bit further than some themes might expect. In the case of Twenty Twenty-One, the only fix I made was to add text-decoration-line: underline to the a element.

With that rendering issue resolved, let’s add the Header Banner Component from Tailwind UI, Tailwind’s first-party component library. Copy the code from the Tailwind UI site and paste it immediately following the “Skip to content” link in header.php:

Pretty good! Because we’re now going to want to use utility classes to override some of the existing higher-specificity classes built into the theme, we’re going to add a single line to the tailwind.config.js file:

module.exports = { important: true, content: ["./**/*.php"], theme: { extend: {}, }, plugins: [], }

This marks all Tailwind CSS utilities as !important so they can override existing classes with a higher specificity. (I’ve never set important to true in production, but I almost certainly would if I were in the process of converting a site from vanilla CSS to Tailwind.)

With a quick no-underline class added to the “Learn more” link and bg-transparent and border-0 added to the dismiss button, we’re all set:

It looks a bit jarring to see Tailwind UI’s components merged into a WordPress default theme, but it’s a great demonstration of Tailwind components and their inherent portability.

Starting from scratch

If you’re creating a new theme with Tailwind, your process will look a lot like the minimal example above. Instead of running the Tailwind CLI directly from the command line, you’ll probably want to create separate npm scripts for development and production builds, and to watch for changes. You may also want to create a separate build specifically for the WordPress editor.

If you’re looking for a starting point beyond the minimal example above — but not so far beyond that it comes with opinionated styles of its own — I’ve created a Tailwind-optimized WordPress theme generator inspired by Underscores (_s), once the canonical WordPress starter theme. Called _tw, this is the quick-start I wish I had when I first combined Tailwind with WordPress. It remains the first step in all of my client projects.

If you’re willing to go further from the structure of a typical WordPress theme and add Laravel Blade templates to your toolkit, Sage is a great choice, and they have a setup guide specific to Tailwind to get you started.

However you choose to begin, I encourage you to take some time to acclimate yourself to Tailwind CSS and to styling HTML documents using utility classes: It may feel unusual at first, but you’ll soon find yourself taking on more client work than before because you’re building sites faster than you used to — and hopefully, like me, having more fun doing it.

Adding Tailwind CSS to New and Existing WordPress Themes originally published on CSS-Tricks. You should get the newsletter.

Making Mermaid Diagrams in Markdown

Mon, 04/18/2022 - 6:01am

Mermaid diagrams and flowcharts have been gaining traction, especially with GitHub’s announcement that they are natively supported in Markdown. Let’s take a look at what they are, how to use them, and just as importantly: why.

Just like you might want to embed your CodePen demo directly in your documentation source, having your diagrams and charts live adjacent to your text helps prevent them from rotting — that is, drifting out of sync with the state of your document. Just as unhelpful, obsolete, or otherwise misleading comments in your code can be objectively worse than no comments, the same goes for diagrams.

Mermaid diagrams pair well with Jamstack and static site generators, which continue to grow in popularity. The pairing is natural. While Mermaid diagrams aren’t Markdown-exclusive, they are Markdown-inspired. Using the same markup abstractions Markdown provides to notate code, Mermaid can be represented the same to output diagrams and flowcharts. And Markdown is to Jamstack and static sites as peanut butter is to jelly.

If your site is authored in Markdown, processed into HTML, and you have enough control to add a bit of custom JavaScript, then you can use the ideas we’re covering in this article to fit your own needs and implement diagrams with Mermaid conveniently alongside the rest of your Markdown. Is “diagrams-as-code” a term yet? It should be.

For example, let’s say you’re working on a fancy new product and you want to provide a roadmap in the form of a Gantt chart (or some other type — say flowcharts, sequences, and class diagrams). With Mermaid, you can do this in a small handful of lines:

gantt title My Product Roadmap dateFormat YYYY-MM-DD section Cool Feature A task :a1, 2022-02-25, 30d Another task :after a1, 20d section Rad Feature Task in sequence :2022-03-04, 12d Task, No. 2 :24d

Which will render a nice SVG diagram like so:

Nine lines of code gets us a full-fledged Gantt chart that can be used for product roadmaps and such.

Pro tip: Mermaid has a live editor which lets you try it out without the commitment over at mermaid.live.

Mermaid diagrams in Markdown

Mermaid goes well with Markdown because it presents itself as just another fenced code block, only using the mermaid language syntax set. For example, this block of code:

```mermaid graph TD; A-->B; A-->C; B-->D; C-->D; ```

…produces an HTML <pre> element with the code block contents inside:

<pre class="mermaid"><code>graph TD; A-->B; A-->C; B-->D; C-->D;</code></pre>

If you’re using a Markdown processor aligned with the CommonMark spec, it’ll more resemble this:

<pre><code class="language-mermaid">graph TD; A-->B; A-->C; B-->D; C-->D; </code></pre> View demo

The Mermaid API’s default behavior expects a <div class="mermaid"> tag that directly contains the contents — so, no <code> or <span> (like from a syntax highlighter) that you might see in the conversion from Markdown-to-HTML.

Finessing with JavaScript

With a bit of JavaScript, it’s reasonable to take the Markdown-generated HTML and finesse it into the <div class="mermaid"> tag that Mermaid targets. It’s worth noting that $element.textContent is purposeful here: Markdown will HTML-encode specific characters (like > into &gt;) that Mermaid uses. It also filters out any erroneous HTML elements that are descendants of the <pre> element.

// select <pre class="mermaid"> _and_ <pre><code class="language-mermaid"> document.querySelectorAll("pre.mermaid, pre>code.language-mermaid").forEach($el => { // if the second selector got a hit, reference the parent <pre> if ($el.tagName === "CODE") $el = $el.parentElement // put the Mermaid contents in the expected <div class="mermaid"> // plus keep the original contents in a nice <details> $el.outerHTML = ` <div class="mermaid">${$el.textContent}</div> <details> <summary>Diagram source</summary> <pre>${$el.textContent}</pre> </details> ` })

Now that our HTML is properly-formatted, let’s implement Mermaid to do the rendering.

Using Mermaid

Mermaid is published as an npm package, so you can grab a copy by using a package-aware CDN, like unpkg. You’ll want to use the minified code (e.g., mermaid.min.js) instead of the default export of mermaid.core.js. For example:

<script src="https://unpkg.com/mermaid@8.14.0/dist/mermaid.min.js"></script>

Mermaid is also ESM-ready, so you can use Skypack to load it up as well:

<script type="module"> import mermaid from "https://cdn.skypack.dev/mermaid@8.14.0"; </script>

You could stop right here if you want to keep things simple. By default, Mermaid will auto-initialize itself when the document is ready. As long as you do the Markdown-to-HTML finessing with JavaScript mentioned earlier — before loading in Mermaid — you’ll be all set.

However, Mermaid has a couple settings worth configuring:

// initialize Mermaid to [1] log errors, [2] have loose security for first-party // authored diagrams, and [3] respect a preferred dark color scheme mermaid.initialize({ logLevel: "error", // [1] securityLevel: "loose", // [2] theme: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "default" // [3] })
  1. logLevel will give you a bit more visibility into any errors that may arise. If you want to see more information, you can choose a more verbose level (or vice versa).
  2. securityLevel relates to the level of trust for the diagram source. If it’s content that you are authoring, then "loose" is fine. If it’s user-generated content, it’s probably best leaving the "strict" default in place.
  3. theme changes the styling of the rendered diagrams. By querying the preferred color scheme and leveraging a ternary operator, we can specify "dark" as appropriate.
All together now!

Here are a couple of Mermaid diagrams examples in Markdown:

CodePen Embed Fallback Deeper waters

This strategy is particularly effective because it’s progressive: if JavaScript is disabled then the original Mermaid source is displayed as-is. No foul.

There’s also a fully-fledged command line interface for Mermaid which, if you’re interesting in exploring, could potentially be leveraged to display diagrams that are completely server-side rendered. Between the Mermaid CLI and the online generator, it may even be possible to hook into whatever build process you use to generate a snapshot of a diagram and display it as an <img> fallback instead of the source code.

Hopefully, we’ll see more native Mermaid integrations like this as Mermaid continues to grow in popularity. The usefulness of having visual charts and diagrams alongside documentation is unquestionable — from product roadmaps to decision trees and everything in between. That’s the sort of information that’s just plain difficult to document with words alone.

Mermaid charts solve that, and in a way that ensures the information can be managed and maintained alongside the rest of the documentation.

Making Mermaid Diagrams in Markdown originally published on CSS-Tricks. You should get the newsletter.

Add a CSS Lens Flare to Photos for a Bright Touch

Tue, 04/12/2022 - 7:10am

I’m a big fan of movies by J.J. Abrams. I enjoy their tight plots, quippy dialog, and of course: anamorphic lens flares. Filmmakers like Abrams use lens flare to add a dash of ‘homemade’ realism to their movies, a technique we can easily recreate in tools like Photoshop, then add to our sites as raster images.

But what if we wanted to apply the same lens flare look without the use of photo editing tools? We can create a CSS lens flare to add a cinematic touch to our gallery images, background photos, or user profiles.

There are different types of flares in photography. The one we’re working with is known as artifacts, as they leave behind little blotches of light that take the shape of a camera’s aperture where the light enters and reflects off the surface of the lens.

Source: Wikipedia

Here’s a good example of the sort of lens flare we’re going to make, pulled straight from a J.J. Abrams movie still, naturally:

Star Trek (2009)

There are a few parts to the lens flare above. Let’s list them out so we know what we’re aiming for:

  1. The center light source appears as a glowing ball of light.
  2. There are some horizontal elliptical light streaks — rays of light that are distorted and blurred, resulting in elongated ellipses.
  3. Random rays of light shoot off from the center light source at various angles.

We start with the HTML elements below that map to our flare components. There is a central light source and two off-diagonal circular flares, three horizontal lens flares, and three conical ray-like flares.

<div class="lens-center"></div> <div class="circle-1"></div> <div class="circle-2"></div> <div class="left-flare horizontal-flare"></div> <div class="right-flare horizontal-flare"></div> <div class="full-flare horizontal-flare"></div> <div class="conic-1"></div> <div class="conic-2"></div> <div class="conic-3"></div> </div>

Lastly, in order for our lens flare to be believably superimposed on an image, its center light source has to be adjustable. This way, we can place it over a believable existing light source on a picture and not overlap with any faces.

The background and light source of a CSS lens flare

Let’s start with a black background and central light source for our CSS lens flare. Most gradients on the web are linear gradients with solid-color transitions, but we can apply alpha channels to them which is actually a nice way to produce a glowing effect. A circular-shaped radial gradient with multiple layers of semi-transparent colors gives us a good camera center effect.

background: radial-gradient( closest-side circle at center, hsl(4 5% 100% / 100%) 0%, hsl(4 5% 100% / 100%) 15%, hsl(4 10% 70% / 70%) 30%, hsl(4 0% 50% / 30%) 55%, hsl(4 0% 10% / 5%) 75%, transparent 99 ); filter: blur(4px);

Curious about that HSL syntax? It’s new and appears to be the future direction of defining alpha transparency in all CSS color functions.

Notice we’re using a CSS blur filter in there to make the gradients look a bit closer to layers of diffused light.

Now that we know how to add circular flares, we will also add a larger, diffused flare behind the light source, as well as three additional flares at a 45deg angle from the center, to give the effect a more realistic look.

CodePen Embed Fallback Setting up horizontal light streaks

Let’s start with horizontal flares. There are a few options we can take, a very elongated ellipse gradient would be the simplest approach. However, I’ve noticed that horizontal lens flares are usually less symmetrical than the ones in my reference photos, so I wanted to make mine a little less symmetrical as well.

Luckily, radial gradients have an optional location argument in CSS. We can create two slightly differently-sized left and right portions of the same horizontal flare, and with slightly different colors. We can also add an opacity filter to make the area where the horizontal flares join the center to make the flare less jarring.

background: radial-gradient( closest-side circle at center, transparent 50%, hsl(4 10% 70% / 40%) 90%, transparent 100% ); filter: blur(5px);

While we are at it, let’s also add a single full elongated elliptical bottom flare three-quarters of the way down the viewport for another touch of “realism.”

CodePen Embed Fallback Creating the diffused rays of light

With both the radial and horizontal flares in place, all we have left are the angled rays of light shooting off from the light source. We could add additional elliptical radial gradients then skew and translate the container to get a close approximation. But we also have a CSS gradient that’s already made for the job, the conic gradient. Below is an example that gives us a 7deg conic gradient at a 5deg offset from its container’s bottom-right corner.

background: conic-gradient( from 5deg at 0% 100%, transparent 0deg, hsl(4 10% 70% / 30%) 7deg, transparent 15deg ); transform-origin: bottom left; transform: rotate(-45deg);

We’ll add a few conic gradients centered at our flare center, with various gradient angles of semi-transparent colors. Because conic gradients can show the corner of its container div, we will rotationally transform them using our light source as its origin, resulting in an offset diffused ray filter effect.

CodePen Embed Fallback Using CSS custom properties for a more flexible lens flare

So far, we’ve created a responsive, but statically-positioned, lens flare effect at a fixed location. It would be difficult to adjust the center of the lens flare without also breaking the horizontal and conic flares around it.

To make the CSS lens flare both adjustable and less brittle, we’ll expose the light source flare’s position, size, and hue via a set of custom properties.

:root { --hue: 4; --lens-center-size: 40%; --lens-spread-size: 80%; --lens-left: 55%; --lens-top: 15%; }

While we are at it, we are also going to adjust the flare hue and the size of the horizontal flare height. For horizontal flare width, we use CSS variable overloading to make them adjustable on their own; otherwise, we fall back to the light source flare center or the image center.

.left-flare { width: var(--left-flare-width, var(--lens-left, 50%)); }

This is what the completed CSS lens flare effect looks like with a photo background and the lens flare moved up so the light source location looks believable. Go ahead, add your own photo to see how it works in different contexts!

CodePen Embed Fallback Other CSS and non-CSS lens flare examples

This is just one way to create a CSS lens flare, of course. I like this approach because it’s flexible in terms of the color, size, and positioning of the flare and its parts. That makes it more of a reusable component that can be used in many contexts.

Here’s one by Keith Grant that uses a linear gradient as well as CSS custom properties. Then it sprinkles some JavaScript in there to randomize the HSLA values.

CodePen Embed Fallback

Nicholas Guest has another CSS lens flare that applies a box shadow on the ::before and ::after pseudo-elements of a .flare element to get the effect, plus a smidge of jQuery that makes the flare follow the mouse on hover.

CodePen Embed Fallback

This one is made with Canvas and is neat in how the light source follows the mouse on hover while showing how the lens flare artifacts change position as the light source position changes.

CodePen Embed Fallback

The same sort of idea here:

CodePen Embed Fallback

And a fun one that uses GSAP, Canvas, and a library called JS.LensFlare:

CodePen Embed Fallback

How would you approach a CSS lens flare effect? Share in the comments!

Add a CSS Lens Flare to Photos for a Bright Touch originally published on CSS-Tricks. You should get the newsletter.

Adding CDN Caching to a Vite Build

Mon, 04/04/2022 - 8:25am

Content delivery networks, or CDNs, allow you to improve the delivery of your website’s static resources, most notably, with CDN caching. They do this by serving your content from edge locations, which are located all over the world. When a user browses to your site, and your site requests resources from the CDN, the CDN will route that request to the nearest edge location. If that location has the requested resources, either from that user’s prior visit, or from another person, then the content will be served from cache. If not, the CDN will request the content from your underlying domain, cache it, and serve it.

There are countless CDNs out there, but for this post we’ll be using AWS CloudFront. We’ll look at setting up a CloudFront distribution to serve all our site’s assets: JavaScript files, CSS files, font files, etc. Then we’ll see about integrating it into a Vite build. If you’d like to learn more about Vite, I have an introduction here.

Setting up a CloudFront CDN distribution

Let’s jump right in and set up our CloudFront CDN distribution.

For any serious project, you should be setting up your serverless infrastructure with code, using something like the Serverless Framework, or AWS’s CDK. But to keep things simple, here, we’ll set up our CDN using the AWS console.

Head on over to the CloudFront homepage. At the top right, you should see an orange button to create a new distribution.

The creation screen has a ton of options, but for the most part the default selections will be fine. First and foremost, add the domain where your resources are located.

Next, scroll down and find the Response headers policy dropdown, and choose “CORS-With-Preflight.”

Lastly, click the Create Distribution button at the bottom, and hopefully you’ll see your new distribution.

Integrating the CDN with Vite

It’s one thing for our CDN to be set up and ready to serve our files. But it’s another for our site to actually know how to request them from our CDN. I’ll walk through integrating with Vite, but other build systems, like webpack or Rollup, will be similar.

When Vite builds our site, it maintains a “graph” of all the JavaScript and CSS files that various parts of our site import, and it injects the appropriate <script> tags, <link> tags, or import() statements to load what’s needed. What we need to do is tell Vite to request these assets from our CDN when in production. Let’s see how.

Open up your vite.config.ts file. First, we’ll need to know if we’re on the live site (production) or in development (dev).

const isProduction = process.env.NODE_ENV === "production";

This works since Vite sets this environment variable when we run vite build, which is what we do for production, as opposed to dev mode with hot module reloading.

Next we tell Vite to draw our assets from our CDN like so, setting the base property of our config object:

export default defineConfig({ base: isProduction ? process.env.REACT_CDN : "",

Be sure to set your REACT_CDN environment variable to your CDN’s location, which in this case, will be our CloudFront distribution’s location. Mine looks something (but not exactly) like this:

https://distributiondomainname.cloudfront.net Watch your VitePWA settings!

As one final piece of cleanup, if you happen to be using the VitePWA plugin, be sure to reset your base property like this:

VitePWA({ base: "/",

Otherwise, your web.manifest file will have invalid settings and cause errors.

Let’s see the CDN work

Once you’re all set up, browse to your site, and inspect any of the network requests for your script or CSS files. For starters, the protocol should be h2.

From there, you can peek into the response headers of any one of those files, and you should see some CloudFront data in there:

Cache busting

It’s hard to talk about CDNs without mentioning cache busting. CDNs like CloudFront have functionality to manually “eject” items from cache. But for Vite-built assets, we get this “for free” since Vite adds fingerprinting, or hash codes, to the filenames of the assets it produces.

So Vite might turn a home.js file into home-abc123.js during a build, but then if you change that file and rebuild, it might become home-xyz987.js. That’s good, as it will “break the cache,” and the newly built file will not be cached, so the CDN will have to turn to our host domain for the actual content.

CDN caching for other static assets

JavaScript, CSS, and font files aren’t the only kinds of assets that can benefit from CDN caching. If you have an S3 bucket you’re serving images out of, consider setting up a CloudFront distribution for it as well. There are options specifically for S3 which makes it a snap to create. Not only will you get the same edge caching, but HTTP/2 responses, which S3 does not provide.

Advanced CDN practices

Integrating a CDN here was reasonably straightforward, but we’re only enjoying a fraction of the potential benefits. Right now, users will browse to our app, our server will serve our root HTML file, and then the user’s browser will connect to our CDN to start pulling down all our static assets.

Going further, we would want to serve our entire site from a CDN. That way, it can communicate with our web server as needed for non-static and non-cached assets.

Conclusion

CDNs are a great way to improve the performance of your site. They provide edge caching and HTTP/2 out of the box. Not only that, but they’re reasonably easy to set up. Now you have a new tool in your belt to both set up a CDN and integrate it with Vite.

Adding CDN Caching to a Vite Build originally published on CSS-Tricks. You should get the newsletter.

©2003 - Present Akamai Design & Development.