← Back to DevBytes

Implementing CSS State Queries in Modern Web Applications

Understanding CSS State Queries

CSS State Queries are a family of modern CSS mechanisms that allow you to apply styles conditionally, based on the current state of an element, its descendants, or its environment. Traditional pseudo-classes like :hover, :focus, and :checked already let us react to user interaction. The new generation of state queries goes much further, enabling you to write styles that respond to complex component states, the presence of certain child elements, custom states exposed by Web Components, and even the computed style values of a container — all without a single line of JavaScript.

In essence, state queries turn CSS from a static declaration of styles into a dynamic, context-aware styling engine. They unlock patterns like parent-element styling based on a child’s focus or validity, declarative theming via container style queries, and deeply encapsulated component styling through the Custom State API.

Why CSS State Queries Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Embracing state queries brings several tangible benefits to modern web applications:

Whether you’re building a design system, a complex form, or a reusable component library, mastering state queries will make your CSS more expressive and your components more robust.

Core State Query Mechanisms

:has() – The Parent State Selector

The :has() pseudo-class is often called a “relational” selector, but it’s fundamentally a state query about descendants. It matches an element if it contains at least one descendant that satisfies the inner selector. Because it can check interactive pseudo-classes like :hover, :focus, or :invalid on children, it effectively lets you style a parent based on the state of its child.

Example: Highlight a form fieldset when any of its inputs are focused, and apply a different style when an input is invalid.

fieldset:has(input:focus) {
  border-color: #007bff;
  box-shadow: 0 0 0 3px rgba(0,123,255,0.25);
}

fieldset:has(input:invalid) {
  border-color: #dc3545;
  background-color: #fff5f5;
}

Another powerful use case is styling a card that contains a specific element, such as an image with a particular attribute, or a call-to-action button.

.card:has(img[src$=".svg"]) {
  border: 2px solid #6f42c1;
}

.card:has(.btn--primary:hover) {
  transform: scale(1.02);
  transition: transform 0.2s;
}

:has() supports full complex selectors inside it, including combinators. You can even chain multiple :has() calls to create sophisticated state-driven logic.

:focus-visible and :focus-within

These two pseudo-classes refine how we handle focus state. :focus-visible applies styles only when the browser determines that focus should be visually indicated (typically keyboard navigation), leaving mouse users unaffected. :focus-within matches an element when it itself or any descendant is focused — a perfect tool for highlighting the active form group or interactive region.

/* Accessible focus ring only for keyboard users */
button:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

/* Highlight the whole form row when an input inside is focused */
.form-group:focus-within {
  background-color: #f0f7ff;
  border-radius: 8px;
}

Using :focus-visible instead of the generic :focus prevents ugly focus rings on mouse clicks, while still providing clear indicators for keyboard navigation.

:modal – Styling Based on Dialog State

The <dialog> element comes with a dedicated :modal pseudo-class that matches when the dialog is opened using showModal(). This lets you style the dialog itself and its backdrop purely with CSS, without maintaining a separate “open” class.

/* Styles applied only when the dialog is a modal overlay */
dialog:modal {
  border: none;
  border-radius: 12px;
  box-shadow: 0 20px 40px rgba(0,0,0,0.3);
  padding: 2rem;
}

dialog::backdrop {
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(4px);
}

The ::backdrop pseudo-element automatically appears behind a modal dialog, so you can create dimming and blur effects without any extra markup.

:popover-open – State for the Popover API

Modern browsers now support the Popover API, which provides a light‑dismiss, non‑modal overlay mechanism. The :popover-open pseudo-class matches an element when it is shown as a popover, enabling state‑driven styling.

/* Style the popover panel when visible */
.my-popover:popover-open {
  background: white;
  border: 1px solid #ccc;
  border-radius: 8px;
  box-shadow: 0 10px 25px rgba(0,0,0,0.15);
  padding: 1rem;
  /* Position is handled via anchor positioning or inset */
  inset: auto;
}

Combined with anchor positioning, :popover-open allows you to build tooltips, menus, and picklists whose appearance is fully controlled by CSS.

:state() – Custom State for Web Components

The CSS Custom State API introduces the :state() pseudo-class, allowing custom elements to expose internal states that can be targeted from outside stylesheets. This is a game‑changer for component encapsulation: component authors define states like --selected, --active, or --loading using the element’s internals, and consumers style them naturally with :state(--selected).

First, define a custom element that toggles a state via ElementInternals:

class ToggleButton extends HTMLElement {
  static get observedAttributes() { return ['pressed']; }

  constructor() {
    super();
    this.internals = this.attachInternals();
    this._pressed = false;
  }

  connectedCallback() {
    this.addEventListener('click', () => {
      this._pressed = !this._pressed;
      this.setAttribute('pressed', this._pressed);
    });
    this._syncState();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'pressed') {
      this._pressed = newVal !== null;
      this._syncState();
    }
  }

  _syncState() {
    if (this._pressed) {
      this.internals.states.add('--pressed');
    } else {
      this.internals.states.delete('--pressed');
    }
  }
}
customElements.define('toggle-btn', ToggleButton);

Now, any stylesheet can react to the --pressed state without knowing the internal structure of the component:

toggle-btn:state(--pressed) {
  background: #007bff;
  color: white;
  box-shadow: inset 0 0 0 2px #0056b3;
}

This pattern keeps styling declarative, even for highly complex components, and avoids leaking implementation details through CSS custom properties or manual class management.

Container Style Queries – Querying Computed State of Containers

Container queries are famous for size-based conditions, but they also support style queries. A style query checks the computed value of a CSS custom property (or any inheritable property) on a container element and applies styles to its descendants accordingly. This effectively queries a “state” expressed as a style value.

Example: a container that sets a --theme custom property, and its children adapt via a style query:

/* Define the container */
.card-container {
  container-type: inline-size;
  /* Also declare it as a style container */
  container-name: card-container;
}

/* Inline a theme custom property */
.card-container[data-theme="dark"] {
  --theme: dark;
}
.card-container[data-theme="light"] {
  --theme: light;
}

/* Style query on the container */
@container card-container style(--theme: dark) {
  .card {
    background: #1e1e1e;
    color: #f0f0f0;
    border-color: #444;
  }
}

@container card-container style(--theme: light) {
  .card {
    background: white;
    color: #333;
    border-color: #ddd;
  }
}

Style queries work with any custom property, enabling context‑aware styling that feels like a native CSS “if” statement. You can query booleans (e.g., --is-compact: true), numbers, or string values, and combine them with size queries for extremely fine‑grained control.

How to Use CSS State Queries in Your Applications

Integrating state queries into your workflow follows a clear path:

Example of combining multiple state queries:

.dashboard-panel:has(.filter-widget:popover-open) {
  z-index: 10;
  background: #f9f9f9;
}

@container dashboard style(--mode: editing) {
  .dashboard-panel:has(input:focus) {
    border: 2px dashed #007bff;
  }
}

Best Practices

Conclusion

CSS State Queries represent a fundamental shift in how we think about styling. By moving state‑driven logic from JavaScript into the stylesheet, we create applications that are more performant, easier to maintain, and naturally resilient to change. From the parent‑aware power of :has() and the accessibility‑focused :focus-visible, to the encapsulated :state() and the context‑savvy container style queries, these tools empower you to write CSS that feels like a first‑class language for dynamic UI. Start using them today, and watch your stylesheets become smarter, cleaner, and more aligned with the component‑driven architecture of modern web applications.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles