DEV Community

Cover image for That One Time a CSS Variable Took Down Production
Abhinav Shinoy
Abhinav Shinoy

Posted on

18 4 3 3 4

That One Time a CSS Variable Took Down Production

Let me tell you a story. This is when I was working at a startup.
It starts like most do: on a Friday, with good intentions and a small, seemingly harmless CSS tweak...

🧪 The Setup

We had just rolled out a long-awaited design system refresh. Among the many visual improvements: cleaner spacing, a refined color palette, and (drumroll) CSS custom properties — aka, variables.

It looked something like this:

:root {
  --primary-color: #1a73e8;
  --button-padding: 0.75rem 1.5rem;
}
Enter fullscreen mode Exit fullscreen mode

We used these everywhere. Clean, reusable, and easy to override — what's not to love?

Except… one tiny variable was waiting to cause chaos.


⚙️ The Change

A designer requested a quick adjustment:

"Can we tone down the primary button color just a bit?"

Easy. I updated --primary-color to a new hex value in the design token file:

--primary-color: var(--color-primary-base);
Enter fullscreen mode Exit fullscreen mode

--color-primary-base was defined in a separate file, but I was confident everything was wired up. After all, we had a theme system, layered variables, and fallbacks. What could go wrong?

I pushed the change. CI passed. Code deployed. Weekend mode: activated.


🔥 The Crash

Within minutes, the support channel lit up.

“Buttons are invisible.”
“I can’t click anything.”
“Login page is blank.”

Wait, what?

I opened the site. The buttons were gone. Not just colorless — completely invisible. No background. No border. Just floating text. In some places, not even that.


🕵️‍♂️ The Investigation

After some rapid-fire inspection in DevTools, I saw this:

background-color: var(--primary-color);
Enter fullscreen mode Exit fullscreen mode

...but --primary-color was resolving to... nothing. undefined. Empty. No fallback.

Why?

Because I mistakenly assumed --color-primary-base was always defined.
But in production, due to conditional theming logic and a bundler optimization, it was missing from the root scope. 😬

As a result:

--primary-color: var(--color-primary-base);
Enter fullscreen mode Exit fullscreen mode

...evaluated to --primary-color: (i.e., empty), and so background-color inherited nothing.

No fallback. No warnings. Silent failure. And boom — the UI melted.


💡 The Fix

I hotfixed it by adding a default fallback:

--primary-color: var(--color-primary-base, #1a73e8);
Enter fullscreen mode Exit fullscreen mode

Then updated the build step to ensure all token layers were correctly bundled and available in the final CSS.

Crisis averted. Lessons painfully learned.


🧠 What I Learned

1. CSS Variables Don’t Fail Loudly

Unlike JavaScript, CSS doesn’t throw. If a variable is missing, it silently fails and can break styles in subtle (or dramatic) ways.

2. Always Use Fallbacks

color: var(--text-color, black);
Enter fullscreen mode Exit fullscreen mode

Always assume a variable might be missing — especially in dynamic themes, design systems, or multi-team environments.

3. Linting Can Save You

A linter or PostCSS plugin that validates custom properties and detects missing ones would’ve caught this. Use tools to catch what your eyes miss.

4. Test Like Production

Don’t assume your local/staging setup reflects real-world conditions. Tree-shaking or conditional loading might alter what CSS is available in production.


🎯 Bonus Tip: Use CSS Custom Properties Intentionally

They’re powerful — great for theming, dynamic UI changes, and reducing repetition.
But they’re also fragile when layered, abstracted, or conditionally injected.

Use them wisely. And don’t forget the humble fallback value.


🫣 Final Thought

This little bug cost us 45 minutes of downtime, a Friday evening fire drill, and a new entry in the postmortem doc titled “CSS Can Break Production Too.”

The moral of the story? Even the smallest change in a CSS variable can ripple into disaster.

So next time you write:

--something: var(--something-else);
Enter fullscreen mode Exit fullscreen mode

Ask yourself: What if that’s undefined?
And maybe — just maybe — you’ll avoid your own invisible button catastrophe.


Heroku

The AI PaaS for deploying, managing, and scaling apps.

Heroku tackles the toil — patching and upgrading, 24/7 ops and security, build systems, failovers, and more. Stay focused on building great data-driven applications.

Get Started

Top comments (12)

Collapse
 
dotallio profile image
Dotallio

Invisible buttons and Friday fire drills are such a nightmare - I've been bitten by silent CSS variable fails too.
Did you end up adding any custom linter rules or automated checks after that?

Collapse
 
abhinavshinoy90 profile image
Abhinav Shinoy

Glad (and sorry?) to hear I’m not alone in the CSS variable pain club. 😅

Yep. We added a stylelint rule to catch CSS variables without fallbacks.
Something like:

"declaration-property-value-disallowed-list": {
  "/.*/": ["var\\(--[a-zA-Z0-9-]+\\)$"]
}
Enter fullscreen mode Exit fullscreen mode

This flags any usage of var(--some-var) without a fallback (i.e., missing the , fallback part).
Simple but super effective — saved us a few times since!

Collapse
 
javascriptwizzard profile image
Pradeep

Beautifully written!

Collapse
 
abhinavshinoy90 profile image
Abhinav Shinoy
Collapse
 
jaybear profile image
Jens

"CSS variable pain club. 😅" ... congrats, you've earned a very new badge today!
Bad-CSS-Badge-2025

Collapse
 
macnick profile image
Nick Haralampopoulos

The moral of the story is: never deploy on Friday.

Collapse
 
michael_liang_0208 profile image
Michael Liang

Great post!

Collapse
 
abhinavshinoy90 profile image
Abhinav Shinoy
Collapse
 
kc900201 profile image
KC

I wonder if the bug can detected early if specific linters for CSS such as style lint is used for checking.

Collapse
 
abhinavshinoy90 profile image
Abhinav Shinoy

Yes @kc900201 , Thats what we did afterwards. We added a stylelint rule to catch CSS variables without fallbacks:

"declaration-property-value-disallowed-list": {
  "/.*/": ["var\\(--[a-zA-Z0-9-]+\\)$"]
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nathan_tarbert profile image
Nathan Tarbert

been there, man. the number of times i’ve watched a 'harmless' tweak nuke something is too high. respect for sharing the pain - helps me remember to double check for those fallbacks.

Collapse
 
abhinavshinoy90 profile image
Abhinav Shinoy

Thanks @nathan_tarbert !

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay