Archiwum kategorii: CSS

Recreating MDN’s Truncated Text Effect

Post pobrano z: Recreating MDN’s Truncated Text Effect

It’s no secret that MDN rolled out a new design back in March. It’s gorgeous! And there are some sweet CSS-y gems in it that are fun to look at. One of those gems is how card components handle truncated text.

Pretty cool, yeah? I wanna tear that apart in just a bit, but a couple of things really draw me into this approach:

  • It’s an example of intentionally cutting off content. We’ve referred to that as CSS data loss in other places. And while data loss is generally a bad thing, I like how it’s being used here since excerpts are meant to be a teaser for the full content.
  • This is different than truncating text with text-overflow: ellipsis, a topic that came up rather recently when Eric Eggert shared his concerns with it. The main argument against it is that there is no way to recover the text that gets cut off in the truncation — assistive tech will announce it, but sighted users have no way to recover it. MDNs approach provides a bit more control in that department since the truncation is merely visual.

So, how did MDN do it? Nothing too fancy here as far the HTML goes, just a container with a paragraph.

<div class="card">
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore consectetur temporibus quae aliquam nobis nam accusantium, minima quam iste magnam autem neque laborum nulla esse cupiditate modi impedit sapiente vero?</p>
</div>

We can drop in a few baseline styles to shore things up.

CodePen Embed Fallback

Again, nothing too fancy. Our goal is cut the content off after, say, the third line. We can set a max-height on the paragraph and hide the overflow for that:

.card p {
  max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */
  overflow: hidden; /* Cut off the content */
}
CodePen Embed Fallback

Whoa whoa, what’s up with that calc() stuff? Notice that I set up a --base variable up front that can be used as a common multiplier. I’m using it to compute the font-size, line-height, padding for the card, and now the max-height of the paragraph. I find it easier to work with a constant values especially when the sizing I need is really based on scale like this. I noticed MDN uses a similar --base-line-height variable, probably for the same purpose.

Getting that third line of text to fade out? It’s a classic linear-gradient() on the pargraph’s :after pseudo-element, which is pinned to the bottom-right corner of the card. So, we can set that up:

.card p:after {
  content: ""; /* Needed to render the pseudo */
  background-image: linear-gradient(to right, transparent, var(--background) 80%);
  position: absolute;
  inset-inline-end: 0; /* Logical property equivalent to `right: 0` */
}

Notice I’m calling a --background variable that’s set to the same background color value that’s used on the .card itself. That way, the text appears to fade into the background. And I found that I needed to tweak the second color stop in the gradient because the text isn’t completely hidden when the gradient blends all the way to 100%. I found 80% to be a sweet spot for my eyes.

And, yes, :after needs a height and width. The height is where that --base variables comes back into play because we want that scaled to the paragraph’s line-height in order to cover the text with the height of :after.

.card p:after {
  /* same as before */
  height: calc(1rem * var(--base) + 1px);
  width: 100%; /* relative to the .card container */
}

Adding one extra pixel of height seemed to do the trick, but MDN was able to pull it off without it when I peeked at DevTools. Then again, I’m not using top (or inset-block-start) to offset the gradient in that direction either. 🤷‍♂️

Now that p:after is absolutely positioned, we need to explicitly declare relative positioning on the paragraph to keep :after in its flow. Otherwise, :after would be completely yanked from the document flow and wind up outside of the card. This becomes the full CSS for the .card paragraph:

.card p {
  max-height: calc(4rem * var(--base)); /* Set a cut-off point for the content */
  overflow: hidden; /* Cut off the content */
  position: relative; /* needed for :after */
}

We’re done, right? Nope! The dang gradient just doesn’t seem to be in the right position.

CodePen Embed Fallback

I’ll admit I brain-farted on this one and fired up DevTools on MDN to see what the heck I was missing. Oh yeah, :after needs to be displayed as a block element. It’s clear as day when adding a red border to it.🤦‍♂️

CodePen Embed Fallback
.card p:after {
  content: "";
  background: linear-gradient(to right, transparent, var(--background) 80%);
  display: block;
  height: calc(1rem * var(--base) + 1px);
  inset-block-end: 0;
  position: absolute;
  width: 100%;
}

All together now!

CodePen Embed Fallback

And, yep, looks sounds like VoiceOver respects the full text. I haven’t tested any other screen readers though.

I also noticed that MDN’s implementation removes pointer-events from p:after. Probably a good defensive tactic to prevent odd behaviors when selecting text. I added it in and selecting text does feel a little smoother, at least in Safari, Firefox, and Chrome.


Recreating MDN’s Truncated Text Effect originally published on CSS-Tricks. You should get the newsletter.

Why I Chose Angular to Build a URL Shortener

Post pobrano z: Why I Chose Angular to Build a URL Shortener

URL Shorteners are tools we use to make links shorter than they actually are. With a URL Shortener, you can transform a long link (maybe for a registration form or article) into a shorter version.

Behind the scenes, the long and short versions of a given link have been stored in some database. Then when a user visits the short link in a browser, the URL Shortener will redirect the user to the long version of the link (where the actual content is found).

Shortened links from URL shorteners are commonly used when the long version of those links would be too long to use. Sharing links on social media or when designing flyers and adverts is a popular use of URL shorteners.

For one of my projects, I created a personal URL shortener. My intention was to use it for links to articles I write or videos I make. I used Firebase to build the backend of the URL shortener. Specifically, I used the Firestore database to store short and long versions of any given link.

To create links, I had to use the Firebase console. This wasn’t a problem but it was cumbersome for the high frequency of editing links. The user experience (UX) was not ideal. Now I was faced with a problem. How do I create, edit, and delete links? I needed to build a frontend for the URL shortener. I needed a website for this.

In this article, we’ll review the available tools used for building this frontend, decision choices, and factors that influenced why they were made.

Problem statement

The project requirements were:

  • Platform/Architecture. The engineering and structure of the coding process.
  • UI Toolkit. Components to use for the various parts of the UI.
  • Convenience. Building the backend was not tough, so this frontend should not be either. I wanted clean code and fast development.

The First Decision Choice: Angular

Many ideas come to mind when starting out to build a frontend. In a broad sense, we could categorize frontend building options into 3 platforms:

  1. Website Builders – like WordPress, Wix, Squarespace, etc.
  2. Vanilla Building – using plain HTML, CSS, and JavaScript.
  3. JavaScript Framework – like React, Vue, Angular, etc.

In my experience, website builders provide a very restricted collection of widgets, components, and templates. Most website builders don’t provide an easy way to integrate an entire custom backend like Firebase. While it is possible to build impressive sites by connecting these pieces together, the degree of complexity of my project was likely beyond what these services typically provide.

Using the no-framework style or vanilla would have been a possibility. However, the deciding factor that made me not choose the pure vanilla route was that the latest non-CDN version of the Firebase JavaScript SDK (Version 9) is designed with installation via npm or yarn and module bundling in mind.

JavaScript frameworks handle frontend core parts (like routing, backend-linking, etc.) to reduce developer efforts. There are many of them and choosing which to use seemed to be a harder choice to make.

There are many JavaScript frameworks for frontend development. Examples include Angular, React, Vue, etc.

Of the available frameworks, I have the most familiarity with Angular. This is because I have used it on previous projects like:

  • Choir Carol Quiz: Portal where Quiz participants competed in two online rounds of timed multiple choice questions on select Bible chapters.
  • Genesys AE-FUNAI Community: Custom Form where members of Genesys Campus Club AE-FUNAI (my community) report their progress and share their achievements.
  • Tutorial Management System: Simple session management dashboard between students and tutors.

This familiarity allows me to build quickly with Angular. Being able to build quickly should not be underestimated.

I chose Angular because of its Object-Oriented Programming (OOP) ability. OOP is a programming paradigm that focuses more on classes, data, or state being managed, rather than on the logic controlling the data, as is the case with functional programming. Separation of concerns is one advantage of using OOP. In other words, OOP permits encapsulation. It permits you to scope aspects of the program to peculiar domains or classes.

In Angular, components (and their lifecycle methods) are scoped to TypeScript classes. This makes you think the OOP way. The OOP advantage reflects in how Angular components serve as reusable UI units in the Angular framework. That way you see an Angular component as some self-sufficient entity that is yet part of a whole. This makes frontend development easy as various parts of the frontend app can be scoped to components and hence can be used where needed.

I also chose Angular because it uses TypeScript. TypeScript is JavaScript with features of a typed programming language. Typing in this context means a variable can’t change the kind of value it holds all through its life. For example, a variable holding a string will not all of a sudden hold a number while it is used in that program. Typing increases code quality and reduces bugs.

As a result of its type system, TypeScript reduces the time spent debugging Angular apps. It gives developer experience as the developer will have more time to build the frontend app. Debugging also becomes easy for the developer.

Note: Here is more on Object-Oriented Programming with TypeScript

Still, on Angular’s advantages, Angular apps come as a complete setup. They handle important features like bundling CSS preprocessors or Angular services by themselves. That said, when using Angular, you don’t need to configure each library independently, Angular takes care of this.

An Angular service is what Angular uses to configure dependency injection. In simple terms, dependency injection is providing an application with what it needs to function (dependencies) without the application having to take care of how the dependencies were gotten. I also chose Angular because of its out-of-the-box handling of services. So Firebase, for example, will be auto-provided to all Angular components that need without any extra configuration.

The benefits of Object-Oriented Programming, TypeScript, and dependency injection make Angular a go-to for frontend development. Coupled with the fact I was already familiar with Angular, Angular was more convenient for this URL shortener project.

Angular articles on CSS-Tricks are also part of the story. They gave me more confidence with using Angular.

The Second Decision Choice: Material Design

After choosing Angular, my next task was to consider how I would handle the user interface (UI).

I could ignore and do vanilla CSS instead but why reinvent the wheel? After all, this would defeat the reason for using a JavaScript framework – convenience.

With choosing a UI toolkit, there seems to be an ocean of options. To name a few, one can use Bootstrap, Bulma, Semantic UI, Tailwind, etc. Each toolkit has its own specifications and motivations.

For the use case of my project, Material Design led the pack.

One of the most important factors was the support for Angular and Material Design. There is an entire Angular-only specification for Material on material.angular.io (that is as a subdomain to the Angular docs itself).

I settled for Material Design because it focuses on components. Unlike other CSS frameworks, it doesn’t have CSS utility classes. This was okay because I only wanted some component kit (buttons, icons, inputs, sidebars, snack bars, etc.) Material also adds animations, ripple, and shadow effects by itself; making it more convenient than other libraries.

Furthermore, Angular Material has out-of-the-box theming support, when initializing Angular Material, you have the option of choosing a pre-defined theme for the entire Angular app or creating a custom one.

Screenshot of installing Angular Material and selecting a theme.

For the sake of convenience, I chose a dark theme while setting up Angular Material.

The Third Decision Choice: Reactive Forms

With a framework and toolkit decided, I turned my attention to one of the most important features of the URL shortener. The core of the URL shortener’s frontend is the form for creating and updating links.

Let’s call this form the links editor. The links editor form has only two inputs, one for the short version of a link and the other for the full URL the short version will redirect to.

For managing forms, Angular comes with two mechanisms. So instead of creating a form and handling its validation and submission as is done in vanilla HTML and JavaScript, you have to use either of the two ways Angular provides. The two methods are:

  1. Template-driven forms
  2. Reactive forms

Template-driven forms as the name imply, involve the HTML (template) code controlling the greater part of the Angular form. This approach is preferable when your form does not do much or is for one-time usage.

Reactive forms, on the other hand, provide a model-driven approach to handling form inputs whose values change over time. I needed reactive forms because it is the same form that I will use to edit different links at any point in time.

Note: Here is more material on using Reactive Forms.

At this point, the benefits of previous choices began playing out. Angular Material has form-field components. The form-field wraps the input as a component and provides animations, ripple effects, and error messages if necessary.

Animated gif of short and long URLs entered into a form.

So I used it for the two inputs of the editor form.

The Fourth Decision Choice: Angular Material Bottom Sheet and Drawer

The final decision involved how to improve the user experience. The URL shortener would need other features like viewing all created links and their analytics. These features would require space on the screen that required me to rethink if there were better solutions to display the links editor form to the user.

If the user has no current need for the links editor form, it makes sense for the links editor form to not always be in view. This would free up space on the UI for other elements.

However, splitting this user experience into two separate pages felt disruptive. Instead, to open the links editor when needed, I added a floating action button on the page for creating links. When clicked, the button will cause the editor form to be opened in any fitting UI component.

bottom sheet, as the name implies, is a UI component that opens from the bottom of the screen. It has interactive content the user can work it. It overlays the current view of a mobile screen (but doesn’t fully block it).

Animated gif of interacting with a form displayed in a Bottom Sheet.

Bottom sheets are usually used in place of pop-ups if the user will spend a long time interacting with their content. So, bottom sheets are a good fit to open the editor on mobile screens. However, interacting with a bottom sheet is hard when the screen is wide. I needed a different UI component for the links editor form on wide screens.

Drawers open by the side. Using a drawer to open the links editor form on a wide screen was the go-to option. Drawers won’t be good for the editor on mobile screens. The screen’s width would be relatively small and the drawer might completely block the screen (which is not a desirable UX).

Animated gif of interacting with a form displayed in a Drawer.

I selected these two UI components from Material Design for the form to have some responsive effect. So whether on my phone or laptop creating links would be done in a fitting UI component.

In the code, Angular checks if the device is of small screen width. If so, it opens a bottom sheet containing the links editor form. On the other hand, if the screen is wide, Angular opens a drawer containing the same form.

Using these two components brought about a minor complication. If my phone is rotated or my laptop’s browser window’s width is reduced, the form opens on the contrary UI component. That is instead of opening in a drawer in a laptop, it will open in a bottom sheet (because the browser’s width was reduced).

Maintenance, Future-proofing, Future Releases

When I thought of opportunities to iterate on this project, I ran into limitations with the current use case designed to support a single administrator. But with authentication and user accounts, it can support additional users managing links and accessing analytics.

In that case, the above choices of components will still be appropriate. The links editor is responsive so on any device, users will have a good user experience.

If I had to do it all over again, I think I would have tried out the vanilla method. Building entirely without any helpers like Angular, Material, or UI components. I would try building from scratch in HTML, CSS, and JavaScript and see if I didn’t lose out on convenience as I thought I would.

Conclusion

You can access the final Angular code here on GitHub.

This was a review of some of the main choices I made when developing my project. Of course, there is more to building the frontend of a URL shortener. But for a start, these UI components made the building process convenient. They made the links editor form responsive and could be of similar use to you in your projects (not necessarily a URL shortener).

There are many other UI components from various libraries you can use for any such project. But as with my case, if convenience is a deciding factor, you would make the right decision choice that would be fitting for the UI.

Ultimately, what shaped my decisions was understanding what my project required, knowledge of tools I had used from previous projects, and expectations with time constraints. My past experience — successes and mistakes — helped guide me too.

Cheers!


Why I Chose Angular to Build a URL Shortener originally published on CSS-Tricks. You should get the newsletter.

Roundup of Recent Document Outline Chatter

Post pobrano z: Roundup of Recent Document Outline Chatter

It’s not everyday that HTML headings are the topic de jour, but my folder of saved links is accumulating articles about the recently merged removal of the document outline algorithm in the WHATWG Living Standard.

First off, you should know that the algorithm never really existed. Sure, it was in the spec. And sure, there was a warning about using it in the spec. But no browser ever implemented it, as Bruce Lawson reminded us. We have been living in a flat document structure the whole time.

This is very old news. Adrian Roselli has been writing about the document outline myth since 2013. But it’s his 2016 post titled “There Is No Document Outline Algorithm” that comprehensively spells it out and has been updated regularly with extra nuggets of context about the conversations and struggles that got us here. This is really the best timeline of the saga. Amelia Bellamy-Royds has also delved into the roots of the dilemma in the past here on CSS-Tricks.

My mind instantly goes to all the work that’s gone into the making of a document outline algorithm that supports sectioning. Removing it from the spec is the right call for sure, but it doesn’t take away from the herculean efforts that went into it even if it is now buried in some version history. I also think about all the well-intentioned folks who have written about the algorithm erroneously over time (including on this very site!) with the expectation that it was just around the corner. There’s nearly seven years of mental and technical debt that we’ve accrued from what appear to be a lack of action.

Looking past the “news” that the algorithm is officially no more, Bruce laments that there is no generic <h> element or the like that can be sectioned to produce the correct heading level. I agree. Having an <h1> element essentially exist as an exposed <title> is constraining, particularly since pages are so rarely structured around a single article with a single top-level heading. I often find myself wincing every time I’m making some sort of card component where using <h3> might be technically correct, but feels out of order. And that’s before we even talk about the styling considerations where a lower heading level now needs to look like a distinct higher heading level.

Speaking of heading level management, Steve Faulkner (who authored the PR that plucked the algorithm from the spec) has a super practical overview of using the <hgroup> element to handle heading patterns that involve subheadings, subtitles, alternative titles, snd taglines. I’m sure you’ve seen markup like this in the wild:

<h1>Disappointingly Average</h1>
<h2>The Autobiography of Geoff Graham</h2>
<h3>by Geoff Graham</h3>

That doesn’t jive with a flat document outline that’s driven by heading levels. Each one of those headings represents a section that forms a hierarchy of information:

Disappointingly Average
└── The Autobiography of Geoff Graham
    └── by Geoff Graham

What we want instead is a group of headings. Cue the <hgroup> element:

When nested within a <hgroup> element, the <p> element’s content represents a subheading, alternative title, or tagline which are not included in the document outline.

So, we get this structure:

<hgroup>
  <h1>Disappointingly Average</h1>
  <p>The Autobiography of Geoff Graham</p>
  <p>by Geoff Graham</p>
</hgroup>

<hgroup> is role=generic at the moment, but Steve points to a proposal that could map it to role=group. If that happens, the accessibility tree will allow assistive tech to assign more semantic meaning to those paragraphs as the subtitle and tagline pieces that they are. Sounds easy but Steve notes challenges that are in the way. He also demos how this sort of pattern could be implemented today with ARIA attributes.

As long as we’re rounding things up, Matthias Ott published a few tips on creating a structured outline with headings. Check out the end for a great list of tools to check your heading outlines.


Roundup of Recent Document Outline Chatter originally published on CSS-Tricks. You should get the newsletter.

Collective Nouns for the Web

Post pobrano z: Collective Nouns for the Web

Melanie Sumner has this super-specific collection of web-related nouns for describing a group or set of something. You know how there’s a school or fish or a herd of cows? Same sort of thing, but for funny web jargon.

Things like:

  • vanity is ten (10) or more domains owned by a single person, where very few are in use.
  • parade of RSS feeds is when you have so many RSS feeds that you have to scroll to see them all.

Don’t lie, Melanie — you’ve gotta vanity go domains, don’t ya? 😂

A few off the top of my head:

  • A braintrust is a meeting of company bigwigs indirectly related to a project but with the power to “swoop and poop” on your work.
  • A diatribe is a Pull Request with so many comments that the original scope of the work has changed.
  • A gommit is a group of commits (group + commit) submitted as a single commit.
  • A hot minute is the number of hours spent fixing a bug that winds up being a simple typo.
  • A roost is four (4) or more levels of nesting in Sass.
  • A tababyte is a unit that represents ten (10) open browser tabs in the same window. For example, two tababytes are equal to 20 open tabs.

Fun times! Anything you’d add to the list? You can submit ideas to the repo.

To Shared LinkPermalink on CSS-Tricks


Collective Nouns for the Web originally published on CSS-Tricks. You should get the newsletter.

Single Element Loaders: Going 3D!

Post pobrano z: Single Element Loaders: Going 3D!

For this fourth and final article of our little series on single-element loaders, we are going to explore 3D patterns. When creating a 3D element, it’s hard to imagine that just one HTML element is enough to simulate something like all six faces of a cube. But  maybe we can get away with something more cube-like instead by showing only the front three sides of the shape — it’s totally possible and that’s what we’re going to do together.

Article series

The split cube loader

Here is a 3D loader where a cube is split into two parts, but is only made with only a single element:

CodePen Embed Fallback

Each half of the cube is made using a pseudo-element:

Cool, right?! We can use a conic gradient with CSS clip-path on the element’s ::before and ::after pseudos to simulate the three visible faces of a 3D cube. Negative margin is what pulls the two pseudos together to overlap and simulate a full cube. The rest of our work is mostly animating those two halves to get neat-looking loaders!

Let’s check out a visual that explains the math behind the clip-path points used to create this cube-like element:

We have our variables and an equation, so let’s put those to work. First, we’ll establish our variables and set the sizing for the main .loader element:

.loader {
  --s: 150px; /* control the size */
  --_d: calc(0.353 * var(--s)); /* 0.353 = sin(45deg)/2 */

  width: calc(var(--s) + var(--_d)); 
  aspect-ratio: 1;
  display: flex;
}

Nothing too crazy so far. We have a 150px square that’s set up as a flexible container. Now we establish our pseudos:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
}

Those are two halves in the .loader container. We need to paint them in, so that’s where our conic gradient kicks in:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
}

The gradient is there, but it looks weird. We need to clip it to the element:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
  clip-path:
    polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}

Let’s make sure the two halves overlap with a negative margin:

.loader::before {
  margin-right: calc(var(--_d) / -2);
}

.loader::after {
  margin-left: calc(var(--_d) / -2);
}

Now let’s make ‘em move!

.loader::before,
.loader::after {
  /* same as before */
  animation: load 1.5s infinite cubic-bezier(0, .5, .5, 1.8) alternate;
}

.loader::after {
  /* same as before */
  animation-delay: -.75s
}

@keyframes load{
  0%, 40%   { transform: translateY(calc(var(--s) / -4)) }
  60%, 100% { transform: translateY(calc(var(--s) / 4)) }
}

Here’s the final demo once again:

CodePen Embed Fallback

The progress cube loader

Let’s use the same technique to create a 3D progress loader. Yes, still only one element!

CodePen Embed Fallback

We’re not changing a thing as far as simulating the cube the same way we did before, other than changing the loader’s height and aspect ratio. The animation we’re making relies on a surprisingly easy technique where we update the width of the left side while the right side fills the remaining space, thanks to flex-grow: 1.

The first step is to add some transparency to the right side using opacity:

CodePen Embed Fallback

This simulates the effect that one side of the cube is filled in while the other is empty. Then we update the color of the left side. To do that, we either update the three colors inside the conic gradient or we do it by adding a background color with a background-blend-mode:

.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
}

This trick only allows us to update the color only once. The right side of the loader blends in with the three shades of white from the conic gradient to create three new shades of our color, even though we’re only using one color value. Color trickery!

CodePen Embed Fallback

Let’s animate the width of the loader’s left side:

CodePen Embed Fallback

Oops, the animation is a bit strange at the beginning! Notice how it sort of starts outside of the cube? This is because we’re starting the animation at the 0% width. But due to the clip-path and negative margin we’re using, what we need to do instead is start from our --_d variable, which we used to define the clip-path points and the negative margin:

@keyframes load {
  0%,
  5% {width: var(--_d); }
  95%,
  100% {width: 100%; }
}

That’s a little better:

CodePen Embed Fallback

But we can make this animation even smoother. Did you notice we’re missing a little something? Let me show you a screenshot to compare what the final demo should look like with that last demo:

It’s the bottom face of the cube! Since the second element is transparent, we need to see the bottom face of that rectangle as you can see in the left example. It’s subtle, but should be there!

We can add a gradient to the main element and clip it like we did with the pseudos:

background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;

Here’s the full code once everything is pulled together:

.loader {
  --s: 100px; /* control the size */
  --_d: calc(0.353*var(--s)); /* 0.353 = sin(45deg) / 2 */

  height: var(--s); 
  aspect-ratio: 3;
  display: flex;
  background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;
  clip-path: polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}
.loader::before,
.loader::after {
  content: "";
  clip-path: inherit;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
     #fff 135deg, #666 0 270deg, #aaa 0);
}
.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
  margin-right: calc(var(--_d) / -2);
  animation: load 2.5s infinite linear;
}
.loader:after {
  flex: 1;
  margin-left: calc(var(--_d) / -2);
  opacity: 0.4;
}

@keyframes load {
  0%,
  5% { width: var(--_d); }
  95%,
  100% { width: 100%; }
}
CodePen Embed Fallback

That’s it! We just used a clever technique that uses pseudo-elements, conic gradients, clipping, background blending, and negative margins to get, not one, but two sweet-looking 3D loaders with nothing more than a single element in the markup.

More 3D

We can still go further and simulate an infinite number of 3D cubes using one element — yes, it’s possible! Here’s a grid of cubes:

CodePen Embed Fallback

This demo and the following demos are unsupported in Safari at the time of writing.

Crazy, right? Now we’re creating a repeated pattern of cubes made using a single element… and no pseudos either! I won’t go into fine detail about the math we are using (there are very specific numbers in there) but here is a figure to visualize how we got here:

We first use a conic-gradient to create the repeating cube pattern. The repetition of the pattern is controlled by three variables:

  • --size: True to its name, this controls the size of each cube.
  • --m: This represents the number of columns.
  • --n: This is the number of rows.
  • --gap: this the gap or distance between the cubes
.cube {
  --size: 40px; 
  --m: 4; 
  --n: 5;
  --gap :10px;

  aspect-ratio: var(--m) / var(--n);
  width: calc(var(--m) * (1.353 * var(--size) + var(--gap)));
  background:
    conic-gradient(from -90deg at var(--size) calc(0.353 * var(--size)),
      #249FAB 135deg, #81C5A3 0 270deg, #26609D 0) /* update the colors here */
    0 0 / calc(100% / var(--m)) calc(100% / var(--n));
}

Then we apply a mask layer using another pattern having the same size. This is the trickiest part of this idea. Using a combination of a linear-gradient and a conic-gradient we will cut a few parts of our element to keep only the cube shapes visible.

.cube {
  /* etc. */
  mask: 
    linear-gradient(to bottom right,
       #0000 calc(0.25 * var(--size)),
       #000 0 calc(100% - calc(0.25 * var(--size)) - 1.414 * var(--gap)),
       #0000 0),
    conic-gradient(from -90deg at right var(--gap) bottom var(--gap), #000 90deg, #0000 0);  
  mask-size: calc(100% / var(--m)) calc(100% / var(--n));
  mask-composite: intersect;
}

The code may look a bit complex but thanks to CSS variables all we need to do is to update a few values to control our matrix of cubes. Need a 10⨉10 grid? Update the --m and --n variables to 10. Need a wider gap between cubes? Update the --gap value. The color values are only used once, so update those for a new color palette!

Now that we have another 3D technique, let’s use it to build variations of the loader by playing around with different animations. For example, how about a repeating pattern of cubes sliding infinitely from left to right?

CodePen Embed Fallback

This loader defines four cubes in a single row. That means our --n value is 4 and --m is equal to 1 . In other words, we no longer need these!

Instead, we can work with the --size and --gap variables in a grid container:

.loader {
  --size: 70px;
  --gap: 15px;  

  width: calc(3 * (1.353 * var(--size) + var(--gap)));
  display: grid;
  aspect-ratio: 3;
}

This is our container. We have four cubes, but only want to show three in the container at a time so that we always have one sliding in as one is sliding out. That’s why we are factoring the width by 3 and have the aspect ratio set to 3 as well.

Let’s make sure that our cube pattern is set up for the width of four cubes. We’re going to do this on the container’s ::before pseudo-element:

.loader::before { 
  content: "";
  width: calc(4 * 100% / 3);
  /*
     Code to create four cubes
  */
}

Now that we have four cubes in a three-cube container, we can justify the cube pattern to the end of the grid container to overflow it, showing the last three cubes:

.loader {
  /* same as before */
  justify-content: end;
}

Here’s what we have so far, with a red outline to show the bounds of the grid container:

CodePen Embed Fallback

Now all we have to do is to move the pseudo-element to the right by adding our animation:

@keyframes load {
  to { transform: translate(calc(100% / 4)); }
}
CodePen Embed Fallback

Did you get the trick of the animation? Let’s finish this off by hiding the overflowing cube pattern and by adding a touch of masking to create that fading effect that the start and the end:

.loader {
  --size: 70px;
  --gap: 15px;  
  
  width: calc(3*(1.353*var(--s) + var(--g)));
  display: grid;
  justify-items: end;
  aspect-ratio: 3;
  overflow: hidden;
  mask: linear-gradient(90deg, #0000, #000 30px calc(100% - 30px), #0000);
}
CodePen Embed Fallback

We can make this a lot more flexible by introducing a variable, --n, to set how many cubes are displayed in the container at once. And since the total number of cubes in the pattern should be one more than --n, we can express that as calc(var(--n) + 1).

Here’s the full thing:

CodePen Embed Fallback

OK, one more 3D loader that’s similar but has the cubes changing color in succession instead of sliding:

CodePen Embed Fallback

We’re going to rely on an animated background with background-blend-mode for this one:

.loader {
  /* ... */
  background:
    linear-gradient(#ff1818 0 0) 0% / calc(100% / 3) 100% no-repeat,
    /* ... */;
  background-blend-mode: multiply;
  /* ... */
  animation: load steps(3) 1.5s infinite;
}
@keyframes load {
  to { background-position: 150%; }
}

I’ve removed the superfluous code used to create the same layout as the last example, but with three cubes instead of four. What I am adding here is a gradient defined with a specific color that blends with the conic gradient, just as we did earlier for the progress bar 3D loader.

From there, it’s animating the background gradient’s background-position as a three-step animation to make the cubes blink colors one at a time.

If you are not familiar with the values I am using for background-position and the background syntax, I highly recommend one of my previous articles and one of my Stack Overflow answers. You will find a very detailed explanation there.

Can we update the number of cubes to make it variables?

Yes, I do have a solution for that, but I’d like you to take a crack at it rather than embedding it here. Take what we have learned from the previous example and try to do the same with this one — then share your work in the comments!

Variations galore!

Like the other three articles in this series, I’d like to leave you with some inspiration to go forth and create your own loaders. Here is a collection that includes the 3D loaders we made together, plus a few others to get your imagination going:

CodePen Embed Fallback

That’s a wrap

I sure do hope you enjoyed spending time making single element loaders with me these past few weeks. It’s crazy that we started with seemingly simple spinner and then gradually added new pieces to work ourselves all the way up to 3D techniques that still only use a single element in the markup. This is exactly what CSS looks like when we harness its powers: scalable, flexible, and reusable.

Thanks again for reading this little series! I’ll sign off by reminding you that I have a collection of more than 500 loaders if you’re looking for more ideas and inspiration.

Article series


Single Element Loaders: Going 3D! originally published on CSS-Tricks. You should get the newsletter.

Bunny Fonts

Post pobrano z: Bunny Fonts

Bunny Fonts bills itself as the “privacy-first web font platform designed to put privacy back into the internet.” According to its FAQ:

With a zero-tracking and no-logging policy, Bunny Fonts helps you stay fully GDPR compliant and puts your user’s personal data into their own hands.

Hard for my mind not to go straight to Google Fonts. Bunny Fonts even says they are a drop-in replacement for Google Fonts. It offers the same open source fonts and holds the same API structure used by Google Fonts.

Now, I’m no GDPR expert but the possibility of Google collecting data through its Fonts API is hardly unsurprising or even unexpected. I was curious to check out Google’s privacy statement for Fonts:

The Google Fonts API logs the details of the HTTP request, which includes the timestamp, requested URL, and all HTTP headers (including referrer and user agent string) provided in connection with the use of our CSS API.

IP addresses are not logged.

Comparing that to what Bunny Fonts says in its FAQ:

When using Bunny Fonts, no personal data or logs are stored. All the requests are processed completely anonymously.

Or perhaps more thoroughly explained on the bunny.net GDPR statement:

In most cases, the data held and collected by bunny.net does not contain any user identifiable data. In some cases, which depend on how you are using bunny.net and how your website is structured, personal data may be collected from your users. Such information includes hosting user uploaded content as well as personal data that might be transmitted in the URL, User-Agent or Referer headers of the HTTP protocol.

Sounds pretty similar, right? Well, it may not have been that similar earlier this year when a German court ruled that embedded Google Fonts violated GDPR compliance. It appears that one line in the Google Fonts privacy statement about IP addresses came after the ruling, once the API scrubbed them from collected data.

So, do you need to ditch Google Fonts to be GDPR compliant? I would imagine not if IP addresses were the sole concern, but I’ll leave that for folks who know the rules to comment on that.

But if you are concerned about Google Font’s GDPR compliance, I guess Bunny Fonts is worth a look! And seeing that it’s powered by bunny.net’s CDN services, you should get pretty comparable performance marks.

To Shared LinkPermalink on CSS-Tricks


Bunny Fonts originally published on CSS-Tricks. You should get the newsletter.

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 directoryFramboise 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.