Skip to content

Instantly share code, notes, and snippets.

@dragontheory
Last active January 19, 2026 21:17
Show Gist options
  • Select an option

  • Save dragontheory/2af4ce48f82e2cf65145d5757db8a37d to your computer and use it in GitHub Desktop.

Select an option

Save dragontheory/2af4ce48f82e2cf65145d5757db8a37d to your computer and use it in GitHub Desktop.
CSS State Systems without JS Frameworks

Image

Quick practical upgrade pack for truly declarative, JS‑free UI logic using modern CSS: :target, :has(), and container style queries.


:target — URL‑driven toggles (no JS)

Use hash links (#id) to open/close sections, focus cards, or reveal details. It’s native, accessible, and back/forward‑button friendly.

<article id="details" hidden>
  <h2>Details</h2>
  <p>...</p>
  <p><a href="#close">Close</a></p>
</article>

<nav>
  <a href="#details">Open details</a>
  <a id="close"></a> <!-- empty anchor to "unset" target -->
</nav>
#details { display: none; }
:target#details { display: block; }

Tips

  • Use an empty “close” anchor to clear :target.
  • Add scroll-margin-block on targets to avoid header overlap.

:has() — parent‑driven state (pure CSS)

Make a container react to what it contains—checked inputs, non‑empty slots, validation, etc.

<section class="card">
  <header>
    <label>
      <input type="checkbox" hidden>
      Toggle
    </label>
    <h3>Card</h3>
  </header>
  <div class="panel">Content…</div>
</section>
.card .panel { display: none; }
.card:has(> header input:checked) .panel { display: block; }

/* Validation example */
.form-row:has(input:user-invalid) { outline: 2px solid color-mix(in oklch, red, Canvas 40%); }

Tips

  • Prefer structural selectors (>, :empty, native attributes) over classes to fit D7460N’s CSS‑first rules.
  • Combine with [open], [aria-expanded="true"], or :modal for native states.

@container style() — react to semantic styles, not classes

Style queries let components adapt based on a parent’s computed styles (tokens), not brittle utility classes. Great for theming, modes, or capability flags.

<div class="shell" style="--mode: 'compact'">
  <ul class="list">
    <li></li>
  </ul>
</div>
/* declare the container */
.shell { container-type: inline-size; container-name: shell; }

/* structural size query (classic container query) */
@container shell (min-width: 42rem) {
  .list { columns: 2; gap: 1rem; }
}

/* style query: react to a *value* instead of a class */
@container style(--mode: 'compact') {
  .list { gap: 0.5rem; font-size: 0.95em; }
}

Tips

  • Use one semantic custom property (e.g., --mode, --intent, --variant) at the shell level.
  • Components inside read the parent’s “state” via @container style(...).

Put it together (D7460N‑friendly, no JS for state)

<main class="app" style="--mode: 'cozy'">
  <section id="panel" class="card">
    <header>
      <h2>Panel</h2>
      <label>
        <input type="checkbox" hidden>
        Expand
      </label>
      <a href="#panel">Focus</a>
      <a href="#_">Unfocus</a>
    </header>
    <div class="body">…content…</div>
  </section>
</main>
/* containers */
.app { container-type: inline-size; container-name: app; }
.card { container-type: inline-size; container-name: card; }

/* :target focuses the card */
.card { outline: none; }
:target.card { outline: 2px solid var(--accent, Highlight); outline-offset: 4px; }

/* :has() opens body */
.card .body { display: none; }
.card:has(> header input:checked) .body { display: block; }

/* size-based layout */
@container app (min-width: 56rem) {
  .card { display: grid; grid-template-columns: 18rem 1fr; gap: 1rem; }
}

/* style query for mode */
@container style(--mode: 'cozy') {
  .card { padding: 1rem; border-radius: 0.75rem; }
}

/* accessible motion preference */
@media (prefers-reduced-motion: no-preference) {
  .card .body { transition: content-visibility 0s, opacity .2s, scale .2s; }
  .card:has(> header input:checked) .body { opacity: 1; scale: 1; }
  .card .body { opacity: .001; scale: .98; }
}

When to use which

  • :target — deep-linkable show/hide, focus, or “open to a section”.
  • :has() — parent reacts to child state (checked, valid, non‑empty).
  • @container style() — components adapt to parent “mode” tokens without classes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment