Open-source project
css-hooks/css-hooks avatar
css-hooks/css-hooks

CSS Hooks: reactive styling that stays inside the style prop

CSS-driven user interaction, layout context, and inherited state, all in the style prop.

608 stars8 forksTypeScriptMIT

At a glance

What is it?
CSS Hooks lets a component respond to hover, to container size and to inherited state through CSS variables, with no runtime style injection and no build step. The repository's default branch is the next branch, so the code most people read is a release candidate.
Who is it for?
CSS Hooks fits teams building component libraries in React, Preact, Solid or Qwik who want interactive and context aware styling that stays in CSS, with no runtime style injection and no extra build step. It does not fit a product that must support browsers below Chrome and Edge 118, Safari 18 and Firefox 151, and it is a poor fit for anyone who wants a stable release, since the default branch is next and the recent tags are all release candidates.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The goal is interactivity without touching CSS at build time

The project's summary is a single line: CSS power, inline style simplicity. It says the library lets you respond to user interaction, layout context, and inherited state, all CSS driven, all without leaving the style prop.

The mechanism is stated in the same paragraph. CSS Hooks delivers component owned styling by exploiting the hidden programmability of CSS variables, without runtime style injection or build steps.

That is a specific architectural claim, and each part of it matters. Without runtime style injection means the library does not attach stylesheet rules to the document at runtime, so there is no style tag appearing in your DOM and no flash of unstyled content caused by late injection. Without build steps means no compiler plugin and no CSS file to author. And because the mechanism is CSS variables rather than a runtime style engine, the resulting behaviour is inspectable in devtools, which matters when a component library has to be debuggable by the people using it.

The audience is component authors, and the tone is deliberately narrow. This is a styling primitive, not a state manager, and the README never claims otherwise.

mergeStyles and on() keep everything in the style prop

The API in practice is two functions. mergeStyles combines a plain style object with hook entries, and on registers a selector with the styles that apply when it matches.

jsx
  style={mergeStyles(
    {
      background: "#004982",
      color: "#eeeff0",
    },
    on("&:hover", {
      background: "#1b659c",
    },
    ),
    on("&:active", {
      background: "#9f3131",
    }),
  )}

The static object is the base style, and each on call adds a state. Because the result is still a style prop value, everything that already accepts a style object keeps working, and the hook entries are just more entries in the same place.

The selectors you pass to on are ordinary CSS selectors, and that is where the CSS power claim comes from. If the browser can express the state as a selector, the library can express it as a style entry, which is why the same mechanism covers hover, container queries and inherited state without a separate API for each.

Container queries become style entries, not a media query workaround

Layout context is the case that separates this from a hover helper. The example shows two spans that appear at different container widths using container queries as the trigger.

jsx
  style={mergeStyles(
    { display: "none" },
    on("@container (width >= 50px)", {
      display: "revert-layer",
    }),
    on("@container (width >= 100px)", {
      display: "none",
    }),
  )}

The mechanism is that a container query is a selector, so it fits the same on() shape as a pseudo class. The base style hides the element, the first hook reveals it once the container is wide enough, and the second hides it again past a larger threshold.

The use of display revert-layer in the revealed state is a deliberate detail. It restores the property to the value it would have had from the cascade outside the hook, rather than forcing a hard coded value, which keeps the component composable with whatever the surrounding layout expects.

Inherited state lets a child react to its parent's hover

The third feature is the least obvious and the most useful in practice. A parent element can set a state on hover, and a child can respond to that state even though the child itself is not hovered.

jsx
<div style={mergeStyles({ padding: 24 }, on("&:hover", enable("%active")))}>
  Hover parent
  <span
    style={mergeStyles(
      { color: "gray" },
      on("%active", {
        color: "purple",
      }),
    )}
  >
    Child responds
  </span>
</div>

There are two pieces to it. enable("%active") is the parent opting in, and the child listens with on("%active", ...). The state name is prefixed with a percent sign, which is how the library distinguishes an inherited state from a plain CSS selector, so the two cannot be confused.

This is the case that a plain CSS custom property cannot express alone, since a descendant needs to know about an ancestor's state. Doing it by hand in CSS normally means writing a separate rule set for each parent and child combination, and it is the kind of duplication that grows badly in a component library.

A monorepo with a package per framework

The repository is a monorepo whose root package.json describes itself as the monorepo for @css-hooks/* packages.

json
  "name": "css-hooks",
  "description": "Monorepo for @css-hooks/* packages",
  "version": "4.0.0-next.70",
  "type": "module",

The workspaces list covers packages/*, example, scripts/* and site, and the documentation pipeline shows which framework bindings exist: the docs script runs api-documenter over the apix output of packages/core, packages/preact, packages/react, packages/qwik and packages/solid. So the core package holds the styling logic and each framework package adapts it.

The rest of the tree shows the surrounding discipline. There is an api-extractor.base.json for API surface tracking, a tsconfig set with a base configuration, an eslint config, prettier with a jsdoc plugin, husky for git hooks with a prepare script, a commitlint config, and renovate.json for dependency updates. The example directory has its own package.json, an index.html, a Vite config and a src folder, which is where you can see the library used in a real build.

The documentation itself lives in docs/ and site/, and the README tells you to visit css-hooks.com to get started rather than putting the guide in the repository.

The default branch is a release candidate, not the stable line

The default branch of this repository is named next, and the README opens with a warning about it: this is the next branch, tracking the upcoming release, and for the latest stable code you should switch to the latest branch.

That branch choice has consequences for how you read everything else here. The badge points at the tag v4.0.0-next.70, the npm badge points at the same version of the core package, and the recent release list is v4.0.0-next.70 on 2026-10-11, v4.0.0-next.69 on 2026-10-11 and v4.0.0-next.68 on 2026-10-10. Three candidate tags in two days is what a prerelease line looks like while it is settling, and it means the version you would install from npm by default is a candidate, not a stable release.

The last push was on 2026-10-22, so work continues behind the candidates. If you are evaluating this library for production, the honest reading is that you would be pinning a prerelease and choosing a branch deliberately, and the README is unusually helpful about which branch that is.

On licensing, the project is offered under the MIT license, with the terms in the LICENSE file at the repository root, and the package metadata records the same licence.

Browser support floors exclude a lot of production traffic

The compatibility section is where the cost of the approach becomes visible. The browser table lists Chrome at 118 and above, Edge at 118 and above, Safari at 18 and above, Firefox at 151 and above, and Opera at 104 and above.

Firefox 151 is the number to notice. Container queries and the CSS variable mechanisms the library relies on are only available in recent engines, and the library states its floor rather than degrading quietly, which is the right thing to do but does mean the supported browser set is narrow. An application with meaningful traffic on older Safari or Firefox releases is outside the range, and there is no polyfill path described in the README.

On the framework side, the table shows React, Preact, Solid and Qwik all supported. That is a sensible set for a styling primitive, since it covers the frameworks whose rendering model leaves the style prop as a plain object. There is no Svelte, Vue or Angular row in the table, and the README does not discuss why, so if your application is built on one of those you should treat compatibility as unverified rather than assumed.

Editorial conclusion

CSS Hooks fits teams building component libraries in React, Preact, Solid or Qwik who want interactive and context aware styling that stays in CSS, with no runtime style injection and no extra build step. It does not fit a product that must support browsers below Chrome and Edge 118, Safari 18 and Firefox 151, and it is a poor fit for anyone who wants a stable release, since the default branch is next and the recent tags are all release candidates. Verify first whether the version on npm is the prerelease you need, and switch to the latest branch if you require stable code.

Frequently asked questions

Which browsers does CSS Hooks support?

The compatibility table lists Chrome 118 and above, Edge 118 and above, Safari 18 and above, Firefox 151 and above, and Opera 104 and above.

Which frameworks does CSS Hooks work with?

The frameworks table marks React, Preact, Solid and Qwik as supported, and the monorepo ships separate packages for core, react, preact, solid and qwik.

Is CSS Hooks available as a stable release?

The default branch is next, which the README warns is tracking the upcoming release, and the recent tags are v4.0.0-next.70, v4.0.0-next.69 and v4.0.0-next.68. For the latest stable code the README tells you to switch to the latest branch.

How does CSS Hooks avoid injecting styles at runtime?

The README states that it delivers flexible component owned styling by exploiting the hidden programmability of CSS variables, without runtime style injection or build steps, so behaviour stays inside the style prop.

What licence is CSS Hooks released under?

The project is offered under the MIT license, with the terms in the LICENSE file at the root of the repository, and the package metadata records the same licence.

Official sources

  1. css-hooks/css-hooks on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/css-hooks-css-hooks.svg)](https://hysenlabs.com/projects/css-hooks-css-hooks)

Was this useful?