I spent years trying to make CSS states predictable

The author discusses the challenges of managing overlapping CSS states and introduces Tasty, a tool that makes component styling deterministic through declarative maps.
Why I spent years trying to make CSS states predictable
Have you ever changed the order of two CSS rules and broken a component without changing the logic?
.btn:hover { background: dodgerblue; }
.btn[disabled] { background: gray; }
Both selectors have specificity (0, 1, 1). When a button is both hovered and disabled, the browser falls back to source order. If the :hover rule comes last, the disabled button turns blue. If the [disabled] rule comes last, it stays gray.
That sounds small, but it points to a bigger problem: component state in CSS often works by overlap. As long as a component has only one or two states, that overlap feels manageable. Once you add :hover, :active, disabled, dark mode, breakpoints, data attributes, container queries, and overrides, it stops feeling manageable very quickly. You are no longer just writing styles. You are maintaining a resolution system in your head.
And that showed up not only as accidental conflicts, but as a growing difficulty in customizing existing components safely as real requirements piled up. That was the problem I kept running into while building component systems. The hardest part was not writing the first version of a component. It was extending it later without reopening the entire state-resolution problem.
At some point I stopped asking, “How do I write this selector?” and started asking a better question:
What if component state could be expressed declaratively, while the compiler handled the selector logic needed to make it deterministic?
That question eventually became Tasty.
The idea in one minute
Instead of writing selectors that compete through cascade and specificity, I wanted to describe a property’s possible states as a map:
import { tasty } from '@tenphi/tasty';
const Button = tasty({
as: 'button',
styles: {
fill: {
'': '#primary',
':hover': '#primary-hover',
':active': '#primary-pressed',
'[disabled]': '#surface',
},
},
});
Applied in order of priority, this means:
- when disabled use
#surface - otherwise, on active use
#primary-pressed - otherwise, on hover use
#primary-hover - otherwise use
#primary
The important part is what happens next. Tasty compiles that state map into selectors that cannot overlap:
/* [disabled] wins outright */
.t0[disabled] { background: var(--surface-color); }
/* :active is excluded when disabled */
.t0:active:not([disabled]) { background: var(--primary-pressed-color); }
/* :hover is excluded when :active or disabled */
.t0:hover:not(:active):not([disabled]) { background: var(--primary-hover-color); }
/* default is excluded when anything above matches */
.t0:not(:hover):not(:active):not([disabled]) { background: var(--primary-color); }
Now there is no argument for the cascade to settle. No two branches can match at the same time. Extending or changing this map is far easier than reopening the equivalent selector logic in traditional CSS.
Why this matters
A hovered disabled button is just the easiest way to see the problem. The real pain starts when states intersect in less obvious ways. Maybe dark mode can come from a root attribute, or from prefers-color-scheme. Maybe spacing changes inside a narrow container, but only on tablet widths. Each one of those rules is understandable in isolation. The difficult part is the interaction surface between them.
The journey
Getting from “this works for simple state conditions” to “this can support real-world component systems” took several years and hundreds of iterations. The hard part was building a system that stayed coherent when pseudo-classes, attributes, media queries, and container queries all showed up together.
Tasty has powered Cube UI Kit from the beginning. That system now spans 100+ components and powers Cube Cloud, a real enterprise product. The model earned its shape under production pressure and team feedback.
Conclusion
I think "mutually exclusive selectors" are interesting because they remove a category of ambiguity that should not be the author’s job in the first place. When I style a component, I want to describe what it should look like in each meaningful state. I do not want to manually encode the browser’s tie-breaker logic every time those states intersect.
Source: Hacker News
















