Css Tricks

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

What I Took From the State of Dev 2025 Survey

Wed, 07/16/2025 - 2:54am

State of Devs 2025 survey results are out! While the survey isn’t directly related to the code part of what we do for work, I do love the focus Devographics took ever since its inception in 2020. And this year it brought us some rather interesting results through the attendance of 8,717 developers, lots of data, and even more useful insights that I think everyone can look up and learn from.

I decided to look at the survey results with an analytical mindset, but wound up pouring my heart out because, well, I am a developer, and the entire survey affects me in a way. I have some personal opinions, it turns out. So, sit back, relax, and indulge me for a bit as we look at a few choice pieces of the survey.

And it’s worth noting that this is only part one of the survey results. A second data dump will be published later and I’m interested to poke at those numbers, too.

An opportunity to connect

One thing I noticed from the Demographics section is how much tech connects us all. The majority of responses come from the U.S. (26%) but many other countries, including Italy, Germany, France, Estonia, Austria, South Africa and many more, account for the remaining 74%.

I mean, I am working and communicating with you right now, all the way from Nigeria! Isn’t that beautiful, to be able to communicate with people around the world through this wonderful place we call CSS-Tricks? And into the bigger community of developers that keeps it so fun?

I think this is a testament to how much we want to connect. More so, the State of Devs survey gives us an opportunity to express our pain points on issues surrounding our experiences, workplace environments, quality of health, and even what hobbies we have as developers. And while I say developers, the survey makes it clear it’s more than that. Behind anyone’s face is someone encountering life challenges. We’re all people and people are capable of pure emotion. We are all just human.

It’s also one of the reasons I decided to open a Bluesky account: to connect with more developers.

I think this survey offers insights into how much we care about ourselves in tech, and how eager we are to solve issues rarely talked about. And the fact that it’s global in nature illustrates how much in common we all have.

More women participated this year

From what I noticed, fewer women participated in the 2024 State of JavaScript and State of CSS fewer women (around 6%), while women represented a bigger share in this year’s State of Devs survey. I’d say 15% is still far too low to fairly “represent” an entire key segment of people, but it is certainly encouraging to see a greater slice in this particular survey. We need more women in this male-dominated industry.

Experience over talent

Contrary to popular opinion, personal performance does not usually equate to higher pay, and this is reflected in the results of this survey. It’s more like, the more experienced you are, the more you’re paid. But even that’s not the full story. If you’re new to the field, you still have to do some personal marketing, find and keep a mentor, and a whole bunch of stuff. Cassidy shares some nice insights on this in a video interview tracing her development career. You should check it out, especially if you’re just starting out.

Notice that the average income for those with 10-14 of experience ($115,833) is on par with those with between 15-29 years of experience ($118,000) and not far from those with 30+ years ($120,401). Experience appears to influence income, but perhaps not to the extent you would think, or else we’d see a wider gap between those with 15 years versus those with more than double the service time.

More than that, notice how income for the most experienced developers (30+ years) is larger on average but the range of how much they make is lower than than those with 10-29 years under their belts. I’m curious what causes that decline. Is it a lack of keeping up with what’s new? Is it ageism? I’m sure there are lots of explanations.

Salary, workplace, and job hunting

I prefer not drill into each and every report. I’m interested in very specific areas that are covered in the survey. And what I take away from the survey is bound to be different than your takeaways, despite numbers being what they are. So, here are a few highlights of what stood out to me personally as I combed through the results.

Your experience, employment status, and company’s employer count seem to directly affect pay. For example, full-timers report higher salaries than freelancers. I suppose that makes sense, but I doubt it provides the full picture because freelancers freelance for a number of reasons, whether its flexible hours, having more choice to choose their projects, or having personal constraints that limit how much they can work. In some ways, freelancers are able to command higher pay while working less.

Bad management and burnout seem to be the most talked-about issues in the workplace. Be on guard during interviews, look up reviews about the company you’re about to work for, and make sure there are far fewer complaints than accolades. Make sure you’re not being too worked up during work hours; breaks are essential for a boost in productivity.

Seventy percent of folks reported no discrimination in the workplace, which means we’re perhaps doing something right. That said, it’s still disheartening that 30% experience some form of discrimination and lowering that figure is something we ought to aim for. I’m hoping companies — particularly the tech giants in our space — take note of this and enforce laws and policies surrounding this. Still, we can always call out discriminatory behavior and make corrections where necessary. And who’s to say that everyone who answered the survey felt safe sharing that sort of thing? Silence can be the enemy of progress.

Never get too comfortable in your job. Although 69% report having never been laid off, I still think that job security is brittle in this space. Always learn, build, and if possible, try to look for other sources of income. Layoffs are still happening, and looking at the news, it’s likely to continue for the foreseeable future, with the U.S., Australia, and U.K. being leading the way.

One number that jumped off the page for me is that it takes an average of four applications for most developers to find a new job. This bamboozles me. I’m looking for a full-time role (yes, I’m available!), and I regularly apply for more than four jobs in a given day. Perhaps I’m doing something wrong, but that’s also not consistent with those in my social and professional circles. I know and see plenty of people who are working hard to find work, and the number of jobs they apply for has to bring that number up. Four applications seems way low, though I don’t have the quantitative proof for it.

Your personal network is still the best way to find a job. We will always and forever be social animals, and I think that’s why most survey participants say that coworker relationships are the greatest perk of a job. I find this to be true with my work here at CSS-Tricks. I get to collaborate with other like-minded CSS and front-end enthusiasts far and wide. I’ve developed close relationships with the editors and other writers, and that’s something I value more than any other benefits I could get somewhere else.

Compensation is still a top workplace challenge. JavaScript is still the king of programming (bias alert), taking the top spot as the most popular programming language. I know you’re interested, that CSS came in at third.

To my surprise, Bluesky is more popular amongst developers than X. I didn’t realize how much toxicity I’ve been exposed to at X until I opened a Bluesky account. I hate saying that the “engagement” is better, or some buzz-worthy thing like that, but I do experience more actual discussions over at Bluesky than I have for a long time at X. And many of you report the same. I hasten to say that Bluesky is a direct replacement for what X (let’s face it, Twitter) used to be, but it seems we at least have a better alternative.

Health issues

Without our health, we are nothing. Embrace your body for what it is: your temple. It’s a symbiotic relationship.

Mrs. N.

I’m looking closer at the survey’s results on health because of the sheer number of responses that report health issues. I struggle with issues, like back pains, and that forced me to upgrade my work environment with a proper desk and chair. I tend to code on my bed, and well, it worked. But perhaps it wasn’t the best thing for my physical health.

I know we can fall into the stereotype of people who spend 8-12 hours staring at two big monitors, sitting in a plush gaming chair, while frantically typing away at a mechanical keyboard. You know, the Hackers stereotype. I know that isn’t an accurate portrayal of who we are, but it’s easy to become that because of how people look at and understand our work.

And if you feel a great deal of pressure to keep up with that image, I think it’s worth getting into a more healthy mindset, one that gets more than a few hours of sleep, prioritizes exercise, maintains a balanced diet, and all those things we know are ultimately good for us. Even though 20% of folks say they have no health issues at all, a whopping 80% struggle with health issues ranging from sleep deprivation to keeping a healthy weight. You are important and deserve to feel healthy.

Think about your health the way you think about the UI/UX of the websites you design and build. It makes up a part of the design, but has the crucial role of turning ordinary tasks into enjoyable experiences, which in turn, transforms into an overall beautiful experience for the user.

Your health is the same. Those small parts often overlooked can and will affect the great machine that is your body. Here’s a small list of life improvements you can make right now.

Closing thoughts

Diversity, representation, experience, income, and health. That’s what stood out to me in the 2025 State of Devs survey results. I see positive trends in the numbers, but also a huge amount of opportunity to be better, particularly when it comes being more inclusive of women, providing ample chances for upward mobility based on experience, and how we treat ourselves.

Please check out the results and see what stands out to you. What do you notice? Is there anything you are able to take away from the survey that you can use in your own work or life? I’d love to know!

What I Took From the State of Dev 2025 Survey originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Setting Line Length in CSS (and Fitting Text to a Container)

Mon, 07/14/2025 - 2:38am

First, what is line length? Line length is the length of a container that holds a body of multi-line text. “Multi-line” is the key part here, because text becomes less readable if the beginning of a line of text is too far away from the end of the prior line of text. This causes users to reread lines by mistake, and generally get lost while reading.

Luckily, the Web Content Accessibility Guidelines (WCAG) gives us a pretty hard rule to follow: no more than 80 characters on a line (40 if the language is Chinese, Japanese, or Korean), which is super easy to implement using character (ch) units:

width: 80ch;

The width of 1ch is equal to the width of the number 0 in your chosen font, so the exact width depends on the font.

Setting the optimal line length

Just because you’re allowed up to 80 characters on a line, it doesn’t mean that you have to aim for that number. A study by the Baymard Institute revealed that a line length of 50-75 characters is the optimal length — this takes into consideration that smaller line lengths mean more lines and, therefore, more opportunities for users to make reading mistakes.

That being said, we also have responsive design to think about, so setting a minimum width (e.g., min-width: 50ch) isn’t a good idea because you’re unlikely to fit 50 characters on a line with, for example, a screen/window size that is 320 pixels wide. So, there’s a bit of nuance involved, and the best way to handle that is by combining the clamp() and min() functions:

  • clamp(): Set a fluid value that’s relative to a container using percentage, viewport, or container query units, but with minimum and maximum constraints.
  • min(): Set the smallest value from a list of comma-separated values.

Let’s start with min(). One of the arguments is 93.75vw. Assuming that the container extends across the whole viewport, this’d equal 300px when the viewport width is 320px (allowing for 20px of spacing to be distributed as you see fit) and 1350px when the viewport width is 1440px. However, for as long as the other argument (50ch) is the smallest of the two values, that’s the value that min() will resolve to.

min(93.75vw, 50ch);

Next is clamp(), which accepts three arguments in the following order: the minimum, preferred, and maximum values. This is how we’ll set the line length.

For the minimum, you’d plug in your min() function, which sets the 50ch line length but only conditionally. For the maximum, I suggest 75ch, as mentioned before. The preferred value is totally up to you — this will be the width of your container when not hitting the minimum or maximum.

width: clamp(min(93.75vw, 50ch), 70vw, 75ch);

In addition, you can use min(), max(), and calc() in any of those arguments to add further nuance.

If the container feels too narrow, then the font-size might be too large. If it feels too wide, then the font-size might be too small.

Fit text to container (with JavaScript)

You know that design trend where text is made to fit the width of a container? Typically, to utilize as much of the available space as possible? You’ll often see it applied to headings on marketing pages and blog posts. Well, Chris wrote about it back in 2018, rounding up several ways to achieve the effect with JavaScript or jQuery, unfortunately with limitations. However, the ending reveals that you can just use SVG as long as you know the viewBox values, and I actually have a trick for getting them.

Although it still requires 3-5 lines of JavaScript, it’s the shortest method I’ve found. It also slides into HTML and CSS perfectly, particularly since the SVG inherits many CSS properties (including the color, thanks to fill: currentColor):

CodePen Embed Fallback <h1 class="container"> <svg> <text>Fit text to container</text> </svg> </h1> h1.container { /* Container size */ width: 100%; /* Type styles (<text> will inherit most of them) */ font: 900 1em system-ui; color: hsl(43 74% 3%); text { /* We have to use fill: instead of color: here But we can use currentColor to inherit the color */ fill: currentColor; } } /* Select all SVGs */ const svg = document.querySelectorAll("svg"); /* Loop all SVGs */ svg.forEach(element => { /* Get bounding box of <text> element */ const bbox = element.querySelector("text").getBBox(); /* Apply bounding box values to SVG element as viewBox */ element.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" ")); }); Fit text to container (pure CSS)

If you’re hell-bent on a pure-CSS method, you are in luck. However, despite the insane things that we can do with CSS these days, Roman Komarov’s fit-to-width hack is a bit complicated (albeit rather impressive). Here’s the gist of it:

  • The text is duplicated a couple of times (although hidden accessibly with aria-hidden and hidden literally with visibility: hidden) so that we can do math with the hidden ones, and then apply the result to the visible one.
  • Using container queries/container query units, the math involves dividing the inline size of the text by the inline size of the container to get a scaling factor, which we then use on the visible text’s font-size to make it grow or shrink.
  • To make the scaling factor unitless, we use the tan(atan2()) type-casting trick.
  • Certain custom properties must be registered using the @property at-rule (otherwise they don’t work as intended).
  • The final font-size value utilizes clamp() to set minimum and maximum font sizes, but these are optional.
<span class="text-fit"> <span> <span class="text-fit"> <span><span>fit-to-width text</span></span> <span aria-hidden="true">fit-to-width text</span> </span> </span> <span aria-hidden="true">fit-to-width text</span> </span> .text-fit { display: flex; container-type: inline-size; --captured-length: initial; --support-sentinel: var(--captured-length, 9999px); & > [aria-hidden] { visibility: hidden; } & > :not([aria-hidden]) { flex-grow: 1; container-type: inline-size; --captured-length: 100cqi; --available-space: var(--captured-length); & > * { --support-sentinel: inherit; --captured-length: 100cqi; --ratio: tan( atan2( var(--available-space), var(--available-space) - var(--captured-length) ) ); --font-size: clamp( 1em, 1em * var(--ratio), var(--max-font-size, infinity * 1px) - var(--support-sentinel) ); inline-size: var(--available-space); &:not(.text-fit) { display: block; font-size: var(--font-size); @container (inline-size > 0) { white-space: nowrap; } } /* Necessary for variable fonts that use optical sizing */ &.text-fit { --captured-length2: var(--font-size); font-variation-settings: "opsz" tan(atan2(var(--captured-length2), 1px)); } } } } @property --captured-length { syntax: "<length>"; initial-value: 0px; inherits: true; } @property --captured-length2 { syntax: "<length>"; initial-value: 0px; inherits: true; } CodePen Embed Fallback Watch for new text-grow/text-shrink properties

To make fitting text to a container possible in just one line of CSS, a number of solutions have been discussed. The favored solution seems to be two new text-grow and text-shrink properties. Personally, I don’t think we need two different properties. In fact, I prefer the simpler alternative, font-size: fit-width, but since text-grow and text-shrink are already on the table (Chrome intends to prototype and you can track it), let’s take a look at how they could work.

The first thing that you need to know is that, as proposed, the text-grow and text-shrink properties can apply to multiple lines of wrapped text within a container, and that’s huge because we can’t do that with my JavaScript technique or Roman’s CSS technique (where each line needs to have its own container).

Both have the same syntax, and you’ll need to use both if you want to allow both growing and shrinking:

text-grow: <fit-target> <fit-method>? <length>?; text-shrink: <fit-target> <fit-method>? <length>?;
  • <fit-target>
    • per-line: For text-grow, lines of text shorter than the container will grow to fit it. For text-shrink, lines of text longer than the container will shrink to fit it.
    • consistent: For text-grow, the shortest line will grow to fit the container while all other lines grow by the same scaling factor. For text-shrink, the longest line will shrink to fit the container while all other lines shrink by the same scaling factor.
  • <fit-method> (optional)
    • scale: Scale the glyphs instead of changing the font-size.
    • scale-inline: Scale the glyphs instead of changing the font-size, but only horizontally.
    • font-size: Grow or shrink the font size accordingly. (I don’t know what the default value would be, but I imagine this would be it.)
    • letter-spacing: The letter spacing will grow/shrink instead of the font-size.
  • <length> (optional): The maximum font size for text-grow or minimum font size for text-shrink.

Again, I think I prefer the font-size: fit-width approach as this would grow and shrink all lines to fit the container in just one line of CSS. The above proposal does way more than I want it to, and there are already a number of roadblocks to overcome (many of which are accessibility-related). That’s just me, though, and I’d be curious to know your thoughts in the comments.

Conclusion

It’s easier to set line length with CSS now than it was a few years ago. Now we have character units, clamp() and min() (and max() and calc() if you wanted to throw those in too), and wacky things that we can do with SVGs and CSS to fit text to a container. It does look like text-grow and text-shrink (or an equivalent solution) are what we truly need though, at least in some scenarios.

Until we get there, this is a good time to weigh-in, which you can do by adding your feedback, tests, and use-cases to the GitHub issue.

Setting Line Length in CSS (and Fitting Text to a Container) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Scroll-Driven Sticky Heading

Fri, 07/11/2025 - 7:11am

Scroll-driven animations are great! They’re a powerful tool that lets developers tie the movement and transformation of elements directly to the user’s scroll position. This technique opens up new ways to create interactive experiences, cuing images to appear, text to glide across the stage, and backgrounds to subtly shift. Used thoughtfully, scroll-driven animations (SDA) can make your website feel more dynamic, engaging, and responsive.

A few weeks back, I was playing around with scroll-driven animations, just searching for all sorts of random things you could do with it. That’s when I came up with the idea to animate the text of the main heading (h1) and, using SDA, change the heading itself based on the user’s scroll position on the page. In this article, we’re going to break down that idea and rebuild it step by step. This is the general direction we’ll be heading in, which looks better in full screen and viewed in a Chromium browser:

CodePen Embed Fallback

It’s important to note that the effect in this example only works in browsers that support scroll-driven animations. Where SDA isn’t supported, there’s a proper fallback to static headings. From an accessibility perspective, if the browser has reduced motion enabled or if the page is being accessed with assistive technology, the effect is disabled and the user gets all the content in a fully semantic and accessible way.

Just a quick note: this approach does rely on a few “magic numbers” for the keyframes, which we’ll talk about later on. While they’re surprisingly responsive, this method is really best suited for static content, and it’s not ideal for highly dynamic websites.

Closer Look at the Animation

Before we dive into scroll-driven animations, let’s take a minute to look at the text animation itself, and how it actually works. This is based on an idea I had a few years back when I wanted to create a typewriter effect. At the time, most of the methods I found involved animating the element’s width, required using a monospace font, or a solid color background. None of which really worked for me. So I looked for a way to animate the content itself, and the solution was, as it often is, in pseudo-elements.

CodePen Embed Fallback

Pseudo-elements have a content property, and you can (kind of) animate that text. It’s not exactly animation, but you can change the content dynamically. The cool part is that the only thing that changes is the text itself, no other tricks required.

Start With a Solid Foundation

Now that you know the trick behind the text animation, let’s see how to combine it with a scroll-driven animation, and make sure we have a solid, accessible fallback as well.

We’ll start with some basic semantic markup. I’ll wrap everything in a main element, with individual sections inside. Each section gets its own heading and content, like text and images. For this example, I’ve set up four sections, each with a bit of text and some images, all about Primary Colors.

<main> <section> <h1>Primary Colors</h1> <p>The three primary colors (red, blue, and yellow) form the basis of all other colors on the color wheel. Mixing them in different combinations produces a wide array of hues.</p> <img src="./colors.jpg" alt="...image description"> </section> <section> <h2>Red Power</h2> <p>Red is a bold and vibrant color, symbolizing energy, passion, and warmth. It easily attracts attention and is often linked with strong emotions.</p> <img src="./red.jpg" alt="...image description"> </section> <section> <h2>Blue Calm</h2> <p>Blue is a calm and cool color, representing tranquility, stability, and trust. It evokes images of the sky and sea, creating a peaceful mood.</p> <img src="./blue.jpg" alt="...image description"> </section> <section> <h2>Yellow Joy</h2> <p>Yellow is a bright and cheerful color, standing for light, optimism, and creativity. It is highly visible and brings a sense of happiness and hope.</p> <img src="./yellow.jpg" alt="...image description"> </section> </main>

As for the styling, I’m not doing anything special at this stage, just the basics. I changed the font and adjusted the text and heading sizes, set up the display for the main and the sections, and fixed the image sizes with object-fit.

CodePen Embed Fallback

So, at this point, we have a simple site with static, semantic, and accessible content, which is great. Now the goal is to make sure it stays that way as we start adding our effect.

The Second First Heading

We’ll start by adding another h1 element at the top of the main. This new element will serve as the placeholder for our animated text, updating according to the user’s scroll position. And yes, I know there’s already an h1 in the first section; that’s fine and we’ll address it in a moment so that only one is accessible at a time.

<h1 class="scrollDrivenHeading" aria-hidden="true">Primary Colors</h1>

Notice that I’ve added aria-hidden="true" to this heading, so it won’t be picked up by screen readers. Now I can add a class specifically for screen readers, .srOnly, to all the other headings. This way, anyone viewing the content “normally” will see only the animated heading, while assistive technology users will get the regular, static semantic headings.

CodePen Embed Fallback

Note: The style for the .srOnly class is based on “Inclusively Hidden” by Scott O’Hara.

Handling Support

As much as accessibility matters, there’s another concern we need to keep in mind: support. CSS Scroll-Driven Animations are fantastic, but they’re still not fully supported everywhere. That’s why it’s important to provide the static version for browsers that don’t support SDA.

The first step is to hide the animated heading we just added using display: none. Then, we’ll add a new @supports block to check for SDA support. Inside that block, where SDA is supported, we can change back the display for the heading.

The .srOnly class should also move into the @supports block, since we only want it to apply when the effect is active, not when it’s not supported. This way, just like with assistive technology, anyone visiting the page in a browser without SDA support will still get the static content.

.scrollDrivenHeading { display: none; } @supports (animation-timeline: scroll()) { .scrollDrivenHeading { display: block; } /* Screen Readers Only */ .srOnly { clip: rect(0 0 0 0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; } } Get Sticky

The next thing we need to do is handle the stickiness of the heading. To make sure the heading always stays on screen, we’ll set its position to sticky with top: 0 so it sticks to the top of the viewport.

While we’re at it, let’s add some basic styling, including a background so the text doesn’t blend with whatever’s behind the heading, a bit of padding for spacing, and white-space: nowrap to keep the heading on a single line.

/* inside the @supports block */ .scrollDrivenHeading { display: block; position: sticky; top: 0; background-image: linear-gradient(0deg, transparent, black 1em); padding: 0.5em 0.25em; white-space: nowrap; }

Now everything’s set up: in normal conditions, we’ll see a single sticky heading at the top of the page. And if someone uses assistive technology or a browser that doesn’t support SDA, they’ll still get the regular static content.

CodePen Embed Fallback

Now we’re ready to start animating the text. Almost…

The Magic Numbers

To build the text animation, we need to know exactly where the text should change. With SDA, scrolling basically becomes our timeline, and we have to determine the exact points on that timeline to trigger the animation.

To make this easier, and to help you pinpoint those positions, I’ve prepared the following script:

@property --scroll-position { syntax: "<number>"; inherits: false; initial-value: 0; } body::after { counter-reset: sp var(--scroll-position); content: counter(sp) "%"; position: fixed; top: 0; left: 0; padding: 1em; background-color: maroon; animation: scrollPosition steps(100); animation-timeline: scroll(); } @keyframes scrollPosition { 0% { --scroll-position: 0; } 100% { --scroll-position: 100; } }

I don’t want to get too deep into this code, but the idea is to take the same scroll timeline we’ll use next to animate the text, and use it to animate a custom property (--scroll-position) from 0 to 100 based on the scroll progress, and display that value in the content.

If we’ll add this at the start of our code, we’ll see a small red square in the top-left corner of the screen, showing the current scroll position as a percentage (to match the keyframes). This way, you can scroll to any section you want and easily mark the percentage where each heading should begin.

CodePen Embed Fallback

With this method and a bit of trial and error, I found that I want the headings to change at 30%, 60%, and 90%. So, how do we actually do it? Let’s start animating.

Animating Text

First, we’ll clear out the content inside the .scrollDrivenHeading element so it’s empty and ready for dynamic content. In the CSS, I’ll add a pseudo-element to the heading, which we’ll use to animate the text. We’ll give it empty content, set up the animation-name, and of course, assign the animation-timeline to scroll().

And since I’m animating the content property, which is a discrete type, it doesn’t transition smoothly between values. It just jumps from one to the next. By setting the animation-timing-function property to step-end, I make sure each change happens exactly at the keyframe I define, so the text switches precisely where I want it to, instead of somewhere in between.

.scrollDrivenHeading { /* style */ &::after { content: ''; animation-name: headingContent; animation-timing-function: step-end; animation-timeline: scroll(); } }

As for the keyframes, this part is pretty straightforward (for now). We’ll set the first frame (0%) to the first heading, and assign the other headings to the percentages we found earlier.

@keyframes headingContent { 0% { content: 'Primary Colors'} 30% { content: 'Red Power'} 60% { content: 'Blue Calm'} 90%, 100% { content: 'Yellow Joy'} }

So, now we’ve got a site with a sticky heading that updates as you scroll.

CodePen Embed Fallback

But wait, right now it just switches instantly. Where’s the animation?! Here’s where it gets interesting. Since we’re not using JavaScript or any string manipulation, we have to write the keyframes ourselves. The best approach is to start from the target heading you want to reach, and build backwards. So, if you want to animate between the first and second heading, it would look like this:

@keyframes headingContent { 0% { content: 'Primary Colors'} 9% { content: 'Primary Color'} 10% { content: 'Primary Colo'} 11% { content: 'Primary Col'} 12% { content: 'Primary Co'} 13% { content: 'Primary C'} 14% { content: 'Primary '} 15% { content: 'Primary'} 16% { content: 'Primar'} 17% { content: 'Prima'} 18% { content: 'Prim'} 19% { content: 'Pri'} 20% { content: 'Pr'} 21% { content: 'P'} 22% { content: 'R'} 23% { content: 'Re'} 24% { content: 'Red'} 25% { content: 'Red '} 26% { content: 'Red P'} 27% { content: 'Red Po'} 28%{ content: 'Red Pow'} 29% { content: 'Red Powe'} 30% { content: 'Red Power'} 60% { content: 'Blue Calm'} 90%, 100% { content: 'Yellow Joy'} }

I simply went back by 1% each time, removing or adding a letter as needed. Note that in other cases, you might want to use a different step size, and not always 1%. For example, on longer headings with more words, you’ll probably want smaller steps.

If we repeat this process for all the other headings, we’ll end up with a fully animated heading.

CodePen Embed Fallback User Preferences

We talked before about accessibility and making sure the content works well with assistive technology, but there’s one more thing you should keep in mind: prefers-reduced-motion. Even though this isn’t a strict WCAG requirement for this kind of animation, it can make a big difference for people with vestibular sensitivities, so it’s a good idea to offer a way to show the content without animations.

If you want to provide a non-animated alternative, all you need to do is wrap your @supports block with a prefers-reduced-motion query:

@media screen and (prefers-reduced-motion: no-preference) { @supports (animation-timeline: scroll()) { /* style */ } } Leveling Up

Let’s talk about variations. In the previous example, we animated the entire heading text, but we don’t have to do that. You can animate just the part you want, and use additional animations to enhance the effect and make things more interesting. For example, here I kept the text “Primary Color” fixed, and added a span after it that handles the animated text.

<h1 class="scrollDrivenHeading" aria-hidden="true"> Primary Color<span></span> </h1>

And since I now have a separate span, I can also animate its color to match each value.

CodePen Embed Fallback

In the next example, I kept the text animation on the span, but instead of changing the text color, I added another scroll-driven animation on the heading itself to change its background color. This way, you can add as many animations as you want and change whatever you like.

CodePen Embed Fallback Your Turn!

CSS Scroll-Driven Animations are more than just a cool trick; they’re a game-changer that opens the door to a whole new world of web design. With just a bit of creativity, you can turn even the most ordinary pages into something interactive, memorable, and truly engaging. The possibilities really are endless, from subtle effects that enhance the user experience, to wild, animated transitions that make your site stand out.

So, what would you build with scroll-driven animations? What would you create with this new superpower? Try it out, experiment, and if you come up with something cool, have some ideas, wild experiments, or even weird failures, I’d love to hear about them. I’m always excited to see what others come up with, so feel free to share your work, questions, or feedback below.

Special thanks to Cristian Díaz for reviewing the examples, making sure everything is accessible, and contributing valuable advice and improvements.

Scroll-Driven Sticky Heading originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The Layout Maestro Course

Fri, 07/11/2025 - 7:07am

Layout. It’s one of those easy-to-learn, difficult-to-master things, like they say about playing bass. Not because it’s innately difficult to, say, place two elements next to each other, but because there are many, many ways to tackle it. And layout is one area of CSS that seems to evolve more than others, as we’ve seen in the past 10-ish years with the Flexbox, CSS Grid, Subgrid, and now Masonry to name but a few. May as well toss in Container Queries while we’re at it. And reading flow. And…

That’s a good way to start talking about a new online course that Ahmad Shadeed is planning to release called The Layout Maestro. I love that name, by the way. It captures exactly how I think about working with layouts: orchestrating how and where things are arranged on a page. Layouts are rarely static these days. They are expected to adapt to the user’s context, not totally unlike a song changing keys.

Ahmad is the perfect maestro to lead a course on layout, as he does more than most when it comes to experimenting with layout features and demonstrating practical use cases, as you may have already seen in his thorough and wildly popular interactive guides on Container Queries, grid areas, box alignment, and positioning (just to name a few).

The course is still in development, but you can get a leg up and sign up to be notified by email when it’s ready. That’s literally all of the information I have at this point, but I still feel compelled to share it and encourage you to sign up for updates because I know few people more qualified to wax on about CSS layout than Ahmad and am nothing but confident that it will be great, worth the time, and worth the investment.

I’m also learning that I have a really hard time typing “maestro” correctly. &#x1f913;

The Layout Maestro Course originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

©2003 - Present Akamai Design & Development.