Front End Web Development

Managing CSS Styles in a WordPress Block Theme

Css Tricks - Mon, 11/07/2022 - 4:05am

The way we write CSS for WordPress themes is in the midst of sweeping changes. I recently shared a technique for adding fluid type support in WordPress by way of theme.json, a new file that WordPress has been pushing hard to become a central source of truth for defining styles in WordPress themes that support full-site editing (FSE) features.

Wait, no style.css file? We still have that. In fact, style.css is still a required file in block themes, though its role is greatly reduced to meta information used for registering the theme. That said, the fact is that theme.json is still in active development, meaning we’re in a transitional period where you might find styles defined there, in styles.css or even at the block level.

So, what does styling actually look like in these WordPress FSE days? That’s what I want to cover in this article. There’s a lack of documentation for styling block themes in the WordPress Theme Developer Handbook, so everything we’re covering here is what I’ve gathered about where things currently are as well as discussions about the future of WordPress theming.

The evolution of WordPress styles

The new developmental features that are included in WordPress 6.1 get us closer to a system of styles that are completely defined in theme.json, but there is still be plenty of work to do before we can fully lean on it. One way we can get an idea of what’s coming in future releases is by using the Gutenberg plugin. This is where experimental features are often given a dry run.

Another way we can get a feel for where we are is by looking at the evolution of default WordPress themes. To date, there are three default themes that support full-site editing:

But don’t start trading the CSS in style.css for JSON property-value pairs in theme.json just yet. There are still CSS styling rules that need to be supported in theme.json before we think about doing that. The remaining significant issues are currently being discussed with an aim to fully move all the CSS style rules into theme.json and consolidate different sources of theme.json into a UI for for setting global styles directly in the WordPress Site Editor.

The Global Styles UI is integrated with the right panel in the Site Editor. (Credit: Learn WordPress)

That leaves us in a relatively tough spot. Not only is there no clear path for overriding theme styles, but it’s unclear where the source of those styles even come from — is it from different layers of theme.json files, style.css, the Gutenberg plugin, or somewhere else?

Why theme.json instead of style.css?

You might be wondering why WordPress is moving toward a JSON-based definition of styles instead of a traditional CSS file. Ben Dwyer from the Gutenberg development team eloquently articulates why the theme.json approach is a benefit for theme developers.

It’s worth reading Ben’s post, but the meat is in this quote:

Overriding CSS, whether layout, preset, or block styles, presents an obstacle to integration and interoperability: visual parity between the frontend and editor becomes more difficult to maintain, upgrades to block internals may conflict with overrides. Custom CSS is, furthermore, less portable across other block themes.

By encouraging theme authors to use theme.json API where possible, the hierarchy of “base > theme > user” defined styles can be resolved correctly.

One of the major benefits of moving CSS to JSON is that JSON is a machine-readable format, which means it can be exposed in the WordPress Site Editor UI by fetching an API, thus allowing users to modify default values and customize a site’s appearance without writing any CSS at all. It also provides a way to style blocks consistently, while providing a structure that creates layers of specificity such that the user settings take the highest priority over those defined in theme.json. That interplay between theme-level styles in theme.json and the user-defined styles in the Global Styles UI is what makes the JSON approach so appealing.

Developers maintain consistency in JSON, and users gain flexibility with code-less customizations. That’s a win-win.

There are other benefits, for sure, and Mike McAlister from WP Engine lists several in this Twitter thread. You can find even more benefits in this in-depth discussion over at the Make WordPress Core blog. And once you’ve given that a read, compare the benefits of the JSON approach with the available ways to define and override styles in classic themes.

Defining styles with JSON elements

We’ve already seen a lot of progress as far as what parts of a theme theme.json is capable of styling. Prior to WordPress 6.1, all we could really do was style headings and links. Now, with WordPress 6.1, we can add buttons, captions, citations, and headings to the mix.

And we do that by defining JSON elements. Think of elements as individual components that live in a WordPress block. Say we have a block that contains a heading, a paragraph, and a button. Those individual pieces are elements, and there’s an elements object in theme.json where we define their styles:

{ "version": 2, "settings": {}, // etc. "styles": { // etc. "elements": { "button": { ... }, "h1": { ... }, "heading": { ... }, }, }, "templateParts": {} }

A better way to describe JSON elements is as low-level components for themes and blocks that do not need the complexity of blocks. They are representations of HTML primitives that are not defined in a block but can be used across blocks. How they are supported in WordPress (and the Gutenberg plugin) is described in the Block Editor Handbook and this Full Site Editing tutorial by Carolina Nymark.

For example, links are styled in the elements object but are not a block in their own right. But a link can be used in a block and it will inherit the styles defined on the object in theme.json. This doesn’t fully encapsulate the definition of an element, though, as some elements are also registered as blocks, such as the Heading and Button blocks — but those blocks can still be used within other blocks.

Here is a table of the elements that are currently available to style in theme.json, courtesy of Carolina:

ElementSelectorWhere it’s supportedlink<a>WordPress Coreh1 – h6The HTML tag for each heading level: <h1>, <h2>, <h3>, <h4>, <h5> and <h6>WordPress CoreheadingStyles all headings globally by individual HTML tag: <h1>, <h2>, <h3>, <h4>, <h5> and <h6>Gutenberg pluginbutton.wp-element-button.wp-block-button__linkGutenberg plugincaption.wp-element-caption,
.wp-block-audio figcaption,
.wp-block-embed figcaption,
.wp-block-gallery figcaption,
.wp-block-image figcaption,
.wp-block-table figcaption,
.wp-block-video figcaptionGutenberg plugincite.wp-block-pullquote citeGutenberg plugin

As you can see, it’s still early days and plenty still needs to move from the Gutenberg plugin into WordPress Core. But you can see how quick it would be to do something like style all headings in a theme globally without hunting for selectors in CSS files or DevTools.

Further, you can also start to see how the structure of theme.json sort of forms layers of specificity, going from global elements (e.g. headings) to individual elements (e.g. h1), and block-level styles (e.g. h1 contained in a block).

Additional information on JSON elements is available in this Make WordPress proposal and this open ticket in the Gutenberg plugin’s GitHub repo.

JSON and CSS specificity

Let’s keep talking about CSS specificity. I mentioned earlier that the JSON approach to styling establishes a hierarchy. And it’s true. Styles that are defined on JSON elements in theme.json are considered default theme styles. And anything that is set by the user in the Global Styles UI will override the defaults.

In other words: user styles carry more specificity than default theme styles. Let’s take a look at the Button block to get a feel for how this works.

I’m using Emptytheme, a blank WordPress theme with no CSS styling. I’m going to add a Button block on a new page.

The background color, text color, and rounded borders come from the block editor’s default settings.

OK, we know that WordPress Core ships with some light styling. Now, I’m going to switch to the default TT3 theme from WordPress 6.1 and activate it. If I refresh my page with the button, the button changes styles.

The background color, text color, and rounded corner styles have changed.

You can see exactly where those new styles are coming from in TT3’s theme.json file. This tells us that the JSON element styles take precedence over WordPress Core styles.

Now I am going to modify TT3 by overriding it with a theme.json file in a child theme, where the default background color of the Button block is set to red.

The default style for the Button block has been updated to red.

But notice the search button in that last screenshot. It should be red, too, right? That must mean it is styled at another level if the change I made is at the global level. If we want to change both buttons, we could do it at the user level using the Global Styles UI in the site editor.

We changed the background color of both buttons to blue and modified the text as well using the Global styles UI. Notice that the blue from there took precedence over the theme styles!

The Style Engine

That’s a very quick, but good, idea of how CSS specificity is managed in WordPress block themes. But it’s not the complete picture because it’s still unclear where those styles are generated. WordPress has its own default styles that come from somewhere, consumes the data in theme.json for more style rules, and overrides those with anything set in Global Styles.

Are those styles inline? Are they in a separate stylesheet? Maybe they’re injected on the page in a <script>?

That’s what the new Style Engine is hopefully going to solve. The Style Engine is a new API in WordPress 6.1 that is meant to bring consistency to how styles are generated and where styles are applied. In other words, it takes all of the possible sources of styling and is singularly responsible for properly generating block styles. I know, I know. Yet another abstraction on top of other abstractions just to author some styles. But having a centralized API for styles is probably the most elegant solution given that styles can come from a number of places.

We’re only getting a first look at the Style Engine. In fact, here’s what has been completed so far, according to the ticket:

  • Audit and consolidate where the code generates block support CSS in the back end so that they are delivered from the same place (as opposed to multiple places). This covers CSS rules such as margin, padding, typography, colors, and borders.
  • Remove repetitive layout-specific styles and generate semantic class names.
  • Reduce the number of inline style tags we print to the page for block, layout, and element support.

Basically, this is the foundation for establishing a single API that contains all the CSS style rules for a theme, wherever they come from. It cleans up the way WordPress would inject inline styles pre-6.1 and establishes a system for semantic class names.

Further details on the long-term and short-term goals of Style Engine can be found in this Make WordPress Core discussion. You can also follow the tracking issue and project board for more updates.

Working with JSON elements

We talked a bit about JSON elements in the theme.json file and how they are basically HTML primitives for defining default styles for things like headings, buttons, and links, among others. Now, let’s look at actually using a JSON element and how it behaves in various styling contexts.

JSON elements generally have two contexts: the global level and the block level. The global level styles are defined with less specificity than they are at the block level to ensure that block-specific styles take precedence for consistency wherever blocks are used.

Global styles for JSON elements

Let’s look at the new default TT3 theme and examine how its buttons are styled. The following is an abbreviated copy-paste of the TT3 theme.json file (here’s the full code) showing the global styles section, but you can find the original code here.

View code { "version": 2, "settings": {}, // ... "styles": { // ... "elements": { "button": { "border": { "radius": "0" }, "color": { "background": "var(--wp--preset--color--primary)", "text": "var(--wp--preset--color--contrast)" }, ":hover": { "color": { "background": "var(--wp--preset--color--contrast)", "text": "var(--wp--preset--color--base)" } }, ":focus": { "color": { "background": "var(--wp--preset--color--contrast)", "text": "var(--wp--preset--color--base)" } }, ":active": { "color": { "background": "var(--wp--preset--color--secondary)", "text": "var(--wp--preset--color--base)" } } }, "h1": { "typography": { } }, // ... "heading": { "typography": { "fontWeight": "400", "lineHeight": "1.4" } }, "link": { "color": { "text": "var(--wp--preset--color--contrast)" }, ":hover": { "typography": { "textDecoration": "none" } }, ":focus": { "typography": { "textDecoration": "underline dashed" } }, ":active": { "color": { "text": "var(--wp--preset--color--secondary)" }, "typography": { "textDecoration": "none" } }, "typography": { "textDecoration": "underline" } } }, // ... }, "templateParts": {} }

All buttons are styled at the global level (styles.elements.button).

Every button in the Twenty Twenty-Three theme shares the same background color, which is set to the --wp--preset--color--primary CSS variable, or #B4FD55.

We can confirm this in DevTools as well. Notice that a class called .wp-element-button is the selector. The same class is used to style the interactive states as well.

Again, this styling is all happening at the global level, coming from theme.json. Whenever we use a button, it is going to have the same background because they share the same selector and no other style rules are overriding it.

As an aside, WordPress 6.1 added support for styling interactive states for certain elements, like buttons and links, using pseudo-classes in theme.json — including :hover, :focus, and :active — or the Global Styles UI. Automattic Engineer Dave Smith demonstrates this feature in a YouTube video.

We could override the button’s background color either in theme.json (preferably in a child theme since we’re using a default WordPress theme) or in the Global Styles settings in the site editor (no child theme needed since it does not require a code change).

But then the buttons will change all at once. What if we want to override the background color when the button is part of a certain block? That’s where block-level styles come into play.

Block-level styles for elements

To understand how we can use and customize styles at the block level, let’s change the background color of the button that is contained in the Search block. Remember, there is a Button block, but what we’re doing is overriding the background color at the block level of the Search block. That way, we’re only applying the new color there as opposed to applying it globally to all buttons.

To do that, we define the styles on the styles.blocks object in theme.json. That’s right, if we define the global styles for all buttons on styles.elements, we can define the block-specific styles for button elements on styles.block, which follows a similar structure:

{ "version": 2, // ... "styles": { // Global-level syles "elements": { }, // Block-level styles "blocks": { "core/search": { "elements": { "button": { "color": { "background": "var(--wp--preset--color--quaternary)", "text": "var(--wp--preset--color--base)" } } }, // ... } } } }

See that? I set the background and text properties on styles.blocks.core/search.elements.button with two CSS variables that are preset in WordPress.

The result? The search button is now red (--wp--preset--color--quaternary), and the default Button block retains its bright green background.

We can see the change in DevTools as well.

The same is true if we want to style buttons that are included in other blocks. And buttons are merely one example, so let’s look at another one.

Example: Styling headings at each level

Let’s drive all this information home with an example. This time, we will:

  • Style all headings globally
  • Style all Heading 2 elements
  • Style Heading 2 elements in the Query Loop block

First, let’s start with the basic structure for theme.json:

{ "version": 2, "styles": { // Global-level syles "elements": { }, // Block-level styles "blocks": { } } }

This establishes the outline for our global and block-level styles.

Style all headings globally

Let’s add the headings object to our global styles and apply some styles:

{ "version": 2, "styles": { // Global-level syles "elements": { "heading": { "color": "var(--wp--preset--color--base)" }, }, // Block-level styles "blocks": { } } }

That sets the color for all headings to the preset base color in WordPress. Let’s change the color and font size of Heading 2 elements at the global level as well:

{ "version": 2, "styles": { // Global-level syles "elements": { "heading": { "color": "var(--wp--preset--color--base)" }, "h2": { "color": "var(--wp--preset--color--primary)", "typography": { "fontSize": "clamp(2.625rem, calc(2.625rem + ((1vw - 0.48rem) * 8.4135)), 3.25rem)" } } }, // Block-level styles "blocks": { } } }

Now, all Heading 2 elements are set to be the primary preset color with a fluid font size. But maybe we want a fixed fontSize for the Heading 2 element when it is used in the Query Look block:

{ "version": 2, "styles": { // Global-level syles "elements": { "heading": { "color": "var(--wp--preset--color--base)" }, "h2": { "color": "var(--wp--preset--color--primary)", "typography": { "fontSize": "clamp(2.625rem, calc(2.625rem + ((1vw - 0.48rem) * 8.4135)), 3.25rem)" } } }, // Block-level styles "blocks": { "core/query": { "elements": { "h2": { "typography": { "fontSize": 3.25rem } } } } } } }

Now we have three levels of styles for Heading 2 elements: all headings, all Heading 2 elements, and Heading 2 elements that are used in the Query Loop block.

Existing theme examples

While we only looked at the styling examples for buttons and headings in this article, WordPress 6.1 supports styling additional elements. There’s a table outlining them in the “Defining styles with JSON elements” section.

You’re probably wondering which JSON elements support which CSS properties, not to mention how you would even declare those. While we wait for official documentation, the best resources we have are going to be the theme.json files for existing themes. I’m going to provide links to themes based on the elements they customize, and point out what properties are customized.

ThemeWhat’s customizedTheme JSONBlockbaseButtons, headings, links, core blocksSource codeBlock CanvasButtons, headings, links, core blocksSource codeDiscoButtons, headings, core blocksSource codeFrostButtons, headings, links, captions, cite, core blocksSource codePixlButtons, headings, links, core blocksSource codeRainfallButtons, headings, links, core blocksSource codeTwenty Twenty-ThreeButtons, headings, links, core blocksSource codeVivreButtons, headings, links, core blocksSource code

Be sure to give each theme.json file a good look because these themes include excellent examples of block-level styling on the styles.blocks object.

Wrapping up

Frequent changes to the full-site editor are becoming a major sources of irritation to many people, including tech-savvy Gutenberg users. Even very simple CSS rules, which work well with classic themes, don’t seem to work for block themes because of the new layers of specificity we covered earlier.

Regarding a GitHub proposal to re-design the site editor in a new browser mode, Sara Gooding writes in a WP Tavern post:

It’s easy to get lost while trying to get around the Site Editor unless you are working day and night inside the tool. The navigation is jumpy and confusing, especially when going from template browsing to template editing to modifying individual blocks.

Even as a keen early rider in the world of Gutenberg block editor and block-eye themes, I do have tons of my own frustrations. I’m optimistic, though, and anticipate that the site editor, once completed, will be a revolutionary tool for users and techno-savvy theme developers alike. This hopeful tweet already confirms that. In the meantime, it seems that we should be preparing for more changes, and perhaps even a bumpy ride.


I’m listing all of the resources I used while researching information for this article.

JSON elements Global Styles Style Engine

Thanks for reading! I’d love to hear your own reflections on using the block themes and how you managing your CSS.

Managing CSS Styles in a WordPress Block Theme originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

A Couple Changes Coming in Chrome 108

Css Tricks - Fri, 11/04/2022 - 3:13am

“A change to overflow on replaced elements in CSS”:

From Chrome 108, the following replaced elements respect the overflow property: img, video and canvas. In earlier versions of Chrome, this property was ignored on these elements.

This means that an image which was earlier clipped to its content box can now draw outside those bounds if specified to do so in a style sheet.

So any image, video, and canvas elements that used to overflow by default might get clipped when Chrome 108 ships. The noted situations where this might affect your existing work:

  • The object-fit property is used to scale the image and fill the box. If the aspect ratio does not match the box, the image will draw outside of the bounds.
  • The border-radius property makes a square image look like a circle, but because overflow is visble the clipping no longer occurs.
  • Setting inherit: all and causing all properties to inherit, including overflow.

Worth reading the full article for code examples, but the solution for unwanted clipping is overriding the UA’s default overflow: clip with overflow: visible:

img { overflow: visible; }

“Prepare for viewport resize behavior changes coming to Chrome on Android”:

In November, with the release of Chrome 108, Chrome will make some changes to how the Layout Viewport behaves when the on-screen keyboard (OSK) gets shown. With this change, Chrome on Android will no longer resize the Layout Viewport, and instead resize only the Visual Viewport. This will bring Chrome on Android’s behavior up to par with that of Chrome on iOS and Safari on iOS.

This is a change related to the common headaches of working with viewport units and fixed positioning on mobile touch devices. We’ve covered (and tried solving) it over the years:

The change means that Chrome on Android will no longer resize the Layout Viewport when the on-screen keyboard is shown. So, the computed values of viewport units will no longer shrink when a device’s keyboard is displayed. Same goes for elements that are designed to take up the full viewport no longer shrinking to accomodate the keyboard. And no longer will a fixed-position element wind up who knows where when the keyboard pops up.

This brings more consistent cross-browser behavior that is on line with Chrome, Safari, and Edge on iOS and iPadOS. That’s great, even if the updated behavior is less than ideal because the keyboard UI can still cover and obscure elements that get in its way.

If you prefer elements to remain visible when that happens, it’s worth looking at a solution Chris shared a long while back that uses the prefixed webkit-fill-available property:

body { min-height: 100vh; min-height: -webkit-fill-available; } html { height: -webkit-fill-available; }

That uses the available space in the viewport rather than what’s covered by the UI… but Chrome currently ignores the property, and I’d bet the nickel in my pocket that it is unlikely to start respecting it in the 108 release. That may be a moot point, though, as Chrome 108 also introduces support for small, large, and dynamic viewport units, which are already supported in Safari and Firefox.

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

DesktopChromeFirefoxIEEdgeSafari108101NoNo15.4Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS SafariNo106No15.4

A Couple Changes Coming in Chrome 108 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The Difference Between Web Sockets, Web Workers, and Service Workers

Css Tricks - Thu, 11/03/2022 - 2:56am

Web Sockets, Web Workers, Service Workers… these are terms you may have read or overheard. Maybe not all of them, but likely at least one of them. And even if you have a good handle on front-end development, there’s a good chance you need to look up what they mean. Or maybe you’re like me and mix them up from time to time. The terms all look and sound awful similar and it’s really easy to get them confused.

So, let’s break them down together and distinguish Web Sockets, Web Workers, and Service Workers. Not in the nitty-gritty sense where we do a deep dive and get hands-on experience with each one — more like a little helper to bookmark the next time I you need a refresher.

Quick reference

We’ll start with a high-level overview for a quick compare and contrast.

FeatureWhat it isWeb SocketEstablishes an open and persistent two-way connection between the browser and server to send and receive messages over a single connection triggered by events.Web WorkerAllows scripts to run in the background in separate threads to prevent scripts from blocking one another on the main thread.Service WorkerA type of Web Worker that creates a background service that acts middleware for handling network requests between the browser and server, even in offline situations. Web Sockets

A Web Socket is a two-way communication protocol. Think of this like an ongoing call between you and your friend that won’t end unless one of you decides to hang up. The only difference is that you are the browser and your friend is the server. The client sends a request to the server and the server responds by processing the client’s request and vice-versa.

The communication is based on events. A WebSocket object is established and connects to a server, and messages between the server trigger events that send and receive them.

This means that when the initial connection is made, we have a client-server communication where a connection is initiated and kept alive until either the client or server chooses to terminate it by sending a CloseEvent. That makes Web Sockets ideal for applications that require continuous and direct communication between a client and a server. Most definitions I’ve seen call out chat apps as a common use case — you type a message, send it to the server, trigger an event, and the server responds with data without having to ping the server over and again.

Consider this scenario: You’re on your way out and you decide to switch on Google Maps. You probably already know how Google Maps works, but if you don’t, it finds your location automatically after you connect to the app and keeps track of it wherever you go. It uses real-time data transmission to keep track of your location as long as this connection is alive. That’s a Web Socket establishing a persistent two-way conversation between the browser and server to keep that data up to date. A sports app with real-time scores might also make use of Web Sockets this way.

The big difference between Web Sockets and Web Workers (and, by extension as we’ll see, Service Workers) is that they have direct access to the DOM. Whereas Web Workers (and Service Workers) run on separate threads, Web Sockets are part of the main thread which gives them the ability to manipulate the DOM.

There are tools and services to help establish and maintain Web Socket connections, including: SocketCluster, AsyncAPI, cowboy, WebSocket King, Channels, and Gorilla WebSocket. MDN has a running list that includes other services.

More Web Sockets information Web Workers

Consider a scenario where you need to perform a bunch of complex calculations while at the same time making changes to the DOM. JavaScript is a single-threaded application and running more than one script might disrupt the user interface you are trying to make changes to as well as the complex calculation being performed.

This is where the Web Workers come into play.

Web Workers allow scripts to run in the background in separate threads to prevent scripts from blocking one another on the main thread. That makes them great for enhancing the performance of applications that require intensive operations since those operations can be performed in the background on separate threads without affecting the user interface from rendering. But they’re not so great at accessing the DOM because, unlike Web Sockets, a web worker runs outside the main thread in its own thread.

A Web Worker is an object that executes a script file by using a Worker object to carry out the tasks. And when we talk about workers, they tend to fall into one of three types:

  • Dedicated Workers: A dedicated worker is only within reach by the script that calls it. It still executes the tasks of a typical web worker, such as its multi-threading scripts.
  • Shared Workers: A shared worker is the opposite of a dedicated worker. It can be accessed by multiple scripts and can practically perform any task that a web worker executes as long as they exist in the same domain as the worker.
  • Service Workers: A service worker acts as a network proxy between an app, the browser, and the server, allowing scripts to run even in the event when the network goes offline. We’re going to get to this in the next section.
More Web Workers information Service Workers

There are some things we have no control over as developers, and one of those things is a user’s network connection. Whatever network a user connects to is what it is. We can only do our best to optimize our apps so they perform the best they can on any connection that happens to be used.

Service Workers are one of the things we can do to progressively enhance an app’s performance. A service worker sits between the app, the browser, and the server, providing a secure connection that runs in the background on a separate thread, thanks to — you guessed it — Web Workers. As we learned in the last section, Service Workers are one of three types of Web Workers.

So, why would you ever need a service worker sitting between your app and the user’s browser? Again, we have no control over the user’s network connection. Say the connection gives out for some unknown reason. That would break communication between the browser and the server, preventing data from being passed back and forth. A service worker maintains the connection, acting as an async proxy that is capable of intercepting requests and executing tasks — even after the network connection is lost.

This is the main driver of what’s often referred to as “offline-first” development. We can store assets in the local cache instead of the network, provide critical information if the user goes offline, prefetch things so they’re ready when the user needs them, and provide fallbacks in response to network errors. They’re fully asynchronous but, unlike Web Sockets, they have no access to the DOM since they run on their own threads.

The other big thing to know about Service Workers is that they intercept every single request and response from your app. As such, they have some security implications, most notably that they follow a same-origin policy. So, that means no running a service worker from a CDN or third-party service. They also require a secure HTTPS connection, which means you’ll need a SSL certificate for them to run.

More Service Workers information Wrapping up

That’s a super high-level explanation of the differences (and similarities) between Web Sockets, Web Workers, and Service Workers. Again, the terminology and concepts are similar enough to mix one up with another, but hopefully, this gives you a better idea of how to distinguish them.

We kicked things off with a quick reference table. Here’s the same thing, but slightly expanded to draw thicker comparisons.

FeatureWhat it isMultithreaded?HTTPS?DOM access?Web SocketEstablishes an open and persistent two-way connection between the browser and server to send and receive messages over a single connection triggered by events.Runs on the main threadNot requiredYesWeb WorkerAllows scripts to run in the background in separate threads to prevent scripts from blocking one another on the main thread.Runs on a separate threadRequiredNoService WorkerA type of Web Worker that creates a background service that acts middleware for handling network requests between the browser and server, even in offline situations.Runs on a separate threadRequiredNo

The Difference Between Web Sockets, Web Workers, and Service Workers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Steven Heller’s Font of the Month: Feneon

Typography - Thu, 11/03/2022 - 1:55am

Read the book, Typographic Firsts

Steven Heller takes a closer look at Craig Eliason’s magnificent Feneon font family.

The post Steven Heller’s Font of the Month: Feneon appeared first on I Love Typography.

Some Links About CSS Gradients

Css Tricks - Wed, 11/02/2022 - 2:59am

Every once in a while, the blogging zeitgiest seems to coalesce around a certain topic and it’s like the saved articles in my bookmarks folder are having a conversation. The conversation sitting in there now is all about CSS Gradients and I thought I’d link some of the more interesting pieces.

Some Links About CSS Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Rendering External API Data in WordPress Blocks on the Back End

Css Tricks - Tue, 11/01/2022 - 2:57am

This is a continuation of my last article about “Rendering External API Data in WordPress Blocks on the Front End”. In that last one, we learned how to take an external API and integrate it with a block that renders the fetched data on the front end of a WordPress site.

The thing is, we accomplished this in a way that prevents us from seeing the data in the WordPress Block Editor. In other words, we can insert the block on a page but we get no preview of it. We only get to see the block when it’s published.

Let’s revisit the example block plugin we made in the last article. Only this time, we’re going to make use of the JavaScript and React ecosystem of WordPress to fetch and render that data in the back-end Block Editor as well.

Working With External APIs in WordPress Blocks Where we left off

As we kick this off, here’s a demo where we landed in the last article that you can reference. You may have noticed that I used a render_callback method in the last article so that I can make use of the attributes in the PHP file and render the content.

Well, that may be useful in situations where you might have to use some native WordPress or PHP function to create dynamic blocks. But if you want to make use of just the JavaScript and React (JSX, specifically) ecosystem of WordPress to render the static HTML along with the attributes stored in the database, you only need to focus on the Edit and Save functions of the block plugin.

  • The Edit function renders the content based on what you want to see in the Block Editor. You can have interactive React components here.
  • The Save function renders the content based on what you want to see on the front end. You cannot have the the regular React components or the hooks here. It is used to return the static HTML that is saved into your database along with the attributes.

The Save function is where we’re hanging out today. We can create interactive components on the front-end, but for that we need to manually include and access them outside the Save function in a file like we did in the last article.

So, I am going to cover the same ground we did in the last article, but this time you can see the preview in the Block Editor before you publish it to the front end.

The block props

I intentionally left out any explanations about the edit function’s props in the last article because that would have taken the focus off of the main point, the rendering.

If you are coming from a React background, you will likely understand what is that I am talking about, but if you are new to this, I would recommend checking out components and props in the React documentation.

If we log the props object to the console, it returns a list of WordPress functions and variables related to our block:

We only need the attributes object and the setAttributes function which I am going to destructure from the props object in my code. In the last article, I had modified RapidAPI’s code so that I can store the API data through setAttributes(). Props are only readable, so we are unable to modify them directly.

Block props are similar to state variables and setState in React, but React works on the client side and setAttributes() is used to store the attributes permanently in the WordPress database after saving the post. So, what we need to do is save them to and then call that as the initial value for the useState() variable.

The edit function

I am going to copy-paste the HTML code that we used in football-rankings.php in the last article and edit it a little to shift to the JavaScript background. Remember how we created two additional files in the last article for the front end styling and scripts? With the way we’re approaching things today, there’s no need to create those files. Instead, we can move all of it to the Edit function.

Full code import { useState } from "@wordpress/element"; export default function Edit(props) { const { attributes, setAttributes } = props; const [apiData, setApiData] = useState(null); function fetchData() { const options = { method: "GET", headers: { "X-RapidAPI-Key": "Your Rapid API key", "X-RapidAPI-Host": "", }, }; fetch( "", options ) .then((response) => response.json()) .then((response) => { let newData = { ...response }; // Deep clone the response data setAttributes({ data: newData }); // Store the data in WordPress attributes setApiData(newData); // Modify the state with the new data }) .catch((err) => console.error(err)); } return ( <div {...useBlockProps()}> <button onClick={() => getData()}>Fetch data</button> {apiData && ( <> <div id="league-standings"> <div className="header" style={{ backgroundImage: `url(${apiData.response[0].league.logo})`, }} > <div className="position">Rank</div> <div className="team-logo">Logo</div> <div className="team-name">Team name</div> <div className="stats"> <div className="games-played">GP</div> <div className="games-won">GW</div> <div className="games-drawn">GD</div> <div className="games-lost">GL</div> <div className="goals-for">GF</div> <div className="goals-against">GA</div> <div className="points">Pts</div> </div> <div className="form-history">Form history</div> </div> <div className="league-table"> {/* Usage of [0] might be weird but that is how the API structure is. */} {apiData.response[0].league.standings[0].map((el) => { {/* Destructure the required data from all */} const { played, win, draw, lose, goals } = el.all; return ( <> <div className="team"> <div class="position">{el.rank}</div> <div className="team-logo"> <img src={} /> </div> <div className="team-name">{}</div> <div className="stats"> <div className="games-played">{played}</div> <div className="games-won">{win}</div> <div className="games-drawn">{draw}</div> <div className="games-lost">{lose}</div> <div className="goals-for">{goals.for}</div> <div className="goals-against">{goals.against}</div> <div className="points">{el.points}</div> </div> <div className="form-history"> {el.form.split("").map((result) => { return ( <div className={`result-${result}`}>{result}</div> ); })} </div> </div> </> ); } )} </div> </div> </> )} </div> ); }

I have included the React hook useState() from @wordpress/element rather than using it from the React library. That is because if I were to load the regular way, it would download React for every block that I am using. But if I am using @wordpress/element it loads from a single source, i.e., the WordPress layer on top of React.

This time, I have also not wrapped the code inside useEffect() but inside a function that is called only when clicking on a button so that we have a live preview of the fetched data. I have used a state variable called apiData to render the league table conditionally. So, once the button is clicked and the data is fetched, I am setting apiData to the new data inside the fetchData() and there is a rerender with the HTML of the football rankings table available.

You will notice that once the post is saved and the page is refreshed, the league table is gone. That is because we are using an empty state (null) for apiData‘s initial value. When the post saves, the attributes are saved to the object and we call it as the initial value for the useState() variable like this:

const [apiData, setApiData] = useState(; The save function

We are going to do almost the same exact thing with the save function, but modify it a little bit. For example, there’s no need for the “Fetch data” button on the front end, and the apiData state variable is also unnecessary because we are already checking it in the edit function. But we do need a random apiData variable that checks for to conditionally render the JSX or else it will throw undefined errors and the Block Editor UI will go blank.

Full code export default function save(props) { const { attributes, setAttributes } = props; let apiData =; return ( <> {/* Only render if apiData is available */} {apiData && ( <div {}> <div id="league-standings"> <div className="header" style={{ backgroundImage: `url(${apiData.response[0].league.logo})`, }} > <div className="position">Rank</div> <div className="team-logo">Logo</div> <div className="team-name">Team name</div> <div className="stats"> <div className="games-played">GP</div> <div className="games-won">GW</div> <div className="games-drawn">GD</div> <div className="games-lost">GL</div> <div className="goals-for">GF</div> <div className="goals-against">GA</div> <div className="points">Pts</div> </div> <div className="form-history">Form history</div> </div> <div className="league-table"> {/* Usage of [0] might be weird but that is how the API structure is. */} {apiData.response[0].league.standings[0].map((el) => { const { played, win, draw, lose, goals } = el.all; return ( <> <div className="team"> <div className="position">{el.rank}</div> <div className="team-logo"> <img src={} /> </div> <div className="team-name">{}</div> <div className="stats"> <div className="games-played">{played}</div> <div className="games-won">{win}</div> <div className="games-drawn">{draw}</div> <div className="games-lost">{lose}</div> <div className="goals-for">{goals.for}</div> <div className="goals-against">{goals.against}</div> <div className="points">{el.points}</div> </div> <div className="form-history"> {el.form.split("").map((result) => { return ( <div className={`result-${result}`}>{result}</div> ); })} </div> </div> </> ); })} </div> </div> </div> )} </> ); }

If you are modifying the save function after a block is already present in the Block Editor, it would show an error like this:

That is because the markup in the saved content is different from the markup in our new save function. Since we are in development mode, it is easier to remove the bock from the current page and re-insert it as a new block — that way, the updated code is used instead and things are back in sync.

This situation of removing it and adding it again can be avoided if we had used the render_callback method since the output is dynamic and controlled by PHP instead of the save function. So each method has it’s own advantages and disadvantages.

Tom Nowell provides a thorough explanation on what not to do in a save function in this Stack Overflow answer.

Styling the block in the editor and the front end

Regarding the styling, it is going to be almost the same thing we looked at in the last article, but with some minor changes which I have explained in the comments. I’m merely providing the full styles here since this is only a proof of concept rather than something you want to copy-paste (unless you really do need a block for showing football rankings styled just like this). And note that I’m still using SCSS that compiles to CSS on build.

Editor styles /* Target all the blocks with the data-title="Football Rankings" */ .block-editor-block-list__layout .block-editor-block-list__block.wp-block[data-title="Football Rankings"] { /* By default, the blocks are constrained within 650px max-width plus other design specific code */ max-width: unset; background: linear-gradient(to right, #8f94fb, #4e54c8); display: grid; place-items: center; padding: 60px 0; /* Button CSS - From: - Some properties really not needed :) */ button.fetch-data { align-items: center; background-color: #ffffff; border: 1px solid rgb(0 0 0 / 0.1); border-radius: 0.25rem; box-shadow: rgb(0 0 0 / 0.02) 0 1px 3px 0; box-sizing: border-box; color: rgb(0 0 0 / 0.85); cursor: pointer; display: inline-flex; font-family: system-ui, -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 600; justify-content: center; line-height: 1.25; margin: 0; min-height: 3rem; padding: calc(0.875rem - 1px) calc(1.5rem - 1px); position: relative; text-decoration: none; transition: all 250ms; user-select: none; -webkit-user-select: none; touch-action: manipulation; vertical-align: baseline; width: auto; &:hover, &:focus { border-color: rgb(0, 0, 0, 0.15); box-shadow: rgb(0 0 0 / 0.1) 0 4px 12px; color: rgb(0, 0, 0, 0.65); } &:hover { transform: translateY(-1px); } &:active { background-color: #f0f0f1; border-color: rgb(0 0 0 / 0.15); box-shadow: rgb(0 0 0 / 0.06) 0 2px 4px; color: rgb(0 0 0 / 0.65); transform: translateY(0); } } } Front-end styles /* Front-end block styles */ .wp-block-post-content .wp-block-football-rankings-league-table { background: linear-gradient(to right, #8f94fb, #4e54c8); max-width: unset; display: grid; place-items: center; } #league-standings { width: 900px; margin: 60px 0; max-width: unset; font-size: 16px; .header { display: grid; gap: 1em; padding: 10px; grid-template-columns: 1fr 1fr 3fr 4fr 3fr; align-items: center; color: white; font-size: 16px; font-weight: 600; background-color: transparent; background-repeat: no-repeat; background-size: contain; background-position: right; .stats { display: flex; gap: 15px; &amp; &gt; div { width: 30px; } } } } .league-table { background: white; box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px; padding: 1em; .position { width: 20px; } .team { display: grid; gap: 1em; padding: 10px 0; grid-template-columns: 1fr 1fr 3fr 4fr 3fr; align-items: center; } .team:not(:last-child) { border-bottom: 1px solid lightgray; } .team-logo img { width: 30px; top: 3px; position: relative; } .stats { display: flex; gap: 15px; &amp; &gt; div { width: 30px; text-align: center; } } .last-5-games { display: flex; gap: 5px; &amp; &gt; div { width: 25px; height: 25px; text-align: center; border-radius: 3px; font-size: 15px; &amp; .result-W { background: #347d39; color: white; } &amp; .result-D { background: gray; color: white; } &amp; .result-L { background: lightcoral; color: white; } } }

We add this to src/style.scss which takes care of the styling in both the editor and the frontend. I will not be able to share the demo URL since it would require editor access but I have a video recorded for you to see the demo:

Check out the demo

Pretty neat, right? Now we have a fully functioning block that not only renders on the front end, but also fetches API data and renders right there in the Block Editor — with a refresh button to boot!

But if we want to take full advantage of the WordPress Block Editor, we ought to consider mapping some of the block’s UI elements to block controls for things like setting color, typography, and spacing. That’s a nice next step in the block development learning journey.

Rendering External API Data in WordPress Blocks on the Back End originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The New CSS Media Query Range Syntax

Css Tricks - Mon, 10/31/2022 - 3:05am

We rely on CSS Media Queries for selecting and styling elements based on a targeted condition. That condition can be all kinds of things but typically fall into two camps: (1) the type of media that’s being used, and (2) a specific feature of the browser, device, or even the user’s environment.

So, say we want to apply certain CSS styling to a printed document:

@media print { .element { /* Style away! */ } }

The fact that we can apply styles at a certain viewport width has made CSS Media Queries a core ingredient of responsive web design since Ethan Marcotte coined the term. If the browser’s viewport width is a certain size, then apply a set of style rules, which allows us to design elements that respond to the size of the browser.

/* When the viewport width is at least 30em... */ @media screen and (min-width: 30em) { .element { /* Style away! */ } }

Notice the and in there? That’s an operator that allows us to combine statements. In that example, we combined a condition that the media type is a screen and that it’s min-width feature is set to 30em (or above). We can do the same thing to target a range of viewport sizes:

/* When the viewport width is between 30em - 80em */ @media screen and (min-width: 30em) and (max-width: 80em) { .element { /* Style away! */ } }

Now those styles apply to an explicit range of viewport widths rather than a single width!

But the Media Queries Level 4 specification has introduced a new syntax for targeting a range of viewport widths using common mathematical comparison operators — things like <, >, and = — that make more sense syntactically while writing less code.

Let’s dig into how that works.

New comparison operators

That last example is a good illustration of how we’ve sort of “faked” ranges by combining conditions using the and operator. The big change in the Media Queries Level 4 specification is that we have new operators that compare values rather than combining them:

  • < evaluates if a value is less than another value
  • > evaluates if a value is greater than another value
  • = evaluates if a value is equal to another value
  • <= evaluates if a value is less than or equal to another value
  • >= evaluates if a value is greater than or equal to another value

Here’s how we might’ve written a media query that applies styles if the browser is 600px wide or greater:

@media (min-width: 600px) { .element { /* Style away! */ } }

Here’s how it looks to write the same thing using a comparison operator:

@media (width >= 600px) { .element { /* Style away! */ } } Targeting a range of viewport widths

Often when we write CSS Media Queries, we’re creating what’s called a breakpoint — a condition where the design “breaks” and a set of styles are applied to fix it. A design can have a bunch of breakpoints! And they’re usually based on the viewport being between two widths: where the breakpoint starts and where the breakpoint ends.

Here’s how we’ve done that using the and operator to combine the two breakpoint values:

/* When the browser is between 400px - 1000px */ @media (min-width: 400px) and (max-width: 1000px) { /* etc. */ }

You start to get a good sense of how much shorter and easier it is to write a media query when we ditch the Boolean and operator in favor of the new range comparison syntax:

@media (400px <= width <= 1000px) { /* etc. */ }

Much easier, right? And it’s clear exactly what this media query is doing.

Browser support

This improved media query syntax is still in its early days at the time of this writing and not as widely supported at the moment as the approach that combines min-width and max-width. We’re getting close, though! Safari is the only major holdout at this point, but there is an open ticket for it that you can follow.

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

DesktopChromeFirefoxIEEdgeSafari10463No104NoMobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari107106107No Let’s look at an example

Here’s a layout for that’s nicely suited for larger screens, like a desktop:

This layout has base styles that are common to all breakpoints. But as the screen gets narrower, we start to apply styles that are conditionally applied at different smaller breakpoints that are ideally suited for tablets all the way down to mobile phones:

To see what’s happening, here’s a how the layout responds between the two smaller breakpoints. The hidden nav list getting displayed as well as title in the main gets increased in font-size.

That change is triggered when the viewport’s changes go from matching one media’s conditions to another:

/* Base styles (any screen size) */ header { display: flex; justify-content: center; } header ul { display: none; } .title p { font-size: 3.75rem; } /* When the media type is a screen with a width greater or equal to 768px */ @media screen and (width >= 768px) { header { justify-content: space-between; } header ul { display: flex; justify-content: space-between; gap: 3rem; } .title p { font-size: 5.75rem; } }

We’ve combined a few of the concepts we’ve covered! We’re targeting devices with a screen media type, evaluating whether the viewport width is greater than or equal to a specific value using the new media feature range syntax, and combining the two conditions with the and operator.

OK, so that’s great for mobile devices below 768px and for other devices equal to or greater than 768px. But what about that desktop layout… how do we get there?

As far as the layout goes:

  • The main element becomes a 12-column grid.
  • A button is displayed on the image.
  • The size of the .title element’s font increases and overlaps the image.

Assuming we’ve done our homework and determined exactly where those changes should take place, we can apply those styles when the viewport matches the width condition for that breakpoint. We’re going to say that breakpoint is at 1000px:

/* When the media type is a screen with a width greater or equal to 1000px */ @media screen and (width >= 1000px) { /* Becomes a 12-column grid */ main { display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: auto 250px; } /* Places the .title on the grid */ .title { grid-row: 1; } /* Bumps up the font-size */ .title p { font-size: 7.75rem; } /* Places .images on the grid */ .images { grid-row: 1 / span 2; align-self: end; position: relative; } /* Displays the button */ .images .button { display: block; position: absolute; inset-block-end: 5rem; inset-inline-end: -1rem; } }

Have a play with it:

CodePen Embed Fallback Why the new syntax is easier to understand

The bottom line: it’s easier to distinguish a comparison operator (e.g. width >= 320px) than it is to tell the difference between min-width and max-width using the and operator. By removing the nuance between min- and max-, we have one single width parameter to work with and the operators tell us the rest.

Beyond the visual differences of those syntaxes, they are also doing slightly different things. Using min- and max- is equivalent to using mathematical comparison operators:

  • max-width is equivalent to the <= operator (e.g. (max-width: 320px) is the same as (width <= 320px)).
  • min-width is equivalent to the >= operator (e.g. (min-width: 320px) is the same as (width >= 320px)).

Notice that neither is the equivalent of the > or < operators.

Let’s pull an example straight from the Media Queries Level 4 specification where we define different styles based on a breakpoint at 320px in the viewport width using min-width and max-width:

@media (max-width: 320px) { /* styles for viewports <= 320px */ } @media (min-width: 320px) { /* styles for viewports >= 320px */ }

Both media queries match a condition when the viewport width is equal to 320px. That’s not exactly what we want. We want either one of those conditions rather than both at the same time. To avoid that implicit changes, we might add a pixel to the query based on min-width:

@media (max-width: 320px){ /* styles for viewports <= 320px */ } @media (min-width: 321px){ /* styles for viewports >= 321px */ }

While this ensures that the two sets of styles don’t apply simultaneously when the viewport width is 320px, any viewport width that fall between 320px and 321px will result in a super small zone where none of the styles in either query are applied — a weird “flash of unstyled content” situation.

One solution is to increase the second comparison scale value (numbers after the decimal point) to 320.01px:

@media (max-width: 320px) { /* styles for viewports <= 320px */ } @media (min-width: 320.01px) { /* styles for viewports >= 320.01px */ }

But that’s getting silly and overly complicated. That’s why the new media feature range syntax is a more appropriate approach:

@media (width <= 320px) { /* styles for viewports <= 320px */ } @media (width > 320px) { /* styles for viewports > 320px */ } Wrapping up

Phew, we covered a lot of ground on the new syntax for targeting viewport width ranges in CSS Media Queries. Now that the Media Queries Level 4 specification has introduced the syntax and it’s been adopted in Firefox and Chromium browsers, we’re getting close to being able to use the new comparison operators and combining them with other range media features besides width, like height and aspect-ratio

And that’s just one of the newer features that the Level 4 specification introduced, alongside a bunch of queries we can make based on user preferences. It doesn’t end there! Check out the Complete Guide to CSS Media Queries for a sneak peek of what might be included in Media Queries Level 5.

The New CSS Media Query Range Syntax originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Fancy Image Decorations: Outlines and Complex Animations

Css Tricks - Fri, 10/28/2022 - 2:45am

We’ve spent the last two articles in this three-part series playing with gradients to make really neat image decorations using nothing but the <img> element. In this third and final piece, we are going to explore more techniques using the CSS outline property. That might sound odd because we generally use outline to draw a simple line around an element — sorta like border but it can only draw all four sides at once and is not part of the Box Model.

We can do more with it, though, and that’s what I want to experiment with in this article.

Fancy Image Decorations series

Let’s start with our first example — an overlay that disappears on hover with a cool animation:

CodePen Embed Fallback

We could accomplish this by adding an extra element over the image, but that’s what we’re challenging ourselves not to do in this series. Instead, we can reach for the CSS outline property and leverage that it can have a negative offset and is able to overlap its element.

img { --s: 250px; /* the size of the image */ --b: 8px; /* the border thickness*/ --g: 14px; /* the gap */ --c: #4ECDC4; width: var(--s); aspect-ratio: 1; outline: calc(var(--s) / 2) solid #0009; outline-offset: calc(var(--s) / -2); cursor: pointer; transition: 0.3s; } img:hover { outline: var(--b) solid var(--c); outline-offset: var(--g); }

The trick is to create an outline that’s as thick as half the image size, then offset it by half the image size with a negative value. Add in some semi-transparency with the color and we have our overlay!

The rest is what happens on :hover. We update the outline and the transition between both outlines creates the cool hover effect. The same technique can also be used to create a fading effect where we don’t move the outline but make it transparent.

CodePen Embed Fallback

Instead of using half the image size in this one, I am using a very big outline thickness value (100vmax) while applying a CSS mask. With this, there’s no longer a need to know the image size — it trick works at all sizes!

You may face issues using 100vmax as a big value in Safari. If it’s the case, consider the previous trick where you replace the 100vmax with half the image size.

We can take things even further! For example, instead of simply clipping the extra outline, we can create shapes and apply a fancy reveal animation.

CodePen Embed Fallback

Cool right? The outline is what creates the yellow overlay. The clip-path clips the extra outline to get the star shape. Then, on hover, we make the color transparent.

Oh, you want hearts instead? We can certainly do that!

CodePen Embed Fallback

Imagine all the possible combinations we can create. All we have to do is to draw a shape with a CSS mask and/or clip-path and combine it with the outline trick. One solution, infinite possibilities!

And, yes, we can definitely animate this as well. Let’s not forget that clip-path is animatable and mask relies on gradients — something we covered in super great detail in the first two articles of this series.

CodePen Embed Fallback

I know, the animation is a bit glitchy. This is more of a demo to illustrate the idea rather than the “final product” to be used in a production site. We’d wanna optimize things for a more natural transition.

Here is a demo that uses mask instead. It’s the one I teased you with at the end of the last article:

CodePen Embed Fallback

Did you know that the outline property was capable of so much awesomeness? Add it to your toolbox for fancy image decorations!

Combine all the things!

Now that we have learned many tricks using gradients, masks, clipping, and outline, it’s time for the grand finale. Let’s cap off this series by combine all that we have learned the past few weeks to showcase not only the techniques, but demonstrate just how flexible and modular these approaches are.

CodePen Embed Fallback CodePen Embed Fallback CodePen Embed Fallback CodePen Embed Fallback CodePen Embed Fallback

If you were seeing these demos for the first time, you might assume that there’s a bunch of extra divs wrappers and pseudo-elements being used to pull them off. But everything is happening directly on the <img> element. It’s the only selector we need to get these advanced shapes and effects!

Wrapping up

Well, geez, thanks for hanging out with me in this three-part series the past few weeks. We explored a slew of different techniques that turn simple images into something eye-catching and interactive. Will you use everything we covered? Certainly not! But my hope is that this has been a good exercise for you to dig into advanced uses of CSS features, like gradients, mask, clip-path, and outline.

And we did everything with just one <img> element! No extra div wrappers and pseudo-elements. Sure, it’s a constraint we put on ourselves, but it also pushed us to explore CSS and try to find innovative solutions to common use cases. So, before pumping extra markup into your HTML, think about whether CSS is already capable of handling the task.

Fancy Image Decorations series

Fancy Image Decorations: Outlines and Complex Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Holographic Trading Card Effect

Css Tricks - Wed, 10/26/2022 - 7:05am

Simon Goellner (@simeydotme)’s collection of Holographic Trading Cards have captured our attention.

Under the hood there is a suite of filter(), background-blend-mode(), mix-blend-mode(), and clip-path() combinations that have been painstakingly tweaked to reach the desired effect. I ended up using a little img { visibility: hidden; } in DevTools to get a better sense of each type of holographic effect.

Josh Dance (@JoshDance) replied with a breakdown of the effects that lets you manually control the inputs.

To Shared LinkPermalink on CSS-Tricks

Holographic Trading Card Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Creating Animated, Clickable Cards With the :has() Relational Pseudo Class

Css Tricks - Tue, 10/25/2022 - 4:15am

The CSS :has() pseudo class is rolling out in many browsers with Chrome and Safari already fully supporting it. It’s often referred to it as “the parent selector” — as in, we can select style a parent element from a child selector — but there is so much more that :has() can help us solve. One of those things is re-inventing the clickable card pattern many of us love to use from time to time.

We’ll take a look at how :has() can help us handle linked cards, but first…

What is this :has() pseudo class?

There is already a bunch of great posts floating around that do an excellent job explaining what :has() is and what it’s used for, but it’s still new enough that we ought to say a few words about it here as well.

:has() is a relational pseudo class that’s part of the W3C Selectors Level 4 working draft. That’s what the parentheses are all about: matching elements that are related to — or, more accurately, contain — certain child elements.

/* Matches an article element that contains an image element */ article:has(img) { } /* Matches an article element with an image contained immediately within it */ article:has(> img) { }

So, you can see why we might want to call it a “parent” selector. But we can also combine it with other functional pseudo classes to get more specific. Say we want to style articles that do not contain any images. We can combine the relational powers of :has() with the negation powers of :not() to do that:

/* Matches an article without images */ article:not(:has(img)) { }

But that’s just the start of how we can combine powers to do more with :has(). Before we turn specifically to solving the clickable card conundrum, let’s look at a few ways we currently approach them without using :has().

How we currently handle clickable cards

There are three main approaches on how people create a fully clickable card these days and to fully understand the power of this pseudo class, it’s nice to have a bit of a round-up.

The “Link as a Wrapper” approach

This approach is something used quite frequently. I never use this approach but I created a quick demo to demonstrate it:

CodePen Embed Fallback

There are a lot of concerns here, especially when it comes to accessibility. When users navigate your website using the rotor function, they will hear the full text inside of that <a> element — the heading, the text, and the link. Someone might not want to sit through all that. We can do better. Since HTML5, we can nest block elements inside of an <a> element. But it never feels right to me, especially for this reason.


  • Quick to implement
  • Semantically correct


  • Accessibility concerns
  • Text not selectable
  • A lot of hassle to overwrite styles that you used on your default links
The JavaScript method

Using JavaScript, we can attach a link to our card instead of writing it in the markup. I found this great CodePen demo by costdev who also made the card text selectable in the process:

CodePen Embed Fallback

This approach has a lot of benefits. Our links are accessible on focus and we can even select text. But there are some drawbacks when it comes to styling. If we want to animate those cards, for example, we would have to add :hover styles on our main .card wrapper instead of the link itself. We also would not benefit from the animations when the links are in focus from keyboard tabbing.


  • Can be made perfectly accessible
  • Ability to select text


  • Requires JavaScript
  • Right clicking not possible (although could be fixed with some extra scripting)
  • Will require a lot of styling on the card itself which would not work when focussing the link
The ::after selector approach

This method requires us to set the card with relative positioning, then set absolute positioning on the link’s ::after pseudo selector of a link. This doesn’t require any JavaScript and is pretty easy to implement:

CodePen Embed Fallback

There are a few drawbacks here, especially when it comes to selecting text. Unless you provide a higher z-index on your card-body, you won’t be able to select text but if you do, be warned that clicking the text will not activate your link. Whether or not you want selectable text is up to you. I think it can be a UX issue, but it depends on the use-case. The text is still accessible to screen readers but my main problem with the method is the lack of animation possibilities.


  • Easy to implement
  • Accessible link without bloated text
  • Works on hover and focus


  • Text is not selectable
  • You can only animate the link as this is the element you’re hovering.
A new approach: Using ::after with :has()

Now that we’ve established the existing approaches for clickable cards, I want to show how introducing :has() to the mix solves most of those shortcomings.

In fact, let’s base this approach on the last one we looked at using ::after on the link element. We can actually use :has() there to overcome that approach’s animation constraints.

Let’s start with the markup:

<article> <figure> <img src="cat.webp" alt="Fluffy gray and white tabby kitten snuggled up in a ball." /> </figure> <div clas="article-body"> <h2>Some Heading</h2> <p>Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.</p> <a href="#"> Read more <svg xmlns="" class="icon" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg> </a> </div> </article>

I will be keeping things as simple as possible by targeting elements in the CSS instead of classes.

For this demo, we’re going to add an image zoom and shadow to the card on hover, and animate the link with an arrow popping up and while changing the link’s text color. To make this easy, we’re going to add some custom properties scoped on our card. Here’s the basic styling:

/* The card element */ article { --img-scale: 1.001; --title-color: black; --link-icon-translate: -20px; --link-icon-opacity: 0; position: relative; border-radius: 16px; box-shadow: none; background: #fff; transform-origin: center; transition: all 0.4s ease-in-out; overflow: hidden; } /* The link's ::after pseudo */ article a::after { content: ""; position: absolute; inset-block: 0; inset-inline: 0; cursor: pointer; }

Great! We added an initial scale for the image (--img-scale: 1.001), the initial color of the card heading (--title-color: black) and some extra properties we will use to make our arrow pop out of the link. We’ve also set an empty state of the box-shadow declaration in order to animate it later . This sets up what we need for the clickable card right now, so let’s add some resets and styling to it by adding those custom properties to the elements we want to animate:

article h2 { margin: 0 0 18px 0; font-family: "Bebas Neue", cursive; font-size: 1.9rem; letter-spacing: 0.06em; color: var(--title-color); transition: color 0.3s ease-out; } article figure { margin: 0; padding: 0; aspect-ratio: 16 / 9; overflow: hidden; } article img { max-width: 100%; transform-origin: center; transform: scale(var(--img-scale)); transition: transform 0.4s ease-in-out; } article a { display: inline-flex; align-items: center; text-decoration: none; color: #28666e; } article a:focus { outline: 1px dotted #28666e; } article a .icon { min-width: 24px; width: 24px; height: 24px; margin-left: 5px; transform: translateX(var(--link-icon-translate)); opacity: var(--link-icon-opacity); transition: all 0.3s; } .article-body { padding: 24px; }

Let’s be kind to people and also add a screen reader class hidden behind the link:

.sr-only:not(:focus):not(:active) { clip: rect(0 0 0 0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; }

Our card is starting to look pretty sweet. It’s time to add a bit of magic to it. With the :has() pseudo class, we can now check if our link is hovered or focused, then update our custom properties and add a box-shadow. With this little chunk of CSS our card really comes to life:

/* Matches an article element that contains a hover or focus state */ article:has(:hover, :focus) { --img-scale: 1.1; --title-color: #28666e; --link-icon-translate: 0; --link-icon-opacity: 1; box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px; }

See what’s up there? Now we get the updated styles if any child element in the card is hovered or focused. And even though the link element is the only thing that can contain a hover or focus state in the ::after clickable card approach, we can use that to match the parent element and apply the transitions.

And there you have it. Just another powerful use case for the :has() selector. Not only can we match a parent element by declaring other elements as arguments, but we can match also use pseudos to match and style parents as well.


  • Accessible
  • Animatable
  • No JavaScript needed
  • Uses :hover on the correct element


  • Text is not easily selectable.
  • Browser support is limited to Chrome and Safari (it’s supported in Firefox behind a flag).

Here is a demo using this technique. You might notice an extra wrapper around the card, but that’s just me playing around with container queries, which is just one of those other fantastic things rolling out in all major browsers.

CodePen Embed Fallback

Got some other examples you wish to share? Other solutions or ideas are more than welcome in the comment section.

Creating Animated, Clickable Cards With the :has() Relational Pseudo Class originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Fancy Image Decorations: Masks and Advanced Hover Effects

Css Tricks - Fri, 10/21/2022 - 2:46am

Welcome to Part 2 of this three-part series! We are still decorating images without any extra elements and pseudo-elements. I hope you already took the time to digest Part 1 because we will continue working with a lot of gradients to create awesome visual effects. We are also going to introduce the CSS mask property for more complex decorations and hover effects.

Fancy Image Decorations series

Let’s turn to the first example we’re working on together…

The Postage Stamp CodePen Embed Fallback

Believe or not, all it takes to make postage stamp CSS effect is two gradients and a filter:

img { --r: 10px; /* control the radius of the circles */ padding: calc(2 * var(--r)); filter: grayscale(.4); background: radial-gradient(var(--r),#0000 98%,#fff) round calc(-1.5 * var(--r)) calc(-1.5 * var(--r)) / calc(3 * var(--r)) calc(3 * var(--r)), linear-gradient(#fff 0 0) no-repeat 50% / calc(100% - 3 * var(--r)) calc(100% - 3 * var(--r)); }

As we saw in the previous article, the first step is to make space around the image with padding so we can draw a background gradient and see it there. Then we use a combination of radial-gradient() and linear-gradient() to cut those circles around the image.

Here is a step-by-step illustration that shows how the gradients are configured:

CodePen Embed Fallback

Note the use of the round value in the second step. It’s very important for the trick as it ensures the size of the gradient is adjusted to be perfectly aligned on all the sides, no matter what the image width or height is.

From the specification: The image is repeated as often as will fit within the background positioning area. If it doesn’t fit a whole number of times, it is rescaled so that it does.

The Rounded Frame

Let’s look at another image decoration that uses circles…

CodePen Embed Fallback

This example also uses a radial-gradient(), but this time I have created circles around the image instead of the cut-out effect. Notice that I am also using the round value again. The trickiest part here is the transparent gap between the frame and the image, which is where I reach for the CSS mask property:

img { --s: 20px; /* size of the frame */ --g: 10px; /* the gap */ --c: #FA6900; padding: calc(var(--g) + var(--s)); background: radial-gradient(farthest-side, var(--c) 97%, #0000) 0 0 / calc(2 * var(--s)) calc(2 * var(--s)) round; mask: conic-gradient(from 90deg at calc(2 * var(--s)) calc(2 * var(--s)), #0000 25%, #000 0) calc(-1 * var(--s)) calc(-1 * var(--s)), linear-gradient(#000 0 0) content-box; }

Masking allows us to show the area of the image — thanks to the linear-gradient() in there — as well as 20px around each side of it — thanks to the conic-gradient(). The 20px is nothing but the variable --s that defines the size of the frame. In other words, we need to hide the gap.

Here’s what I mean:

CodePen Embed Fallback

The linear gradient is the blue part of the background while the conic gradient is the red part of the background. That transparent part between both gradients is what we cut from our element to create the illusion of an inner transparent border.

The Inner Transparent Border

For this one, we are not going to create a frame but rather try something different. We are going to create a transparent inner border inside our image. Probably not that useful in a real-world scenario, but it’s good practice with CSS masks.

CodePen Embed Fallback

Similar to the previous example, we are going to rely on two gradients: a linear-gradient() for the inner part, and a conic-gradient() for the outer part. We’ll leave a space between them to create the transparent border effect.

img { --b: 5px; /* the border thickness */ --d: 20px; /* the distance from the edge */ --_g: calc(100% - 2 * (var(--d) + var(--b))); mask: conic-gradient(from 90deg at var(--d) var(--d), #0000 25%, #000 0) 0 0 / calc(100% - var(--d)) calc(100% - var(--d)), linear-gradient(#000 0 0) 50% / var(--_g) var(--_g) no-repeat; }

You may have noticed that the conic gradient of this example has a different syntax from the previous example. Both are supposed to create the same shape, so why are they different? It’s because we can reach the same result using different syntaxes. This may look confusing at first, but it’s a good feature. You are not obliged to find the solution to achieve a particular shape. You only need to find one solution that works for you out of the many possibilities out there.

Here are four ways to create the outer square using gradients:

CodePen Embed Fallback

There are even more ways to pull this off, but you get the point.

There is no Best™ approach. Personally, I try to find the one with the smallest and most optimized code. For me, any solution that requires fewer gradients, fewer calculations, and fewer repeated values is the most suitable. Sometimes I choose a more verbose syntax because it gives me more flexibility to change variables and modify things. It comes with experience and practice. The more you play with gradients, the more you know what syntax to use and when.

Let’s get back to our inner transparent border and dig into the hover effect. In case you didn’t notice, there is a cool hover effect that moves that transparent border using a font-size trick. The idea is to define the --d variable with a value of 1em. This variables controls the distance of the border from the edge. We can transform like this:

--_d: calc(var(--d) + var(--s) * 1em)

…giving us the following updated CSS:

img { --b: 5px; /* the border thickness */ --d: 20px; /* the distance from the edge */ --o: 15px; /* the offset on hover */ --s: 1; /* the direction of the hover effect (+1 or -1)*/ --_d: calc(var(--d) + var(--s) * 1em); --_g: calc(100% - 2 * (var(--_d) + var(--b))); mask: conic-gradient(from 90deg at var(--_d) var(--_d), #0000 25%, #000 0) 0 0 / calc(100% - var(--_d)) calc(100% - var(--_d)), linear-gradient(#000 0 0) 50% / var(--_g) var(--_g) no-repeat; font-size: 0; transition: .35s; } img:hover { font-size: var(--o); }

The font-size is initially equal to 0 ,so 1em is also equal to 0 and --_d is be equal to --d. On hover, though, the font-size is equal to a value defined by an --o variable that sets the border’s offset. This, in turn, updates the --_d variable, moving the border by the offset. Then I add another variable, --s, to control the sign that decides whether the border moves to the inside or the outside.

CodePen Embed Fallback

The font-size trick is really useful if we want to animate properties that are otherwise unanimatable. Custom properties defined with @property can solve this but support for it is still lacking at the time I’m writing this.

The Frame Reveal

We made the following reveal animation in the first part of this series:

CodePen Embed Fallback

We can take the same idea, but instead of a border with a solid color we will use a gradient like this:

CodePen Embed Fallback

If you compare both codes you will notice the following changes:

  1. I used the same gradient configuration from the first example inside the mask property. I simply moved the gradients from the background property to the mask property.
  2. I added a repeating-linear-gradient() to create the gradient border.

That’s it! I re-used most of the same code we already saw — with super small tweaks — and got another cool image decoration with a hover effect.

/* Solid color border */ img { --c: #8A9B0F; /* the border color */ --b: 10px; /* the border thickness*/ --g: 5px; /* the gap on hover */ padding: calc(var(--g) + var(--b)); --_g: #0000 25%, var(--c) 0; background: conic-gradient(from 180deg at top var(--b) right var(--b), var(--_g)) var(--_i, 200%) 0 / 200% var(--_i, var(--b)) no-repeat, conic-gradient(at bottom var(--b) left var(--b), var(--_g)) 0 var(--_i, 200%) / var(--_i, var(--b)) 200% no-repeat; transition: .3s, background-position .3s .3s; cursor: pointer; } img:hover { --_i: 100%; transition: .3s, background-size .3s .3s; } /* Gradient color border */ img { --b: 10px; /* the border thickness*/ --g: 5px; /* the gap on hover */ background: repeating-linear-gradient(135deg, #F8CA00 0 10px, #E97F02 0 20px, #BD1550 0 30px); padding: calc(var(--g) + var(--b)); --_g: #0000 25%, #000 0; mask: conic-gradient(from 180deg at top var(--b) right var(--b), var(--_g)) var(--_i, 200%) 0 / 200% var(--_i, var(--b)) no-repeat, conic-gradient(at bottom var(--b) left var(--b), var(--_g)) 0 var(--_i, 200%) / var(--_i, var(--b)) 200% no-repeat, linear-gradient(#000 0 0) content-box; transition: .3s, mask-position .3s .3s; cursor: pointer; } img:hover { --_i: 100%; transition: .3s, mask-size .3s .3s; }

Let’s try another frame animation. This one is a bit tricky as it has a three-step animation:

CodePen Embed Fallback

The first step of the animation is to make the bottom edge bigger. For this, we adjust the background-size of a linear-gradient():

CodePen Embed Fallback

You are probably wondering why I am also adding the top edge. We need it for the third step. I always try to optimize the code I write, so I am using one gradient to cover both the top and bottom sides, but the top one is hidden and revealed later with a mask.

For the second step, we add a second gradient to show the left and right edges. But this time, we do it using background-position:

CodePen Embed Fallback

We can stop here as we already have a nice effect with two gradients but we are here to push the limits so let’s add a touch of mask to achieve the third step.

The trick is to make the top edge hidden until we show the bottom and the sides and then we update the mask-size (or mask-position) to show the top part. As I said previously, we can find a lot of gradient configurations to achieve the same effect.

Here is an illustration of the gradients I will be using:

CodePen Embed Fallback

I am using two conic gradients having a width equal to 200%. Both gradients cover the area leaving only the top part uncovered (that part will be invisible later). On hover, I slide both gradients to cover that part.

Here is a better illustration of one of the gradients to give you a better idea of what’s happening:

CodePen Embed Fallback

Now we put this inside the mask property and we are done! Here is the full code:

img { --b: 6px; /* the border thickness*/ --g: 10px; /* the gap */ --c: #0E8D94; padding: calc(var(--b) + var(--g)); --_l: var(--c) var(--b), #0000 0 calc(100% - var(--b)), var(--c) 0; background: linear-gradient(var(--_l)) 50%/calc(100% - var(--_i,80%)) 100% no-repeat, linear-gradient(90deg, var(--_l)) 50% var(--_i,-100%)/100% 200% no-repeat; mask: conic-gradient(at 50% var(--b),#0000 25%, #000 0) calc(50% + var(--_i, 50%)) / 200%, conic-gradient(at 50% var(--b),#000 75%, #0000 0) calc(50% - var(--_i, 50%)) / 200%; transition: .3s calc(.6s - var(--_t,.6s)) mask-position, .3s .3s background-position, .3s var(--_t,.6s) background-size, .4s transform; cursor: pointer; } img:hover { --_i: 0%; --_t: 0s; transform: scale(1.2); }

I have also introduced some variables to optimize the code, but you should be used to this right now.

What about a four-step animation? Yes, it’s possible!

CodePen Embed Fallback

No explanation for this because it’s your homework! Take all that you have learned in this article to dissect the code and try to articulate what it’s doing. The logic is similar to all the previous examples. The key is to isolate each gradient to understand each step of the animation. I kept the code un-optimized to make things a little easier to read. I do have an optimized version if you are interested, but you can also try to optimize the code yourself and compare it with my version for additional practice.

Wrapping up

That’s it for Part 2 of this three-part series on creative image decorations using only the <img> element. We now have a good handle on how gradients and masks can be combined to create awesome visual effects, and even animations — without reaching for extra elements or pseudo-elements. Yes, a single <img> tag is enough!

We have one more article in this series to go. Until then, here is a bonus demo with a cool hover effect where I use mask to assemble a broken image.

CodePen Embed Fallback Fancy Image Decorations series

Fancy Image Decorations: Masks and Advanced Hover Effects originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Responsive Animations for Every Screen Size and Device

Css Tricks - Thu, 10/20/2022 - 2:42am

Before I career jumped into development, I did a bunch of motion graphics work in After Effects. But even with that background, I still found animating on the web pretty baffling.

Video graphics are designed within a specific ratio and then exported out. Done! But there aren’t any “export settings” on the web. We just push the code out into the world and our animations have to adapt to whatever device they land on.

So let’s talk responsive animation! How do we best approach animating on the wild wild web? We’re going to cover some general approaches, some GSAP-specific tips and some motion principles. Let’s start off with some framing…

How will this animation be used?

Zach Saucier’s article on responsive animation recommends taking a step back to think about the final result before jumping into code.

Will the animation be a module that is repeated across multiple parts of your application? Does it need to scale at all? Keeping this in mind can help determine the method in which an animation should be scaled and keep you from wasting effort.

This is great advice. A huge part of designing responsive animation is knowing if and how that animation needs to scale, and then choosing the right approach from the start.

Most animations fall into the following categories:

  • Fixed: Animations for things like icons or loaders that retain the same size and aspect ratio across all devices. Nothing to worry about here! Hard-code some pixel values in there and get on with your day.
  • Fluid: Animations that need to adapt fluidly across different devices. Most layout animations fall into this category.
  • Targeted: Animations that are specific to a certain device or screen size, or change substantially at a certain breakpoint, such as desktop-only animations or interactions that rely on device-specific interaction, like touch or hover.

Fluid and targeted animations require different ways of thinking and solutions. Let’s take a look…

Fluid animation

As Andy Bell says: Be the browser’s mentor, not its micromanager — give the browser some solid rules and hints, then let it make the right decisions for the people that visit it. (Here are the slides from that presentation.)

Fluid animation is all about letting the browser do the hard work. A lot of animations can easily adjust to different contexts just by using the right units from the start. If you resize this pen you can see that the animation using viewport units scales fluidly as the browser adjusts:

CodePen Embed Fallback

The purple box even changes width at different breakpoints, but as we’re using percentages to move it, the animation scales along with it too.

Animating layout properties like left and top can cause layout reflows and jittery ‘janky’ animation, so where possible stick to transforms and opacity.

We’re not just limited to these units though — let’s take a look at some other possibilities.

SVG units

One of the things I love about working with SVG is that we can use SVG user units for animation which are responsive out of the box. The clue’s in the name really — Scalable Vector Graphic. In SVG-land, all elements are plotted at specific coordinates. SVG space is like an infinite bit of graph paper where we can arrange elements. The viewBox defines the dimensions of the graph paper we can see.

viewBox="0 0 100 50”

In this next demo, our SVG viewBox is 100 units wide and 50 units tall. This means if we animate the element by 100 units along the x-axis, it will always move by the entire width of its parent SVG, no matter how big or small that SVG is! Give the demo a resize to see.

CodePen Embed Fallback

Animating a child element based on a parent container’s width is a little tricker in HTML-land. Up until now, we’ve had to grab the parent’s width with JavaScript, which is easy enough when you’re animating from a transformed position, but a little fiddlier when you’re animating to somewhere as you can see in the following demo. If your end-point is a transformed position and you resize the screen, you’ll have to manually adjust that position. Messy… &#x1f914;

CodePen Embed Fallback

If you do adjust values on resize, remember to debounce, or even fire the function after the browser is finished resizing. Resize listeners fire a ton of events every second, so updating properties on each event is a lot of work for the browser.

But, this animation speed-bump is soon going to be a thing of the past! Drum roll please… &#x1f941;

After nearly 15 years as a highly-requested (impossible!?) feature, size-based Container Queries & units have shipped in both Chrome/Edge 105 & Safari 16! Firefox is not far behind.

(also: there's a prototype of style queries!)

— Mia (@TerribleMia) September 15, 2022

Container Units! Lovely stuff. At the time I’m writing this, they only work in Chrome and Safari — but maybe by the time you read this, we’ll have Firefox too. Check them out in action in this next demo. Look at those little lads go! Isn’t that exciting, animation that’s relative to the parent elements!

CodePen Embed Fallback

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

DesktopChromeFirefoxIEEdgeSafari105NoNo10516.0Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari107No10716.0 Fluid layout transitions with FLIP

As we mentioned earlier, in SVG-land every element is neatly placed on one grid and really easy to move around responsively. Over in HTML-land it’s much more complex. In order to build responsive layouts, we make use of a bunch of different positioning methods and layout systems. One of the main difficulties of animating on the web is that a lot of changes to layout are impossible to animate. Maybe an element needs to move from position relative to fixed, or some children of a flex container need to be smoothly shuffled around the viewport. Maybe an element even needs to be re-parented and moved to an entirely new position in the DOM.

Tricky, huh?

Well. The FLIP technique is here to save the day; it allows us to easily animate these impossible things. The basic premise is:

  • First: Grab the initial position of the elements involved in the transition.
  • Last: Move the elements and grab the final position.
  • Invert: Work out the changes between the first and last state and apply transforms to invert the elements back to their original position. This makes it look like the elements are still in the first position but they’re actually not.
  • Play: Remove the inverted transforms and animate to their faked first state to the last state.

Here’s a demo using GSAP’s FLIP plugin which does all the heavy lifting for you!

CodePen Embed Fallback

If you want to understand a little more about the vanilla implementation, head over to Paul Lewis’s blog post — he’s the brain behind the FLIP technique.

Fluidly scaling SVG

You got me… this isn’t really an animation tip. But setting the stage correctly is imperative for good animation! SVG scales super nicely by default, but we can control how it scales even further with preserveAspectRatio, which is mega handy when the SVG element’s aspect ratio and the viewBox aspect ratio are different. It works much in the same way as the background-position and background-size properties in CSS. The declaration is made up of an alignment value (background-position) and a Meet or Slice reference (background-size).

As for those Meet and Slice references — slice is like background size: cover, and meet is like background-size: contain.

  • preserveAspectRatio="MidYMax slice" — Align to the middle of the x-axis, the bottom of the y-axis, and scale up to cover the entire viewport.
  • preserveAspectRatio="MinYMin meet" — Align to the left of the x-axis, the top of the y-axis, and scale up while keeping the entire viewBox visible.
CodePen Embed Fallback

Tom Miller takes this a step further by using overflow: visible in CSS and a containing element to reveal “stage left” and “stage right” while keeping the height restricted:

For responsive SVG animations, it can be handy to make use of the SVG viewbox to create a view that crops and scales beneath a certain browser width, while also revealing more of the SVG animation to the right and left when the browser is wider than that threshold. We can achieve this by adding overflow visible on the SVG and teaming it up with a max-height wrapper to prevent the SVG from scaling too much vertically.

CodePen Embed Fallback Fluidly scaling canvas

Canvas is much more performant for complex animations with lots of moving parts than animating SVG or HTML DOM, but it’s inherently more complex too. You have to work for those performance gains! Unlike SVG that has lovely responsive units and scaling out of the box, <canvas> has to be bossed around and micromanaged a bit.

I like setting up my <canvas> so that it works much in the same way as SVG (I may be biased) with a lovely unit system to work within and a fixed aspect ratio. <canvas> also needs to be redrawn every time something changes, so remember to delay the redraw until the browser is finished resizing, or debounce!

CodePen Embed Fallback

George Francis also put together this lovely little library which allows you to define a Canvas viewBox attribute and preserveAspectRatio — exactly like SVG!

Targeted animation

You may sometimes need to take a less fluid and more directed approach to your animation. Mobile devices have a lot less real estate, and less animation-juice performance-wise than a desktop machine. So it makes sense to serve reduced animation to mobile users, potentially even no animation:

Sometimes the best responsive animation for mobile is no animation at all! For mobile UX, prioritize letting the user quickly consume content versus waiting for animations to finish. Mobile animations should enhance content, navigation, and interactions rather than delay it. Eric van Holtz

In order to do this, we can make use of media queries to target specific viewport sizes just like we do when we’re styling with CSS! Here’s a simple demo showing a CSS animation being handled using media queries and a GSAP animation being handled with gsap.matchMedia():

CodePen Embed Fallback

The simplicity of this demo is hiding a bunch of magic! JavaScript animations require a bit more setup and clean-up in order to correctly work at only one specific screen size. I’ve seen horrors in the past where people have just hidden the animation from view in CSS with opacity: 0, but the animation’s still chugging away in the background using up resources. &#x1f631;

If the screen size doesn’t match anymore, the animation needs to be killed and released for garbage collection, and the elements affected by the animation need to be cleared of any motion-introduced inline styles in order to prevent conflicts with other styling. Up until gsap.matchMedia(), this was a fiddly process. We had to keep track of each animation and manage all this manually.

gsap.matchMedia() instead lets you easily tuck your animation code into a function that only executes when a particular media query matches. Then, when it no longer matches, all the GSAP animations and ScrollTriggers in that function get reverted automatically. The media query that the animations are popped into does all the hard work for you. It’s in GSAP 3.11.0 and it’s a game changer!

We aren’t just constrained to screen sizes either. There are a ton of media features out there to hook into!

(prefers-reduced-motion) /* find out if the user would prefer less animation */ (orientation: portrait) /* check the user's device orientation */ (max-resolution: 300dpi) /* check the pixel density of the device */

In the following demo we’ve added a check for prefers-reduced-motion so that any users who find animation disorienting won’t be bothered by things whizzing around.

CodePen Embed Fallback

And check out Tom Miller’s other fun demo where he’s using the device’s aspect ratio to adjust the animation:

CodePen Embed Fallback Thinking outside of the box, beyond screen sizes

There’s more to thinking about responsive animation than just screen sizes. Different devices allow for different interactions, and it’s easy to get in a bit of a tangle when you don’t consider that. If you’re creating hover states in CSS, you can use the hover media feature to test whether the user’s primary input mechanism can hover over elements.

@media (hover: hover) { /* CSS hover state here */ }

Some advice from Jake Whiteley:

A lot of the time we base our animations on browser width, making the naive assumption that desktop users want hover states. I’ve personally had a lot of issues in the past where I would switch to desktop layout >1024px, but might do touch detection in JS – leading to a mismatch where the layout was for desktops, but the JS was for mobiles. These days I lean on hover and pointer to ensure parity and handle ipad Pros or windows surfaces (which can change the pointer type depending on whether the cover is down or not)

/* any touch device: */ (hover: none) and (pointer: coarse) /* iPad Pro */ (hover: none) and (pointer: coarse) and (min-width: 1024px)

I’ll then marry up my CSS layout queries and my JavaScript queries so I’m considering the input device as the primary factor supported by width, rather than the opposite.

ScrollTrigger tips

If you’re using GSAP’s ScrollTrigger plugin, there’s a handy little utility you can hook into to easily discern the touch capabilities of the device: ScrollTrigger.isTouch.

  • 0 – no touch (pointer/mouse only)
  • 1 – touch-only device (like a phone)
  • 2 – device can accept touch input and mouse/pointer (like Windows tablets)
if (ScrollTrigger.isTouch) { // any touch-capable device... } // or get more specific: if (ScrollTrigger.isTouch === 1) { // touch-only device }

Another tip for responsive scroll-triggered animation…

The following demo below is moving an image gallery horizontally, but the width changes depending on screen size. If you resize the screen when you’re halfway through a scrubbed animation, you can end up with broken animations and stale values. This is a common speedbump, but one that’s easily solved! Pop the calculation that’s dependent on screen size into a functional value and set invalidateOnRefresh:true. That way, ScrollTrigger will re-calculate that value for you when the browser resizes.

CodePen Embed Fallback Bonus GSAP nerd tip!

On mobile devices, the browser address bar usually shows and hides as you scroll. This counts as a resize event and will fire off a ScrollTrigger.refresh(). This might not be ideal as it can cause jumps in your animation. GSAP 3.10 added ignoreMobileResize. It doesn’t affect how the browser bar behaves, but it prevents ScrollTrigger.refresh() from firing for small vertical resizes on touch-only devices.

ScrollTrigger.config({ ignoreMobileResize: true }); Motion principles

I thought I’d leave you with some best practices to consider when working with motion on the web.

Distance and easing

A small but important thing that’s easy to forget with responsive animation is the relationship between speed, momentum, and distance! Good animation should mimic the real world to feel believable, and it takes a longer in the real world to cover a larger distance. Pay attention to the distance your animation is traveling, and make sure that the duration and easing used makes sense in context with other animations.

You can also often apply more dramatic easing to elements with further to travel to show the increased momentum:

CodePen Embed Fallback

For certain use cases it may be helpful to adjust the duration more dynamically based on screen width. In this next demo we’re making use of gsap.utils to clamp the value we get back from the current window.innerWidth into a reasonable range, then we’re mapping that number to a duration.

CodePen Embed Fallback Spacing and quantity

Another thing to keep in mind is the spacing and quantity of elements at different screen sizes. Quoting Steven Shaw:

If you have some kind of environmental animation (parallax, clouds, trees, confetti, decorations, etc) that are spaced around the window, make sure that they scale and/or adjust the quantity depending on screen size. Large screens probably need more elements spread throughout, while small screens only need a few for the same effect.

I love how Opher Vishnia thinks about animation as a stage. Adding and removing elements doesn’t just have to be a formality, it can be part of the overall choreography.

When designing responsive animations, the challenge is not how to cram the same content into the viewport so that it “fits”, but rather how to curate the set of existing content so it communicates the same intention. That means making a conscious choice of which pieces content to add, and which to remove. Usually in the world of animation things don’t just pop in or out of the frame. It makes sense to think of elements as entering or exiting the “stage”, animating that transition in a way that makes visual and thematic sense.

CodePen Embed Fallback

And that’s the lot. If you have any more responsive animation tips, pop them in the comment section. If there’s anything super helpful, I’ll add them to this compendium of information!


One more note from Tom Miller as I was prepping this article:

I’m probably too late with this tip for your responsive animations article, but I highly recommend “finalize all the animations before building”. I’m currently retrofitting some site animations with “mobile versions”. Thank goodness for gsap.matchMedia… but I sure wish we’d known there’d be separate mobile layouts/animations from the beginning.

I think we all appreciate that this tip to “plan ahead” came at the absolute last minute. Thanks, Tom, and best of luck with those retrofits.

Responsive Animations for Every Screen Size and Device originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

How to Make a Folder “Slit” Effect With CSS

Css Tricks - Wed, 10/19/2022 - 2:56am

When you put something — say a regular sheet of paper — in a manilla folder, a part of that thing might peek out of the folder a little bit. The same sort of thing with a wallet and credit cards. The cards poke out just a smidge so you can get a quick glance of which cards you’re carrying.

Credit: Stephen Phillips on Unsplash

I call this sort of thing a “slit”. A slit is where we create the illusion of an opening through which we can tease a visual element peeking out of it. And we can do that in CSS!

The crucial part of the design is the shadow, which is what gives the visual cue of there being a slit. Then there’s the cover for the slit which provides the space for the exhibited element to peek through from under.

Here’s what we’re going to make together:

CodePen Embed Fallback Let’s begin with creating the shadow

You might be surprised that the shadow in the example is not created with an actual CSS shadow, like box-shadow or a drop-shadow() filter. Instead, the shadow is a separate element in itself, dark and blurred out. This is important in order to make the design more adaptable, both in its default and animated states.

The cover is the other element in the design. The cover is what I call the element that overlaps the shadow. Here’s a figure depicting how the shadow and cover come together.

The shadow is made from a small upright rectangle that has a gradient background. The gradient is darker in the middle. So when the element is blurred, it creates a shadow that’s darker in the middle; hence more dimensional.

CodePen Embed Fallback

Now, the left half of the recreated shadow is covered with a rectangle on top, colored exactly the same as the background of its container.

Both the cover and the shadow are then moved to the left ever so slightly so it appears to be layered

Working on the cover

For the cover to blend with the design’s background, its background color is inherited from its containing element. Alternatively, you can also try to blend the cover to its background using standards like CSS masks and blend modes, depending on your design choices and requirements.

CodePen Embed Fallback

To learn some basics on how these standards might be applied, you can refer to these articles: Sarah Drasner’s “Masking vs. Clipping: When to Use Each” provides an excellent primer on masks. I’ve also written about CSS blend modes in this article where you can brush up on the topic.

In the source code of my example, you’ll notice that I aligned and stacked the elements inside the <main> element using CSS Grid, which is a layout standard I often use in my demos. If you’re recreating a similar design, use a layout method that fits the best for your application to align the different parts of the design. In this case, I’ve set up a single-column grid on the <main> element which allows me to center align the cover, shadow, and image.

What CSS Grid also allows me to do is set all three of those divs so they are all full-width in the <main> container:

main > div { grid-area: 1 / 1; }

This gets everything to stack on top of one another. Normally, we work hard to avoid covering elements with other elements in a grid. But this example relies on it. I’ve given the .slit-cover at width of 50% which naturally reveals the image underneath it. From there, I set a transform on it that moves it 50% in the negative direction, plus the small amount I shifted the shadow earlier (25px) to make sure that is revealed as well.

.slit-cover { width: 50%; transform: translatex(calc(-50% - 25px)); /* etc. */ }

And there we have it! A pretty natural-looking slit that mimics something peeking out of a folder, wallet, or whatever.

CodePen Embed Fallback

There are more ways to do this! For one, Flexbox can get elements to line up in a row and align in the center like this. There are lots of ways to get things side-by-side. And maybe you have a way to use the box-shadow property, drop-shadow() filter, or even SVG filters to get the same sort of shadow effect that really sells the illusion.

And you can totally riff on this to get your own look and feel. For example, try swapping the position of the shadow and image. Or play with the color combinations and change the blur() filter value. The shape of the cover and the shadow can also be tweaked — I bet you can create a curved shadow instead of a straight one and share it with us in the comments!

How to Make a Folder “Slit” Effect With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Making friends with SideNote

Typography - Tue, 10/18/2022 - 2:46pm

Read the book, Typographic Firsts

Meet the brand new SideNote typeface by Jamie Clarke Type. A warm, friendly, approachable and incredibly versatile new font family that is informal without being comic. It's a kind of humanist sans meets handwriting font that gets a thumbs-up, 5-star rating.

The post Making friends with SideNote appeared first on I Love Typography.

Pure CSS Bezier Curve Motion Paths

Css Tricks - Mon, 10/17/2022 - 5:30am

Are you a Bezier curve lover like I am?

CodePen Embed Fallback

Besides being elegant, Bezier curves have nice mathematical properties due to their definition and construction. No wonder they are widely used in so many areas:

  • As a drawing/design tool: They are often refered to as “paths” in vector drawing software.
  • As a format of representing curves: They are used in SVG, fonts and many other vector graphic formats.
  • As a mathematical function: Often used to control animation timing.

Now, how about using Bezier curves as motion paths with CSS?

Quick Recap

Depending on the context, when referring to a “Bezier curve”, we often assume a 2D cubic Bezier curve.

Such a curve is defined by four points:

MarianSigler, Public domain, via Wikimedia Commons

Note: In this article, we generally refer to P0 and P3 as endpoints, P1 and P2 as control points.

The word “cubic” means the underlying function of the curve is a cubic polynomial. There are also “quadratic” Bezier curves, which are similar, but with one fewer control point.

The Problem

Say you are given an arbitrary 2D cubic Beizer curve, how would you animate an element with pure CSS animation, such that it moves precisely along the curve?

As an example, how would you recreate this animation?

CodePen Embed Fallback

In this article we will explore three methods with different flavors. For each solution we will present an interactive demo, then explain how it works. There are lots of mathematical calculations and proofs behind the scene, but don’t worry, we will not go very deep.

Let’s start!

Method 1: Time Warp

Here’s the basic idea:

  • Set up @keyframes to move the element from one endpoint of the curve to the other.
  • Distort the time for each coordinate individually, using animation-timing-function.

Note: There are lots of examples and explanations in Temani Afif’s article (2021).

Using the cubic-bezier() function with correct parameters, we can create a motion path of any cubic Bezier curve:

CodePen Embed Fallback

This demo shows a pure CSS animation. Yet canvas and JavaScript are used, which serve two purposes:

  • Visualize the underlying Bezier curve (red curve).
  • Allow adjusting the curve with the typical “path” UI.

You can drag the two endpoints (black dots) and the two control points (black squares). The JavaScript code will update the animation accordingly, by updating a few CSS variables.

Note: Here’s a pure CSS version for reference.

How it works

Suppose the desired cubic Bezier curve is defined by four points: p0, p1, p2, and p3. We set up CSS rules as following:

/* pseudo CSS code */ div { animation-name: move-x, move-y; /* Define: f(x, a, b) = (x - a) / (b - a) qx1 = f(p1.x, p0.x, p3.x) qx2 = f(p2.x, p0.x, p3.x) qy1 = f(p1.y, p0.y, p3.y) qy2 = f(p2.y, p0.y, p3.y) */ animation-timing-function: cubic-bezier(1/3, qx1, 2/3, qx1), cubic-bezier(1/3, qy1, 2/3, qy2); } @keyframes move-x { from { left: p0.x; } to { left: p3.x; } } @keyframes move-y { from { top: p0.y; } to { top: p3.y; } }

The @keyframes rules move-x and move-y determine the starting and finishing locations of the element. In animation-timing-function we have two magical cubic-bezier() functions, the parameters are calculated such that both top and left always have the correct values at any time.

I’ll skip the math, but I drafted a brief proof here, for your curious math minds.


This method should work well for most cases. You can even make a 3D cubic Bezier curve, by introducing another animation for the z value.

However there are a few minor caveats:

  • It does not work when both endpoints lie on a horizontal or vertical line, because of the division-by-zero error.

Note: In practice, you can just add a tiny offset as a workaround.

  • It does not support Bezier curves with an order higher than 3.
  • Options for animation timing are limited.
    • We use 1/3 and 2/3 above to achieve a linear timing.
    • You can tweak both values to adjust the timing, but it is limited compared with other methods. More on this later.
Method 2: Competing Animations

As a warm-up, imagine an element with two animations:

div { animation-name: move1, move2; }

What is the motion path of the element, if the animations are defined as following:

@keyframes move1 { to { left: 256px; } } @keyframes move2 { to { top: 256px; } }

As you may have guessed, it moves diagonally:

CodePen Embed Fallback

Now, what if the animations are defined like this instead:

@keyframes move1 { to { transform: translateX(256px); } } @keyframes move2 { to { transform: translateY(256px); } }

“Aha, you cannot trick me!” you might say, as you noticed that both animations are changing the same property, “move2 must override move1 like this:”

CodePen Embed Fallback

Well, earlier I had thought so, too. But actually we get this:

CodePen Embed Fallback

The trick is that move2 does not have a from frame, which means the starting position is animated by move1.

In the following demo, the starting position of move2 is visualized as the moving blue dot:

CodePen Embed Fallback Quadratic Bezier Curves

The demo right above resembles the construction of a quadratic Bezier curve:

Phil Tregoning, Public domain, via Wikimedia Commons

But they look different. The construction has three linearly moving dots (two green, one black), but our demo has only two (the blue dot and the target element).

Actually the motion path in the demo is a quadratic Bezier curve, we just need to tune the keyframes carefully. I will skip the math and just reveal the magic:

Suppose a quadratic Bezier curve is defined by points p0, p1, and p2. In order to move an element along the curve, we do the following:

/* pseudo-CSS code */ div { animation-name: move1, move2; } @keyframes move1 { from { transform: translate3d(p0.x, p0.y, p0.z); } /* define q1 = (2 * p1 - p2) */ to { transform: translate3d(q1.x, q1.y, q1.z); } } @keyframes move2 { to { transform: translate3d(p2.x, p2.y, p2.z); } } CodePen Embed Fallback

Similar to the demo of Method 1, you can view or adjust the curve. Additionally, the demo also shows two more pieces of information:

  • The mathematical construction (gray moving parts)
  • The CSS animations (blue parts)

Both can be toggled using the checkboxes.

Cubic Bezier Curves

This method works for cubic Bezier curves as well. If the curve is defined by points p0, p1, p2, and p3. The animations should be defined like this:

/* pseudo-CSS code */ div { animation-name: move1, move2, move3; } @keyframes move1 { from { transform: translate3d(p0.x, p0.y, p0.z); } /* define q1 = (3 * p1 - 3 * p2 + p3) */ to { transform: translate3d(q1.x, q1.y, q1.z); } } @keyframes move2 { /* define q2 = (3 * p2 - 2 * p3) */ to { transform: translate3d(q2.x, q2.y, q2.z); } } @keyframes move3 { to { transform: translate3d(p3.x, p3.y, p3.z); } } CodePen Embed Fallback Extensions

What about 3D Bezier Curves? Actually, the truth is, all the previous examples were 3D curves, we just never bothered with the z values.

What about higher-order Bezier curves? I am 90% sure that the method can be naturally extended to higher orders. Please let me know if you have worked out the formula for fourth-order Bezier curves, or even better, a generic formula for Bezier curves of order N.

Method 3: Standard Bezier Curve Construction

The mathematical construction of Bezier Curves already gives us a good hint.

Phil Tregoning, Public domain, via Wikimedia Commons

Step-by-step, we can determine the coordinates of all moving dots. First, we determine the location of the green dot that is moving between p0 and p1:

@keyframes green0 { from { --green0x: var(--p0x); --green0y: var(--p0y); } to { --green0x: var(--p1x); --green0y: var(--p1y); } }

Additional green dots can be constructed in a similar way.

Next, we can determine the location of a blue dot like this:

@keyframes blue0 { from { --blue0x: var(--green0x); --blue0y: var(--green0y); } to { --blue0x: var(--green1x); --blue0y: var(--green1y); } }

Rinse and repeat, eventually we will get the desired curve.

CodePen Embed Fallback

Similar to Method 2, with this method we can easily build a 3D Bezier Curve. It is also intuitive to extend the method for higher-order Bezier curves.

The only downside is the usage of @property, which is not supported by all browsers.

Animation Timing

All the examples so far have the “linear” timing, what about easing or other timing functions?

Note: By “linear” we mean the variable t of the curve linearly changes from 0 to 1. In other words, t is the same as animation progress.

animation-timing-function is never used in Method 2 and Method 3. Like other CSS animations, we can use any supported timing function here, but we need to apply the same function for all animations (move1, move2, and move3) at the same time.

Here’s an example of animation-timing-function: cubic-bezier(1, 0.1, 0, 0.9):

CodePen Embed Fallback

And here’s how it looks like with animation-timing-function: steps(18, end):

CodePen Embed Fallback

On the other hand, Method 1 is trickier, because it already uses a cubic-bezier(u1, v1, u2, v2) timing function. In the examples above we have u1=1/3 and u2=2/3. In fact we can tweak the timing by changing both parameters. Again, all animations (e.g., move-x and move-y) must have the same values of u1 and u2.

Here’s how it looks like when u1=1 and u2=0:

CodePen Embed Fallback

With Method 2, we can achieve exactly the same effect by setting animation-timing-function to cubic-bezier(1, 0.333, 0, 0.667):

CodePen Embed Fallback

In fact, it works in a more general way:

Suppose that we are given a cubic Bezier curve, and we created two animations for the curve with Method 1 and Method 2 respectively. For any valid values of u1 and u2, the following two setups have the same animation timing:

  • Method 1 with animation-timing-function: cubic-bezier(u1, *, u2, *).
  • Method 2 with animation-timing-function: cubic-bezier(u1, 1/3, u2, 2/3).

Now we see why Method 1 is “limited”: with Method 1 we can only cubic-bezier() with two parameters, but with Method 2 and Method 3 we can use any CSS animation-timing-function.


In this article, we discussed 3 different methods of moving elements precisely along a Bezier curve, using only CSS animations.

While all 3 methods are more or less practical, they have their own pros and cons:

  • Method 1 might be more intuitive for those familiar with the timing function hack. But it is less flexible with animation timing.
  • Method 2 has very simple CSS rules. Any CSS timing function can be applied directly. However, it could be hard to remember the formulas.
  • Method 3 make more sense for those familiar with the math construction of Bezier curves. Animation timing is also flexible. On the other hand, not all modern browsers are supported, due the usage of @property.

That’s all! I hope you find this article interesting. Please let me know your thoughts!

Pure CSS Bezier Curve Motion Paths originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Behind the CSScenes, October 2022

Css Tricks - Fri, 10/14/2022 - 11:40am

Well, hey, welcome back to Behind the CSScenes! These posts are like little check-ins we’re doing each month to give you a peek behind what we’re doing here at CSS-Tricks, as well as a chance for us to pause and celebrate a few things.

Last month, we shared a small taste of a redesign for this very site. Thanks to all the folks who wrote in to comment on it! Seems the overwhelming response is pretty dang positive, though the background color was a mixed bag of reactions.

There’s more work to do, of course! This month, we have a little more to share with you on the development side of things, plus a roundup of some recent activity around here. So, let’s dial in Haley Mills for the latest.

Content updates

[Haley:] September was a busy month for our team! We published 16 articles by guest authors and another 15 by folks on our team. In addition to these, we’ve been updating many of the older articles, including adding freshly-linked resources. We’ve already updated over 50 articles and will continue these efforts as part of our day-to-day work. Thank you to the team and the awesome authors who contributed this content!

To build on our efforts to provide front-end developers with the tools they need, I’m also super excited to share that we are starting to kick the tires on new guides. Guides have been a free, helpful resource to the CSS-Tricks Community since the first one was published in 2019, and we’ve known from day 1 that we need to keep this tradition going. Since this is a new process for our team, we want to ensure that the workflow for authors is completely snag-free before opening it up to the public to apply. Until then, we will be working with hand-selected authors to bring you new guides on a variety of topics.

Have a suggestion for a guide that you’d love to see? Let us know in the comments!

Finally, you might’ve heard the news that CloudWays is joining DigitalOcean and is offering a $50 credit to folks in our community! While our top priority will always be to provide platform-agnostic resources to CSS-Tricks readers, you can expect to see helpful content about CloudWays in the future because we truly believe that their managed solution could be a great fit for the community.

Thanks for reading the content updates! Next up we have our senior web developer, David Berg, with an update on the back-end work we’re doing to move CSS-Tricks to a new CMS.

What’s happening on the back end

[David Berg:] The DigitalOcean team is actively working to pluck CSS-Tricks from WordPress and drop it into the same hand-rolled CMS we use for our other sites, including DigitalOcean’s library of tutorials. Don’t worry! CSS-Tricks will still be the same standalone site at the same domain, hyphenated and all. But it’s a ton of work, as you might imagine!

If anything has been challenging so far, it’s been aligning the WordPress data from the current site with the structure of our internal system. Our team currently uses a properly vetted, maintained, and organized PostgreSQL database that interfaces with the client through an array of in-repository services over which we maintain tight code control and quality. The WordPress database structure is unintuitive — at least to me and our team — and has required our team to find obscure and maybe overly complex solutions to mash these things together.

That said, migrating CSS-Tricks content over to our in-house solution allows us to ensure the integrity and future-proofing of complex database relationships. Through this process, we can slim down the time it takes to query the database, improve efficiencies of three (or more)-dimensional relationships, and accurately model new relationships according to new features we might develop down the road.

In short, that means we will no longer be reliant on a monolithic WordPress instance to serve a response to every request. We can statically export all the publicly accessible content to a CDN, with the services handling edit operations only when needed.

It’s easier to submit article proposals!

[Haley:] Speaking of a more robust back end architecture, something else happening behind the scenes is a new form for guest authors submitting article proposals to us for publication.

We used a form before this one, but we had it tied up with Jira in a way that helps us manage the proposals and keep track of where they are in the editing flow. That integration was crumbling right before our eyes, so we went with a Typeform-powered version instead.

While it might seem like a fairly minor thing, it’s a big deal as far as making it easier to share your proposals and making sure nothing falls through the cracks — so we have fresh new front-end content to publish for you on a consistent basis!

Oh, and if you happen to submit a proposal (and you should!) please let us know if you see any opportunities for us to make it even easier and more helpful.

Passing it back to Geoff with my favorite part of these updates: author highlights!

Some fresh new faces around here

[Geoff:] The articles you read here at CSS-Tricks are written by folks like yourself. It’s amazing just how gosh darned smart this community is and all the ideas that get passed around here. In fact, we welcomed 5 new voices this past month:

What a great bunch, right? Give ‘em all some love for taking time out of their busy lives to share their wisdom and clever tricks with the rest of us. And a shout out to familiar faces, like Temani Afif, Preethi, Ollie Williams, and Mojtaba Seyedi for all the hard work they continue to do that keeps pushing this thing we call front-end development forward.

High fives to all these folks, and to you for reading. ✋ We wouldn’t be doing this without y’all.

Behind the CSScenes, October 2022 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Fancy Image Decorations: Single Element Magic

Css Tricks - Fri, 10/14/2022 - 5:37am

As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? Decorate an image using only the <img> tag and nothing more.

That right, no extra markup, no divs, and no pseudo-elements. Just the one tag.

Sounds difficult, right? But by the end of this article — and the others that make up this little series — I’ll prove that CSS is powerful enough to give us great and stunning results despite the limitation of working with a single element.

Fancy Image Decorations series Let’s start with our first example CodePen Embed Fallback

Before digging into the code let’s enumerate the possibilities for styling an <img> without any extra elements or pseudo-elements. We can use border, box-shadow, outline, and, of course, background. It may look strange to add a background to an image because we cannot see it as it will be behind the image — but the trick is to create space around the image using padding and/or border and then draw our background inside that space.

I think you know what comes next since I talked about background, right? Yes, gradients! All the decorations we are going to make rely on a lot of gradients. If you’ve followed me for a while, I think this probably comes as no surprise to you at all. &#x1f601;

Let’s get back to our first example:

img { --s: 10px; /* control the size */ padding: var(--s); border: calc(2 * var(--s)) solid #0000; outline: 1px solid #000; outline-offset: calc(-1 * var(--s)); background: conic-gradient(from 90deg at 1px 1px, #0000 25%, #000 0); }

We are defining padding and a transparent border using the variable --s to create a space around our image equal to three times that variable.

Why are we using both padding and border instead of one or the other? We can get by using only one of them but I need this combination for my gradient because, by default, the initial value of background-clip is border-box and background-origin is equal to padding-box.

Here is a step-by-step illustration to understand the logic:

CodePen Embed Fallback

Initially, we don’t have any borders on the image, so our gradient will create two segments with 1px of thickness. (I am using 3px in this specific demo so it’s easier to see.) We add a colored border and the gradient still gives us the same result inside the padding area (due to background-origin) but it repeats behind the border. If we make the color of the border transparent, we can use the repetition and we get the frame we want.

The outline in the demo has a negative offset. That creates a square shape at the top of the gradient. That’s it! We added a nice decoration to our image using one gradient and an outline. We could have used more gradients! But I always try to keep my code as simple as possible and I found that adding an outline is better that way.

Here is a gradient-only solution where I am using only padding to define the space. Still the same result but with a more complex syntax.

CodePen Embed Fallback

Let’s try another idea:

CodePen Embed Fallback

For this one, I took the previous example removed the outline, and applied a clip-path to cut the gradient on each side. The clip-path value is a bit verbose and confusing but here is an illustration to better see its points:

I think you get the main idea. We are going to combine backgrounds, outlines, clipping, and some masking to achieve different kinds of decorations. We are also going to consider some cool hover animations as an added bonus! What we’ve looked at so far is merely a small overview of what’s coming!

The Corner-Only Frame CodePen Embed Fallback

This one takes four gradients. Each gradient covers one corner and, on hover, we expand them to create a full frame around the image. Let’s dissect the code for one of the gradients:

--b: 5px; /* border thickness */ background: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 90deg, darkblue 0) 0 0; background-size: 50px 50px; background-repeat: no-repeat;

We are going to draw a gradient with a size equal to 50px 50px and place it at the top-left corner (0 0). For the gradient’s configuration, here’s a step-by-step illustration showing how I reached that result.

CodePen Embed Fallback

We tend to think that gradients are only good for transitioning between two colors. But in reality, we can do so much more with them! They are especially useful when it comes to creating different shapes. The trick is to make sure we have hard stops between colors — like in the example above — rather than smooth transitions:

#0000 25%, darkblue 0

This is basically saying: “fill the gradient with a transparent color until 25% of the area, then fill the remaining area with darkblue.

You might be scratching your head over the 0 value. It’s a little hack to simplify the syntax. In reality, we should use this to make a hard stop between colors:

#0000 25%, darkblue 25%

That is more logical! The transparent color ends at 25% and darkblue starts exactly where the transparency ends, making a hard stop. If we replace the second one with 0, the browser will do the job for us, so it is a slightly more efficient way to go about it.

Somewhere in the specification, it says:

if a color stop or transition hint has a position that is less than the specified position of any color stop or transition hint before it in the list, set its position to be equal to the largest specified position of any color stop or transition hint before it.

0 is always smaller than any other value, so the browser will always convert it to the largest value that comes before it in the declaration. In our case, that number is 25%.

Now, we apply the same logic to all the corners and we end with the following code:

img { --b: 5px; /* border thickness */ --c: #0000 90deg, darkblue 0; /* define the color here */ padding: 10px; background: conic-gradient(from 90deg at top var(--b) left var(--b), var(--c)) 0 0, conic-gradient(from 180deg at top var(--b) right var(--b), var(--c)) 100% 0, conic-gradient(from 0deg at bottom var(--b) left var(--b), var(--c)) 0 100%, conic-gradient(from -90deg at bottom var(--b) right var(--b), var(--c)) 100% 100%; background-size: 50px 50px; /* adjust border length here */ background-repeat: no-repeat; }

I have introduced CSS variables to avoid some redundancy as all the gradients use the same color configuration.

For the hover effect, all I’m doing is increasing the size of the gradients to create the full frame:

img:hover { background-size: 51% 51%; }

Yes, it’s 51% instead of 50% — that creates a small overlap and avoids possible gaps.

Let’s try another idea using the same technique:

CodePen Embed Fallback

This time we are using only two gradients, but with a more complex animation. First, we update the position of each gradient, then increase their sizes to create the full frame. I also introduced more variables for better control over the color, size, thickness, and even the gap between the image and the frame.

img { --b: 8px; /* border thickness*/ --s: 60px; /* size of the corner*/ --g: 14px; /* the gap*/ --c: #EDC951; padding: calc(var(--b) + var(--g)); background-image: conic-gradient(from 90deg at top var(--b) left var(--b), #0000 25%, var(--c) 0), conic-gradient(from -90deg at bottom var(--b) right var(--b), #0000 25%, var(--c) 0); background-position: var(--_p, 0%) var(--_p, 0%), calc(100% - var(--_p, 0%)) calc(100% - var(--_p, 0%)); background-size: var(--s) var(--s); background-repeat: no-repeat; transition: background-position .3s var(--_i,.3s), background-size .3s calc(.3s - var(--_i, .3s)); } img:hover { background-size: calc(100% - var(--g)) calc(100% - var(--g)); --_p: calc(var(--g) / 2); --_i: 0s; }

Why do the --_i and --_p variables have an underscore in their name? The underscores are part of a naming convention I use to consider “internal” variables used to optimize the code. They are nothing special but I want to make a difference between the variables we adjust to control the frame (like --b, --c, etc.) and the ones I use to make the code shorter.

The code may look confusing and not easy to grasp but I wrote a three-part series where I detail such technique. I highly recommend reading at least the first article to understand how I reached the above code.

Here is an illustration to better understand the different values:

The Frame Reveal

Let’s try another type of animation where we reveal the full frame on hover:

CodePen Embed Fallback

Cool, right? And you if you look closely, you will notice that the lines disappear in the opposite direction on mouse out which makes the effect even more fancy! I used a similar effect in a previous article.

CodePen Embed Fallback

But this time, instead of covering all the element, I cover only a small portion by defining a height to get something like this:

CodePen Embed Fallback

This is the top border of our frame. We repeat the same process on each side of the image and we have our hover effect:

img { --b: 10px; /* the border thickness*/ --g: 5px; /* the gap on hover */ --c: #8A9B0F; padding: calc(var(--g) + var(--b)); --_g: no-repeat linear-gradient(var(--c) 0 0); background: var(--_g) var(--_i, 0%) 0, var(--_g) 100% var(--_i, 0%), var(--_g) calc(100% - var(--_i, 0%)) 100%, var(--_g) 0 calc(100% - var(--_i, 0%)); background-size: var(--_i, 0%) var(--b),var(--b) var(--_i, 0%); transition: .4s, background-position 0s; cursor: pointer; } img:hover { --_i: 100%; }

As you can see, I am applying the same gradient four times and each one has a different position to cover only one side at a time.

Another one? Let’s go!

CodePen Embed Fallback

This one looks a bit tricky and it indeed does require some imagination to understand how two conic gradients are pulling off this kind of magic. Here is a demo to illustrate one of the gradients:

CodePen Embed Fallback

The pseudo-element simulates the gradient. It’s initially out of sight and, on hover, we first change its position to get the top edge of the frame. Then we increase the height to get the right edge. The gradient shape is similar to the ones we used in the last section: two segments to cover two sides.

But why did I make the gradient’s width 200%? You’d think 100% would be enough, right?

100% should be enough but I won’t be able to move the gradient like I want if I keep its width equal to 100%. That’s another little quirk related to how background-position works. I cover this in a previous article. I also posted an answer over at Stack Overflow dealing with this. I know it’s a lot of reading, but it’s really worth your time.

Now that we have explained the logic for one gradient, the second one is easy because it’s doing exactly the same thing, but covering the left and bottom edges instead. All we have to do is to swap a few values and we are done:

img { --c: #8A9B0F; /* the border color */ --b: 10px; /* the border thickness*/ --g: 5px; /* the gap */ padding: calc(var(--g) + var(--b)); --_g: #0000 25%, var(--c) 0; background: conic-gradient(from 180deg at top var(--b) right var(--b), var(--_g)) var(--_i, 200%) 0 / 200% var(--_i, var(--b)) no-repeat, conic-gradient( at bottom var(--b) left var(--b), var(--_g)) 0 var(--_i, 200%) / var(--_i, var(--b)) 200% no-repeat; transition: .3s, background-position .3s .3s; cursor: pointer; } img:hover { --_i: 100%; transition: .3s, background-size .3s .3s; }

As you can see, both gradients are almost identical. I am simply swapping the values of the size and position.

The Frame Rotation

This time we are not going to draw a frame around our image, but rather adjust the look of an existing one.

CodePen Embed Fallback

You are probably asking how the heck I am able to transform a straight line into an angled line. No, the magic is different than that. That’s just the illusion we get after combining simple animations for four gradients.

Let’s see how the animation for the top gradient is made:

CodePen Embed Fallback

I am simply updating the position of a repeating gradient. Nothing fancy yet! Let’s do the same for the right side:

CodePen Embed Fallback

Are you starting to see the trick? Both gradients intersect at the corner to create the illusion where the straight line is changed to an angled one. Let’s remove the outline and hide the overflow to better see it:

CodePen Embed Fallback

Now, we add two more gradients to cover the remaining edges and we are done:

img { --g: 4px; /* the gap */ --b: 12px; /* border thickness*/ --c: #669706; /* the color */ padding: calc(var(--g) + var(--b)); --_c: #0000 0 25%, var(--c) 0 50%; --_g1: repeating-linear-gradient(90deg ,var(--_c)) repeat-x; --_g2: repeating-linear-gradient(180deg,var(--_c)) repeat-y; background: var(--_g1) var(--_p, 25%) 0, var(--_g2) 0 var(--_p, 125%), var(--_g1) var(--_p, 125%) 100%, var(--_g2) 100% var(--_p, 25%); background-size: 200% var(--b), var(--b) 200%; transition: .3s; } img:hover { --_p: 75%; }

If we take this code and slightly adjust it, we can get another cool animation:

CodePen Embed Fallback

Can you figure out the logic in this example? That’s your homework! The code may look scary but it uses the same logic as the previous examples we looked at. Try to isolate each gradient and imagine how it animates.

Wrapping up

That’s a lot of gradients in one article!

It sure is and I warned you! But if the challenge is to decorate an image without an extra elements and pseudo-elements, we are left with only a few possibilities and gradients are the most powerful option.

Don’t worry if you are a bit lost in some of the explanations. I always recommend some of my old articles where I go into greater detail with some of the concepts we recycled for this challenge.

I am gonna leave with one last demo to hold you over until the next article in this series. This time, I am using radial-gradient() to create another funny hover effect. I’ll let you dissect the code to grok how it works. Ask me questions in the comments if you get stuck!

CodePen Embed Fallback Fancy Image Decorations series

Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Some Things I Took Away From An Event Apart 2022 in Denver

Css Tricks - Thu, 10/13/2022 - 10:01am

An Event Apart 2022 Denver wrapped up yesterday. And while I was unable to make it to all three days this time, I did catch yesterday’s action — and it was awesome. I’m not very social or outgoing, but this was the first conference I’ve been to in at least a couple of years, and seeing folks in person was incredibly refreshing.

I took notes, of course! I thought I’d post ’em here because sharing is caring. At least, that’s what my six-year-old told me the other day when asking for a bite of my dessert last night.

I’ll break this down by speaker. Fair warning: I’m all about handwritten notes and a pretty visual fella, so my notes tend to be less… structured than most. And these notes are just things that stood out to me. They may not even be the presenter’s main idea, but they caught my attention!

Chris Coyier: Websites Are Good Now High-resolution

Chris has given this talk before (we linked it up just last week), but this time expanded it substantially, particularly with details on container relative units which, when combined with clamp(), make for more accurate responsiveness because the values are relative to the container rather than the viewport. So, you know how we often use viewport width (vh) units for fluid type?

font-size: clamp(1rem, 1rem + 2vw, 2rem);

Well, we can use a container relative unit like container query inline-size (cqi) instead, where 1cqi is equal to 1% of the container’s inline size (here’s the draft spec on that):

font-size: clamp(1rem, 1rem + 1cqi, 2rem);

Chris also talked quite a bit about the performance benefits of hosting at the edge. Probably no surprise because he’s written about it here more than a few times. Even as someone who had already read those articles, I honestly didn’t realize the complete concept of computing at the edge.

The idea is deceptively simple: global CDNs can serve assets quickly because they host them geographically close to the user. That’s pretty standard practice for serving raster images. But it has extended to static files, such as the same HTML, CSS, and JavaScript files that power a site — build them in advance and serve the already compiled and optimized files from the speedy global CDN. That’s the whole Jamstack concept!

But what if you still require a server response from it? That ain’t very edge-y, is it? Well, now we have handlers capable of running on a single URL fetching data in advance, and injecting it ahead of render — directly from the CDN. Sure, there’s extra work happening in the background. Still, the fact that we can dynamically fetch data, inject it, pre-build it, serve it statically on demand, and have it run geographically closer to the user makes this blazingly fast.

Tolu Adegbite: How to Win at ARIA and Influence Web Accessibility High resolution

Good gosh was this an excellent presentation! Tolu Adegbite schooled me so hard on WAI-ARIA that I had a hard time jotting down all the gems she shared — Roles! States! Labeling! Descriptions! Everything was extraordinarily well-covered, and stuff that I know I’ll be coming back to time and again.

But one specific thing that caught my attention is the accessibility of inline SVG. Even though SVG is related to other types of design assets, the fact that it is markup at the end of the day sets it apart because it isn’t always identified as an image.

<!-- Image tag is easily recognized as an image --> <img src="cat.svg" alt="An illustrated brown and white tabby kitten looking lovingly into the camera."> <!-- Could be an image, maybe not? --> <svg viewBox="0 0 100 100"> <!-- etc. --> </svg>

Assistive tech is more likely to read inline SVG as an image by giving it a proper accessible role and label:

<svg role="image" aria-label="An illustrated brown and white tabby kitten looking lovingly into the camera." viewBox="0 0 100 100" > <!-- etc. --> </svg> Miriam Suzanne: Cascading Layers of !mportance High resolution

Hey, another CSS-Tricks alum! Miriam has been spending a bunch of time and effort on the Cascade Layers specification. She also wrote a big ol’ guide about them here at CSS-Tricks and talked about them at An Event Apart.

What has stuck with me most is how big of a mental shift this is. The concept isn’t complicated, per se. Declare @layer at the top of the CSS document, list the layers in order of specificity, then write styles in those layers. But for an old dinosaur like me who has been writing CSS for a while, I’m going to have to get used to the fact that Cascade Layers make it possible for a simple class selector to beat out something that usually wields a higher specificity, like an ID.

CodePen Embed Fallback


Miriam also reminded the room that Cascade Layers are just one tool we have in our specificity-managing toolbelt, in addition to selectors that affect specificity (e.g., :is(), :where(), and :has()).

Oh, and this is an interesting tidbit. As Miriam walked through the history of specificity in CSS, she recalled that !important was initially designed as a tool for users to override user agent and author styles. But somewhere down the line, we’ve adopted it to force author styles to the top. Cascade Layers help remove the excuse need to use !important because they provide us the power to “prioritize layers and protect inheritance.”

That is beautifully said, Miriam!

Dave Rupert: Unblocking Your Accessibility Backlog High resolution

Imagine waking up one day to hundreds of GitHub notifications about reported issues on your site. Where do you even begin? Maybe close your laptop and get a root canal instead? That happened to Dave! An automated accessibility audit returned a massive pile of errors and assigned them as tickets for Dave to fix.

But he noticed a pattern after taking an Excel spreadsheet of those issues, moving them to Notion for a better view, hiding unnecessary columns, categorizing everything, and displaying the results in logical groups. Many of the reported issues were the same issue repeated on multiple pages. Just because an automated test returns a handful of errors doesn’t mean they’re all unique. That reduced a nice chunk of the tickets.

He went on to show how — with relatively little effort — the backlog of issues dwindled by nearly 50%.

There’s a lot to glean there, especially regarding how we process and organize our work. The biggest takeaway for me is when Dave said we have to emphasize individuals and interactions over processes and tools. Tools like the one scanning for accessibility errors are helpful, but they might not tell the entire story. Rather than take them at their words, it’s worth asking questions and gaining more context before diving into the mess.

As a bonus, reorganizing the issues in Notion allowed Dave to group issues in a way that clearly shows which impairments his product was actively discriminating against, giving him greater empathy for those misses and how to prioritize them.

One more virtual session by Hui Jing Chen capped the day, but admittedly, I missed about half of it because I was having a hallway conversation. The conversation was worth it, even though I am bummed I missed the presentation. I’ll be watching the video of it when it’s published!

Some Things I Took Away From An Event Apart 2022 in Denver originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

A Pure CSS Gallery Focus Effect with :not

Css Tricks - Thu, 10/13/2022 - 5:10am

Oftentimes in the past, I needed to figure out how to add styles to all elements inside the container but not the hovered one.

Demo of the expected “fade-out” effect on siblings to let users “focus” on a particular element.

This effect requires selecting the siblings of a hovered element. I used to apply JavaScript for this, adding or removing the class that defined the proper CSS rules on mouseenter and mouseleave events, similar to this:

CodePen Embed Fallback

Although the code does the trick, my gut feeling always told me that there must be some pure-CSS way to achieve the same result. A few years ago, while working on a certain slider for my company, I came up with a solution similar to how Chris Geelhoed recreated the famous Netflix homepage animation and I understood that I didn’t need JavaScript for this anymore.

A couple of months ago I was trying to implement the same approach to a grid-based feed on my company website and — boom — it didn’t work because of the gap between the elements!

Luckily for me, it appeared that it doesn’t have to stay like this, and once again I didn’t need JavaScript for it.

Markup and base CSS

Let’s start coding by preparing the proper markup:

  • .grid is a grid-based <ul> list;
  • and .grid__child elements are <li> children that we want to interact with.

The markup looks like this:

<ul class="grid"> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> </ul>

The style should look like this:

.grid { display: grid; grid-template-columns: repeat(auto-fit, 15rem); grid-gap: 1rem; } .grid__child { background: rgba(0, 0, 0, .1); border-radius: .5rem; aspect-ratio: 1/1; }

This example code will create three list items occupying three columns in a grid.

The power of CSS selectors

Now, let’s add some interactivity. The approach that I initially applied was based on two steps:

  1. hovering on the container should change the styles of all elements inside…  
  2. …except the one that cursor is hovering at the moment.

Let’s start with grabbing every child while the cursor is hovering over the container:

.grid:hover .grid__child { /* ... */ }

Secondly, let’s exclude the currently hovered item and reduce the opacity of any other child:

.grid:hover .grid__child:not(:hover) { opacity: 0.3; }

And this would be perfectly enough for containers without gaps between the child elements:

Demo of a solution that works without gaps.

However, in my case, I couldn’t remove these gaps:

Demo of the problem encountered when gaps are introduced.

When I was moving the mouse between the tiles all of the children elements were fading out.

Ignoring the gaps

We can assume that gaps are parts of the container that are not overlayed by its children. We don’t want to run the effect every time the cursor enters the container, but rather when it hovers over one of the elements inside. Can we ignore the cursor moving above the gaps then? 

Yes, we can, using pointer-events: none on the .grid container and bringing them back with pointer-events: auto on its children:

.grid { /* ... */ pointer-events: none; } /* ... */ .grid__child { /* ... */ pointer-events: auto; }

Let’s just add some cool transition on opacity and we have a ready component:

CodePen Embed Fallback

It’s probably even cooler when we add more tiles and create a 2-dimensional layout:

CodePen Embed Fallback

The final CSS looks like this:

.grid { display: grid; grid-template-columns: repeat(auto-fit, 15rem); grid-gap: 3rem; pointer-events: none; } .grid:hover .grid__child:not(:hover) { opacity: 0.3; } .grid__child { background: rgba(0, 0, 0, .1); border-radius: .5rem; aspect-ratio: 1/1; pointer-events: auto; transition: opacity 300ms; }

With only 2 additional lines of code we overcame the gap problem!

Possible issues

Although it’s a compact solution, there are some situations where it might require some workarounds.

Unfortunately, this trick won’t work when you want the container to be scrollable, e.g., like in some kind of horizontal slider. The pointer-events: none style would ignore not only the hover event but all the others, too. In such situations, you can wrap the .grid in another container, like this:

<div class="container"> <ul class="grid"> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> <li class="grid__child"></li> </ul> </div> Summary

I strongly encourage you to experiment and try to find a simpler and more native approach for tasks that are usually expected to have some level of complexity. Web technologies, like CSS, are getting more and more powerful and by using out-of-the-box native solutions you can achieve great results without the need of maintaining your code and cede it to browser vendors.

I hope that you liked this short tutorial and found it useful. Thanks!

The author selected the Tech Education to receive a donation as part of the Write for DOnations program.

A Pure CSS Gallery Focus Effect with :not originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

From Rock to Screw to New York Times: An interview with Steven Heller

Typography - Wed, 10/12/2022 - 4:46pm

Read the book, Typographic Firsts

Travel back to the sixties, and the scene of underground design, in this live interview with Steven Heller discussing his memoir Growing Up Underground: A Memoir of Counterculture New York.

The post From Rock to Screw to New York Times: <br>An interview with Steven Heller appeared first on I Love Typography.

Syndicate content
©2003 - Present Akamai Design & Development.