Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Custom Property Patterns: Fallback Chains and the Invalid-Value Trap

C
Carlo Fontanos
· 2 min read

Everyone uses custom properties as constants (--brand: #4f46e5 and done). Their actual power is that they cascade and inherit like any CSS value - which enables patterns constants can't do, and one failure mode constants don't have.

Pattern 1: component APIs via fallbacks

.badge {
    background: var(--badge-bg, #e2e8f0);
    color: var(--badge-text, #334155);
    padding-inline: var(--badge-pad, 8px);
}

/* Consumers configure without new classes or !important: */
.card-header { --badge-bg: #fef3c7; --badge-text: #92400e; }

The badge publishes its knobs; any ancestor sets them; unset knobs fall back to defaults. This is how you build components that are customizable without exposing their internal selectors - the CSS equivalent of function parameters. Fallbacks even chain: var(--badge-bg, var(--surface, white)) tries the specific, then the general, then the literal.

Pattern 2: scoped theming beats class explosions

:root      { --surface: #fff;    --ink: #0f172a; }
[data-theme="dark"] { --surface: #0f172a; --ink: #e2e8f0; }

/* Components never mention themes - they just drink from the variables */
.panel { background: var(--surface); color: var(--ink); }

Because inheritance flows down the tree, data-theme works on any subtree, not just root - a permanently-dark hero section inside a light page is one attribute. (The full production version of this - OS preference, toggle, no-flash SSR - is its own tutorial.)

The trap: invalid at computed-value time

Here's the one that costs people an afternoon. What happens with this?

.btn {
    background: red;
    background: var(--accent);   /* --accent is undefined... or holds garbage */
}

Intuition says "falls back to red like any invalid declaration". Wrong. The parser can't validate var() substitutions at parse time, so the declaration is kept - and when substitution later produces nonsense, the property becomes unset (inherited or initial), not the previous declaration. Your button doesn't stay red; it goes transparent. The classic cascade fallback pattern silently doesn't work with variables.

Defenses: always provide var() fallbacks for possibly-unset properties; and for properties that animate or must stay type-safe, register them with @property, which gives real types plus an enforced initial-value - garbage assignments then fall back to the registered initial instead of unsetting.

Small pattern grab-bag

  • currentColor is the original custom property: border-color: currentColor and SVG fill: currentColor track text color through every theme automatically.
  • Space toggles are a smell: if you're doing the --on: ; trick for boolean logic, you probably want :has() or a data-attribute.
  • DevTools tip: the computed panel shows resolved variable values per element - hunting "where did this --spacing come from" beats grepping.

Variables look trivial and mostly are - right up until the unset trap. Learn that once and the rest is pure leverage.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading