Jump to content

Welcome to CodeNameJessica

Welcome to CodeNameJessica!

💻 Where tech meets community.

Hello, Guest! 👋
You're just a few clicks away from joining an exclusive space for tech enthusiasts, problem-solvers, and lifelong learners like you.

🔐 Why Join?
By becoming a member of CodeNameJessica, you’ll get access to:
In-depth discussions on Linux, Security, Server Administration, Programming, and more
Exclusive resources, tools, and scripts for IT professionals
A supportive community of like-minded individuals to share ideas, solve problems, and learn together
Project showcases, guides, and tutorials from our members
Personalized profiles and direct messaging to collaborate with other techies

🌐 Sign Up Now and Unlock Full Access!
As a guest, you're seeing just a glimpse of what we offer. Don't miss out on the complete experience! Create a free account today and start exploring everything CodeNameJessica has to offer.

  • Entries

    207
  • Comments

    0
  • Views

    4145

Entries in this blog

by: Andy Clarke
Fri, 18 Jul 2025 16:12:05 +0000


A while back, our man Geoff Graham treated us to a refresher on the CSS initial-letter property, but how can you style drop and initial caps to reflect a brand’s visual identity and help to tell its stories?

Here’s how I do it in CSS by combining ::first-letter and initial-letter with other unexpected properties, including border-image, and clip-path.

Showing three screenshots of Patty Meltt's website, including a homepage a videos page, and a discography, all side-by-side.
Patty Meltt is an up-and-coming country music sensation.

My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.

First, a drop cap recap. Chris Coyier wrote about drop caps several years ago. They are a decorative letter at the beginning of a paragraph, often spanning several lines of text. It’s a typographic flourish found in illuminated manuscripts and traditional book design, where it adds visual interest and helps guide a reader’s eye to where they should begin.

Study manuscripts from the Middle Ages onwards, and you’ll find hand-decorated illuminated capitals. The artists who made these initial letters were fabulously called “illuminators.” These medieval versals went beyond showing someone where to start reading; historiated letters also illustrated the stories, which was especially useful since most people in the Middle Ages couldn’t read.

The first letter is a P that is four lines tall, bold, and colored light gray.
A basic drop cap

On the web, drop caps can improve readability and reflect a brand’s visual identity.

A brief refresher on properties and values

In CSS, drop caps are created using the ::first-letter pseudo-element in combination with initial-letter. As you might expect, ::first-letter targets the very first letter of a block of text, enabling you to style it independently from the rest of a paragraph. The first number sets how many lines tall the letter appears, and the second controls its baseline alignment — that is, which line of text the bottom of the cap sits on.

p::first-letter {
  -webkit-initial-letter: 3 3;
  initial-letter: 3 3;
}

Because browser support still varies, it’s common to include both the unprefixed and -webkit- prefixed properties for maximum compatibility. And speaking of browser support, it’s also sensible to wrap the initial-letter property inside an @supports CSS at-rule so we can check for browser support and provide a fallback, if needed:

@supports (initial-letter:2) or (-webkit-initial-letter:2) {
  p::first-letter {
    -webkit-initial-letter: 3 3;
    initial-letter: 3 3;
  }
}

The initial-letter property automatically calculates the font size to match the number of lines a drop cap spans. On its own, this can make for quite a first impression. However, drop caps really start to come to life when you combine initial-letter with other CSS properties.

Tip: Interactive examples from this article are available in my lab.

Shadows

The first example is a single shadow and the second uses two shadows to create a 3D effect.
Text shadows applied to first letters (live demo)

When I want to lift a drop cap off the page, I can add a single text-shadow. Shadows can be colourful and don’t have to be black. I created a full live demo you can check out.

p::first-letter {
  /* ... *//
  text-shadow: 6px 6px 0 #e6d5b3;
}

But why use just one shadow when two hard-edged shadows will turn a cap into a classic graphic typographic element?

p::first-letter {
  /* ... */
  text-shadow: 
    -6px -6px 0 #7d6975, 
    6px 6px 0 #e6d5b3;
}
Examples showing unstyled, single text shadow, and two text shadows (live demo)

Strokes

A text shadow applied to a first letter (live demo)

The text-stroke property — shorthand for text-stroke-width and text-stroke-color — adds an outline to the centre of the text shape. It’s a Baseline feature and is now widely available. I can make the cap text transparent or colour it to match the page background.

p::first-letter {
  /* ... */
  text-stroke: 5px #e6d5b3;
}

Backgrounds

Solid and gradient backgrounds applied to first letters (live demo)

Adding a background is a simple way to start making a cap more decorative. I could start by adding a solid background-color.

p::first-letter {
  /* ... */
  background-color: #97838f;
}

To add a lighting effect, I could apply a conical, linear, or radial gradient background image (here’s a demo):

p::first-letter {
  /* ... */
  background-color: #e6d5b3;
  background-image: linear-gradient(135deg,#c8b9c2 0%, #7d6975 50%);
}

And even an image URL to use a bitmap or vector image as a background (and here’s that demo):

p::first-letter {
  /* ... */
  background-color: #e6d5b3;
  background-image: url(...);
  background-size: cover;
}
Background images and a background clipped to text

Things become even more interesting by clipping a bitmap, gradient, or vector background image to the text while setting its colour to transparent. Now, the image will only appear inside the text space (demo).

p::first-letter {
  /* ... */
  background-clip: text;
  color: transparent;
}

Borders

Two examples of borders applied to first letters, one square and one rounded

You might think borders are boring, but there’s plenty you can do to make them look interesting. I could start by applying a solid border to surround the cap box (demo).

p::first-letter {
  /* ... */
  border: 5px solid #e6d5b3;
}

Then, I could apply border-radius to slightly round all its corners (demo).

p::first-letter {
  /* ... */
  border-radius: 1rem;
}

Or, I might round individual corners for a more interesting look (demo):

p::first-letter {
  /* ... */
  border-top-left-radius: 3rem;
  border-bottom-right-radius: 3rem;
}
A border radius applied to the first letter, where the top-left and bottom-right edges are rounded (live demo)

And then there’s the border-image property, a powerful, yet often overlooked CSS tool. By slicing, repeating, and outsetting images, you can create intricate borders and decorative drop caps with minimal code.

Initial letter with a thick gradient border.
A CSS border image applied to a first letter (live demo)

You can insert a bitmap or vector format image, or drop a CSS gradient into the border space:

p::first-letter {
  /* ... */
  border-style: solid;
  border-width: 10px;
  border-image: conic-gradient(...) 1;
}

Clipping

The first example is an arrow-shaped background pointing toward the right. The second example is a background cut out like a sunburst.
Clipping first letters

The clip-path property lets you define a custom shape that controls which parts of an element are visible and which are hidden. Instead of always showing a rectangular box, you can use clip-path to crop elements into circles, polygons, or even complex shapes defined with SVG paths. It’s an effective way to create visual effects like this right-facing arrow. Clipping the drop cap into an arrow shape isn’t just decorative — it reinforces direction and hierarchy, literally pointing readers to where the story begins. Here’s a demo of the following example.

p::first-letter {
  /* ... */
  padding-inline: 1rem 2rem;
  background-color: #e6d5b3;
  clip-path: polygon(...);
}

Or a glossy sticker shape cap, made by combining clip-path with a gradient background image and a text shadow (demo).

Transforms

Two examples of transforming first letters, one rotated (demo) and one scaled (demo)

You can transform a drop cap independently from the rest of a paragraph by rotating, scaling, skewing, or translating it to make it feel more dynamic:

p::first-letter {
  /* ... */
  margin-inline-end: 2.5em;
  transform: skew(20deg, 0deg);
}
Two examples of skewed first letters, angling each one so that they are slanted backward towards the left.

And with a little trial and error to arrive at the correct values, you could even flow the remaining paragraph text around the cap using the shape-outside property (demo):

p::first-letter {
  /* ... */
  display: block;
  float: left;
  shape-outside: polygon(0 0, 0 200px, 250px 600px);
  shape-margin: 50px;
  transform: skew(20deg, 0deg) translateX(-60px);
}

Drop caps don’t just help guide a reader’s eye to where they should begin; they also set the tone for what follows. A well-designed drop cap adds visual interest at the start of a block of text, drawing attention in a way that feels intentional and designed. Because it’s often the first element the reader sees, caps can carry a lot of visual weight, making them powerful tools for expressing a brand’s identity.

Designing for Patty Meltt

Patty Meltt wanted a website packed with design details. Every element added to a design is an opportunity to be expressive, and that includes her drop caps.

Her biography page is presentable, but we felt a focus on where someone should start reading was lacking.

Patty Meltt’s biography without a drop cap

From the selection of designs I showed her, she felt the sticker-style cap best suited her brand.

Showing all letters of the alphabet styled in the artist’s preferred way, with a circular background shaped like a sun with sharp angles.

To implement it, first, I added a cursive typeface which matches her branding and contrasts with the rest of her typographic design:

p::first-letter {
  font-family: "Lobster Two", sans-serif;
  font-weight: 700;
}

I changed the cap colour to match the page background and added a semi-transparent text shadow:

p::first-letter {
  /* ... */
  color: #140F0A;
  text-shadow: 6px 6px 0 rgba(163,148, 117, .8);
}

Next, I clipped the cap box to a visible area shaped like a sticker:

p::first-letter {
  /* ... */
  clip-path: polygon(...);
}

…before applying two background images — a noise-filled SVG and a radial gradient — that I blended using a background-blend-mode:

p::first-letter {
  /* ... */
  background-image: url(img/cap-noise.svg), 
  radial-gradient(circle, #e6d5b3 0%, #cdaa65 100%);
  background-blend-mode: soft-light, normal;
}
Patty Meltt’s biography with a stylsh new drop cap (demo)

The result is a drop cap that’s as stylish as cut-off jeans and a pair of gator-skinned boots.

Conclusion

Styling drop caps isn’t just about decoration — it’s about setting a tone, drawing readers in, and using every detail to express a brand’s voice. CSS has the tools to go beyond the default: gradients, textures, borders, and even complex shapes all help transform first letters into statements. So don’t waste the opportunities that drop caps give you. Make ’em sing.


Getting Creative With Versal Letters originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Thu, 17 Jul 2025 13:23:52 +0000


Folks have a lot to say about “liquid glass,” the design aesthetic that Apple introduced at WWDC 2025. Some love it, some hate it, and others jumped straight into seeing how to they could create it in CSS.

There’s a lot to love, hate, and experience with liquid glass. You can love the way content reflects against backgrounds. You can hate the poor contrast between foreground and background. And you can be eager to work with it. All of those can be true at the same time.

Image credit: Apple

I, for one, am generally neutral with things like this for that exact reason. I’m intrigued by liquid glass, but hold some concern about legibility, particularly as someone who already struggles with the legibility of Apple’s existing design system (notably in Control Center). And I love looking at the many and clever ways that devs have tried to replicate liquid glass in their own experiments.

So, I’m in the process of gathering notes on the topic as I wrap my head around this “new” (or not-so-new, depending on who’s talking) thing and figure out where it fits in my own work. These links are a choice selection of posts that I’ve found helpful and definitely not meant to be an exhaustive list of what’s out there.

WWDC Introduction

Always a good idea to start with information straight from the horse’s mouth.

In short:

  • It’s the first design system that is universally applied to all of Apple’s platforms, as opposed to a single platform like Apple’s last major overhaul, iOS 7.
  • It’s designed to refract light and dynamically react to user interactions.
  • By “dynamic” we’re referring to UI elements updating into others as the context changes, such as displaying additional controls. This sounds a lot like the Dynamic Island, supporting shape-shifting animations.
  • There’s a focus on freeing up space by removing hard rectangular edges, allowing UI elements to become part of the content and respond to context.

Apple also released a more in-depth video aimed at introducing liquid glass to designers and developers.

In short:

  • Liquid glass is an evolution of the “aqua” blue interface from macOS 10, the real-time introduced in iOS 7, the “fluidity” of iOS 10, the flexibility of the Dynamic Island, and the immersive interface of visionOS.
  • It’s a “digital meta-material” that dynamically bends and shapes light while moving fluidly like water.
  • It’s at least partially a response to hardware devices adopting deeper rounded corners.
  • Lensing: Background elements are bended and warped rather than scattering light as it’s been in previous designs. There’s gel-like feel to elements.
  • Translucence helps reveal what is underneath a control, such as a progress indicator you can scrub more precisely by seeing what is behind the surface.
  • Controls are persistent between views for establishing a relationship between controls and states. This reminds me of the View Transition API.
  • Elements automatically adapt to light and dark modes.
  • Liquid glass is composed of layers: highlight (light casting and movement), shadow (added depth for separation between foreground and background), and illumination (the flexible properties of the material).
  • It is not meant to be used everywhere but is most effective for the navigation layer. And avoid using glass on glass.
  • There are two variants: regular (most versatile) and clear (does not have adaptive behaviors for allowing content to be more visible below the surface).
  • Glass can be tinted different colors.

Documentation

Wireframe of a mobile app screen header with boxes representing the parts of a navigation.

Right on cue, Apple has already made a number of developer resources available for using and implementing liquid glass that are handy references.

‘Beautiful’ and ‘Hard to Read’: Designers React to Apple’s Liquid Glass Update

This Wired piece is a nice general overview of what liquid glass is and context about how it was introduced at WWDC 2025. I like getting a take on this from a general tech perspective as opposed to, say, someone’s quick hot take. It’s a helpful pulse on what’s happening from a high level without a bunch of hyperbole, setting the stage for digging deeper into things.

In short:

  • Apple is calling this “Liquid Glass.”
  • It’s Apple’s first significant UI overhaul in 10 years.
  • It will be implemented across all of Apple’s platforms, including iOS, macOS, iPadOS, and even the Vision Pro headset from which it was inspired.
  • “From a technical perspective, it’s a very impressive effect. I applaud the time and effort it must have taken to mimic refraction and dispersion of light to such a high degree.”
  • “Similar to the first beta for iOS 7, what we’ve seen so far is rough on the edges and potentially veers into distracting or challenging to read, especially for users with visual impairments.”

Accessibility

Let’s get right to the heart of where the pushback against liquid glass is coming from. While the aesthetic, purpose, and principles of liquid glass are broadly applauded, many are concerned about the legibility of content against a glass surface.

Traditionally, we fill backgrounds with solid or opaque solid color to establish contrast between the foreground and background, but with refracted light, color plays less a role and it’s possible that highlighting or dimming a light source will not produce enough contrast, particularly for those with low-vision. WCAG 2.2 emphasizes color and font size for improving contrast and does provide guidance for something that’s amorphous like liquid glass where bending the content below it is what establishes contrast.

“Apple’s “Liquid Glass” and What It Means for Accessibility”:

  • “When you have translucent elements letting background colors bleed through, you’re creating variable contrast ratios that might work well over one background, but fail over a bright photo of the sunset.”
  • “Apple turned the iPhone’s notch into the Dynamic Island, Android phones that don’t have notches started making fake notches, just so they could have a Dynamic Island too. That’s influence. But here they are making what looks like a purely aesthetic decision without addressing the accessibility implications.”
  • “People with dyslexia, who already struggle with busy backgrounds and low-contrast text, now deal with an interface where visual noise is baked into the design language. People with attention disorders may have their focus messed up when they see multiple translucent layers creating a whole lot of visual noise.”
  • “It’s like having a grand entrance and a side door marked ‘accessible.’ Technically compliant. But missing the point.”
  • “The legal landscape adds another layer. There’s thousands of digital accessibility lawsuits filed in the U.S. yearly for violating the ADA, or the American Disabilities Act. Companies are paying millions in settlements. But this is Apple. They have millions. Plus all the resources in the world to save them from legal risks. But their influence means they’re setting precedents.”

“Liquid Glass: Apple vs accessibility”:

  • “Yet even in Apple’s press release, linked earlier, there are multiple screenshots where key interface components are, at best, very difficult to read. That is the new foundational point for Apple design. And those screenshots will have been designed to show the best of things.”
  • “Apple is still very often reactive rather than proactive regarding vision accessibility. Even today, there are major problems with the previous versions of its operating systems (one example being the vestibular trigger if you tap-hold the Focus button in Control Centre). One year on, they aren’t fixed.”
  • “State, correctly, that Apple is a leader in accessibility. But stop assuming that just because this new design might be OK for you and because Apple has controls in place that might help people avoid the worst effects of design changes, everything is just peachy. Because it isn’t.”

“Liquid Glass” by Hardik Pandya

  • “The effect is technically impressive, but it introduces a layer of visual processing between you and your memories. What was once immediate now feels mediated. What was once direct now feels filtered.”
  • “While Apple’s rationale for Liquid Glass centers on ‘seeing’ content through a refractive surface, user interface controls are not meant to be seen—they are meant to be operated. When you tap a button, slide a slider, or toggle a switch, you are not observing these elements. You are manipulating them directly.”
  • “Buttons become amorphous shapes. Sliders lose their mechanical clarity. Toggle switches abandon their physical affordances. They appear as abstract forms floating behind glass—beautiful perhaps, but disconnected from the fundamental purpose of interface controls: to invite and respond to direct manipulation.”
  • “The most forward-thinking interface design today focuses on invisibility – making the interaction so seamless that the interface itself disappears. Liquid Glass makes the interface more visible, more present, and more demanding of attention.”

“Liquid glass, now with frosted tips”:

  • It’s easy to dump on liquid glass in its introductory form, but it’s worth remembering that it’s in beta and that Apple is actively developing it ahead of its formal release.
  • A lot has changed between the Beta 2 and Beta 3 releases. The opacity between glass and content has been bumped up in several key areas.
Comparing Developer Beta 2 (top) and Developer Beta 3 (bottom) of a bottom navigation bar. The previous version is clearer and more difficult to read because it blends in with the background more than the newer version, which increases the amount of frost in the background layer.

Tutorials, Generators, and Frameworks

It’s fun to see the difference approaches many folks have used to re-create the liquid glass effect in these early days. It amazes me that there is already a deluge of tutorials, generators, and even UI frameworks when we’re only a month past the WWDC 2025 introduction.

Experiments

Let’s drop in a few interesting demos that folks have created. To be clear, glass-based interfaces are not new and have been plenty explored, which you can find over at CodePen in abundance. These are recent experiments. The most common approaches appear to reach for SVG filters and background blurs, though there are many programmatic demos as well.

Using a CSS-only approach with an SVG filter with backdrop-filter with a series of nested containers that sorta mimics how Apple describes glass as being composed of three layers (highlight, shadow and illumination):

Same sort of deal here, but in the context of a theme toggle switch that demonstrates how glass can be tinted:

Comparing a straight-up CSS blur with an SVG backdrop:

Contextual example of a slider component:

Using WebGL:

A few more links from this browser tab group I have open:


Getting Clarity on Apple’s Liquid Glass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Neeraj Mishra
Thu, 17 Jul 2025 03:36:01 +0000


DevOps tooling has become mission-critical. What used to be a niche engineering function is now a core business driver. And with the DevOps automation market projected to hit $72.81 billion by 2032, your choice of tools can literally make or break your product velocity.

Whether you’re a CTO at a scale-up, a DevOps engineer in the trenches, or a founder juggling release chaos, choosing the right automation tools is no longer optional, it’s strategic.

After diving deep into what’s working across the industry, here’s a fresh, no-fluff look at the top DevOps automation tools actually making a difference in 2025. Data-driven, real-world examples, and yes, links included.

Why It Matters More Than Ever

DevOps isn’t just a buzzword anymore it’s the backbone of modern software delivery. If you’re looking to implement or optimize your pipeline, check out our DevOps services to get started the right way.

  • 85% of organizations are using DevOps practices
  • 49% say it helps them ship faster
  • Teams are saving up to 30% in infrastructure costs
  • DevOps-driven pipelines are 60% faster than traditional approaches

In a world where product timelines are measured in sprints, not quarters these numbers aren’t just impressive. They’re survival stats.

Top 10 DevOps Tools Making Waves in 2025

devops automation tools

1. GitHub Actions – The Community-Powered Automation Engine

If your team lives in GitHub, Actions probably already lives in your life. With 13,000+ prebuilt workflows, it turns complex CI/CD tasks into something even your intern can automate.

Used by: Stripe, deploying thousands of times per day with custom workflows that boosted deployment success from 94% to 99.2%.

Why it works:

  • No infra to manage
  • Huge community marketplace
  • Limited flexibility for very custom setups

Best for: Startups, OSS projects, GitHub-native teams

2. GitLab CI/CD – The All-in-One Platform

GitLab isn’t just a Git repoit’s your DevOps Swiss Army knife. CI/CD, issue tracking, security scanning, and even Kubernetes deploys, all in one platform.

Used by: GitLab itself (meta!) over 300 daily deployments using AutoDevOps.

Why it works:

  • Unified platform, strong DevSecOps features
  • Bit heavy for small teams

Best for: Mid-to-large orgs, security-conscious teams

3. Jenkins – The Customization King

Love it or hate it, Jenkins still powers some of the biggest pipelines in tech. With 1,800+ plugins, it can be whatever you need if you have the patience.

Used by: Netflix, with thousands of custom Jenkins jobs powering their microservices.

Why it works:

  • Total flexibility
  • Requires hands-on care (and probably a Jenkins wizard)

Best for: Large orgs with complex deployment flows

4. CircleCI – The Speed Demon

If you need fast, reliable builds, Circle Is your tool. Intelligent caching and parallelization cut build times dramatically.

Used by: Shopify, who dropped test time from 25 to 8 minutes and doubled deployments.

Why it works:

  • Fast builds, Docker-first
  • Free tier is tight, pricing can scale quickly

Best for: Fast-growing teams, Docker-heavy apps

5. Azure DevOps – The Enterprise Glue

Deep Microsoft integration and enterprise features make Azure DevOps a top pick for large organizations. It’s not the flashiest, but it gets the job done.

Used by: Progressive Insurance Cutting deploy times from 4 hours to 30 minutes.

Why it works:

  • Enterprise-grade, multi-platform, hybrid-friendly
  • Can feel heavy if you’re not a Microsoft shop

Best for: Enterprises, .NET-heavy teams

6. Space lift – IaC Done Right

If Terraform is at the heart of your infrastructure, Space lift should be on your radar. It’s like Jenkins, but designed for infra-as-code from the ground up.

Used by: Revolut to manage AWS deployments across 30+ accounts catching 15+ production issues early.

Why it works:

  • Drift detection, policy-as-code, great Terraform support
  • Newer ecosystem

Best for: Platform teams, IaC-heavy orgs

7. Tekton – Kubernetes-Native Pipelines

Built for Kubernetes, Tekton brings CI/CD inside your cluster. It’s lightweight, cloud-native, and plays nicely with K8s workflows.

Used by: Red Hat for OpenShift Pipelines.

Why it works:

  • Kubernetes-native, scalable, cloud-agnostic
  • Needs K8s know-how

Best for: Cloud-native teams, Kubernetes shops

8. AWS Code Pipeline – The Serverless Solution

For AWS-native apps, Code Pipeline offers end-to-end CI/CD with zero servers to manage.

Used by: Airbnb Running 2,000+ daily deployments with Lambda and Code Pipeline.

Why it works:

  • Fully managed, integrates tightly with AWS
  • Vendor lock-in, not as flexible

Best for: AWS-heavy teams, serverless apps

9. TeamCity – The Developer-Friendly CI/CD

Built by JetBrains, TeamCity is loved for its smooth UI and tight IDE integration.

Used by: Stack Overflow to streamline builds and reduce runtime by 40%.

Why it works:

  • Great UX, smart analytics, IDE sync
  • Better suited for JetBrains/tooling-heavy orgs

Best for: Dev-focused teams, .NET lovers

10. Harness – The AI-Powered Newcomer

Harness is all about intelligence AI that predicts failures, optimizes deploys, and offers killer rollback strategies.

Used by: JP Morgan Chase Detecting and preventing 23+ production issues last year.

Why it works:

  • Smart automation, strong security, observability built-in
  • Pricey and complex for smaller teams

Best for: Large enterprises, mission-critical apps

Which One Should You Choose?

No tool is one-size-fits-all. The best advice? Start with your team’s size, budget, stack, and pain points, then choose the DevOps automation tools that actually fit—not the ones with the flashiest feature list.

Final Take

The DevOps world is growing 17.7% CAGR fast. And the tools you choose today? They’ll shape your team’s velocity, stability, and culture for years to come.

Just remember: simplicity scales. Don’t over-engineer your pipeline if you don’t have to.

The post 10 Best DevOps Automation Tools in 2025 appeared first on The Crazy Programmer.

402: Bookmarks

by: Chris Coyier
Wed, 16 Jul 2025 19:42:27 +0000


Pins are dead!

Long live bookmarks!

Pins was never a good name for the feature we have on CodePen where you can mark a Pen or Collection to more quickly jump back to it from anywhere on the site. The word is too similar to “Pen” that it’s just awkward, not to mention it’s not exactly and obvious metaphor. A bookmark is a much more clear term and icon, so we decided to switch to it.

Switching the UI is kind of the easy part. It’s kind of a cultural thing at CodePen, but when we make a change like this, we change it 100% through the entire code base, down to the database itself. In order to do that, we had to chunk it into stages so that those stages can roll out independently, but in order, to make it seamless.

Now that it’s done, we were able to extend the functionality of Bookmarks a bit, such that bookmarking a template is extra useful. One place to see that is on the new Create page.

Time Jumps

  • 00:05 You’re still here! Thanks for listening!
  • 01:51 Pins are changing to…
  • 04:51 How do you get alignment on changes or new features?
  • 08:35 Figuring out the new icon
  • 09:44 Updating the documentation
  • 17:32 How does this affect templates?

by: Sunkanmi Fafowora
Wed, 16 Jul 2025 12:54:14 +0000


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

Country or Region bar chart. USA is 26%, UK is 9%, Germany is 8%, and France is at 6%.

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

Bar chart breaking down developers by gender. Males are 82%, female are 15%, non-binary and hotlists do not have percentages, but represent 20 of the responses out of more than 7,000.

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

Bar chart showing income ranges based on years of experience.

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

Bar chart showing health issues. Poor sleep is 42%, weight is 31%, mental health is 30%, and back pain is 28%.

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.

by: Chris Coyier
Tue, 15 Jul 2025 14:56:20 +0000


It’s such a small, simple thing, but I’m very grateful that we’re getting “Gap Decorations” in CSS.

Microsoft is working on “gap decorations” and have put together a nice playground to explore them, and I had a play. The idea is drawing lines where gaps would be, rather than empty space. It’s really quite well done with lots of control (do they hit the edges or stop short? do they overlap or not? which direction is on top? etc).

I took a look at it the other day. Oh hey, that link is a lightly edited video I did from a stream I did. I’m enjoying the whole streaming thing. I’d love it if you came along for the ride:

Part of what I like about it is that you don’t necessarily need to put grid items onto the grid to get the lines. Grid items on the grid might affect it, but it’s not required. That naturally leads on to think about styling grid areas without having to put HTML elements there to style. That’d be head, huh?

The Chrome blog has the post on it for whatever reason (does Microsoft not have a good blog for this?). The demos are pretty compelling in that blog post, showcasing some of the more exotic syntax possible.


Speaking of turning a stream into a nicer video and demo, I did the same thing for an idea I had about reordering list items with View Transitions.

I probably won’t give every little thing I do on a stream the full content round trip like this, but I thought the result was super fun and cool and I JUST WANTED TO OK.

I’m also super envious of Bramus’ idea of If View Transitions and Scroll-Driven Animations having a baby, which is just an ultra-cool idea. He got some good mileage out of that idea, including a meetup talk. 💪


I’ve had Ryan Mulligan’s Scrolling Rails and Button Controls bookmarked for quite a while. He made a <scrolly-rail> web component that makes simple, nice horizontal scrolling elements. That includes scroll snapping, smooth scrolling, pagination controls, and more.

The timing of it is a little funny, as all this CSS Carousels stuff dropped very shortly after his work. Not that Ryan’s work was in vain, as it’s more cross-browser friendly than stuff like ::scroll-button() and the fancy features necessary for CSS carousels are.

But me, I’m so lazy into progressive enhancement, the last time I wanted to do a carousel, I went right for the fancy new stuff. That was for a template I was working on for our 2.0 Editor, which I streamed about, naturally.

by: Daniel Schwarz
Mon, 14 Jul 2025 12:38:23 +0000


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):

<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;
}

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.

by: Geoff Graham
Fri, 11 Jul 2025 17:07:13 +0000


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


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

by: Amit Sheen
Fri, 11 Jul 2025 12:43:59 +0000


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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

by: Chris Coyier
Thu, 10 Jul 2025 11:04:57 +0000


Hi!

We’re back!

Weird right? It’s been over 2 years. 

We took a break after episode 400, not because we ran out of things to talk about, but because we were so focused on our CodePen 2.0 work, it got old not being able to discuss it yet. We’ll be talking plenty about that going forward. But CodePen has a ton of moving parts, so we’ll be talking about all of it. 

This week we’ll be kicking off the podcast again talking about a huge and vital bit of CodePen infastructure: our email system. Outgoing email, that is. We get plenty of incoming email from y’all as well, but this is about the app itself sending email. 

Timeline

  • 00:06 We’re back!
  • 01:22 Our transactional email system
  • 05:21 Templating in Postmark
  • 08:31 Hitting APIs to send emails
  • 10:23 Building a sponsored email
  • 17:20 Marie’s Monday morning routine
  • 24:19 Analytics and metrics
  • 26:55 Dealing with large images
  • 30:12 MGML framework for email

Links

by: Temani Afif
Mon, 07 Jul 2025 12:48:29 +0000


This is the fourth post in a series about the new CSS shape() function. So far, we’ve covered the most common commands you will use to draw various shapes, including lines, arcs, and curves. This time, I want to introduce you to two more commands: close and move. They’re fairly simple in practice, and I think you will rarely use them, but they are incredibly useful when you need them.

Better CSS Shapes Using shape()

  1. Lines and Arcs
  2. More on Arcs
  3. Curves
  4. Close and Move (you are here!)

The close command

In the first part, we said that shape() always starts with a from command to define the first starting point but what about the end? It should end with a close command.

But you never used any close command in the previous articles!?

That’s true. I never did because I either “close” the shape myself or rely on the browser to “close” it for me. Said like that, it’s a bit confusing, but let’s take a simple example to better understand:

clip-path: shape(from 0 0, line to 100% 0, line to 100% 100%) 

If you try this code, you will get a triangle shape, but if you look closely, you will notice that we have only two line commands whereas, to draw a triangle, we need a total of three lines. The last line between 100% 100% and 0 0 is implicit, and that’s the part where the browser is closing the shape for me without having to explicitly use a close command.

I could have written the following:

clip-path: shape(from 0 0, line to 100% 0, line to 100% 100%, close)

Or instead, define the last line by myself:

clip-path: shape(from 0 0, line to 100% 0, line to 100% 100%, line to 0 0)

But since the browser is able to close the shape alone, there is no need to add that last line command nor do we need to explicitly add the close command.

This might lead you to think that the close command is useless, right? It’s true in most cases (after all, I have written three articles about shape() without using it), but it’s important to know about it and what it does. In some particular cases, it can be useful, especially if used in the middle of a shape.

In this example, my starting point is the center and the logic of the shape is to draw four triangles. In the process, I need to get back to the center each time. So, instead of writing line to center, I simply write close and the browser will automatically get back to the initial point!

Intuitively, we should write the following:

clip-path: shape( 
  from center, 
  line to 20%  0,   hline by 60%, line to center, /* triangle 1 */
  line to 100% 20%, vline by 60%, line to center, /* triangle 2 */
  line to 20% 100%, hline by 60%, line to center, /* triangle 3 */
  line to 0   20%,  vline by 60% /* triangle 4 */
)

But we can optimize it a little and simply do this instead:

clip-path: shape( 
  from center, 
  line to 20%  0,   hline by 60%, close,
  line to 100% 20%, vline by 60%, close,
  line to 20% 100%, hline by 60%, close,
  line to 0    20%, vline by 60%
)

We write less code, sure, but another important thing is that if I update the center value with another position, the close command will follow that position.

Don’t forget about this trick. It can help you optimize a lot of shapes by writing less code.

The move command

Let’s turn our attention to another shape() command you may rarely use, but can be incredibly useful in certain situations: the move command.

Most times when we need to draw a shape, it’s actually one continuous shape. But it may happen that our shape is composed of different parts not linked together. In these situations, the move command is what you will need.

Let’s take an example, similar to the previous one, but this time the triangles don’t touch each other:

Intuitively, we may think we need four separate elements, with its own shape() definition. But the that example is a single shape!

The trick is to draw the first triangle, then “move” somewhere else to draw the next one, and so on. The move command is similar to the from command but we use it in the middle of shape().

clip-path: shape(
  from    50% 40%, line to 20%  0,   hline by 60%, close, /* triangle 1 */
  move to 60% 50%, line to 100% 20%, vline by 60%, close, /* triangle 2 */
  move to 50% 60%, line to 20% 100%, hline by 60%, close, /* triangle 3 */
  move to 40% 50%, line to 0   20%,  vline by 60% /* triangle 4 */
)

After drawing the first triangle, we “close” it and “move” to a new point to draw the next triangle. We can have multiple shapes using a single shape() definition. A more generic code will look like the below:

clip-path: shape(
  from    X1 Y1, ..., close, /* shape 1 */
  move to X2 Y2, ..., close, /* shape 2 */
  ...
  move to Xn Yn, ... /* shape N */
)

The close commands before the move commands aren’t mandatory, so the code can be simplified to this:

clip-path: shape(
  from    X1 Y1, ..., /* shape 1 */
  move to X2 Y2, ..., /* shape 2 */
  ...
  move to Xn Yn, ... /* shape N */
)

Let’s look at a few interesting use cases where this technique can be helpful.

Cut-out shapes

Previously, I shared a trick on how to create cut-out shapes using clip-path: polygon(). Starting from any kind of polygon, we can easily invert it to get its cut-out version:

We can do the same using shape(). The idea is to have an intersection between the main shape and the rectangle shape that fits the element boundaries. We need two shapes, hence the need for the move command.

The code is as follows:

.shape {
  clip-path: shape(from ...., move to 0 0, hline to 100%, vline to 100%, hline to 0);
}

You start by creating your main shape and then you “move” to 0 0 and you create the rectangle shape (Remember, It’s the first shape we create in the first part of this series). We can even go further and introduce a CSS variable to easily switch between the normal shape and the inverted one.

.shape {
  clip-path: shape(from .... var(--i,));
}
.invert {
  --i:,move to 0 0, hline to 100%, vline to 100%, hline to 0;
}

By default, --i is not defined so var(--i,)will be empty and we get the main shape. If we define the variable with the rectangle shape, we get the inverted version.

Here is an example using a rounded hexagon shape:

In reality, the code should be as follows:

.shape {
  clip-path: shape(evenodd from .... var(--i,));
}
.invert {
  --i:,move to 0 0, hline to 100%, vline to 100%, hline to 0;
}

Notice the evenodd I am adding at the beginning of shape(). I won’t bother you with a detailed explanation on what it does but in some cases, the inverted shape is not visible and the fix is to add evenodd at the beginning. You can check the MDN page for more details.

Another improvement we can do is to add a variable to control the space around the shape. Let’s suppose you want to make the hexagon shape of the previous example smaller. It‘s tedious to update the code of the hexagon but it’s easier to update the code of the rectangle shape.

.shape {
  clip-path: shape(evenodd from ... var(--i,)) content-box;
}
.invert {
  --d: 20px;
  padding: var(--d);
  --i: ,move to calc(-1*var(--d)) calc(-1*var(--d)),
        hline to calc(100% + var(--d)),
        vline to calc(100% + var(--d)),
        hline to calc(-1*var(--d));
}

We first update the reference box of the shape to be content-box. Then we add some padding which will logically reduce the area of the shape since it will no longer include the padding (nor the border). The padding is excluded (invisible) by default and here comes the trick where we update the rectangle shape to re-include the padding.

That is why the --i variable is so verbose. It uses the value of the padding to extend the rectangle area and cover the whole element as if we didn’t have content-box.

Not only you can easily invert any kind of shape, but you can also control the space around it! Here is another demo using the CSS-Tricks logo to illustrate how easy the method is:

This exact same example is available in my SVG-to-CSS converter, providing you with the shape() code without having to do all of the math.

Repetitive shapes

Another interesting use case of the move command is when we need to repeat the same shape multiple times. Do you remember the difference between the by and the to directives? The by directive allows us to define relative coordinates considering the previous point. So, if we create our shape using only by, we can easily reuse the same code as many times as we want.

Let’s start with a simple example of a circle shape:

clip-path: shape(from X Y, arc by 0 -50px of 1%, arc by 0 50px of 1%)

Starting from X Y, I draw a first arc moving upward by 50px, then I get back to X Y with another arc using the same offset, but downward. If you are a bit lost with the syntax, try reviewing Part 1 to refresh your memory about the arc command.

How I drew the shape is not important. What is important is that whatever the value of X Y is, I will always get the same circle but in a different position. Do you see where I am going with this idea? If I want to add another circle, I simply repeat the same code with a different X Y.

clip-path: shape(
  from    X1 Y1, arc by 0 -50px of 1%, arc by 0 50px of 1%,
  move to X2 Y2, arc by 0 -50px of 1%, arc by 0 50px of 1%
)

And since the code is the same, I can store the circle shape into a CSS variable and draw as many circles as I want:

.shape {
  --sh:, arc by 0 -50px of 1%, arc by 0 50px of 1%;
  
  clip-path: shape(
    from    X1 Y1 var(--sh),
    move to X2 Y2 var(--sh),
    ... 
    move to Xn Yn var(--sh)
  ) 
}

You don’t want a circle? Easy, you can update the --sh variable with any shape you want. Here is an example with three different shapes:

And guess what? You can invert the whole thing using the cut-out technique by adding the rectangle shape at the end:

This code is a perfect example of the shape() function’s power. We don’t have any code duplication and we can simply adjust the shape with CSS variables. This is something we are unable to achieve with the path() function because it doesn’t support variables.

Conclusion

That’s all for this fourth installment of our series on the CSS shape() function! We didn’t make any super complex shapes, but we learned how two simple commands can open a lot of possibilities of what can be done using shape().

Just for fun, here is one more demo recreating a classic three-dot loader using the last technique we covered. Notice how much further we could go, adding things like animation to the mix:

Better CSS Shapes Using shape()

  1. Lines and Arcs
  2. More on Arcs
  3. Curves
  4. Close and Move (you are here!)

Better CSS Shapes Using shape() — Part 4: Close and Move originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Patrick Brosset
Tue, 01 Jul 2025 12:42:38 +0000


Four years ago, I wrote an article titled Minding the “gap”, where I talked about the CSS gap property, where it applied, and how it worked with various CSS layouts.

At the time, I described how easy it was to evenly space items out in a flex, grid, or multi-column layout, by using the gap property. But, I also said that styling the gap areas was much harder, and I shared a workaround.

However, workarounds like using extra HTML elements, pseudo-elements, or borders to draw separator lines tend to come with drawbacks, especially those that impact your layout size, interfere with assistive technologies, or pollute your markup with style-only elements.

Today, I’m writing again about layout gaps, but this time, to tell you all about a new and exciting CSS feature that’s going to change it all. What you previously had to use workarounds for, you’ll soon be able to do with just a few simple CSS properties that make it easy, yet also flexible, to display styled separators between your layout items.

There’s already a specification draft for the feature you can peruse. At the time I’m writing this, it is available in Chrome and Edge 139 behind a flag. But I believe it won’t be long before we turn that flag on. I believe other browsers are also very receptive and engaged.

Displaying decorative lines between items of a layout can make a big difference. When used well, these lines can bring more structure to your layout, and give your users more of a sense of how the different regions of a page are organized.

Introducing CSS gap decorations

If you’ve ever used a multi-column layout, such as by using the column-width property, then you might already be familiar with gap decorations. You can draw vertical lines between the columns of a multi-column layout by using the column-rule property:

article {
  column-width: 20rem;
  column-rule: 1px solid black;
}
Two 1-pixel solid black vertical lines separate a row of three text blocks.

The CSS gap decorations feature builds on this to provide a more comprehensive system that makes it easy for you to draw separator lines in other layout types.

For example, the draft specification says that the column-rule property also works in flexbox and grid layouts:

.my-grid-container {
  display: grid;
  gap: 2px;
  column-rule: 2px solid pink;
}
A 2-pixel solid light pink vertical line separates two side-by-side text blocks.

No need for extra elements or borders! The key benefit here is that the decoration happens in CSS only, where it belongs, with no impacts to your semantic markup.

The CSS gap decorations feature also introduces a new row-rule property for drawing lines between rows:

.my-flex-container {
  display: flex;
  gap: 10px;
  row-rule: 10px dotted limegreen;
  column-rule: 5px dashed coral;
}
Six items flowing horizontally in two rows in a flex container, separated by 5-pixel dashed coral-colored vertical lines and a single 10-pixel dotted lime-green line between the two rows.

But that’s not all, because the above syntax also allows you to define multiple, comma-separated, line style values, and use the same repeat() function that CSS grid already uses for row and column templates. This makes it possible to define different styles of line decorations in a single layout, and adapt to an unknown number of gaps:

.my-container {
  display: grid;
  gap: 2px;
  row-rule:
    repeat(2, 1px dashed red),
    2px solid black,
    repeat(auto, 1px dotted green);
}
Seven text blocks stacked vertically separated by horizontal lines that are styled differently.

Finally, the CSS gap decorations feature comes with additional CSS properties such as row-rule-break, column-rule-break, row-rule-outset, column-rule-outset, and gap-rule-paint-order, which make it possible to precisely customize the way the separators are drawn, whether they overlap, or where they start and end.

And of course, all of this works across grid, flexbox, multi-column, and soon, masonry!

Browser support

Currently, the CSS gap decorations feature is only available in Chromium-based browsers.

The feature is still early in the making, and there’s time for you all to try it and to provide feedback that could help make the feature better and more adapted to your needs.

If you want to try the feature today, make sure to use Edge or Chrome, starting with version 139 (or another Chromium-based browser that matches those versions), and enable the flag by following these steps:

  1. In Chrome or Edge, go to about://flags.
  2. In the search field, search for Enable Experimental Web Platform Features.
  3. Enable the flag.
  4. Restart the browser.

To put this all into practice, let’s walk through an example together that uses the new CSS gap decorations feature. I also have a final example you can demo.

Using CSS gap decorations

Let’s build a simple web page to learn how to use the feature. Here is what we’ll be building:

Webpage titled My Personal Site in the header above a horizontal navigation and a staggered, masonry-like layout of text and images with thin lines between them. The design is in black and white.

The above layout contains a header section with a title, a navigation menu with a few links, a main section with a series of short paragraphs of text and photos, and a footer.

We’ll use the following markup:

<body>
<header>
  <h1>My personal site</h1>
</header>
<nav>
  <ul>
    <li><a href="#">Home</a></li>
    <li><a href="#">Blog</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Links</a></li>
  </ul>
</nav>
<main>
  <article>
    <p>...</p>
  </article>
  <article>
    <img src="cat.jpg" alt="A sleeping cat.">
  </article>
  <article>
    <p>...</p>
  </article>
  <article>
    <img src="tree.jpg" alt="An old olive tree trunk.">
  </article>
  <article>
    <p>...</p>
  </article>
  <article>
    <p>...</p>
  </article>
  <article>
    <p>...</p>
  </article>
  <article>
    <img src="strings.jpg" alt="Snow flakes falling in a motion blur effect.">
  </article>
</main>
<footer>
  <p>© 2025 Patrick Brosset</p>
</footer>
</body>

We’ll start by making the <body> element be a grid container. This way, we can space out the <header>, <nav>, <main>, and <footer> elements apart in one go by using the gap property:

body {
  display: grid;
  gap: 4rem;
  margin: 2rem;
}

Let’s now use the CSS gap decorations feature to display horizontal separator lines within the gaps we just defined:

body {
  display: grid;
  gap: 4rem;
  margin: 2rem;
 
  row-rule: 1rem solid #efefef;
}

This gives us the following result:

The basic layout for the webpage. The title is the same but the navigation and layout are both vertically stacked. There are no lines between items in the layout.

We can do a bit better by making the first horizontal line look different than the other two lines, and simplify the row-rule value by using the repeat() syntax:

body {
  display: grid;
  gap: 4rem;
  margin: 2rem;
 
  row-rule:
    1rem solid #efefef,
    repeat(2, 2px solid #efefef);
}

With this new row-rule property value, we’re telling the browser to draw the first horizontal separator as a 1rem thick line, and the next two separators as 2px thick lines, which gives the following result:

The webpage is largely the same, but the border between the site title and the navigation is much thicker.

Now, let’s turn our attention to the navigation element and its list of links. We’ll use flexbox to display the links in a single row, where each link is separated from the other links by a gap and a vertical line:

nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  column-rule: 2px dashed #666;
}

Very similarly to how we used the row-rule property before, we’re now using the column-rule property to display a dashed 2px thick separator between the links.

Our example web page now looks like this:

The webpage is still largely the same, but now the navigation is horizontal and there is a light dashed line between the links.

The last thing we need to change is the <main> element and its paragraphs and pictures. We’ll use flexbox again and display the various children in a wrapping row of varying width items:

main {
  display: flex;
  flex-wrap: wrap;
  gap: 4rem;
}


main > * {
  flex: 1 1 200px;
}


main article:has(p) {
  flex-basis: 400px;
}

In the above code snippet, we’re setting the <main> element to be a wrapping flex container with a 4rem gap between items and flex lines. We’re also making the items have a flex basis size of 200px for pictures and 400px for text, and allowing them to grow and shrink as needed. This gives us the following result:

The webpage layout has been established but there are no lines between items.

Let’s use CSS gap decorations to bring a little more structure to our layout by drawing 2px thick separator lines between the rows and columns of the layout:

main {
  display: flex;
  flex-wrap: wrap;
  gap: 4rem;
  row-rule: 2px solid #999;
  column-rule: 2px solid #999;
}

This gives us the following result, which is very close to our expected design:

Thin light lines have been added between the layout of text and images, creating a masonry-like layout. The lines extend all the way across each item like enclosed boxes.

The last detail we want to change is related to the vertical lines. We don’t want them to span across the entire height of the flex lines but instead start and stop where the content starts and stops.

With CSS gap decorations, we can easily achieve this by using the column-rule-outset property to fine-tune exactly where the decorations start and end, relative to the gap area:

main {
  display: flex;
  flex-wrap: wrap;
  gap: 4rem;
  row-rule: 2px solid #999;
  column-rule: 2px solid #999;
  column-rule-outset: 0;
}

The column-rule-outset property above makes the vertical column separators span the height of each row, excluding the gap area, which is what we want:

Spacing has been added between the layout items so that the lines between them are no longer connected, creating an elegant layout.

And with that, we’re done with our example. Check out the live example, and source code.

Learn more

There’s more to the feature and I mentioned a couple more CSS properties earlier

  • gap-rule-paint-order, which lets you control which of the decorations, rows or columns, appear above the other ones.
  • row-rule-break / column-rule-break, which sets the behavior of the decoration lines at intersections. In particular, whether they are made of multiple segments, which start and end at intersections, or single, continuous lines.

Because the feature is new, there isn’t MDN documentation about it yet. So to learn more, check out:

The Edge team has also created an interactive playground where you can use visual controls to configure gap decorations.

And, of course, the reason this is all implemented behind a flag is to elicit feedback from developers like you! If you have any feedback, questions, or bugs about this feature, I definitely encourage you to open a new ticket on the Chromium issue tracker.


The Gap Strikes Back: Now Stylable originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 30 Jun 2025 17:04:57 +0000


Mr. Brad Frost, and his brother Ian, have a new course they are selling called Subatomic: The Complete Guide to Design Tokens.

To be honest, I was a smidge skeptical. I know what a design token is. It’s a variable of a color or font-family or something. I pretty much only work on websites, so that exposes itself as a --custom-property and I already know that using those to abstract common usage of colors and fonts is smart and helpful. Done. I get that people managing a whole fleet of sites (and apps running in who-knows-what technologies) need a fancier token system, but that ain’t me.

But then we had those fellas on ShopTalk Show and I’ve updated my thinking that you really do want to lean on the expertise of people that have done this time and time again at scale.

(p.s. they also gave us a 20% discount code when they were on the show: SHOPTALKSHOWISAWESOME)

Spoiler: they advocate for a three-tier system of custom properties. The first is just the raw ingredients. Colors, but you’re just naming the color; sizes, but you’re just naming the sizes. Then there is this middle tier where you are essentially crafting a theme from those raw ingredients. And this layer is the most important, as it gives you this perfect layer of abstraction where you’re both not reaching into the raw ingredients and you’re not being too overly specific, like naming individual parts of components. The third layer should be avoided as best as it can, but if you absolutely need to get hyper specific, this is where you do it, and are still keeping in the land of custom properties.

This feels particularly smart to me, and I wish I had the benefit of the Frost Brother’s expertise on this before building some custom property systems I have built in the past. I tend to have that first layer with just the raw ingredients, and then jump right to what they’d call the third tier, which leads to a real blowing up of how many custom properties are in use, to the point that it feels overly cumbersome and like the whole system isn’t even helping that much. I’ll definitely be thinking about the theming tier next time I have a good refactoring opportunity.

Brad has also been steady on his global design system idea. I’ve posted my thoughts on this before, but I keep coming back to this one:

It feels like every decision you make will chip away at who will use it.

I’m fascinated at seeing how decisions get made that keep this thing as “global” as possible. That absolutely must be done, otherwise it’s just another design system which I thinks falls short of the goal. I appreciated Brian’s deep thoughts on it all as well, and I’m basically writing all this as an excuse to link to that.

Would a global design system have any design to it at all? Maybe; maybe not. It makes me wonder if the era of “flat design” that it seems like we’ve been in for a decade or so was partially the result of design systems, where the simpler things look the more practical it is to build all the “lego blocks” of a cohesive aesthetic. But it’s likely design trends move on. Maybe flat is over. Are design systems ready for very fancy/complex looks? Definitely worth a read is Amelia’s thoughts on “balancing the hard structure and soft flexibility” of UIs.

Speaking of design tokens, designtokens.fyi is a nice site for defining all the terms that design systems/tokens people like to throw around. A site with word definitions can be awfully boring so I appreciate the fun design here. I like the idea of calling a value system a “t-shirt” where you’re actually defining, say, a set of padding options, but the options follow the mental model of t-shirt sizes.

Sometimes you just need to look and see what other people are doing. In design, there always has been and will be design galleries full of inspirational stuff. But instead of linking to one of those, I’m going to link to to the “Home of the internet’s finest website headlines.” I love a good headline, myself. I’ve seen far too many sites that do a terrible job of just saying what their point is.

by: Zell Liew
Mon, 30 Jun 2025 13:16:43 +0000


Adam Wathan has (very cleverly) built Tailwind with CSS Cascade Layers, making it extremely powerful for organizing styles by priority.

@layer theme, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);

The core of Tailwind are its utilities. This means you have two choices:

  1. The default choice
  2. The unorthodox choice

The default choice

The default choice is to follow Tailwind’s recommended layer order: place components first, and Tailwind utilities last.

So, if you’re building components, you need to manually wrap your components with a @layer directive. Then, overwrite your component styles with Tailwind, putting Tailwind as the “most important layer”.

/* Write your components */
@layer components {
  .component {
    /* Your CSS here */
  }
}
<!-- Override with Tailwind utilities --> 
<div class="component p-4"> ... </div>

That’s a decent way of doing things.

But, being the bad boy I am, I don’t take the default approach as the “best” one. Over a year of (major) experimentation with Tailwind and vanilla CSS, I’ve come across what I believe is a better solution.

The Unorthodox Choice

Before we go on, I have to tell you that I’m writing a course called Unorthodox Tailwind — this shows you everything I know about using Tailwind and CSS in synergistic ways, leveraging the strengths of each.

Shameless plug aside, let’s dive into the Unorthodox Choice now.

In this case, the Unorthodox Choice is to write your styles in an unnamed layer — or any layer after utilities, really — so that your CSS naturally overwrites Tailwind utilities.

Of these two, I prefer the unnamed layer option:

/* Unnamed layer option */
@layer theme, base, components, utilities; 

/* Write your CSS normally here */ 
.component { /* ... */ }
/* Named layer option */
/* Use whatever layer name you come up with. I simply used css here because it made most sense for explaining things */
@layer theme, base, components, utilities, css; 

@layer css {
  .component { /* ... */ }
}

I have many reasons why I do this:

  1. I don’t like to add unnecessary CSS layers because it makes code harder to write — more keystrokes, having to remember the specific layer I used it in, etc.
  2. I’m pretty skilled with ITCSS, selector specificity, and all the good-old-stuff you’d expect from a seasoned front-end developer, so writing CSS in a single layer doesn’t scare me at all.
  3. I can do complex stuff that are hard or impossible to do in Tailwind (like theming and animations) in CSS.

Your mileage may vary, of course.

Now, if you have followed my reasoning so far, you would have noticed that I use Tailwind very differently:

  • Tailwind utilities are not the “most important” layer.
  • My unnamed CSS layer is the most important one.

I do this so I can:

  • Build prototypes with Tailwind (quickly, easily, especially with the tools I’ve created).
  • Shift these properties to CSS when they get more complex — so I don’t have to read messy utility-littered HTML that makes my heart sink. Not because utility HTML is bad, but because it takes lots of brain processing power to figure out what’s happening.

Finally, here’s the nice thing about Tailwind being in a utility layer: I can always !important a utility to give it strength.

<!-- !important the padding utility -->
<div class="component !p-4"> ... </div>

Whoa, hold on, wait a minute! Isn’t this wrong, you might ask?

Nope. The !important keyword has traditionally been used to override classes. In this case, we’re leveraging on the !important feature in CSS Layers to say the Tailwind utility is more important than any CSS in the unnamed layer.

This is perfectly valid and is a built-in feature for CSS Layers.

Besides, the !important is so explicit (and used so little) that it makes sense for one-off quick-and-dirty adjustments (without creating a brand new selector for it).

Tailwind utilities are more powerful than they seem

Tailwind utilities are not a 1:1 map between a class and a CSS property. Built-in Tailwind utilities mostly look like this so it can give people a wrong impression.

Tailwind utilities are more like convenient Sass mixins, which means we can build effective tools for layouts, theming, typography, and more, through them.

You can find out about these thoughts inside Unorthodox Tailwind.

Thanks for reading and I hope you’re enjoying a new way of looking at (or using) Tailwind!


Using CSS Cascade Layers With Tailwind Utilities originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Neeraj Mishra
Fri, 27 Jun 2025 17:48:27 +0000


MetaTrader 5 (MT5) is an advanced trading platform supporting a multitude of different assets like Forex, cryptos, commodities, and so on. It is incredibly popular among Japanese traders and regulated brokers. Many programmers in Japan are employing its MQL5 programming language to develop advanced trading algorithms and we are going to explain how they are using MT5 for advanced algorithm development and trading below.

Identical syntax to C/C++

MT5 is free and offered by many reputable brokers that are regulated in Japan, making it a simple process to use the platform’s advanced features. 

The main advantage of MQL5 is its similarity to the popular programming language C++, which makes it very easy to adopt and learn. The syntax of MQL5 is nearly identical and data types are also familiar, like int, double, char, bool, and string. Functions are declared and used the same way, and MQL5 also supports classes, inheritance, and other OOP (Object-Oriented Programming) objects like C++. You can also pass parameters by reference using &.

MQL5 programming language

Integrated IDE

MetaEditor, which is a native integrated development environment, is built into the MT5 trading platform. This is super flexible as users can switch back and forth between MT5 and MetaEditor with just one click of a mouse or F4 button.

After programming in the MQL5 editor, users can switch back to the MT5 platform quickly and test their indicators or Expert Advisors (EAs) using the strategy editor.

No need for APIs

The pricing data is also provided directly by the broker to your MT5 platform and when testing the algorithm, there is a strategy tester plugin on MT5 to test EAs. There is no need for API calls and other functions which makes the whole process not only comfortable but also very fast.

Built-in functions

Instead of writing your own low-level hardware codes, MQL5 comes with built-in functions like:

  • OrderSend() to open trades
  • iMA() – to call indicators like moving averages.
  • SymbolInfoDouble()

All built-in indicators come with built-in functions which makes it very comfortable to summon them in your EA.

Unlike other platforms or programming languages, developers do not need to construct candle data or anything. Instead, just apply your EA to your preferred instrument, timeframe, and chart types and it’s ready to go.

Push notifications and alerts

MQL5 comes with several alert functions which enable notifications. Users can define where their EAs will send notifications when predefined events occur. SMS, email, and platform alerts are all supported to develop powerful trading algorithms.

Faster trade execution and social features

MT5 supports even faster trade execution natively which is perfect for HFT and other algorithms that rely on fast trade execution for profits. Users can deploy their EA and be sure that it can open and close trades in milliseconds which enables them to deploy a wide range of trading strategies, including arbitrage and scalping techniques.

Trading signals and community integration

The platform integrates copy trading and community features. Traders can easily use copy trading services while developers can develop and sell their EAs to generate passive income. MT5 provides direct access to the MQL5.com community from the platform which makes it very easy to use EAs from the official store. Developers can deploy their EAs in the store to generate revenue which makes it very lucrative to learn and code robots.

Large community

Are newcomers to MT5 and MQL5? Then there is good news for you. There is a plethora of educational content provided freely on MQL5 forums where even beginners can learn MQL5 and MT5 programming. The built-in chat system enables communication with other users as well.

Free EAs and custom indicators

Another big advantage for Japanese programmers is the availability of free EAs and custom indicators. There is an online store to upload and sell or rent your algorithms, which is very flexible. The platform also supports scripts and utilities and by using a free code base, developers can quickly find complex functions and use them easily to enhance their algorithms and reduce time needed for development.

The bottom line

Japanese traders choose MT5 for algorithmic trading because it combines a familiar C/C++-like language (MQL5) with fully integrated IDE (MetaEditor) and built-in data feeds. MQL5 provides an extensive library of functions and supports OOP. As a result, Japanese developers can develop complex Expert Advisors with ease. Real-time alerts, and ultra-low latency trade execution, make MT5 perfect for scalping algorithms.

Overall, MT5’s MQL5 provides an all-in-one solution to develop, test, and sell or rent EAs quickly.

The post How Japanese Programmers Are Leveraging MT5 for Advanced Algorithmic Trading appeared first on The Crazy Programmer.

by: Juan Diego Rodríguez
Fri, 27 Jun 2025 13:48:41 +0000


Blob, Blob, Blob. You hate them. You love them. Personally, as a design illiterate, I like to overuse them… a lot. And when you repeat the same process over and over again, it’s only a question of how much you can optimize it, or in this case, what’s the easiest way to create blobs in CSS? Turns out, as always, there are many approaches.

To know if our following blobs are worth using, we’ll need them to pass three tests:

  1. They can be with just a single element (and preferably without pseudos).
  2. They can be easily designed (ideally through an online tool).
  3. We can use gradient backgrounds, borders, shadows, and other CSS effects on them.

Without further ado, let’s Blob, Blob, Blob right in.

Just generate them online

I know it’s disenchanting to click on an article about making blobs in CSS just for me to say you can generate them outside CSS. Still, it’s probably the most common way to create blobs on the web, so to be thorough, these are some online tools I’ve used before to create SVG blobs.

  • Haikei. Probably the one I have used the most since, besides blobs, it can also generate lots of SVG backgrounds.
  • Blobmaker. A dedicated tool for making blobs. It’s apparently part of Haikei now, so you can use both.
  • Lastly, almost all graphic programs let you hand-draw blobs and export them as SVGs.

For example, this is one I generated just now. Keep it around, as it will come in handy later.

Randomly shaped blob in bright red.
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
  <path
    fill="#FA4D56"
    d="M65.4,-37.9C79.2,-13.9,81,17,68.1,38C55.2,59.1,27.6,70.5,1.5,69.6C-24.6,68.8,-49.3,55.7,-56,38.2C-62.6,20.7,-51.3,-1.2,-39,-24.4C-26.7,-47.6,-13.3,-72,6.2,-75.6C25.8,-79.2,51.6,-62,65.4,-37.9Z"
    transform="translate(100 100)"
  />
</svg>

Using border-radius

While counterintuitive, we can use the border-radius property to create blobs. This technique isn’t new by any means; it was first described by Nils Binder in 2018, but it is still fairly unknown. Even for those who use it, the inner workings are not entirely clear.

To start, you may know the border-radius is a shorthand to each individual corner’s radius, going from the top left corner clockwise. For example, we can set each corner’s border-radius to get a bubbly square shape:

<div class="blob"></div>
.blob {
  border-radius: 25% 50% 75% 100%;
}

However, what border-radius does — and also why it’s called “radius” — is to shape each corner following a circle of the given radius. For example, if we set the top left corner to 25%, it will follow a circle with a radius 25% the size of the shape.

.blob {
  border-top-left-radius: 25%;
}

What’s less known is that each corner property is still a shortcut towards its horizontal and vertical radii. Normally, you set both radii to the same value, getting a circle, but you can set them individually to create an ellipse. For example, the following sets the horizontal radius to 25% of the element’s width and the vertical to 50% of its height:

.blob {
  border-top-left-radius: 25% 50%;
}

We can now shape each corner like an ellipse, and it is the combination of all four ellipses that creates the illusion of a blob! Just take into consideration that to use the horizontal and vertical radii syntax through the border-radius property, we’ll need to separate the horizontal from the vertical radii using a forward slash (/).

.blob {
  border-radius:
    /* horizontal */
    100% 30% 60% 70% /
    /* vertical */
    50% 40% 70% 70%;
}

The syntax isn’t too intuitive, so designing a blob from scratch will likely be a headache. Luckily, Nils Binder made a tool exactly for that!

Blobbing blobs together

This hack is awesome. We aren’t supposed to use border-radius like that, but we still do. Admittedly, we are limited to boring blobs. Due to the nature of border-radius, no matter how hard we try, we will only get convex shapes.

Concave and convex shapes

Just going off border-radius, we can try to minimize it a little by sticking more than one blob together:

However, I don’t want to spend too much time on this technique since it is too impractical to be worth it. To name a few drawbacks:

  1. We are using more than one element or, at the very least, an extra pseudo-element. Ideally, we want to keep it to one element.
  2. We don’t have a tool to prototype our blobby amalgamations, so making one is a process of trial and error.
  3. We can’t use borders, gradients, or box shadows since they would reveal the element’s outlines.

Multiple backgrounds and SVG filters

This one is an improvement in the Gooey Effect, described here by Lucas Bebber, although I don’t know who first came up with it. In the original effect, several elements can be morphed together like drops of liquid sticking to and flowing out of each other:

It works by first blurring shapes nearby, creating some connected shadows. Then we crank up the contrast, forcing the blur out and smoothly connecting them in the process. Take, for example, this demo by Chris Coyer (It’s from 2014, so more than 10 years ago!):

If you look at the code, you’ll notice Chris uses the filter property along the blur() and contrast() functions, which I’ve also seen in other blob demos. To be specific, it applies blur() on each individual circle and then contrast() on the parent element. So, if we have the following HTML:

<div class="blob">
  <div class="subblob"></div>
  <div class="subblob"></div>
  <div class="subblob"></div>
</div>

…we would need to apply filters and background colors as such:

.blob {
  filter: contrast(50);
  background: white; /* Solid colors are necessary */
}

.subblob {
  filter: blur(15px);
  background: black; /* Solid colors are necessary */
}

However, there is a good reason why those demos stick to white shapes and black backgrounds (or vice versa) since things get unpredictable once colors aren’t contrast-y enough. See it for yourself in the following demo by changing the color. Just be wary: shades get ugly.

To solve this, we will use an SVG filter instead. I don’t want to get too technical on SVG (if you want to, read Luca’s post!). In a nutshell, we can apply blurring and contrast filters using SVGs, but now, we can also pick which color channel we apply the contrast to, unlike normal contrast(), which modifies all colors.

Since we want to leave color channels (RG and B) untouched, we will only crank the contrast up for the alpha channel. That translates to the next SVG filter, which can be embedded in the HTML:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="position: absolute;">
  <defs>
    <filter id="blob">
      <feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blur" />
      <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -6" result="goo" />
      <feBlend in="SourceGraphic" in2="blob" />
    </filter>
  </defs>
</svg>

To apply it, we will use again filter, but this time we’ll set it to url("#blob"), so that it pulls the SVG from the HTML.

.blob {
  filter: url("#blob");
}

And now we can even use it with gradient backgrounds!

That being said, this approach comes with two small, but important, changes to common CSS filters:

  1. The filter is applied to the parent element, not the individual shapes.
  2. The parent element must be transparent (which is a huge advantage). To change the background color, we can instead change the body or other ancestors’ background, and it will work with no issues.

What’s left is to place the .subblob elements together such that they make a blobby enough shape, then apply the SVG filters to morph them:

Making it one element

This works well, but it has a similar issue to the blob we made by morphing several border-radius instances: too many elements for a simple blob. Luckily, we can take advantage of the background property to create multiple shapes and morph them together using SVG filters, all in a single element. Since we are keeping it to one element, we will go back to just one empty .blob div:

<div class="blob"></div>

To recap, the background shorthand can set all background properties and also set multiple backgrounds at once. Of all the properties, we only care about the background-imagebackground-position and background-size.

First, we will use background-image along with radial-gradient() to create a circle inside the element:

body {
  background: radial-gradient(farthest-side, var(--blob-color) 100%, #0000);
  background-repeat: no-repeat; /* Important! */
}

Here is what each parameter does:

  • farthest-side: Confines the shape to the element’s box farthest from its center. This way, it is kept as a circle.
  • var(--blob-color) 100%: Fills the background shape from 0 to 100% with the same color, so it ends up as a solid color.
  • #0000: After the shape is done, it makes a full stop to transparency, so the color ends.

The next part is moving and resizing the circle using the background-position and background-size properties. Luckily, both can be set on background after the gradient, separated from each other by a forward slash (/).

body {
  background: radial-gradient(...) 20% 30% / 30% 40%;
  background-repeat: no-repeat; /* Important! */
}

The first pair of percentages sets the shape’s horizontal and vertical position (taking as a reference the top-left corner), while the second pair sets the shape’s width and height (taking as a reference the element’s size).

As I mentioned, we can stack up different backgrounds together, which means we can create as many circles/ellipses as we want! For example, we can create three ellipses on the same element:

.blob {
  background:
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 20% 30% / 30% 40%, 
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 80% 50% / 40% 60%, 
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 50% 70% / 50% 50%;
  background-repeat: no-repeat;
}

What’s even better is that SVG filters don’t care whether shapes are made of elements or backgrounds, so we can also morph them together using the last url(#blob) filter!

While this method may be a little too much for blobs, it unlocks squishing, stretching, dividing, and merging blobs in seamless animations.

Again, all these tricks are awesome, but not enough for what we want! We accomplished reducing the blob to a single element, but we still can’t use gradients, borders, or shadows on them, and also, they are tedious to design and model. Then, that brings us to the ultimate blob approach…

Using the shape() function

Fortunately, there is a new way to make blobs that just dropped to CSS: the shape() function!

I’ll explain shape()‘s syntax briefly, but for an in-depth explanation, you’ll want to check out both this explainer from the CSS-Tricks Almanac as well as Temani Afif‘s three-part series on the shape() function, as well as his recent article about blobs.

First off, the CSS shape() function is used alongside the clip-path property to cut elements into any shape we want. More specifically, it uses a verbal version of SVG’s path syntax. The syntax has lots of commands for lots of types of lines, but when blobbing with shape(), we’ll define curves using the curve command:

.blob {
  clip-path: shape(
    from X0 Y0, 
    curve to X1 Y1 with Xc1 Yc1, 
    curve to X2 Y2 with Xc21 Yc21 / Xc22 Yc22
    /* ... */
  );
}

Let’s break down each parameter:

  • X0 Y0 defines the starting point of the shape.
  • curve starts the curve where X1 Y1 is the next point of the shape, while Xc1 Yc1 defines a control point used in Bézier curves.
  • The next parameter is similar, but we used Xc21 Yc21 / Xc22 Yc22 instead to define two control points on the Bézier curve.

I honestly don’t understand Bézier curves and control points completely, but luckily, we don’t need them to use shape() and blobs! Again, shape() uses a verbal version of SVG’s path syntax, so it can draw any shape an SVG can, which means that we can translate the SVG blobs we generated earlier… and CSS-ify them. To do so, we’ll grab the d attribute (which defines the path) from our SVG and paste it into Temani’s SVG to shape() generator.

This is the exact code the tool generated for me:

.blob {
  aspect-ratio: 0.925; /* Generated too! */

  clip-path: shape(
    from 91.52% 26.2%,
    curve to 93.52% 78.28% with 101.76% 42.67%/103.09% 63.87%,
    curve to 44.11% 99.97% with 83.95% 92.76%/63.47% 100.58%,
    curve to 1.45% 78.42% with 24.74% 99.42%/6.42% 90.43%,
    curve to 14.06% 35.46% with -3.45% 66.41%/4.93% 51.38%,
    curve to 47.59% 0.33% with 23.18% 19.54%/33.13% 2.8%,
    curve to 91.52% 26.2% with 62.14% -2.14%/81.28% 9.66%
  );
}

As you might have guessed, it returns our beautiful blob:

Let’s check if it passes our requirements:

  1. Yes, they can be made of a single element.
  2. Yes, they can also be created in a generator and then translated into CSS.
  3. Yes, we can use gradient backgrounds, but due to the nature of clip-path(), borders and shadows get cut out.

Two out of three? Maybe two and a half of three? That’s a big improvement over the other approaches, even if it’s not perfect.

Conclusion

So, alas, we failed to find what I believe is the perfect CSS approach to blobs. I am, however, amazed how something so trivial designing blobs can teach us about so many tricks and new CSS features, many of which I didn’t know myself.


CSS Blob Recipes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

KelpUI

by: Geoff Graham
Thu, 26 Jun 2025 16:42:32 +0000


KelpUI is new library that Chris Ferdinandi is developing, designed to leverage newer CSS features and Web Components. I’ve enjoyed following Chris as he’s published an ongoing series of articles detailing his thought process behind the library, getting deep into his approach. You really get a clear picture of his strategy and I love it.

He outlined his principles up front in a post back in April:

I’m imagining a system that includes…

  • Base styles for all of the common HTML elements.
  • Loads of utility classes for nudging and tweaking things.
  • Group classes for styling more complex UI elements without a million little classes.
  • Easy customization with CSS variables.
  • Web Components to progressively add interactivity to functional HTML.
  • All of the Web Component HTML lives in the light DOM, so its easy to style and reason about.

I’m imagining something that can be loaded directly from a CDN, downloaded locally, or imported if you want to roll your own build.

And that’s what I’ve seen so far. The Cascade is openly embraced and logically structured with Cascade Layers. Plenty of utility classes are included, with extra care put into how they are named. Selectors are kept simple and specificity is nice and low, where needed. Layouts are flexible with good constraints. Color palettes are accessible and sport semantic naming.

Chris has even put a ton of thought into how KelpUI is licensed.

KelpUI is still evolving, and that’s part of the beauty of looking at it now and following Chris’s blog as he openly chronicles his approach. There’s always going to be some opinionated directions in a library like this, but I love that the guiding philosophy is so clear and is being used as a yardstick to drive decisions. As I write this, Chris is openly questioning the way he optimizes the library, demonstrating the tensions between things like performance and a good developer experience.

Looks like it’ll be a good system, but even more than that, it’s a wonderful learning journey that’s worth following.


KelpUI originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Daniel Schwarz
Wed, 25 Jun 2025 14:33:45 +0000


Chrome 137 shipped the if() CSS function, so it’s totally possible we’ll see other browsers implement it, though it’s tough to know exactly when. Whatever the case, if() enables us to use values conditionally, which we can already do with queries and other functions (e.g., media queries and the light-dark() function), so I’m sure you’re wondering: What exactly does if() do?

Sunkanmi gave us a nice overview of the function yesterday, poking at the syntax at a high level. I’d like to poke at it a little harder in this article, getting into some possible real-world usage.

To recap, if() conditionally assigns a value to a property based on the value of a CSS variable. For example, we could assign different values to the color and background properties based on the value of --theme:

  • --theme: "Shamrock"
    • color: ‌hsl(146 50% 3%)
    • background: hsl(146 50% 40%)
  • --theme: Anything else
    • color: hsl(43 74% 3%)
    • background: hsl(43 74% 64%)
:root {
  /* Change to fall back to the ‘else’ values */
  --theme: "Shamrock";

  body {
    color: if(style(--theme: "Shamrock"): hsl(146 50% 3%); else: hsl(43 74% 3%));
    background: if(style(--theme: "Shamrock"): hsl(146 50% 40%); else: hsl(43 74% 64%));
  }
}

I don’t love the syntax (too many colons, brackets, and so on), but we can format it like this (which I think is a bit clearer):

color: if(
  style(--theme: "Shamrock"): hsl(146 50% 3%);
  else: hsl(43 74% 3%)
);

We should be able to do a crazy number of things with if(), and I hope that becomes the case eventually, but I did some testing and learned that the syntax above is the only one that works. We can’t base the condition on the value of an ordinary CSS property (instead of a custom property), HTML attribute (using attr()), or any other value. For now, at least, the condition must be based on the value of a custom property (CSS variable).

Exploring what we can do with if()

Judging from that first example, it’s clear that we can use if() for theming (and design systems overall). While we could utilize the light-dark() function for this, what if the themes aren’t strictly light and dark, or what if we want to have more than two themes or light and dark modes for each theme? Well, that’s what if() can be used for.

First, let’s create more themes/more conditions:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Saffron"; /* ...I choose you! */

  body {
    color: if(
      style(--theme: "Shamrock"): hsl(146 50% 3%);
      style(--theme: "Saffron"): hsl(43 74% 3%);
      style(--theme: "Amethyst"): hsl(282 47% 3%)
    );
    background: if(
      style(--theme: "Shamrock"): hsl(146 50% 40%);
      style(--theme: "Saffron"): hsl(43 74% 64%);
      style(--theme: "Amethyst"): hsl(282 47% 56%)
    );
    transition: 300ms;
  }
}

Pretty simple really, but there are a few easy-to-miss things. Firstly, there’s no “else condition” this time, which means that if the theme isn’t Shamrock, Saffron, or Amethyst, the default browser styles are used. Otherwise, the if() function resolves to the value of the first true statement, which is the Saffron theme in this case. Secondly, transitions work right out of the box; in the demo below, I’ve added a user interface for toggling the --theme, and for the transition, literally just transition: 300ms alongside the if() functions:

Note: if theme-swapping is user-controlled, such as selecting an option, you don’t actually need if() at all. You can just use the logic that I’ve used at the beginning of the demo (:root:has(#shamrock:checked) { /* Styles */ }). Amit Sheen has an excellent demonstration over at Smashing Magazine.

To make the code more maintainable though, we can slide the colors into CSS variables as well, then use them in the if() functions, then slide the if() functions themselves into CSS variables:

/* Setup */
:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  /* Base colors */
  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  /* Base colors, but at 3% lightness */
  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  --background: if(
    style(--theme: "Shamrock"): var(--shamrock);
    style(--theme: "Saffron"): var(--saffron);
    style(--theme: "Amethyst"): var(--amethyst)
  );

  --color: if(
    style(--theme: "Shamrock"): var(--shamrock-complementary);
    style(--theme: "Saffron"): var(--saffron-complementary);
    style(--theme: "Amethyst"): var(--amethyst-complementary)
  );

  /* Usage */
  body {
    /* One variable, all ifs! */
    background: var(--background);
    color: var(--color);
    accent-color: var(--color);

    /* Can’t forget this! */
    transition: 300ms;
  }
}

As well as using CSS variables within the if() function, we can also nest other functions. In the example below, I’ve thrown light-dark() in there, which basically inverts the colors for dark mode:

--background: if(
  style(--theme: "Shamrock"): light-dark(var(--shamrock), var(--shamrock-complementary));
  style(--theme: "Saffron"): light-dark(var(--saffron), var(--saffron-complementary));
  style(--theme: "Amethyst"): light-dark(var(--amethyst), var(--amethyst-complementary))
);

if() vs. Container style queries

If you haven’t used container style queries before, they basically check if a container has a certain CSS variable (much like the if() function). Here’s the exact same example/demo but with container style queries instead of the if() function:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  body {
    /* Container has chosen Shamrock! */
    @container style(--theme: "Shamrock") {
      --background: light-dark(var(--shamrock), var(--shamrock-complementary));
      --color: light-dark(var(--shamrock-complementary), var(--shamrock));
    }

    @container style(--theme: "Saffron") {
      --background: light-dark(var(--saffron), var(--saffron-complementary));
      --color: light-dark(var(--saffron-complementary), var(--saffron));
    }

    @container style(--theme: "Amethyst") {
      --background: light-dark(var(--amethyst), var(--amethyst-complementary));
      --color: light-dark(var(--amethyst-complementary), var(--amethyst));
    }

    background: var(--background);
    color: var(--color);
    accent-color: var(--color);
    transition: 300ms;
  }
}

As you can see, where if() facilitates conditional values, container style queries facilitate conditional properties and values. Other than that, it really is just a different syntax.

Additional things you can do with if() (but might not realize)

Check if a CSS variable exists:

/* Hide icons if variable isn’t set */
.icon {
  display: if(
    style(--icon-family): inline-block;
    else: none
  );
}

Create more-complex conditional statements:

h1 {
  font-size: if(
    style(--largerHeadings: true): xxx-large;
    style(--theme: "themeWithLargerHeadings"): xxx-large
  );
}

Check if two CSS variables match:

/* If #s2 has the same background as #s1, add a border */
#s2 {
  border-top: if(
    style(--s2-background: var(--s1-background)): thin solid red
  );
}

if() and calc(): When the math isn’t mathing

This won’t work (maybe someone can help me pinpoint why):

div {
  /* 3/3 = 1 */
  --calc: calc(3/3);
  /* Blue, because if() won’t calculate --calc */
  background: if(style(--calc: 1): red; else: blue);
}

To make if() calculate --calc, we’ll need to register the CSS variable using @property first, like this:

@property --calc {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

Closing thoughts

Although I’m not keen on the syntax and how unreadable it can sometimes look (especially if it’s formatted on one line), I’m mega excited to see how if() evolves. I’d love to be able to use it with ordinary properties (e.g., color: if(style(background: white): black; style(background: black): white);) to avoid having to set CSS variables where possible.

It’d also be awesome if calc() calculations could be calculated on the fly without having to register the variable.

That being said, I’m still super happy with what if() does currently, and can’t wait to build even simpler design systems.


Poking at the CSS if() Function a Little More: Conditional Color Theming originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Daniel Schwarz
Wed, 25 Jun 2025 14:33:45 +0000


Chrome 137 shipped the if() CSS function, so it’s totally possible we’ll see other browsers implement it, though it’s tough to know exactly when. Whatever the case, if() enables us to use values conditionally, which we can already do with queries and other functions (e.g., media queries and the light-dark() function), so I’m sure you’re wondering: What exactly does if() do?

Sunkanmi gave us a nice overview of the function yesterday, poking at the syntax at a high level. I’d like to poke at it a little harder in this article, getting into some possible real-world usage.

To recap, if() conditionally assigns a value to a property based on the value of a CSS variable. For example, we could assign different values to the color and background properties based on the value of --theme:

  • --theme: "Shamrock"
    • color: ‌hsl(146 50% 3%)
    • background: hsl(146 50% 40%)
  • --theme: Anything else
    • color: hsl(43 74% 3%)
    • background: hsl(43 74% 64%)
:root {
  /* Change to fall back to the ‘else’ values */
  --theme: "Shamrock";

  body {
    color: if(style(--theme: "Shamrock"): hsl(146 50% 3%); else: hsl(43 74% 3%));
    background: if(style(--theme: "Shamrock"): hsl(146 50% 40%); else: hsl(43 74% 64%));
  }
}

I don’t love the syntax (too many colons, brackets, and so on), but we can format it like this (which I think is a bit clearer):

color: if(
  style(--theme: "Shamrock"): hsl(146 50% 3%);
  else: hsl(43 74% 3%)
);

We should be able to do a crazy number of things with if(), and I hope that becomes the case eventually, but I did some testing and learned that the syntax above is the only one that works. We can’t base the condition on the value of an ordinary CSS property (instead of a custom property), HTML attribute (using attr()), or any other value. For now, at least, the condition must be based on the value of a custom property (CSS variable).

Exploring what we can do with if()

Judging from that first example, it’s clear that we can use if() for theming (and design systems overall). While we could utilize the light-dark() function for this, what if the themes aren’t strictly light and dark, or what if we want to have more than two themes or light and dark modes for each theme? Well, that’s what if() can be used for.

First, let’s create more themes/more conditions:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Saffron"; /* ...I choose you! */

  body {
    color: if(
      style(--theme: "Shamrock"): hsl(146 50% 3%);
      style(--theme: "Saffron"): hsl(43 74% 3%);
      style(--theme: "Amethyst"): hsl(282 47% 3%)
    );
    background: if(
      style(--theme: "Shamrock"): hsl(146 50% 40%);
      style(--theme: "Saffron"): hsl(43 74% 64%);
      style(--theme: "Amethyst"): hsl(282 47% 56%)
    );
    transition: 300ms;
  }
}

Pretty simple really, but there are a few easy-to-miss things. Firstly, there’s no “else condition” this time, which means that if the theme isn’t Shamrock, Saffron, or Amethyst, the default browser styles are used. Otherwise, the if() function resolves to the value of the first true statement, which is the Saffron theme in this case. Secondly, transitions work right out of the box; in the demo below, I’ve added a user interface for toggling the --theme, and for the transition, literally just transition: 300ms alongside the if() functions:

Note: if theme-swapping is user-controlled, such as selecting an option, you don’t actually need if() at all. You can just use the logic that I’ve used at the beginning of the demo (:root:has(#shamrock:checked) { /* Styles */ }). Amit Sheen has an excellent demonstration over at Smashing Magazine.

To make the code more maintainable though, we can slide the colors into CSS variables as well, then use them in the if() functions, then slide the if() functions themselves into CSS variables:

/* Setup */
:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  /* Base colors */
  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  /* Base colors, but at 3% lightness */
  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  --background: if(
    style(--theme: "Shamrock"): var(--shamrock);
    style(--theme: "Saffron"): var(--saffron);
    style(--theme: "Amethyst"): var(--amethyst)
  );

  --color: if(
    style(--theme: "Shamrock"): var(--shamrock-complementary);
    style(--theme: "Saffron"): var(--saffron-complementary);
    style(--theme: "Amethyst"): var(--amethyst-complementary)
  );

  /* Usage */
  body {
    /* One variable, all ifs! */
    background: var(--background);
    color: var(--color);
    accent-color: var(--color);

    /* Can’t forget this! */
    transition: 300ms;
  }
}

As well as using CSS variables within the if() function, we can also nest other functions. In the example below, I’ve thrown light-dark() in there, which basically inverts the colors for dark mode:

--background: if(
  style(--theme: "Shamrock"): light-dark(var(--shamrock), var(--shamrock-complementary));
  style(--theme: "Saffron"): light-dark(var(--saffron), var(--saffron-complementary));
  style(--theme: "Amethyst"): light-dark(var(--amethyst), var(--amethyst-complementary))
);

if() vs. Container style queries

If you haven’t used container style queries before, they basically check if a container has a certain CSS variable (much like the if() function). Here’s the exact same example/demo but with container style queries instead of the if() function:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  body {
    /* Container has chosen Shamrock! */
    @container style(--theme: "Shamrock") {
      --background: light-dark(var(--shamrock), var(--shamrock-complementary));
      --color: light-dark(var(--shamrock-complementary), var(--shamrock));
    }

    @container style(--theme: "Saffron") {
      --background: light-dark(var(--saffron), var(--saffron-complementary));
      --color: light-dark(var(--saffron-complementary), var(--saffron));
    }

    @container style(--theme: "Amethyst") {
      --background: light-dark(var(--amethyst), var(--amethyst-complementary));
      --color: light-dark(var(--amethyst-complementary), var(--amethyst));
    }

    background: var(--background);
    color: var(--color);
    accent-color: var(--color);
    transition: 300ms;
  }
}

As you can see, where if() facilitates conditional values, container style queries facilitate conditional properties and values. Other than that, it really is just a different syntax.

Additional things you can do with if() (but might not realize)

Check if a CSS variable exists:

/* Hide icons if variable isn’t set */
.icon {
  display: if(
    style(--icon-family): inline-block;
    else: none
  );
}

Create more-complex conditional statements:

h1 {
  font-size: if(
    style(--largerHeadings: true): xxx-large;
    style(--theme: "themeWithLargerHeadings"): xxx-large
  );
}

Check if two CSS variables match:

/* If #s2 has the same background as #s1, add a border */
#s2 {
  border-top: if(
    style(--s2-background: var(--s1-background)): thin solid red
  );
}

if() and calc(): When the math isn’t mathing

This won’t work (maybe someone can help me pinpoint why):

div {
  /* 3/3 = 1 */
  --calc: calc(3/3);
  /* Blue, because if() won’t calculate --calc */
  background: if(style(--calc: 1): red; else: blue);
}

To make if() calculate --calc, we’ll need to register the CSS variable using @property first, like this:

@property --calc {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

Closing thoughts

Although I’m not keen on the syntax and how unreadable it can sometimes look (especially if it’s formatted on one line), I’m mega excited to see how if() evolves. I’d love to be able to use it with ordinary properties (e.g., color: if(style(background: white): black; style(background: black): white);) to avoid having to set CSS variables where possible.

It’d also be awesome if calc() calculations could be calculated on the fly without having to register the variable.

That being said, I’m still super happy with what if() does currently, and can’t wait to build even simpler design systems.


Poking at the CSS if() Function a Little More: Conditional Color Theming originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sunkanmi Fafowora
Tue, 24 Jun 2025 15:17:10 +0000


We’ve known it for a few weeks now, but the CSS if() function officially shipped in Chrome 137 version. It’s really fast development for a feature that the CSSWG resolved to add less than a year ago. We can typically expect this sort of thing — especially one that is unlike anything we currently have in CSS — to develop over a number of years before we can get our dirty hands on it. But here we are!

I’m not here to debate whether if() in CSS should exist, nor do I want to answer whether CSS is a programming language; Chris already did that and definitely explained how exhausting that fun little argument can be.

What I am here to do is poke at if() in these early days of support and explore what we know about it today at a pretty high level to get a feel for its syntax. We’ll poke a little harder at it in another upcoming post where we’ll look at a more heady real-world example.

Yes, it’s already here!

Conditional statements exist everywhere in CSS. From at-rules to the parsing and matching of every statement to the DOM, CSS has always had conditionals. And, as Lea Verou put it, every selector is essentially a conditional! What we haven’t had, however, is a way to style an element against multiple conditions in one line, and then have it return a result conditionally.

The if() function is a more advanced level of conditionals, where you can manipulate and have all your conditional statements assigned to a single property.

.element {
  color: if(style(--theme: dark): oklch(52% 0.18 140); else: oklch(65% 0.05 220));
}

How does if() work?

Well before Chrome implemented the feature, back in 2021 when it was first proposed, the early syntax was like this:

<if()> = if( <container-query>, [<declaration-value>]{1, 2} )

Now we’re looking at this instead:

<if()> = if(
  [<if-statement>: <result>]*;
  <if-statement>: <result> ;?
)

Where…

  • The first <if-statement> represents conditions inside either style()media(), or supports() wrapper functions. This allows us to write multiple if statements, as many as we may desire. Yes, you read that right. As many as we want!
  • The final <if-statement> condition (else) is the default value when all other if statements fail.

That’s the “easy” way to read the syntax. This is what’s in the spec:

<if()> = if( [ <if-branch> ; ]* <if-branch> ;? )
<if-branch> = <if-condition> : <declaration-value>?
<if-condition> = <boolean-expr[ <if-test> ]> | else
<if-test> =
  supports( [ <ident> : <declaration-value> ] | <supports-condition> )
  media( <media-feature> | <media-condition> ) |
  style( <style-query> )

A little wordy, right? So, let’s look at an example to wrap our heads around it. Say we want to change an element’s padding depending on a given active color scheme. We would set an if() statement with a style() function inside, and that would compare a given value with something like a custom variable to output a result. All this talk sounds so complicated, so let’s jump into code:

.element {
  padding: if(style(--theme: dark): 2rem; else: 3rem);
}

The example above sets the padding to 2rem… if the --theme variable is set to dark. If not, it defaults to 3rem. I know, not exactly the sort of thing you might actually use the function for, but it’s merely to illustrate the basic idea.

Make the syntax clean!

One thing I noticed, though, is that things can get convoluted very very fast. Imagine you have three if() statements like this:

:root {
  --height: 12.5rem;
  --width: 4rem;
  --weight: 2rem;
}

.element {
  height: if(
    style(--height: 3rem): 14.5rem; style(--width: 7rem): 10rem; style(--weight: 100rem): 2rem; else: var(--height)
  );
}

We’re only working with three statements and, I’ll be honest, it makes my eyes hurt with complexity. So, I’m anticipating if() style patterns to be developed soon or prettier versions to adopt a formatting style for this.

For example, if I were to break things out to be more readable, I would likely do something like this:

:root {
  --height: 12.5rem;
  --width: 4rem;
  --weight: 2rem;
}

/* This is much cleaner, don't you think? */
.element {
  height: if(
    style(--height: 3rem): 14.5rem; 
    style(--width: 7rem): 10rem; 
    style(--weight: 100rem): 2rem; 
    else: var(--height)
  );
}

Much better, right? Now, you can definitely understand what is going on at a glance. That’s just me, though. Maybe you have different ideas… and if you do, I’d love to see them in the comments.

Here’s a quick demo showing multiple conditionals in CSS for this animated ball to work. The width of the ball changes based on some custom variable values set. Gentle reminder that this is only supported in Chrome 137+ at the time I’m writing this:

The supports() and media() statements

Think of supports() the same way you would use the @supports at-rule. In fact, they work about the same, at least conceptually:

/* formal syntax for @supports */
@supports <supports-condition> {
  <rule-list>
}

/* formal syntax for supports() */
supports( [ <ident> : <declaration-value> ] | <supports-condition> )

The only difference here is that supports() returns a value instead of matching a block of code. But, how does this work in real code?

The <ident>: <declaration-value> you see here is, in this case, the property name: property value e.g. display: flex.

Let’s say you want to check for support for the backdrop-filter property, particularly the blur() function. Typically, you can do this with @supports:

/* Fallback in case the browser doesn't support backdrop-filter */
.card {
  backdrop-filter: unset;
  background-color: oklch(20% 50% 40% / 0.8);
}

@supports (backdrop-filter: blur(10px)) {
  .card {
    backdrop-filter: blur(10px);
    background-color: oklch(20% 50% 40% / 0.8);
  }
}

But, with CSS if(), we can also do this:

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    else: unset
  );
}

Note: Think of unset here as a possible fallback for graceful degradation.

That looks awesome, right? Multiple conditions can be checked as well for supports() and any of the supported functions. For example:

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    supports(backdrop-filter: invert(50%)): invert(50%);
    supports(backdrop-filter: hue-rotate(230deg)): hue-rotate(230deg);;
    else: unset
  );
}

Now, take a look at the @media at-rule. You can compare and check for a bunch of stuff, but I’d like to keep it simple and check for whether or not a screen size is a certain width and apply styles based on that:

h1 {
  font-size: 2rem;
}

@media (min-width: 768px) {
  h1 {
    font-size: 2.5rem;
  }
}

@media (min-width: 1200px) {
  h1 {
    font-size: 3rem;
  }
}

The media() wrapper works almost the same way as its at-rule counterpart. Note its syntax from the spec:

/* formal syntax for @media */
@media <media-query-list> {
  <rule-list>
}

/* formal syntax for media() */
media( <media-feature> | <media-condition> )

Notice how at the end of the day, the formal syntax (<media-query>) is the same as the syntax for the media() function. And instead of returning a block of code in @media, you’d have something like this in the CSS inline if():

h1 {
  font-size: if(
    media(width >= 1200px): 3rem;
    media(width >= 768px): 2.5rem;
    else: 2rem
  );
}

Again, these are early days

As of the time of this writing, only the latest update of Chrome supports if()). I’m guessing other browsers will follow suit once usage and interest come in. I have no idea when that will happen. Until then, I think it’s fun to experiment with this stuff, just as others have been doing:

Experimenting with early features is how we help CSS evolve. If you’re trying things out, consider adding your feedback to the CSSWG and Chromium. The more use cases, the better, and that will certain help make future implementations better as well.

Now that we have a high-level feel for the if()syntax, we’ll poke a little harder at the function in another article where we put it up against a real-world use case. We’ll link that up when it publishes tomorrow.


Lightly Poking at the CSS if() Function in Chrome 137 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 23 Jun 2025 15:47:48 +0000


I like the term “content aware components” like Eric Bailey uses in the Piccalilli article Making content-aware components using CSS :has(), grid, and quantity queries. Does a card have a photo? Yes, do one thing, no, do another. That sort of thing. Eric has some good examples where a UI component has a bunch more “tags” than another, so the layout adjusts to accommodate them better.

Menu items with descriptions and pricing, featuring a Chicken Fajita Omelette and Poached Eggs, along with badge labels indicating their popularity and other attributes.

Thanks to :has(), the idea of “quantity queries” (e.g. style an element if there are, say, 4 or more of an element) have gotten a lot easier. The way I figure it, we could do like:

.card:has(.tag:nth-child(4)) {
  /* apply styles to the card because there are at least 4 tags */
}

Admittedly, the logic gets a little bit more complicated if you want like “3 or less tags”, but that’s exactly what Eric covers in the article, linking up the original ideas and resources, so you’re in luck.

At CSS Day the other week, I listened to Ahmad Shadeed have a wonderful idea on stage. Imagine a layout with one item, you show it. Two items? Side by side? Three items? Maybe two on top and one on the bottom. Four? Two by Two. Five? Well! That’s getting to be a lot of items. We could break them into a carousel. All entirely in CSS. That’s wild. I’d love to build that one day. Maybe I’ll try to stream it one of these days. (Of course, as I write this Kevin Powell put out a video that verges on the idea, and it’s very clever.)

Speaking of Ahmad, he’s got a great article introducing the big improvements that the attr() function has gotten. I absolutely love how we can pluck attribute values out of HTML and actually have them be useful now. I think we’ll realize the power of that more and more. But it occurs to me here that it could factor into this quantity query situation. Say you trust your server to know and output stuff like this. So like:

<div class="card-grid" data-cards="13">
  ...
</div>

You can get your hands on 13, as an actual number not a string, in CSS now like:

attr(data-cards type(<number>), 2)

The number 2 above is a fallback. With a number like that, it makes me think that maybe-just-maybe, we could combine it with the newfangled if() commands in CSS (See Una’s great video) to write the same kind of “quantity query” logic.

Ya know how Sass has @mixin to repeat blocks of CSS? Native CSS doesn’t have that yet, but style queries are pretty close. I snagged this screenshot out of Kevin’s video (in CodePen, naturally):

See how he just flips on a --custom-property then the style query matches when that custom property is set and outputs a block of styles? That feels an awful lot like a mixin to me. Miriam has a nice homebase page for native mixins, which links to some very active discussion on it. At the pace CSS is moving I imagine we’ll have it before we know it. Just @applying a @mixin seems like a more straightforward approach than the style query approach, and more flexible as it’s likely they’ll take parameters, and possibly even slots.

CSS carousels amount to a pretty hefty amount of CSS. Wouldn’t it be cool to make that into a hefty @mixin that takes parameters on what features you want? Ship it.


In other news, gap decorations are starting to be a thing and that’s wonderful. Hoping they’ll move right onto styling a grid area without needing an HTML element there, that would be just as wonderful. I’m still hesitant on making entire columns of article content into grids, but that’s a me-problem, I see others are coming around on the idea.

by: Zell Liew
Mon, 23 Jun 2025 13:41:34 +0000


In a previous article, I showed you how to refactor the Resize Observer API into something way simpler to use:

// From this
const observer = new ResizeObserver(observerFn)

function observerFn (entries) {
  for (let entry of entries) {
    // Do something with each entry
  }
}

const element = document.querySelector('#some-element')
observer.observe(element);
// To this 
const node = document.querySelector('#some-element')
const obs = resizeObserver(node, {
  callback({ entry }) {
    // Do something with each entry
  }
})

Today, we’re going to do the same for MutationObserver and IntersectionObserver.

Refactoring Mutation Observer

MutationObserver has almost the same API as that of ResizeObserver. So we can practically copy-paste the entire chunk of code we wrote for resizeObserver to mutationObserver.

export function mutationObserver(node, options = {}) {
  const observer = new MutationObserver(observerFn)
  const { callback, ...opts } = options
  observer.observe(node, opts)

  function observerFn(entries) {
    for (const entry of entries) {
      // Callback pattern
      if (options.callback) options.callback({ entry, entries, observer })
      // Event listener pattern
      else {
        node.dispatchEvent(
          new CustomEvent('mutate', {
            detail: { entry, entries, observer },
          })
        )
      }
    }
  }
}

You can now use mutationObserver with the callback pattern or event listener pattern.

const node = document.querySelector('.some-element')

// Callback pattern 
const obs = mutationObserver(node, {
  callback ({ entry, entries }) {
    // Do what you want with each entry
  }
})

// Event listener pattern
node.addEventListener('mutate', event => {
  const { entry } = event.detail
  // Do what you want with each entry
})

Much easier!

Disconnecting the observer

Unlike ResizeObserver who has two methods to stop observing elements, MutationObserver only has one, the disconnect method.

export function mutationObserver(node, options = {}) {
  // ... 
  return {
    disconnect() {
      observer.disconnect()
    }
  }
}

But, MutationObserver has a takeRecords method that lets you get unprocessed records before you disconnect. Since we should takeRecords before we disconnect, let’s use it inside disconnect.

To create a complete API, we can return this method as well.

export function mutationObserver(node, options = {}) {
  // ... 
  return {
    // ...
    disconnect() {
      const records = observer.takeRecords()
      observer.disconnect()
      if (records.length > 0) observerFn(records)
    }
  }
}

Now we can disconnect our mutation observer easily with disconnect.

const node = document.querySelector('.some-element')
const obs = mutationObserver(/* ... */)

obs.disconnect()

MutationObserver’s observe options

In case you were wondering, MutationObserver’s observe method can take in 7 options. Each one of them determines what to observe, and they all default to false.

  • subtree: Monitors the entire subtree of nodes
  • childList: Monitors for addition or removal children elements. If subtree is true, this monitors all descendant elements.
  • attributes: Monitors for a change of attributes
  • attributeFilter: Array of specific attributes to monitor
  • attributeOldValue: Whether to record the previous attribute value if it was changed
  • characterData: Monitors for change in character data
  • characterDataOldValue: Whether to record the previous character data value

Refactoring Intersection Observer

The API for IntersectionObserver is similar to other observers. Again, you have to:

  1. Create a new observer: with the new keyword. This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method (depending on which Observer you’re using).

But IntersectionObserver requires you to pass the options in Step 1 (instead of Step 3). So here’s the code to use the IntersectionObserver API.

// Step 1: Create a new observer and pass in relevant options
const options = {/*...*/}
const observer = new IntersectionObserver(observerFn, options)

// Step 2: Do something with the observed changes
function observerFn (entries) {
  for (const entry of entries) {
    // Do something with entry
  }
}

// Step 3: Observe the element
const element = document.querySelector('#some-element')
observer.observe(element)

// Step 4 (optional): Disconnect the observer when we're done using it
observer.disconnect(element)

Since the code is similar, we can also copy-paste the code we wrote for mutationObserver into intersectionObserver. When doing so, we have to remember to pass the options into IntersectionObserver and not the observe method.

export function mutationObserver(node, options = {}) {
  const { callback, ...opts } = options
  const observer = new MutationObserver(observerFn, opts)
  observer.observe(node)

  function observerFn(entries) {
    for (const entry of entries) {
      // Callback pattern
      if (options.callback) options.callback({ entry, entries, observer })
      // Event listener pattern
      else {
        node.dispatchEvent(
          new CustomEvent('intersect', {
            detail: { entry, entries, observer },
          })
        )
      }
    }
  }
}

Now we can use intersectionObserver with the same easy-to-use API:

const node = document.querySelector('.some-element')

// Callback pattern 
const obs = intersectionObserver(node, {
  callback ({ entry, entries }) {
    // Do what you want with each entry
  }
})

// Event listener pattern
node.addEventListener('intersect', event => {
  const { entry } = event.detail
  // Do what you want with each entry
})

Disconnecting the Intersection Observer

IntersectionObserver‘s methods are a union of both resizeObserver and mutationObserver. It has four methods:

  • observe: observe an element
  • unobserve: stops observing one element
  • disconnect: stops observing all elements
  • takeRecords: gets unprocessed records

So, we can combine the methods we’ve written in resizeObserver and mutationObserver for this one:

export function intersectionObserver(node, options = {}) {
  // ...
  return {
    unobserve(node) {
      observer.unobserve(node)
    },

    disconnect() {
      // Take records before disconnecting.
      const records = observer.takeRecords()
      observer.disconnect()
      if (records.length > 0) observerFn(records)
    },
    
    takeRecords() {
      return observer.takeRecords()
    },
  }
}

Now we can stop observing with the unobserve or disconnect method.

const node = document.querySelector('.some-element')
const obs = intersectionObserver(node, /*...*/)

// Disconnect the observer
obs.disconnect()

IntersectionObserver options

In case you were wondering, IntersectionObserver takes in three options:

  • root: The element used to check if observed elements are visible
  • rootMargin: Lets you specify an offset amount from the edges of the root
  • threshold: Determines when to log an observer entry

Here’s an article to help you understand IntersectionObserver options.

Using this in practice via Splendid Labz

Splendid Labz has a utils library that contains resizeObserver, mutationObserver and IntersectionObserver.

You can use them if you don’t want to copy-paste the above snippets into every project.

import { 
  resizeObserver, 
  intersectionObserver, 
  mutationObserver 
} from 'splendidlabz/utils/dom'

const mode = document.querySelector(‘some-element’)

const resizeObs = resizeObserver(node, /* ... */)
const intersectObs = intersectionObserver(node, /* ... */)
const mutateObs = mutationObserver(node, /* ... */)

Aside from the code we’ve written together above (and in the previous article), each observer method in Splendid Labz is capable of letting you observe and stop observing multiple elements at once (except mutationObserver because it doesn’t have a unobserve method)

const items = document.querySelectorAll('.elements')
const obs = resizeObserver(items, {
  callback ({ entry, entries }) {
    /* Do what you want here */
  }
})

// Unobserves two items at once
const subset = [items[0], items[1]]
obs.unobserve(subset)

So it might be just a tad easier to use the functions I’ve already created for you. 😉

Shameless Plug: Splendid Labz contains a ton of useful utilities — for CSS, JavaScript, Astro, and Svelte — that I have created over the last few years.

I’ve parked them all in into Splendid Labz, so I no longer need to scour the internet for useful functions for most of my web projects. If you take a look, you might just enjoy what I’ve complied!

(I’m still making the docs at the time of writing so it can seem relatively empty. Check back every now and then!)

Learning to refactor stuff

If you love the way I explained how to refactor the observer APIs, you may find how I teach JavaScript interesting.

In my JavaScript course, you’ll learn to build 20 real life components. We’ll start off simple, add features, and refactor along the way.

Refactoring is such an important skill to learn — and in here, I make sure you got cement it into your brain.

That’s it! Hope you had fun reading this piece!


A Better API for the Intersection and Mutation Observers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Juan Diego Rodríguez
Fri, 20 Jun 2025 14:04:12 +0000


I have had the opportunity to edit over a lot of the new color entries coming to the CSS-Tricks Almanac. We’ve already published several with more on the way, including a complete guide on color functions:

And I must admit: I didn’t know a lot about color in CSS (I still used rgb(), which apparently isn’t what cool people do anymore), so it has been a fun learning experience. One of the things I noticed while trying to keep up with all this new information was how long the glossary of color goes, especially the “color” concepts. There are “color spaces,” “color models,” “color gamuts,” and basically a “color” something for everything.

They are all somewhat related, and it can get confusing as you dig into using color in CSS, especially the new color functions that have been shipped lately, like contrast-color() and color-mix(). Hence, I wanted to make the glossary I wish I had when I was hearing for the first time about each concept, and that anyone can check whenever they forget what a specific “color” thing is.

As a disclaimer, I am not trying to explain color, or specifically, color reproduction, in this post; that would probably be impossible for a mortal like me. Instead, I want to give you a big enough picture for some technicalities behind color in CSS, such that you feel confident using functions like lab() or oklch() while also understanding what makes them special.

What’s a color?

Let’s slow down first. In order to understand everything in color, we first need to understand the color in everything.

While it’s useful to think about an object being a certain color (watch out for the red car, or cut the white cable!), color isn’t a physical property of objects, or even a tangible thing. Yes, we can characterize light as the main cause of color1, but it isn’t until visible light enters our eyes and is interpreted by our brains that we perceive a color. As said by Elle Stone:

Light waves are out there in the world, but color happens in the interaction between light waves and the eye, brain, and mind.

Even if color isn’t a physical thing, we still want to replicate it as reliably as possible, especially in the digital era. If we take a photo of a beautiful bouquet of lilies (like the one on my desk) and then display it on a screen, we expect to see the same colors in both the image and reality. However, “reality” here is a misleading term since, once again, the reality of color depends on the viewer. To solve this, we need to understand how light wavelengths (something measurable and replicable) create different color responses in viewers (something not so measurable).

Luckily, this task was already carried out 95 years ago by the International Commission on Illumination (CIE, by its French name). I wish I could get into the details of the experiment, but we haven’t gotten into our first color thingie yet. What’s important is that from these measurements, the CIE was able to map all the colors visible to the average human (in the experiment) to light wavelengths and describe them with only three values.

Initially, those three primary values corresponded to the red, green, and blue wavelengths used in the experiment, and they made up the CIERGB Color Space, but researchers noticed that some colors required a negative wavelength2 to represent a visible color. To avoid that, a series of transformations were performed on the original CIERGB and the resulting color space was called CIEXYZ.

This new color space also has three values, X and Z represent the chromaticity of a color, while Y represents its luminance. Since it has three axes, it makes a 3D shape, but if we slice it such that its luminance is the same, we get all the visible colors for a given luminance in a figure you have probably seen before.

xy chromaticitydiagram showing all visible colors

This is called the xy chromaticity diagram and holds all the colors visible by the average human eye (based on the average viewer in the CIE 1931 experiment). Colors inside the shape are considered real, while those outside are deemed imaginary.

Color Spaces

The purpose of the last explanation was to reach the CIEXYZ Color Space concept, but what exactly is a “color space”? And why is the CIEXYZ Color Space so important?

The CIEXYZ Color Space is a mapping from all the colors visible by the average human eye into a 3D coordinate system, so we only need three values to define a color. Then, a color space can be thought of as a general mapping of color, with no need to include every visible color, and it is usually defined through three values as well.

RGB Color Spaces

The most well-known color spaces are the RGB color spaces (note the plural). As you may guess from the name, here we only need the amount of red, green, and blue to describe a color. And to describe an RGB color space, we only need to define its “reddest”, “greenest”, and “bluest” values3. If we use coordinates going from 0 to 1 to define a color in the RGB color space, then:

  • (1, 0, 0) means the reddest color.
  • (0, 1, 0) means the greenest color.
  • (0, 0, 1) means the bluest color.

However, “reddest”, “bluest”, and “greenest” are only arbitrary descriptions of color. What makes a color the “bluest” is up to each person. For example, which of the following colors do you think is the bluest?

Different shades of blue

As you can guess, something like “bluest” is an appalling description. Luckily, we just have to look back at the CIEXYZ color space — it’s pretty useful! Here, we can define what we consider the reddest, greenest, and bluest colors just as coordinates inside the xy chromaticity diagram. That’s all it takes to create an RGB color space, and why there are so many!

sRGV and WideGamutRGB in the xy chromaticity diagram
Credit: Elle Stone

In CSS, the most used color space is the standard RGB (sRGB) color space, which, as you can see in the last image, leaves a lot of colors out. However, in CSS, we can use modern RGB color spaces with a lot more colors through the color() function, such as display-p3prophoto-rgb, and rec2020.

Comparing the shapes of various color spaces, including sRGB, Display P3, A98-RGB, Rec2020, and ProPhoto.
Credit: Chrome Developer Team

Notice how the ProPhoto RGB color space goes out of the visible color. This is okay. Colors outside are clamped; they aren’t new or invisible colors.

In CSS, besides sRGB, we have two more color spaces: the CIELAB color space and the Oklab color space. Luckily, once we understood what the CIEXYZ color space is, then these two should be simpler to understand. Let’s dig into that next.

CIELAB and Oklab Color Spaces

As we saw before, the sRGB color space lacks many of the colors visible by the average human eye. And as modern screens got better at displaying more colors, CSS needed to adopt newer color spaces to fully take advantage of those newer displays. That wasn’t the only problem with sRGB — it also lacks perceptual uniformity, meaning that changes in the color’s chromaticity also change its perceived lightness. Check, for example, this demo by Adam Argyle:

Created in 1976 by the CIE, CIELAB, derived from CIEXYZ, also encompasses all the colors visible by the human eye. It works with three coordinates: L for perceptual lightness, a for the amount of red-green, and b* for the amount of yellow-blue in the color.

A diagram in the shape of a sphere containing a range of colors that go from white to black vertically and green to blue to yellow to red horizontally.
Credit: Linshang Technology

It has a way better perceptual uniformity than sRGB, but it still isn’t completely uniform, especially in gradients involving blue. For example, in the following white-to-blue gradient, CIELAB shifts towards purple.

Comparing blue gradients in Oklahoma and CIELAB. The CIELAB gradients contains shades of purple.
Image Credits to Björn Ottosson

As a final improvement, Björn Ottosson came up with the Oklab color space, which also holds all colors visible by the human eye while keeping a better perceptual uniformity. Oklab also uses the three L*a*b* coordinates. Thanks to all these improvements, it is the color space I try to use the most lately.

Color Models

When I was learning about these concepts, my biggest challenge after understanding color spaces was not getting them confused with color models and color gamuts. These two concepts, while complementary and closely related to color spaces, aren’t the same, so they are a common pitfall when learning about color.

A color model refers to the mathematical description of color through tuples of numbers, usually involving three numbers, but these values don’t give us an exact color until we pair them with a color space. For example, you know that in the RGB color model, we define color through three values: red, green, and blue. However, it isn’t until we match it to an RGB color space (e.g., sRGB with display-p3) that we have a color. In this sense, a color space can have several color models, like sRGB, which uses RGB, HSL, and HWB. At the same time, a color model can be used in several color spaces.

I found plenty of articles and tutorials where “color spaces” and “color models” were used interchangeably. And some places were they had a different definition of color spaces and models than the one provided here. For example, Chrome’s High definition CSS color guide defines CSS’s RGB and HSL as different color spaces, while MDN’s Color Space entry does define RGB and HSL as part of the sRGB color space.

Personally, in CSS, I find it easier to understand the idea of RGB, HSL and HWB as different models to access the sRGB color space.

Color Gamuts

A color gamut is more straightforward to explain. You may have noticed how we have talked about a color space having more colors than another, but it would be more correct to say it has a “wider” gamut, since a color gamut is the range of colors available in a color space. However, a color gamut isn’t only restricted by color space boundaries, but also by physical limitations. For example, an older screen may decrease the color gamut since it isn’t able to display each color available in a given color space. In this case where a color can’t be represented (due to physical limitation or being outside the color space itself), it’s said to be “out of gamut”.

Color Functions

In CSS, the only color space available used to be sRGB. Nowadays, we can work with a lot of modern color spaces through their respective color functions. As a quick reference, each of the color spaces in CSS uses the following functions:

  • sRGB: We can work in sRGB using the ol’ hexadecimal notation, named colors, and the rgb()rgba()hsl()hsla() and hwb() functions.
  • CIELAB: Here we have the lab() for Cartesian coordinates and lch() for polar coordinates.
  • Oklab: Similar to CIELAB, we have oklab() for Cartesian coordinates and oklch() for polar coordinates.
  • More through the color() and color-mix(). Outside these three color spaces, we can use many more using the color() and color-mix() functions. Specifically, we can use the RGB color spaces: rgb-lineardisplay-p3a98-rgbprophoto-rgbrec2020 and the XYZ color space: xyzxyz-d50, or xyz-d65.

TL;DR

  1. Color spaces are a mapping between available colors and a coordinate system. In CSS, we have three main color spaces: sRGB, CIELAB, and Oklab, but many more are accessible through the color() function.
  2. Color models define color with tuples of numbers, but they don’t give us information about the actual color until we pair them with a color space. For example, the RGB model doesn’t mean anything until we assign it an RGB color space.
  3. Most of the time, we want to talk about how many colors a color space holds, so we use the term color gamut for the task. However, a color gamut is also tied to the physical limitations of a camera/display. A color may be out-of-gamut, meaning it can’t be represented in a given color space.
  4. In CSS, we can access all these color spaces through color functions, of which there are many.
  5. The CIEXYZ color space is extremely useful to define other color spaces, describe their gamuts, and convert between them.

References

Footnotes

1 Light is the main cause of color, but color can be created by things other than light. For example, rubbing your closed eyes mechanically stimulates your retina, creating color in what’s called phosphene. ⤴️

2 If negative light also makes you scratch your head, and for more info on how the CIEXYZ color space was created, I highly recommend Douglas A. Kerr The CIE XYZ and xyY Color Spaces paper. ⤴️

3 We also need to define the darkest dark color (“black”) and the lightest light color (“white”). However, for well-behaved color spaces, these two can be abstracted from the reddest, blues, and greenest colors. ⤴️


Color Everything in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sunkanmi Fafowora
Thu, 19 Jun 2025 15:01:18 +0000


If you asked me a few months ago, “What does it take for a website to stand out?” I may have said fancy animations, creative layouts, cool interactions, and maybe just the general aesthetics, without pointing out something in particular. If you ask me now, after working on color for the better part of the year, I can confidently say it’s all color. Among all the aspects that make a design, a good color system will make it as beautiful as possible.

However, color in CSS can be a bit hard to fully understand since there are many ways to set the same color, and sometimes they even look the same, but underneath are completely different technologies. That’s why, in this guide, we will walk through all the ways you can set up colors in CSS and all the color-related properties out there!


Colors are in everything

They are in your phone, in what your eye sees, and on any screen you look at; they essentially capture everything. Design-wise, I see the amazing use of colors on sites listed over at awwwards.com, and I’m always in awe.

Not all color is the same. In fact, similar colors can live in different worlds, known as color spaces. Take for example, sRGB, the color space used on the web for the better part of its existence and hence the most known. While it’s the most used, there are many colors that are simply missing in sRGB that new color spaces like CIELAB and Oklab bring, and they cover a wider range of colors sRGB could only dream of, but don’t let me get ahead of myself.


What’s a color space?

A color space is the way we arrange and represent colors that exist within a device, like printers and monitors. We have different types of color spaces that exist in media (Rec2020, Adobe RGB, etc), but not all of them are covered in CSS. Luckily, the ones we have are sufficient to produce all the awesome and beautiful colors we need. In this guide, we will be diving into the three main color spaces available in CSS: sRGB, CIELAB, and OkLab.


The sRGB Color Space

The sRGB is one of the first color spaces we learn. Inside, there are three color functions, which are essentially notations to define a color: rgb()hsl(), and hwb().

sRGB has been a standard color space for the web since 1996. However, it’s closer to how old computers represented color, rather than how humans understand it, so it had some problems like not being able to capture the full gamut of modern screens. Still, many modern applications and websites use sRGB, so even though it is the “old way” of doing things, it is still widely accepted and used today.

The rgb() function

Diagram of the rgb function showing arguments for red, green, blue, and alpha.

rgb() uses three values, rg, and b which specifies the redness, greenness, and blueness of the color you want.

All three values are non-negative, and they go from 0 to 255.

.element {
  color: rgb(245 123 151);
}

It also has an optional value (the alpha value) preceded by a forward slash. It determines the level of opacity for the color, which goes from 0 (or 0%) for a completely transparent color, to 1 (or 100%) for a fully opaque one.

.element {
  color: rgb(245 123 151 / 20%);
}

There are two ways you can write inside rgb(). Either using the legacy syntax that separates the three values with commas or the modern syntax that separates each with spaces.

You want to combine the two syntax formats, yes? That’s a no-no. It won’t even work.

/* This would not work */
.element {
  color: rgb(225, 245, 200 / 0.5);
}

/* Neither will this */
.element {
  color: rgb(225 245 200, 0.5);
}

/* Or this */
.element {
  color: rgb(225, 245 200 / 0.5);
}

But, following one consistent format will do the trick, so do that instead. Either you’re so used to the old syntax and it’s hard for you to move on, continue to use the legacy syntax, or you’re one who’s willing to try and stick to something new, use the modern syntax.

/* Valid (Modern syntax)  */
.element {
  color: rgb(245 245 255 / 0.5);
}

/* Valid (Legacy syntax)  */
.element {
  color: rgb(245, 245, 255, 0.5);
}

The rgba() function

rgba() is essentially the same as rgb() with an extra alpha value used for transparency.

In terms of syntax, the rgba() function can be written in two ways:

  • Comma-separated and without percentages
  • Space-separated, with the alpha value written after a forward slash (/)
.element {
  color: rgba(100, 50, 0, 0.5);
}

.element {
  color: rgba(100 50 0 / 0.5);
}

So, what’s the difference between rgba() and rgb()?

Breaking news! There is no difference. Initially, only rgba() could set the alpha value for opacity, but in recent years, rgb() now supports transparency using the forward slash (/) before the alpha value.

rgb() also supports legacy syntax (commas) and modern syntax (spaces), so there’s practically no reason to use rgba() anymore; it’s even noted as a CSS mistake by folks at W3C.

In a nutshell, rgb() and rgba() are the same, so just use rgb().

/* This works */
.element-1 {
    color: rgba(250 30 45 / 0.8);
}

/* And this works too, so why not just use this? */
.element-2 {
    color: rgb(250 30 45 / 0.8);
}

The hexadecimal notation

Diagram of the hex color notation showing how #3dFa4C corresponds to the #RRGGBB color channels.

The hexadecimal CSS color code is a 3, 4, 6, or 8 (being the maximum) digit code for colors in sRGB. It’s basically a shorter way of writing rgb(). The hexadecimal color (or hex color) begins with a hash token (#) and then a hexadecimal number, which means it goes from 0 to 9 and then skips to letters a to f (a being 10, b being 11, and so on, up to f for 15).

In the hexadecimal color system, the 6-digit style is done in pairs. Each pair represents red (RR), blue (BB), and green (GG).

Each value in the pair can go from 00 to FF, which it’s equivalent to 255 in rgb().

Notice how I used caps for the letters (F) and not lowercase letters like I did previously? Well, that’s because hexadecimals are not case-sensitive in CSS, so you don’t have to worry about uppercase or lowercase letters when dealing with hexadecimal colors.

  • 3-digit hexadecimal. The 3-digit hexadecimal system is a shorter way of writing the 6-digit hexadecimal system, where each value represents the color’s redness, greenness, and blueness, respectively
.element {
  color: #abc;
}

In reality, each value in the 3-digit system is duplicated and then translated to a visible color

.element {
  color: #abc; /* Equals #AABBCC  */
}

BUT, this severely limits the colors you can set. What if I want to target the color 213 in the red space, or how would I get a blue of value 103? It’s impossible. That’s why you can only get a total number of 4,096 colors here as opposed to the 17 million in the 6-digit notation. Still, if you want a fast way of getting a certain color in hexadecimal without having to worry about the millions of other colors, use the 3-digit notation.

  • 4-digit hexadecimal. This is similar to the 3-digit hexadecimal notation except it includes the optional alpha value for opacity. It’s a shorter way of writing the 8-digit hexadecimal which also means that all values here are repeated once during color translation.
.element {
  color: #ABCD2;
}

For the alpha value, 0 represents 00 (a fully transparent color) and F represents FF (a fully opaque color).

.element {
  color: #abcd; /* Same as #AABBCCDD */
}
  • 6-digit hexadecimal. The 6-digit hexadecimal system just specifies a hexadecimal color’s redness, blueness, and greenness without its alpha value for color opacity.
.element {
  color: #abcdef;
}
  • 8-digit hexadecimal. This 8-digit hexadecimal system specifies hexadecimal color’s redness, blueness, greenness, and its alpha value for color opacity. Basically, it is complete for color control in sRGB.
.element {
  color: #faded101;
}

The hsl() function

Diagram of the hsl function showing arguments for hue, saturation, lightness, and alpha.

Both hsl() and rgb() live in the sRGB space, but they access colors differently. And while the consensus is that hsl() is far more intuitive than rgb(), it all boils down to your preference.

hsl() takes three values: hs, and l, which set its hue, saturation, and lightness, respectively.

  • The hue sets the base color and represents a direction in the color wheel, so it’s written in angles from 0deg to 360deg.
  • The saturation sets how much of the base color is present and goes from 0 (or 0%) to 100 (or 100%).
  • The lightness represents how close to white or black the color gets.

One cool thing: the hue angle goes from (0deg360deg), but we might as well use negative angles or angles above 360deg, and they will circle back to the right hue. Especially useful for infinite color animation. Pretty neat, right?

Plus, you can easily get a complementary color from the opposite angle (i.e., adding 180deg to the current hue) on the color wheel.

/* Current color */
.element {
  color: hsl(120deg 40 60 / 0.8);
}

/* Complementary color */
.element {
  color: hsl(300deg 40 60 / 0.8);
}

You want to combine the two syntax formats like in rgb(), yes? That’s also a no-no. It won’t work.

/* This would not work */
.element {
  color: hsl(130deg, 50, 20 / 0.5);
}

/* Neither will this */
.element {
  color: hsl(130deg 50 20, 0.5);
}

/* Or this */
.element {
  color: hsl(130deg 50, 20 / 0.5);
}

Instead, stick to one of the syntaxes, like in rgb():

/* Valid (Modern syntax)  */ 
.element {
  color: hsl(130deg 50 20 / 0.5);
}

/* Valid (Modern syntax)  */ 
.element {
  color: hsl(130deg, 50, 20, 0.5);
}

The hsla() function

hsla() is essentially the same with hsl(). It uses three values to represent its color’s hue (h), saturation (s), and lightness (l), and yes (again), an alpha value for transparency (a). We can write hsla() in two different ways:

  • Comma separated
  • Space separated, with the alpha value written after a forward slash (/)
.element {
  color: hsla(120deg, 100%, 50%, 0.5);
}

.element {
  color: hsla(120deg 100% 50% / 0.5);
}

So, what’s the difference between hsla() and hsl()?

Breaking news (again)! They’re the same. hsl() and hsla() both:

  • Support legacy and modern syntax
  • Have the power to increase or reduce color opacity

So, why does hsla() still exist? Well, apart from being one of the mistakes of CSS, many applications on the web still use hsla() since there wasn’t a way to set opacity with hsl() when it was first conceived.

My advice: just use hsl(). It’s the same as hsla() but less to write.

/* This works */
.element-1 {
    color: hsla(120deg 80 90 / 0.8);
}

/* And this works too, so why not just use this? */
.element-2 {
    color: hsl(120deg 80 90 / 0.8);
}

The hwb() function

Diagram of the hwb color function showing arguments for hue, whiteness, blackness, and alpha.

hwb() also uses hue for its first value, but instead takes two values for whiteness and blackness to determine how your colors will come out (and yes, it also does have an optional transparency value, a, just like rgb() and hsl()).

.element {
  color: hwb(80deg 20 50 / 0.5);
}
  • The first value h is the same as the hue angle in hsl(), which represents the color position in the color wheel from 0 (or 0deg) to 360 (or 360deg).
  • The second value, w, represents the whiteness in the color. It ranges from 0/0% (no white) to 100/100% (full white if b is 0).
  • The third value, b, represents the blackness in the color. It ranges from 0/0% (no black) to 100/100% (fully black if w is 0).
  • The final (optional) value is the alpha value, a, for the color’s opacity, preceded by a forward slash The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Although this color function is barely used, it’s completely valid to use, so it’s up to personal preference.

Named colors

CSS named colors are hardcoded keywords representing predefined colors in sRGB. You are probably used to the basic: whiteblueblackred, but there are a lot more, totaling 147 in all, that are defined in the Color Modules Level 4 specification.

Named colors are often discouraged because their names do not always match what color you would expect.


The CIELAB Color Space

The CIELAB color space is a relatively new color space on the web that represents a wider color gamut, closer to what the human eye can see, so it holds a lot more color than the sRGB space.

The lab() function

Diagram of the lab color function showing arguments for lightness, greenness to redness, blueness to yellowness, and the alpha transparency.

For this color function, we have three axes in a space-separated list to determine how the color is set.

.element {
    color: lab(50 20 20 / 0.9);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0/(or 0%) (black) to 100 (or 100%) (white).
  • The second value a represents the degree of greenness to redness of the color. Its range being from -125/0% (green) to125 (or 100%) (red).
  • The third value b represents the degree of blueness to yellowness of the color. Its range is also from -125 (or 0%) (blue) to 125 (or 100%) (red).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

This is useful when you’re trying to obtain new colors and provide support for screens that do support them. Actually, most screens and all major browsers now support lab(), so you should be good.

The CSS lab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, at practice, those are their limits according to the spec.

The lch() function

Diagram of the lch color function chowing arguments for whiteness to blackness, chroma, hue, and the alpha transparency.

The CSS lch() color function is said to be better and more intuitive than lab().

.element {
    color: lch(10 30 300deg);
}

They both use the same color space, but instead of having l, a, and b, lch uses lightness, chroma, and hue.

  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 100 (or 100%) (white).
  • The second value c represents the color’s chroma (which is like saturation). Its range being from 0 (or 100%) to 150 or (or 100%).
  • The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS lch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, in practice, the chroma values above are the limits according to the spec.


The OkLab Color Space

Björn Ottosson created this color space as an “OK” and even better version of the lab color space. It was created to solve the limitations of CIELAB and CIELAB color space like image processing in lab(), such as making an image grayscale, and perceptual uniformity. The two color functions in CSS that correspond to this color space are oklab() and oklch().

Perceptual uniformity occurs when there’s a smooth change in the direction of a gradient color from one point to another. If you notice stark contrasts like the example below for rgb() when transitioning from one hue to another, that is referred to as a non-uniform perceptual colormap.

Notice how the change from one color to another is the same in oklab() without any stark contrasts as opposed to rgb()? Yeah, OKLab color space solves the stark contrasts present and gives you access to many more colors not present in sRGB.

OKlab actually provides a better saturation of colors while still maintaining the hue and lightness present in colors in CIELAB (and even a smoother tranisition between colors!).

The oklab() function

Diagram of the oklahoma color function syntax showing arguments for whiteness to blackness, green-ness to redness, blueness to yellowness, and the alpha transparency.

The oklab() color function, just like lab(), generates colors according to their lightness, red/green axis, blue/yellow axis, and an alpha value for color opacity. Also, the values for oklab() are different from that of lab() so please watch out for that.

.element {
  color: oklab(30% 20% 10% / 0.9);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 0.1 (or 100%) (white).
  • The second value a represents the degree of greenness to redness of the color. Its range being from -0.4 (or -100%) (green) to 0.4 (or 100%) (red).
  • The third value b represents the degree of blueness to yellowness of the color. The value’s range is also from -0.4 (or 0%) (blue) to 0.4 (or -100%) (red).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Again, this solves one of the issues in lab which is perceptual uniformity so if you’re looking to use a better alternative to lab, use oklab().

The CSS oklab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, theoretically, those are the limits for the values according to the spec.

The oklch() function

Diagram of the oklch function showing the arguments for whiteness, chroma, hue, and alpha.

The oklch() color function, just like lch(), generates colors according to their lightness, chroma, hue, and an alpha value for color opacity. The main difference here is that it solves the issues present in lab() and lch().

.element {
  color: oklch(40% 20% 100deg / 0.7);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0.0 (or 0%) (black) to 1.0 (or 100%) (white).
  • The second value c represents the color’s chroma. Its range being from 0 (or 0%) to 0.4 (or 100%) (it theoretically doesn’t exceed 0.5).
  • The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS oklch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, theoretically, the chroma values above are the limits according to the spec.


The color() function

Example of the color function syntax showing the arguments for the colorspace, c1, c2, and c3, and the alpha transparency channel.

The color() function allows access to colors in nine different color spaces, as opposed to the previous color functions mentioned, which only allow access to one.

To use this function, you must simply be aware of these 6 parameters:

  • The first value specifies the color space you want to access colors from. They can either be srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, or xyz-d65
  • The next three values (c1, c2, and c3) specifies the coordinates in the color space for the color ranging from 0.0 – 1.0.
  • The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The color-mix() function

Example of the color mix function syntax labelling the in keyword, the the color space, and the inputs followed by the alpha transparency.

The color-mix() function mixes two colors of any type in a given color space. Basically, you can create an endless number of colors with this method and explore more options than you normally would with any other color function. A pretty powerful CSS function, I would say.

.element{
  color-mix(in oklab, hsl(40 20 60) 80%, red 20%);
}

You’re basically mixing two colors of any type in a color space. Do take note, the accepted color spaces here are different from the color spaces accepted in the color() function.

To use this function, you must be aware of these three values:

  • The first value in colorspace specifies the interpolation method used to mix the colors, and these can be any of these 15 color spaces: srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, lab, oklab, xyz, xyz-d50, xyz-d65, hsl, hwb, lch, and oklch.
  • The second and third values specifies an accepted color value and a percentage from 0% to 100%.

The Relative Color Syntax

Example of the relative color syntax labelling the color function, the mandatory keyword, the origin color, the color channel inputs, and the alpha value.

Here’s how it works. We have:

.element{
  color-function(from origin-color c1 c2 c3 / alpha)
}
  • The first value from is a mandatory keyword you must set to extract the color values from origin-color.
  • The second value, origin-color, represents a color function or value or even another relative color that you want to get color from.
  • The next three values, c1, c2, and c3 represent the current color function’s color channels and they correspond with the color function’s valid color values.
  • The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%) which either set from the origin-color or set manually,

Let’s take an example, say, converting a color from rgb() to lab():

.element {
  color: lab(from rgb(255 210 01 / 0.5) l a b / a);
}

All the values above will be translated to the corresponding colors in rgb(). Now, let’s take a look at another example where we convert a color from rgb() to oklch():

.element {
    color: oklch(from rgb(255 210 01 / 0.5) 50% 20% h / a);
}

Although, the l and c values were changed, the h and a would be taken from the original color, which in this case is a light yellowish color in rgb().

You can even be wacky and use math functions:

All CSS color functions support the relative color syntax. The relative color syntax, simply put, is a way to access other colors in another color function or value, then translating it to the values of the current color function. It goes “from <color>” to another.

.element {
  color: oklch(from rgb(255 210 01 / 0.5) calc(50% + var(--a)) calc(20% + var(--b)) h / a);
}

The relative color syntax is, however, different than the color() function in that you have to include the color space name and then fully write out the channels, like this:

.element {
  color: color(from origin-color colorspace c1 c2 c3 / alpha);
}

Remember, the color-mix() function is not a part of this. You can have relative color functions inside the color functions you want to mix, yes, but the relative color syntax is not available in color-mix() directly.


Color gradients

Gradient transitioning from a rich magenta color to bright orange at a 145 degree angle.

CSS is totally capable of transitioning from one color to another. See the “CSS Gradients Guide” for a full run-down, including of the different types of gradients with examples.


Properties that support color values

There are a lot of properties that support the use of color. Just so you know, this list does not contain deprecated properties.

accent-color

This CSS property sets the accent color for UI controls like checkboxes and radio buttons, and any other form element

progress {
  accent-color: lightgreen;
}

Accent colors are a way to style unique elements in respect to the chosen color scheme.

background-color

Applies solid colors as background on an element.

.element {
  background-color: #ff7a18;
}
border-color

Shorthand for setting the color of all four borders.

/* Sets all border colors */
.element {
    border-color: lch(50 50 20);
}

/* Sets top, right, bottom, left border colors */
.element {
  border-color: black green red blue;
}
box-shadow

Adds shadows to element for creating the illusion of depth. The property accepts a number of arguments, one of which sets the shadow color.

.element {
  box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
}
caret-color

Specifies the color of the text input cursor (caret).

.element {
    caret-color: lch(30 40 40);
}
color

Sets the foreground color of text and text decorations.

.element {
  color: lch(80 10 20);
}
color-rule-color

Sets the color of a line between columns in a multi-column layout. This property can’t act alone, so you need to set the columns and column-rule-style property first before using this.

.element {
  column: 3;
  column-rule-style: solid;
  column-rule-color: lch(20 40 40); /* highlight */
}
fill

Sets the color of the SVG shape

.element {
  fill: lch(40 20 10);
}
flood-color

Specifies the flood color to use for <feFlood> and <feDropShadow> elements inside the <filter> element for <svg>. This should not be confused with the flood-color CSS attribute, as this is a CSS property and that’s an HTML attribute (even though they basically do the same thing). If this property is specified, it overrides the CSS flood-color attribute

.element {
  flood-color: lch(20 40 40);
}
lighting-color

Specifies the color of the lighting source to use for <feDiffuseLighting> and <feSpecularLighting> elements inside the <filter> element for <svg>.

.element {
  lighting-color: lch(40 10 20);
}
outline-color

Sets the color of an element’s outline.

.element {
  outline-color: lch(20 40 40);
}
stop-color

Specifies the color of gradient stops for the <stop> tags for <svg>.

.element {
  stop-color: lch(20 40 40);
}
stroke

Defines the color of the outline of <svg>.

.element {
  stroke: lch(20 40 40);
}
text-decoration-color

Sets the color of text decoration lines like underlines.

.element {
  text-decoration-color: lch(20 40 40);
}
text-emphasis-color

Specifies the color of emphasis marks on text.

.element {
  text-emphasis-color: lch(70 20 40);
}
text-shadow

Applies shadow effects to text, including color.

.element {
  text-shadow: 1px 1px 1px lch(50 10 30);
}

Almanac references

Color functions
Color properties


CSS Color Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sacha Greif
Tue, 17 Jun 2025 13:13:15 +0000


How do you keep up with new CSS features?

Let’s say for example that, hypothetically speaking, you run a popular web development survey focused on CSS, and need to figure out what to include in this year’s edition. (In a total coincidence the aforementioned State of CSS survey for this year is actually open right now — go take it to see what’s new in CSS!)

You might think you can just type “new CSS features 2025” in Google and be done with it. But while this does give us a few promising leads, it also unearths a lot of cookie-cutter content that proclaims CSS Grid as the “next big thing”, despite the fact it’s been well-supported for over eight years now. 

We need a better approach. 

I’ll focus on CSS in this article, but all the resources linked here cover all web platform features, including JavaScript and HTML.

Web.dev

New to the web platform in April

A good general starting point is Google’s web.dev blog, and more specifically Rachel Andrew‘s monthly web platform recaps. Here’s a small sample of those:

CSS-Tricks (and others)

I’d be remiss to not mention that CSS-Tricks is also a great source for up-to-date CSS knowledge, including an ever-growing almanac of CSS features. But you probably already know that since you’re reading this.

And let’s not discount other fine publications that cover CSS. Here are just a few:

Web Platform Features Explorer

Web Platform Features Explorer

If you need something a bit more structured to help you figure out what’s new, Web Platform Features Explorer is great way to look up features based on their Baseline status.

Web Platform Status

Web Platform Status

A similar tool is the Web Platform Status dashboard. This one features more fine-grained filtering tools, letting you narrow down features by Baseline year or even show features mentioned as Top CSS Interop in the latest State of CSS survey!

Another very cool feature is the ability to view a feature’s adoption rate, as measured in terms of percentage of Chrome page views where that feature was used, such as here for the popover HTML attribute:

Showing browser usage statistics of a feature in Chrome.

An important caveat: since sites like Facebook and Google account for a very large percentage of all measured page views, this metric can become skewed once one of these platforms adopts a new feature.

The Web Platform Status’s stats section also features the “chart of shame” (according to Lea Verou), which highlights how certain browsers might be slightly lagging behind their peers in terms of new feature adoption.

Chrome Platform Status

Chrome Platform Status webpage showing usage statistics for CSS subgrid.

That same adoption data can also be found on the Chrome Platform Status dashboard, which gives you even more details, such as usage among top sites, as well as sample URLs of sites that are using a feature. 

Polypane Experimental Chromium Features Dashboard

Polypane webpage showing a search bar for Experimental Chromium Web Platform Features.

Polypane is a great developer-focused browser that provides a ton of useful tools like contrast checkers, multi-viewport views, and more. 

They also provide an experimental Chromium features explorer that breaks new features down by Chromium version, for those of you who want to be at the absolute top of the cutting edge. 

Kevin Powell’s YouTube Channel

A still frame of Kevin Powell in a video on YouTube.

As YouTube’s de facto CSS expert, Kevin Powell often puts up great video recaps of new features. You should definitely be following him, but statistically speaking you probably already are! It’s also worth mentioning that Kevin runs a site that publishes weekly HTML and CSS tips.

CSS Working Group

Of course, you can always also go straight to the source and look at what the CSS Working Group itself has been working on! They have a mailing list you can subscribe to keep tabs on things straight from your inbox, as well as an RSS feed.

Browser release notes

Most browsers publish a set of release notes any time a new version ships. For the most part, you can get a good pulse on when new CSS features are released by following the three big names in browsers:

ChatGPT

Another way to catch up with CSS is to just ask ChatGPT! This sample prompt worked well enough for me:

What are the latest CSS features that have either become supported by browsers in the past year, or will soon become supported? 

Other resources

If you really want to get in the weeds, Igalia’s BCD Watch displays changes to MDN’s browser-compat-data repo, which itself tracks which features are supported in which browsers. 

Also, the latest editions of the HTTP Archive Web Almanac do not seem to include a CSS section specifically, but past editions did feature one, which was a great way to catch up with CSS once a year. 

There’s also caniuse has a news section which does not seem to be frequently updated at the moment, but could potentially become a great resource for up-to-date new feature info in the future.

The IntentToShip bot (available on Bluesky, Mastodon, Twitter) posts whenever a browser vendor ships or changes a feature. You can’t get more cutting-edge than that!

And lastly, there’s a ton of folks on social media who are frequently discussing new CSS features and sharing their own thoughts and experiments with them. If you’re on Bluesky, there’s a starter pack of CSS-Tricks authors that’s a good spot to find a deep community of people.

Wrapping up

Of course, another great way to make sure no new features are slipping through the cracks is to take the State of CSS survey once a year. I use all the resources mentioned above to try and make sure each survey includes every new important feature. What’s more, you can bookmark features by adding them to your “reading list” as you take the survey to get a nice recap at the end.

So go take this year’s State of CSS survey and then let me know on Bluesky how many new features you learned about!


How to Keep Up With New CSS Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Important Information

Terms of Use Privacy Policy Guidelines We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.