loke.dev
Header image for My Components Finally Have a Pulse: How I Replaced 100 'Variant' Props with CSS Style Queries

My Components Finally Have a Pulse: How I Replaced 100 'Variant' Props with CSS Style Queries

I stopped polluting my React components with endless style-driven props and finally let the CSS engine handle contextual theming through the power of @container style().

· 5 min read

My Components Finally Have a Pulse: How I Replaced 100 'Variant' Props with CSS Style Queries

I spent twenty minutes yesterday trying to remember if the isGhost prop on my Button component played nicely with the hasBorder prop, or if they just canceled each other out into a transparent void. It was a moment of clarity: my React components weren't actually components anymore—they were just giant, bloated switch statements masquerading as UI elements.

We’ve all been there. You start with a simple Button. Then marketing wants a "secondary" version. Then a "danger" version. Then a "small-on-mobile-but-large-on-desktop" version. Before you know it, your TypeScript interface looks like a grocery list for a very confused chef.

// The "Prop Soup" we've all written
<Button 
  variant="primary" 
  size="large" 
  elevation="high" 
  isRounded 
  hasIcon 
  iconPosition="right" 
  theme="dark"
>
  Click Me
</Button>

The problem isn't React; it's that we've been forcing our components to handle logic that belongs to the browser. But CSS has been quietly evolving. While we were arguing about which CSS-in-JS library had the smallest runtime, CSS Style Queries arrived to save us from our own prop-drilling madness.

What are Style Queries, anyway?

You’re probably familiar with Container Queries (searching for a parent's width or height). Style Queries are the cooler, more sophisticated sibling. Instead of asking "how wide is my parent?", they ask "what is the value of this CSS variable on my parent?"

This is a game-changer. It means your component can react to its *context* without you having to pass a single prop down through five layers of the DOM.

The Old Way: Prop Purgatory

Let’s look at a typical "Card" component. Usually, if you want the card to look different when it’s inside a "Featured" section, you’d do something like this:

// React logic creep
const Card = ({ title, isFeatured }) => {
  const className = isFeatured ? 'card card--featured' : 'card';
  return (
    <div className={className}>
      <h3>{title}</h3>
      <Button color={isFeatured ? 'gold' : 'blue'}>Read More</Button>
    </div>
  );
};

This is fine for one level. But when the Button inside the Card also needs to know it's "featured" to change its hover state, you’re stuck passing isFeatured down forever. It’s exhausting.

The New Way: Giving Components a Pulse

With Style Queries, we stop treating components like static templates and start treating them like living things that respond to their environment.

First, we set a CSS variable on a container. This variable acts as our "state," but it lives entirely in the CSS engine.

/* The Parent Context */
.featured-section {
  --theme-mode: ultra-premium;
}

.standard-section {
  --theme-mode: basic;
}

Now, the child components can "query" that style. No props required. Here is how the CSS looks:

/* The Card Component */
.card {
  background: white;
  padding: 1rem;
  border: 1px solid #ddd;
}

/* This is the magic part */
@container style(--theme-mode: ultra-premium) {
  .card {
    background: linear-gradient(to bottom, #fff, #f9f9f9);
    border-color: gold;
    box-shadow: 0 10px 20px rgba(0,0,0,0.1);
  }

  .card h3 {
    color: #b8860b;
    font-size: 1.5rem;
  }
}

Why this actually matters

You might be thinking, "Isn't this just descendant selectors like .featured-section .card?"

Not exactly. Descendant selectors are rigid and create high specificity. Style queries are encapsulated. The component isn't looking for a specific class name on a parent (which might change or be renamed); it’s looking for a *specific design intent* defined by a variable.

I’ve started using this for "Theme Contexts" that don't require the React Context API. If I want a specific part of the page to have a "Card High Contrast" look, I just wrap it in a div that sets --card-style: high-contrast. Every component inside that tree that knows how to handle high-contrast will just... wake up and do it.

A Practical Example: The Context-Aware Button

Let's say you have a button that needs to change its background based on whether it’s sitting on a dark or light background. Instead of isDarkBackground={true}, try this:

.button {
  background: var(--btn-bg, #eee);
  color: var(--btn-text, #333);
}

/* If the parent says we are in 'dark' mode */
@container style(--surface: dark) {
  .button {
    --btn-bg: #444;
    --btn-text: #fff;
    border: 1px solid #666;
  }
}

In your React code, the component stays incredibly clean:

// Look ma, no props!
const Button = ({ children }) => (
  <button className="button">{children}</button>
);

The "Gotchas" (Because there's always a catch)

I’d love to tell you to go delete all your props right now, but we need to talk about reality.

1. Browser Support: As of right now, Style Queries for custom properties are mostly a Chromium thing (Chrome, Edge, Opera). Safari and Firefox are dragging their feet slightly on the style() part of the spec, though they support size queries.
2. Performance: If you query 5,000 components for a style change at once, the browser is doing a lot of work. In practice, I haven't seen a hit on standard dashboards, but don't go putting a style query on every single table cell in a 10,000-row grid.
3. Naming is Hard: Since you’re moving "logic" to CSS variables, your naming convention for variables needs to be bulletproof. I recommend prefixing them, like --comp-card-state.

Moving Forward

Switching to Style Queries felt like finally letting my components breathe. They aren't tethered to a specific React state or a messy chain of props. They just exist, and if the environment around them changes, they adapt.

If you’re working in an environment where you can target modern browsers (like an Electron app or a modern internal dashboard), give this a shot. Start small. Pick one component—maybe a Tooltip or a Card—and see if you can strip away those "variant" props.

Your components might just find their pulse, and your codebase will definitely be a lot quieter.