Archiwum kategorii: CSS

Technical Writing for Developers

Post pobrano z: Technical Writing for Developers

HTML, CSS, JavaScript, Python, PHP, C++, Dart — there are so many programming languages out there and you may even be totally fluent in several of them! But as we aim to write more and better code, the way we write and communicate in everyday language becomes more and more important… and perhaps even overlooked.

The way we write about and around code is arguably as important as the code itself. And despite where you fall on that line, we can all agree that our words have the potential to both help and hurt code’s effectiveness.

In this article, I want to outline how these two seemingly distinct fields — programming and writing — can come together and take our developer skills to the next level.

Wait, technical writing? Yes, that’s exactly what I mean. I truly believe we are all writers in one sense or another. And I’m here to give you a primer with writing tips, advice, and examples for how it can make you both a better developer and communicator.

Technical writing is everywhere

Last year, the team behind the popular Mac Git client, Tower, polled more than 4,000 developers and found that nearly 50% of them spent between 3-6 hours a day writing code.

Bar chart showing actual programming time per day.

And yes, that’s one survey polling a pretty niche group, but I imagine many of us fall somewhere in that range. Whatever the case, a developer isn’t writing code 24/7, because as this poll suggests, we’re spending plenty of time doing other things.

That might include:

  • demoing a new feature,
  • documenting that new feature,
  • updating a work ticket related to that new feature, or
  • backlogging work to support that new feature.

Of course, there’s always time for bathroom breaks and Wordle too.

Anyway, most of the things we typically do involve communicating with people like your team, colleagues, clients, users, and other developers.

So we do spend a good chunk of our time communicating with humans through words in addition to the communication we have with computers through code. Words are written language. And if we wrote our words better, we’d communicate better. When we communicate better, we’re more likely to get what we want.

That’s Technical Writing 101.

Venn diagram showing the overlap between technical writing and coding.

And it doesn’t even end here.. Some programmers also like to make their own products, which means they need to make marketing part of their job. Technical writing plays a huge role in that too. So, yeah. I think it’s pretty fair to say that technical writing is indeed everywhere.

What is good grammar?

With so many programming languages out there, the last thing we want is to learn another one.

Grammar is an integral part of English, and it unlocks the full potential of communication. It makes us more formal, professional, and coherent.

Let me give you a quick rundown on language.

The English syntax

Just like programming languages, English has a well-defined syntax, and it starts with words.

Words are the building blocks of English, and they fall into eight buckets:

Color coded sentence showing the English syntax.
Nouns

These can be names of people, animals, places, concepts, and objects.

Example:
CSS is one of the core languages of front-end development.

Verbs

Verbs convey action. Even “is” can be considered an action.

Example:
Marcia codes in the morning and answers emails in the afternoon.

Adjectives

Adjectives are how we describe nouns. They’re like meta that adds more detail to a sentence to paint a vivid picture.

Examples:

  • CSS is an elegant and poetic language.
  • The HTML for tables is complex and cumbersome.
  • The Box Model is important to understand CSS.
Prepositions

Prepositions create a relationship between a noun and other words, often indicating direction, time, location, and space.

Examples:

  • Did you commit your work to the repo?
  • What is the best approach for this component?
  • We conducted interviews with real users.
Adverbs

Sometimes actions need to be more specific, so we use adverbs such as “runs fast” and “compiles slowly.” They often end in “-ly.”

Examples:

  • This is easily the best idea of them all.
  • Chip waited patiently for Dale’s feedback.
  • The team worked diligently on the project.
Conjunctions

Conjunctions connect phrases in a sentence. Remember this classic song from the show School House Rocks?

Examples:

  • CSS for styling while HTML is for markup.
  • Yes, I write code, but I also work on design.
  • That fixes the bug. Yet it introduced a new one.
Transitions

Paragraphs are made of sentences that are connected to each other using transitions.

Examples:

  • There are many programming languages. However, only a few are used in the web industry.
  • First, clone the directory.
  • I like this approach but on the other hand, I know another one.
Pronouns

When nouns become repetitive, we replace them with pronouns such as: “he,” “it,” and “that.”

Examples:

  • CSS is a stylesheet language. We use it to style websites.
  • Tony loves to code and he practices every day.
  • Our customers are tech-savvy because they know code.

Think of these like UI components: they are modular pieces you can move around to construct a complete and robust sentence, the same way you might piece together a complete and robust UI. Do all of the components need to be there all of the time? Certainly not! Assemble a sentence with the pieces you need to complete the experience, just as you would with an interface.

Voice and tone

Vocabulary, punctuation, sentence structure, and word choice. These are all the ingredients of English. We use them to share ideas, communicate with our friends and family, and send emails to our coworkers.

But it’s crucial to consider the sound of our messages. It’s amazing how one exclamation point can completely shift the tone of a message:

  1. I like programming.
  2. I like programming! 🙂

It’s easy to confuse voice for tone, and vice versa.

Voice is what concerns our choice of words, which depends on context. For example, a tutorial for beginners is more likely to use slang and informal language to convey a friendly voice, whereas documentation might be written in a formal, serious, and professional manner in an effort to get straight to the point.

The same message, written in two different voices:

  • Fun: “Expand your social network and stay updated on what’s trending now.”
  • Serious: “Find jobs on one of the largest social networking apps and online jobs market.”

It’s not unusual to accidentally write messages that come across as condescending, offensive, and unprofessional. This is where tone comes into play. Read your messages out loud, get other people to read them for you, and experiment with your punctuation and sentence structure. That’s how you hone your tone.

Here’s another way to think of it: your voice never changes, but your tone does. Your voice is akin to who you are as a person, whereas tone is how you respond in a given situation.

Active and passive voice

A sentence always contains an actor, a verb, and a target. The order in which these come determines if the sentence is written in an active or passive voice.

The actor comes first in an active voice. For example: “CSS paints the background.”

Sentences that use an active voice are more straightforward than their counterparts. They’re clearer, shorter, and more understandable — perfect for a more professional voice that gets straight to the point.

With a passive voice, the actor comes last. (See what I did there?) That means our actor — CSS in this case — comes at the end like this: “The background is painted by CSS.”

Readers usually convert a passive voice to an active voice in their heads, resulting in more processing time. If you’ve ever heard that writing in an active voice is better, this is usually the reason why. Tech writers prefer the active voice most of the time, with very few exceptions such as citing research: “It has been suggested that …”

But that doesn’t mean you should always strive for an active voice. Switching from one to the other — even in the same paragraph — can make your content flow more seamlessly from one sentence to another if used effectively.

Avoiding mistakes

Grammar is all about the structure and correctness of language, and there’s nothing better to achieve that than a quick proofreading of your document. It’s very important to rid your writings of spelling mistakes, grammar issues, and semantic imperfections.

At the end of this article, I’ll show you the invaluable tools that professionals use to avoid writing mistakes. Obviously, there are built-in spell checkers in just about everything these days; our code editors even have spell-checking and linting plugins to help prevent mistakes. 

But if you’re looking for a one-stop tool for all-things grammar, Grammarly is one of the most widely-used tools. I’m not getting a kickback for that or anything. It’s just a really great tool that many editors and writers use to write clean and clear content — similar to how you might use Emmet, eslint, or any other linter to write clean and clear code.

Writing code comments

The things we write for other developers can have a big impact on the overall quality of our work, whether it’s what we write in the code, how we explain the code, or how we give feedback on a piece of code.

It’s interesting that every programming language comes with a standard set of features to write a comment. They should explain what the code is doing. By that, I don’t mean vague comments like this:

red *= 1.2 // Multiply `red` by 1.2 and re-assign it

Instead, use comments that provide more information:

red *= 1.2 // Apply a 'reddish' effect to the image

It’s all about context. “What kind of program am I building?” is exactly the kind of question you should be asking yourself.

Comments should add value

Before we look at what makes a “good” code comment, here are two examples of lazy comments:

const age = 32 // Initialize `age` to 32
filter: blur(32px); /* Create a blur effect with a 32px radius */

Remember that the purpose of a comment is to add value to a piece of code, not to repeat it. If you can’t do that, you’re better off just leaving the code as-is. What makes these examples “lazy” is that they merely restate what the code is obviously doing. In this case, the comments are redundant because they tell us what we already know — they aren’t adding value!

Comments should reflect the current code

Out-of-date comments are no rare sight in large projects; dare I say in most projects.

Let’s imagine David, a programmer and an all-around cool guy to hang out with. David wants to sort a list of strings alphabetically from A to Z, so he does the obvious in JavaScript:

cities = sortWords(cities) // sort cities from A to Z

David then realizes that sortWords() actually sorts lists from Z to A. That’s not a problem, as he can simply reverse the output:

cities = sortWords(cities) // sort cities from A to Z
cities = reverse(cities)

Unfortunately, David didn’t update his code comment.

Now imagine that I didn’t tell you this story, and all you saw was the code above. You’d naturally think that after running that second line of code, `cities` would be sorted from Z to A! This whole confusion fiasco was caused by a stale comment.

While this might be an exaggerated example, something similar can (and often does) happen if you’re racing against a close deadline. Thankfully, this can be prevented by following one simple rule… change your comments the same time you change the code.

That’s one simple rule that will save you and your team from a lot of technical debt.

Now that we know what poorly written comments look like, let’s look at some good examples.

Comments should explain unidiomatic code

Sometimes, the natural way of doing things isn’t right. Programmers might have to “break” the standards a bit, but when they do, it’s advisable to leave a little comment explaining their rationale:

 function addSetEntry(set, value) {    
  /* Don't return `set.add` because it's not chainable in IE 11. */  
  set.add(value);
  return set;
}

That’s helpful, right? If you were responsible for reviewing this code, you may have been tempted to correct it without that comment there explaining what’s up.

Comments can identify future tasks

Another useful thing to do with comments is to admit that there’s more work to be done.

// TODO: use a more efficient algorithm
linearSort(ids)

This way, you can stay focused on your flow. And at a later date, you (or someone else) can come back and fix it.

Comments can link back to the source

So, you just found a solution to your problem on StackOverflow. After copy-pasting that code, it’s sometimes a good thing to keep a link to the answer that helped you out so you can come back to it for future reference.

Screenshot of copying a link at StackOverflow.
// Adds handling for legacy browsers
// https://stackoverflow.com/a/XXXXXXX

This is important because solutions can change. It’s always good to know where your code came from in case it ever breaks.

Writing pull requests

Pull requests (PRs) are a fundamental aspect of any project. They sit at the heart of code reviews. And code reviews can quickly become a bottleneck in your team’s performance without good wording.

A good PR description summarizes what change is being made and why it’s being made. Large projects have a pull request template, like this one adapted from a real example:

## Proposed changes
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.

## Types of changes
What types of changes does your code introduce to Appium?
 - [ ] Bugfix (non-breaking change which fixes an issue)
 - [ ] New feature (non-breaking change which adds functionality)
 - ...

## Checklist
 - [ ] I have read the CONTRIBUTING doc
 - [ ] I have signed the CLA
 - [ ] Lint and unit tests pass locally with my changes

## Further comments
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc…

Avoid vague PR titles

Please avoid titles that look like this:

  • Fix build.
  • Fix bug.
  • Add patch.

These don’t even attempt to describe what build, bug, or patch it is we’re dealing with. A little extra detail on what part of the build was fixed, which bug was squashed, or what patch was added can go a long way to establishing better communication and collaboration with your colleagues. It level-sets and gets folks on the same page.

PR titles are traditionally written in imperative tense. They’re a one-line summary of the entire PR, and they should describe what is being done by the PR.

Here are some good examples:

  • Support custom srcset attributes in NgOptimizedImage
  • Default image config to 75% image quality
  • Add explicit selectors for all built-in ControlValueAccessors

Avoid long PRs

A large PR means a huge description, and no one wants to review hundreds or thousands of lines of code, sometimes just to end-up dismissing the whole thing!

Instead, you could:

  • communicate with your team through Issues,
  • make a plan,
  • break down the problem into smaller pieces, or
  • work on each piece separately with its own PR.

Isn’t it much cleaner now?

Provide details in the PR body

Unlike the PR title, the body is the place for all the details, including:

  • Why is the PR being done?
  • Why is this the best approach?
  • Any shortcomings to the approach, and ideas to solve them if possible
  • The bug or ticket number, benchmark results, etc.

Reporting bugs

Bug reports are one of the most important aspects of any project. And all great projects are built on user feedback. Usually, even after countless tests, it’s the users that find most bugs. Users are also great idealists, and sometimes they have feature ideas; please listen to them!

For technical projects, all of this stuff is done by reporting issues. A well-written issue is easy for another developer to find and respond to.

For example, most big projects come with a template:

 <!-- Modified from angular-translate/angular-translate -->
 ### Subject of the issue
 Describe your issue here.

 ### Your environment
 * version of angular-translate
 * version of angular
 * which browser and its version

 ### Steps to reproduce
 Tell us how to reproduce this issue.

 ### Expected behavior
 Tell us what should happen.

 ### Actual behavior
 Tell us what happens instead.

Gather screenshots

Capture the issue using your system’s screen-shooting utility.

If it’s a screenshot of a CLI program, make sure that the text is clear. If it’s a UI program, make sure the screenshot captures the right elements and states.

You may need to capture an actual interaction to demonstrate the issue. If that’s the case, try to record GIFs using a screen-recording tool.

How to reproduce the problem

It’s much easier for programmers to solve a bug when it’s live on their computer. That’s why a good commit should come with the steps to precisely reproduce the problem.

Here’s an example:

Update: you can actually reproduce this error with objects:

 ```html
 <div *ngFor="let value of objs; let i = index">
   <input [ngModel]="objs[i].v" (ngModelChange)="setObj(i, $event)" />
 </div>
 ```

 ```js
 export class OneComponent {
   obj = {v: '0'};
   objs = [this.obj, this.obj, this.obj, this.obj];
 ​
  setObj(i: number, value: string) {
     this.objs[i] = {v: value};
  }
 }
 ```

 The bug is reproducible as long as the trackBy function returns the same value for any two entries in the array. So weird behavior can occur with any duplicate values.

Suggest a cause

You’re the one who caught the bug, so maybe you can suggest some potential causes for why it’s there. Maybe the bug only happens after you encounter a certain event, or maybe it only happens on mobile.

It also can’t hurt to explore the codebase, and maybe identify what’s causing the problem. Then, your Issue will be closed much quicker and you’re likely to be assigned to the related PR.

Communicating with clients

You may work as a solo freelancer, or perhaps you’re the lead developer on a small team. In either case, let’s say you’re responsible for interfacing with clients on a project. 

Now, the programmer stereotype is that we’re poor communicators. We’ve been known to use overly technical jargon, tell others what is and is not possible, and even get defensive when someone questions our approach.

So, how do we mitigate that stereotype? Ask clients what they want, and always listen to their feedback. Here’s how to do that.

Ask the right questions

Start by making sure that you and the client are on the same page:

  • Who is your target audience?
  • What is the goal of the site?
  • Who is your closest competitor and what are they doing right?

Asking questions is also a good way to write positively, particularly in situations when you disagree with a client’s feedback or decision. Asking questions forces that person to support their own claims rather than you attacking them by defending your own position:

  • Are you OK with that even if it comes with an additional performance cost?
  • Does moving the component help us better accomplish our objective?
  • Great, who is responsible to maintain that after launch? 
  • Do you know offhand if the contrast between those two colors passes WCAG AA standards?

Questions are a lot more innocent and promote curiosity over animosity.

Sell yourself

If you’re making a pitch to a prospective client, you’re going to need to convince them to hire you. Why should the client choose you? It’s important to specify the following:

  • Who you are
  • What you do
  • Why you’re a good fit for the job
  • Links to relevant work you’ve done

And once you get the job and need to write up a contract, remember that there’s no content more intimidating than a bunch of legalese. Even though it’s written for design projects, the Contract Killer can be a nice starting point for writing something much friendlier.

Your attention to detail could be the difference between you and another developer trying to win the same project. In my experience, clients will just as easily hire a develop they think they will enjoy working with than the one who is technically the most competent or experienced for the job.

Writing microcopy

Microcopy is the art of writing user-friendly UI messages, such as errors. I’ll bet there have been times where you as a developer had to write error messages because they were put on the backburner all the way to launch time.

That may be why we sometimes see errors like this:

Error: Unexpected input (Code 693)

Errors are the last thing that you want your users to deal with. But they do happen, and there’s nothing we can do about it. Here are some tips to improve your microcopy skills.

Avoid technical jargon

Most people don’t know what a server is, while 100% of programmers do. That’s why it’s not unusual to see uncommon terms written in an error message, like API or “timeout execution.”

Unless you’re dealing with a technical client or user base, It’s likely that most of your users didn’t take a computer science course, and don’t know how the Internet works, and why a particular thing doesn’’t work. Hence, the error.

Therefore, a good error message shouldn’t explain why something went wrong, because such explanations might require using scary technical terms. That’s why it’s very important to avoid using technical jargon.

Never blame the user

Imagine this: I’m trying to log into your platform. So I open my browser, visit your website, and enter my details. Then I’m told: “Your email/password is incorrect.”

Even though it seems dramatic to think that this message is hostile, it subconsciously makes me feel stupid. Microcopy says that it’s never okay to blame the user. Try changing your message to something less finger-pointy, like this this example adapted from Mailchimp’s login: “Sorry, that email-password combination isn’t right. We can help you recover your account.”

I’d also like to add the importance of avoiding ALL CAPS and exclamation points! Sure, they can be used to convey excitement, but in microcopy they create a sense of hostility towards the user.

Don’t overwhelm the user

Using humor in your microcopy is a good idea! It can lighten up the mood, and it’s an easy way to curb the negativity caused by even the worst errors.

But if you don’t use it perfectly, it can come across as condescending and insulting to the user. That’s just a big risk to take.

Mailchimp says it well:

[D]on’t go out of your way to make a joke — forced humor can be worse than none at all. If you’re unsure, keep a straight face.

(Emphasis mine)

Writing accessible markup

We could easily spend an entire article about accessibility and how it relates to technical writing. Heck, accessibility is often included in content style guides, including those for Microsoft and Mailchimp.

You’re a developer and probably already know so much about accessibility. You may even be one of the more diligent developers that makes accessibility a core part of your workflow. Still, it’s incredible how often accessibility considerations are put on the back burner, no matter how important we all know it is to make accessible online experiences that are inclusive of all abilities.

So, if you find yourself implementing someone else’s copywriting into your code, writing documentation for other developers, or even writing UI copy yourself, be mindful of some fundamental accessibility best practices, as they round out all the other advice for technical writing.

Things like:

Andy Bell offers some relatively small things you can do to make content more accessible, and it’s worth your time checking them out. And, just for kicks, John Rhea shows off some neat editing tricks that are possible when we’re working with semantic HTML elements.

Conclusion

Those were six ways that demonstrate how technical writing and development coincide. While the examples and advice may not be rocket science, I hope that you found them useful, whether it’s collaborating with other developers, maintaining your own work, having to write your own copy in a pinch, or even drafting a project proposal, among other things.

The bottom line: sharpening your writing skills and putting a little extra effort into your writing can actually make you a better developer.

Technical writing resources

If you’re interested in technical writing:

If you’re interested in copywriting:

If you’re interested in microcopy:

If you’re interested in using a professional style guide to improve your writing:

If you’re interested in writing for accessibility:


Technical Writing for Developers originally published on CSS-Tricks. You should get the newsletter.

Creating Style Variations in WordPress Block Themes

Post pobrano z: Creating Style Variations in WordPress Block Themes

Global styles, a feature of the block themes, is one of my favorite parts of creating block themes. The concept of global style variations in WordPress were introduced in Gutenberg 12.5 which would allow theme authors to create alternate variations of a block theme with different combinations of colors, fonts, typography, spacing, etc. Different theme.json files stored under /styles folder “lets users quickly and easily switch between different looks in the same theme.”

The global styles panel UI is in active development iteration. More details on the development of this feature can be found and tracked here at this GitHub ticket (#35619).

In this article, I will walk through creating a proof-of-concept global style variation using alternate /styles/theme.json files and create child themes with different color modes by swapping color palettes only.

Table of contents

Prerequisites

This article is intended for those who have basic understanding of WordPress block themes and some familiarity of using Full Site Editor (FSE) interface. If you’re new to block themes and the FSE, you can get started here on CSS-Tricks with this deep introduction to WordPress block themes and site editor documentation. This Full Site Editing website is one of the most up-to-date tutorial guides to learn all FSE features including block themes and styles variations discussed in this article.

Global style variations

For some background, let’s briefly overview global style variation. Twenty Twenty-Two (TT2) theme lead and Automattic design director Kjell Reigstad introduced global styles variations with this tweet and GitHub ticket #292 as child themes. In the ticket, Kjell notes that they were initially intended as alternate color patterns and fonts combinations, but they can be used for building simple child themes.

This example from Kjell demonstrates how different style combinations could be selected from options available in the sidebar.

Since then, the Automattic theme team has been experimenting with the concept to create variable child themes (variable color and fonts only), including the following:

  • geologist with blue, cream, slate, yellow variations
  • quadrat with black, green, red, white, and yellow versions

Global style switcher

The Gutenberg 12.5 release has introduced a global styles switcher which would allow users quickly and easily switch between different looks in the same theme via different theme.json files stored under a /styles folder.

The concept of allowing switching global style variation via theme.json has been discussed on GitHub for a while now. Gutenberg lead engineer Matias Ventura gave renewed importance to it by adding it to the WordPress 6.0 roadmap recently.

Embrace style alternates driven by json variations. This was teased in various videos around the new default theme and should be fully unveiled and presented in 6.0. One of the parallel goals is to create a few distinct variations of TT2 made just with styles. (35619)

Matias Ventura, “Preliminary Roadmap to 6.0”

The latest development iteration of theme style variation switcher is available with Gutenberg 13.0 and included in WordPress 6.0. In this Exploring WordPress 6.0 video, Automattic product liaison Anne McCarthy provides an overview of its major features, including style variations and Webfonts API (starting 5:18) discussed in this article.

Theme style variation versus child theme

In my previous article, I briefly covered building block child themes. Global style variations have blurred the line between alternate-theme.json and child themes. For example, the only difference between a recently released alante-dark child theme and its parent theme is an alternate.json file in the child theme that overrides the global theme styles like this:

Screenshot of the Visual Studio Code UI displaying the contents of alante-dark.
The alante-dark theme.

Likewise, the two recent Alara child themes in the WordPress directory — Framboise and Richmond — differ only in their single theme.json file.

Section 1: Creating theme style variations

At the root of your child theme folder, create a /styles folder, which holds style variations as JSON files. For this demo example, I created three variations of Twenty Twenty-Two’s theme.json color palettes — blue.json, maroon.json, and pink.json — by swapping the foreground and background colors:

Screenshot of the Visual Studio Code UI displaying the child theme file structure of "blue.json", "maroon.json", and "pink.json" in the styles directory.
The child theme file structure of “blue.json”, “maroon.json”, and “pink.json” in the styles directory.

Here is the final result after clicking the styles icon from the admin dashboard (located at Appearance → Editor):

Animated GIF showing the theme variations in WordPress.
Walking through the WordPress admin interface to select the “blue”, “maroon”, and “pink” styles.

Click the Other Styles button (recently revised to Browser styles), which displays “blue”, “maroon”, and “pink” color style icons in addition to its original styles.

To change and choose a style, select your preferred variation and click the Save button (top-right), which is displayed on the front end of your browser.

Adding labels to alternate style variations and file name with hover animation effect are available in Gutenberg 13.0.

Step 1: Setup and installation

First, install and set up a WordPress site with some dummy content. For this demo, I made a fresh WordPress install, activated Twenty Twenty-Two theme, and added Gutenberg test data.

The theme style variations and WebFonts API discussed in this article require installation and activation of the Gutenberg 13.0 plugin or WordPress 6.0.

Step 2: Create a TT2 child theme

In this demo child theme example, let’s slightly vary the body color from the header and footer color, with all site content centered:

The lower part of the site design are not visible because it is not scrolled into view. Site navigation is present in the header. A large banner image with a bird is visible. A date and title for the latest blog entry is also visible.
Screenshot of the default appearance of the demo theme in a browser window.

Step 3: Create JSON files

Create /styles in your child theme’s root folder with blue, maroon, and pink.json files:

__ style.css
__ theme.json
__ functions.php
__ index.php
__ templates
__ ...
__ parts
__ ...
__ styles
__ blue.json
__ maroon.json
__ pink.json

Step 4: Create alternate theme JSON files

Next up, create your alternate-theme.json files with desired color pallets under /styles folder. For this demo example, I created three color palettes (blue, maroon, and pink). Here is the code for maroon.json:

{
  "version": 2,
  "title": "Maroon",
  "settings": {
    "color": {
      "palette": [
        { "slug": "foreground", "color": "#7C290F", "name": "Foreground" },
        { "slug": "background", "color": "#ffffff", "name": "Background" },
        { "slug": "foreground-dark", "color": "#000000", "name": "Foreground Dark" },
        { "slug": "background-body", "color": "#ffd8be", "name": "Background Body" },
        { "slug": "primary", "color": "#000000", "name": "Primary" },
        { "slug": "secondary", "color": "#ffe2c7", "name": "Secondary" },
        { "slug": "tertiary", "color": "#55ACEE", "name": "Tertiary" }
      ]
  },
  "typography": {}
},
"styles": {
  "color":
      {
        "background": "var(--wp--preset--color--background-body)",
        "text": "var(--wp--preset--color--foreground-dark)"
      },
  "elements": {
      "link": {
        "color": { "text": "var(--wp--preset--color--primary)" }
      }
    }
  }
}

The other two alternate blue.json and pink.json files swap values of foreground and background-body, foreground-dark and primary color properties with their respective blue and pink hex color values.

Section 2: An example of a use case

As I noted in my previous article, I have been working on block themes and using them for my own personal project site. Inspired by the theme style variations and Webfonts API features in Gutenberg plugin, I started tweaking my work-in-progress block theme with an alternate dark color mode and by configuring the Webfonts API.

In this section, I will walk you through how I created TT2 Gopher Blocks, a demo sibling of my work-in-progress block theme created for this article. The theme includes maroon, dark, and light color modes created using theme style variations and Webfonts API that became available with the Gutenberg 12.8 release.

Showing the homepage we are creating with style variations in WordPress.
Screenshot displaying a sample site using the TT2 Gopher theme with maroon default color.

Some highlights of the TT2 Gopher theme include centered, single-column content display, distinct header and footer, more user-friendly archive and search pages.

A copy of TT2 Gopher Blocks is available at the GitHub repository, which you can fork and customize.

Creating dark mode on WordPress

First, some background on dark mode. Dark mode is a personal preference and developers offer it or other mode toggle switches like on this site, which is not a small job for most regular developers. Creating dark mode is well-covered here at CSS-Tricks, including this complete guide to dark mode and dark mode typography.

In a WordPress site, we can add a dark mode toggle using the WP Dark Mode plugin. Erin Myers of WP Engine and WPBeginner describe how to use the WP Dark Mode plugin, while Brenda Barron lists other dark mode plugin options in this WPExplorer post.

Creating a dark mode in WordPress block themes without a plugin involves several steps. Over a year ago, Ari Stathopoulos created a dark support for the TT1 Blocks theme at the GitHub. Looking at the example here, it involves some JavaScript knowledge to create assets (e.g., toggler, customize, editor-mode-support), dark color CSS variables, and expanded functions.php files.

In this short video, Automattic’s Anne McCarthy demonstrates how simple it is to create a dark mode of TT2 block theme with global style variation by adding kllejr’s gist of JSON snippets in the TT2 /styles folder.

Creating the demo TT2 Gopher blocks theme

The TT2 Gopher is a very simple and modified version of the default Twenty Twenty-Two theme. It includes three theme style variations — maroon, dark, and white.

Describing each customization step is beyond the scope of this article, but you can learn more from my deep introduction to WordPress block themes as well as the Block Editor Handbook over at WordPress.org.

A brief overview of the TT2 Gopher theme color and font combinations include:

  • Light mode
    • The header is white and the footer has a smoky body background color.
    • Open Sans is the primary font.
  • Dark mode
    • The header and footer are black with lighter dark colors for the body backgrounds.
    • Source Serif Pro is the primary font.
  • Maroon mode
    • The header and footer are both a dark maroon color, with a lighter yellowish body background.
    • Work Sans is the primary font.

Let me briefly walk you through how I created theme style variations.

Adding and configuring webfonts

The Gutenberg 12.8 plugin introduced a new Webfonts API that allows the authors to load local (bundled) fonts “in a performance-friendly, privacy-friendly, and future-proof manner.” This feature can be implemented in a block theme the PHP way or the theme.json way.

Currently this feature works only with fonts bundled with block themes and does not support Google-hosted fonts because of privacy concerns. More details on the current status of Webfonts API development are covered in this make WordPress core article and this WP Tavern article.

Step 1: Download and add fonts in block theme

The TT2 theme adds Source Serif Pro font files to the theme’s assets/fonts folder. Two additional fonts — Work Sans and Public Sans — are also provided in he GitHub repository.

Step 2: Registering webfonts

In the TT2 theme, local Source Serif Pro webfonts are registered with PHP in its functions.php file:

function twentytwentytwo_get_font_face_styles() {
  return "
  @font-face{
    font-family: 'Source Serif Pro';
    font-weight: 200 900;
    font-style: normal;
    font-stretch: normal;
    font-display: swap;
    src: url('" . get_theme_file_uri( 'assets/fonts/SourceSerif4Variable-Roman.ttf.woff2' ) . "') format('woff2');
  }
  @font-face{
    font-family: 'Source Serif Pro';
    font-weight: 200 900;
    font-style: italic;
    font-stretch: normal;
    font-display: swap;
    src: url('" . get_theme_file_uri( 'assets/fonts/SourceSerif4Variable-Italic.ttf.woff2' ) . "') format('woff2');
  }
  ";
}

Gutenberg 12.8 introduced the ability to register local web fonts with theme.json file. The following theme.json snippets from the demo TT2 Gopher theme show how local Work Sans web fonts are registered in the maroon theme style variation:

"typography": {
  "fontFamilies": [
    {
      "fontFamily": "'Work Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Helvetica', sans-serif",
      "slug": "work-sans",
      "name": "Work Sans",
      "fontFace": [
        { "fontFamily": "Work Sans", "fontDisplay": "block", "fontWeight": "400", "fontStyle": "normal", "fontStretch": "normal", "src": [ "file:./assets/fonts/work-sans/WorkSans-VariableFont_wght.ttf" ] },
        { "fontFamily": "Work Sans", "fontDisplay": "block", "fontWeight": "700", "fontStyle": "normal", "fontStretch": "normal", "src": [ "file:./assets/fonts/work-sans/WorkSans-VariableFont_wght.ttf" ] },
        { "fontFamily": "Work Sans", "fontDisplay": "block", "fontWeight": "400", "fontStyle": "italic", "fontStretch": "normal", "src": [ "file:./assets/fonts/work-sans/WorkSans-Italic-VariableFont_wght.ttf" ] },
        { "fontFamily": "Work Sans", "fontDisplay": "block", "fontWeight": "700", "fontStyle": "italic", "fontStretch": "normal", "src": [ "file:./assets/fonts/work-sans/WorkSans-Italic-VariableFont_wght.ttf" ] }
      ]
    }
  ]
}

Additional information on how to register and use local webfonts in block themes is described in this tutorial and this WP Tavern article.

Creating theme style variations

Following the steps described in the previous section, I created two alternate versions of the theme.json file — white.json and black.json — with different color and fonts combinations inside the child theme’s /styles folder.

This feature requires version 2 of theme.json. Since Gutenberg 12.5, title can also be added at theme.json to display style label in the site editor or file name (without extension) will be displayed by default.

Here is an example of white.json:

{
  "version": 2,
  "title": "White",
  "settings": {
    "color": {
      "palette": [
        { "slug": "foreground", "color": "#000000", "name": "Foreground" },
        { "slug": "background", "color": "#f2f2f2", "name": "Background" },
        { "slug": "background-header", "color": "#ffffff", "name": "Background header" },
        { "slug": "primary", "color": "#0d0d0d", "name": "Primary" },
        { "slug": "secondary", "color": "#F0EAE6", "name": "Secondary" },
        { "slug": "tertiary", "color": "#eb3425", "name": "Tertiary" },
        { "slug": "quaternary", "color": "#7c7e83", "name": "Quaternary" }
      ]
    },
    "typography": {
      "fontFamilies": [
        {
        "fontFamily": "\"Public Sans\", sans-serif",
        "name": "Public Sans",
        "slug": "public-sans",
        "fontFace": [
          { "fontFamily": "Public Sans", "fontDisplay": "block", "fontStyle": "normal", "fontStretch": "normal", "src": [ "file:.assets/fonts/publicSans/PublicSans-VariableFont_wght.ttf.woff2" ] },
          { "fontFamily": "Public Sans", "fontDisplay": "block", "fontStyle": "italic", "fontStretch": "normal", "src": [ "file:./assets/fonts/publicSans/PublicSans-Italic-VariableFont_wght.ttf.woff2" ] }
        ]
      }
    ]
  }
},
"styles": {
  "blocks": {
    "core/image": {
      "filter": { "duotone": "var(--wp--preset--duotone--default-filter)" }
    },
    "core/post-title": {
      "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "700", "fontSize": "var(--wp--custom--typography--font-size--gigantic)" }
    },
    "core/query-title": {
      "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "300", "fontSize": "var(--wp--custom--typography--font-size--gigantic)" }
    },
    "core/post-featured-image": {
      "filter": { "duotone": "var(--wp--preset--duotone--default-filter)" }
    },
    "core/site-logo": {
      "filter": { "duotone": "var(--wp--preset--duotone--default-filter)" }
    },
    "core/site-title": {
      "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontSize": "var(--wp--preset--font-size--normal)", "fontWeight": "normal" }
    }
    },
    "color": { "background": "var(--wp--preset--color--background)", "text": "var(--wp--preset--color--foreground)" },
    "elements": {
      "h1": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "600", "fontSize": "var(--wp--custom--typography--font-size--colossal)" }
      },
      "h2": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "600", "fontSize": "var(--wp--custom--typography--font-size--gigantic)" }
      },
      "h3": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "300", "fontSize": "var(--wp--custom--typography--font-size--huge)" }
      },
      "h4": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "300", "fontSize": "var(--wp--preset--font-size--x-large)" }
      },
      "h5": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "700", "textTransform": "uppercase", "fontSize": "var(--wp--preset--font-size--medium)" }
      },
      "h6": {
        "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontWeight": "400", "textTransform": "uppercase", "fontSize": "var(--wp--preset--font-size--medium)" }
      },
      "link": {
        "color": { "text": "var(--wp--custom--color--foreground)" }
      }
    },
    "typography": { "fontFamily": "var(--wp--preset--font-family--public-sans)", "fontSize": "var(--wp--preset--font-size--normal)" }
  }
}

This code swaps color palettes from theme.json and also registers and defines the local Public Sans font files.

The black.json is also very similar and uses Source Serif Pro fonts registered in the functions.php file.

Screenshot of the light color theme on the left. And screenshot of the dark color theme on the right. The heading navigation and first blog entry are visible.
Side-by-side comparison of the light (left) and dark (right) color themes for TT2 Gopher.

Example of block themes with theme styles variations

  • Twenty Twenty-Two – the first default theme to include style variations. Its updated 1.2, bundled with WordPress 6.0 includes three style variations — “Blue”, “Pink”, and “Swiss” — allowing users to quickly swap between different visual styles.
  • Frost – an experimental block theme with dark theme style variation.
  • Alara – has above 100 active installs and includes 7 style variations.
  • Wabi– which powers Rich Tabor website contains 3 style variants and 300+ active installations.
  • Brisky – has more than 600 installs and one dark theme style variation.
  • Pendant – a theme by Automattic theme team under development at GitHub contains 3 theme style variation.

In this WP Tavern article, Justin speculates that this new feature may be utilized by theme authors by tying to the site visitor’s settings, while some users may prefer to tweak their site giving a seasonal or event-based design look. This is probably a little early, but only time will tell how this powerful feature would be utilized by both theme authors and users.

Wrapping up

Creating style variations of a block theme with different typography and color combination has been greatly simplified, without using plugins. It’s one of my favorite feature of the block editor that I plan to apply in my personal projects.

In my opinion, theme style variations are definitely a game changer for block themes and with this handy feature there might not be a need for child themes or even many cooky-cutter block themes. A few well-designed base block themes, similar to Automattic theme team’s block-canvas or blockbase (work-in-progress base block themes at GitHub), could be customized with theme style variation.


Resources

Dark Mode


Creating Style Variations in WordPress Block Themes originally published on CSS-Tricks. You should get the newsletter.

Cool Hover Effects That Use CSS Text Shadow

Post pobrano z: Cool Hover Effects That Use CSS Text Shadow

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

text-shadow but no text shadows?

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

CodePen Embed Fallback

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

The text layers are completely made with text-shadow!

Hover effect #1

CodePen Embed Fallback

Let’s pick apart the CSS:

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

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

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

Diagram of the start and end of the hover effect.

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

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

.hover-1 {
  --h: 1.2em;

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

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

.hover-1 {
  --h: 1.2em;   

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

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

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

CodePen Embed Fallback

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

Hover effect #2

CodePen Embed Fallback

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

CodePen Embed Fallback
.hover-2 {
  /* the height */
  --h: 1.2em;

  line-height: var(--h);
  color: #0000;
  text-shadow: 
    0 var(--_t, var(--h)) #fff,
    0 0 var(--_c, #000);
  transition: 0.3s;
}
.hover-2:hover {
  --_t: 0;
  --_c: #0000;
}

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

CodePen Embed Fallback

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

CodePen Embed Fallback
.hover-2 {
  /* the height */
  --h: 1.2em;

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

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

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

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

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

.hover-2 {
  /* the height */
  --h: 1.2em;

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

Hover effect #3

CodePen Embed Fallback

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

.hover-3 {
  /* the color  */
  --c: #1095c1;
  /* the height */
  --h: 1.2em;

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

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

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

.hover-3 {
  /* the color  */
  --c: #1095c1;
  /* the height */
  --h: 1.2em;

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

Hover effect #4

CodePen Embed Fallback

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

CodePen Embed Fallback

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

Diagram of the start and end of the text hover.

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

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

CodePen Embed Fallback

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

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

And on mouse out, we do the opposite:

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

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

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

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

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

CodePen Embed Fallback

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

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

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

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

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

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

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

What about the DRY switch variable optimization?

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

Your turn!

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

CodePen Embed Fallback

Wrapping up

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

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


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

A CSS Slinky in 3D? Challenge Accepted!

Post pobrano z: A CSS Slinky in 3D? Challenge Accepted!

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

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

Animated Gif of a Slinky toy going down stairs.
A slinking Slinky

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

Setting up the Slinky HTML

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

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

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

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

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

The initial Slinky CSS

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

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

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

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

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

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

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

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

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

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

CodePen Embed Fallback

We now have a styled scene! 💪

CodePen Embed Fallback

Styling the Slinky’s rings

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

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

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

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

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

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

CodePen Embed Fallback

Transforming the Slinky rings

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

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

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

We could tidy this up a bit for sure!

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

.ring {
  transform-origin: var(--ring-transform);
}

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

CodePen Embed Fallback

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

CodePen Embed Fallback

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

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

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

Now try moving the stack! We’re getting there. 🙌

CodePen Embed Fallback

Animating the rings

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

.ring {
  animation-name: slink;
  animation-duration: 2s;
  animation-fill-mode: both;
  animation-iteration-count: infinite;
}

@keyframes slink {
  0%, 5% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  25% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  45%, 100% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(var(--destination-z))
      rotateY(180deg);
  }
}

Oof, that’s not right at all!

CodePen Embed Fallback

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

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

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

CodePen Embed Fallback

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

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

That’s better! ✨

CodePen Embed Fallback

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

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

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

CodePen Embed Fallback

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

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

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

CodePen Embed Fallback

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

import gsap from 'https://cdn.skypack.dev/gsap'

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

And there it is!

CodePen Embed Fallback

But how can we do this without JavaScript?

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

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

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

.box {
  animation: spin 2s infinite;
}

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

Almost.

CodePen Embed Fallback

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

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

And there we have it:

CodePen Embed Fallback

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

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

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

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

It would make for an interesting addition for sure!

So, we need keyframes for all those rings?

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

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

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

Good thing we only need to write this once:

// STYLUS GENERATED KEYFRAMES BE HERE...
$ring-count = 10
$animation-window = 50
$animation-step = $animation-window / $ring-count

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

Here’s what those variables mean:

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

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

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

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

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

Which gives us this when compiled:

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

And then our styling can be updated accordingly:

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

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

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

CodePen Embed Fallback

Creating an infinite animation

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

.scene {
  animation: step-up var(--speed) infinite linear both;
}

@keyframes step-up {
  to {
    transform: translate3d(-100%, 0, var(--depth));
  }
}

Wow, that took very little effort!

CodePen Embed Fallback

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

CodePen Embed Fallback

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

CodePen Embed Fallback

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

Fun variations

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

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

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

.flipper {
  animation: flip-flop calc(var(--speed) * 2) infinite steps(1);
  height: 100%;
  width: 100%;
}

@keyframes flip-flop {
  0% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

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

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

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

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

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

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

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

CodePen Embed Fallback

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

Final demos

CodePen Embed Fallback
CodePen Embed Fallback

That’s it!

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

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


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

Improving Icons for UI Elements with Typographic Alignment and Scale

Post pobrano z: Improving Icons for UI Elements with Typographic Alignment and Scale

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

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

Example 1 — Site messaging

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

CodePen Embed Fallback

Identified issues

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

Recommendations

Screenshot of the site messaging element. It is overlayed with a red-dashed line indicating the icon's top edge and a blue-dashed line indicating the text's topmost point. The red-dashed line is slightly higher than the blue-dashed line.
Indicating the issues with aligning the icon and typography.

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

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

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

Screenshot of the site messaging element. It is overlayed with arrows indicating the baseline distance from the baseline of one line to the next line's baseline.
Calculating the baseline distance.

Baseline distance is font-size * line-height.

We’ll store that in a CSS custom property:

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

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

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

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

  • baseline distance = 16px * 1.5 = 24px
  • icon offset = (24px – 16px) / 2 = 4px

Demo: before and after

CodePen Embed Fallback

Example 2 – unordered lists

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

CodePen Embed Fallback

Identified issues

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

Recommendations

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

Start with the baseline distance calculation:

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

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

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

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

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

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

  • baseline distance = 32px * 1.2 = 38.4px
  • icon offset = (38.4px – 64px) / 2 = -12.8px

Demo: before and after

CodePen Embed Fallback

Conclusion

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

See also: Previous discussion on aligning icon to text.


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

Diagonal Stripes Wipe Animation

Post pobrano z: Diagonal Stripes Wipe Animation

I was playing this game on Apple Arcade the other day called wurdweb. It’s a fun little game! Little touches like the little shape dudes that walk around the screen (but otherwise don’t do anything) give it a lot of character. I kinda want little shape dudes that walk around on websites. But another UI choice caught my eye, the way that transitions between screens have these diagonal lines that grow and fill the screen, like window blinds closing, kinda.

Here’s a quick screencast showing how those wipes work:

I wanted to have a crack at building this.

The first thing that went through my mind is repeating-linear-gradient and how that can be used to build stripes. So say we set up like this:

.gradient {
  background-image:
    repeating-linear-gradient(
      45deg,
      #ff8a00,
      #ff8a00 10px,
      #e52e71 10px,
      #e52e71 20px
    );
}

That would buy us stripes like this:

We can use transparent as a color though. Meaning if we covered the screen with stripes like these, we could see through where that color is. Say like this:

CodePen Embed Fallback

In that gradient definition, we use 10px as the “start” and 20px as the “end” of the gradient before it repeats. Part of the trick here is keeping that 20px “end” the same and animating the “start” number up to it. When we do that, it actually covers the screen in a solid color. The problem is… how do you animate it? You can’t do this:

Screenshot of a CSS code snippet on a dark gray background with syntax highlighting. An arrow is pointing from the repeating linear gradient on the element to another repeating linear gradient inside keyframes. A note that says not going to animate is displayed in large white letters above a crying emoji.

What we need to do is animate that “start” pixel value number alone. We can use a custom property, but it’s a little tricky because without declaring them, custom properties are just strings, and not animatable lengths. So we’d have to do it like this.

@property --start {
  syntax: "<length>";
  inherits: false;
  initial-value: 10px;
}
#cover {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-image: repeating-linear-gradient(
    45deg,
    #ff8a00,
    #ff8a00 var(--start),
    transparent var(--start),
    transparent var(--end, 20px)
  );
  animation: cover 1s linear infinite;
}
@keyframes cover {
  to {
    --start: 20px;
  }
}

We’ve got to use @property here to do this, which I really like but, sadly, has limited browser support. It does work though! I’ve got all that set up, including a quick prefers-reduced-motion media query. I’m using a smidge of JavaScript to change the background halfway through the animation (while the screen is covered) so you can see how it might be used for a screen transition. Again, note that this is only working in Chromium-based browsers at the moment:

CodePen Embed Fallback

Notice I’ve used CSS custom properties for other things as well, like the angle and size of the stripes and the speed of the animation. They are both very trivial to change! I’ve chucked in knobs so you can adjust things to your liking. Knobs? Yeah, they are cool:

Like and subscribe

This whole thing started as a tweet. In this case, I’m glad I did as Temani Afif chimed in with a way to do it with masks as well, meaning pretty solid support across all browsers:

CodePen Embed Fallback

I don’t think animating background color stops or a mask position is particularly performant, but since we’re talking “screen wipes” here, one could imagine that the page isn’t likely to be interacted with anymore until the page transition is over, so maybe that’s not the world’s biggest deal.