Introduction
For years, web developers have asked, how can I write a:hover
in inline
styles? In times past, this was usually dismissed as a newbie question,
something you can't do and shouldn't want to because inline styles were widely
assumed to be less maintainable than style sheets. But as component-based
architecture gained traction, it became increasingly clear to many that the
opposite was actually true: Inline styles would allow a component to encapsulate
markup, styling, and behavior, avoiding both the context switch of maintaining
an external CSS file and the error-prone nature of indirect style references
(via selector logic). The problem: It was still true that inline styles didn't
support pseudo-classes, media queries, or various other CSS features.
As a result, many heavy-handed styling solutions emerged to address these feature gaps, ranging from
- Atomic CSS, which asks you to learn a new, non-standard, perhaps inexpressive, utility class syntax; to
- CSS-in-JS, which generates and injects style sheets into the DOM as the user traverses the application; to
- "Zero-runtime" solutions, which replace runtime style injection with complicated build processes in order to produce static CSS files.
These solutions have all delivered tremendous value and demonstrated what modern UI architecture should look like. But each one is also prone to cascade defects and, as with most complex systems, any number of other unique downsides.
CSS Hooks takes a new approach of extending the capabilities of the style
prop, rather than replacing it with an entirely different system. The :hover
pseudo-class is just the beginning: You can use the full range of
pseudo-classes, custom selector logic, @media
queries, and more. The best part
is that it is simple, requiring no build steps, no runtime style injection,
and no changes to existing inline style syntax. If it sounds too good to be
true, you're not alone in this belief. But read on, and you'll find that an
obscure CSS technique provides just the mechanism we need to do the "impossible"
with inline styles. It's built on CSS Variables, but you probably haven't seen
them used quite like this.
CSS Variables
Consider a conventional use case for CSS Variables:
<span style="--success-color: green; color: var(--success-color)">...</span>
Notice that the var()
function is a simple reference to the --success-color
property, making the whole inline style equivalent to color: green
. In this
trivial example, the use of a variable clearly doesn't provide any benefit; but,
since these so-called custom properties can be inherited from ancestor
elements, you can use them to avoid repeating a value (like a color) throughout
your application, or even for more advanced use cases (like theming).
Especially when considering inheritance, it's easy to imagine a scenario where
you might reference a custom property that hasn't been set. Fortunately, the
var()
function accounts for this possibility by allowing you to specify a
fallback value, e.g. var(--success-color, #0c0)
—#0c0
being the fallback
value to use in case --success-color
isn't set or is set to an invalid
value.
The fallback trick
The CSS Custom Properties spec describes two special values that, in conjunction, actually make CSS Variables programmable:
- The
initial
value of a custom property is a guaranteed-invalid value. - An empty value written explicitly, e.g.
--foo: ;
is a valid value.
In other words:
- When you reference a variable whose value is
initial
, the fallback value will be used instead. - When you reference a variable whose value is empty, that empty value will be used (but it will have no effect on the declaration).
With this knowledge, you can do the unthinkable—implement a hover effect within an inline style:
<a href style="color: var(--hover-on, red) var(--hover-off, blue)">
Hover me
</a>
<style>
* {
--hover-off: initial;
--hover-on: ;
}
:hover {
--hover-off: ;
--hover-on: initial;
}
</style>
As you can see, a style sheet is technically still required, but it only
controls the states of the --hover-off
and --hover-on
variables, allowing
you to toggle between arbitrary values defined within the inline style. Because
it doesn't define any presentational values, this hover "hook" can be used over
and over again throughout your application without any additional CSS overhead.
The pitfalls of the fallback trick
Although the mechanism underlying the fallback trick is quite simple, actually using this technique directly is a bit complicated. The pain points include:
- Defining "hooks". Although the "hook pattern" in the style sheet does click after a while, this remains a tedious and repetitive task.
- Aligning variable names. Just as with a class name, you need to ensure that the variable names in the style sheet match the ones referenced in the markup.
- Challenging syntax. CSS Variable syntax is widely regarded as less than
beautiful, and it is even less readable when using the fallback trick.
Combining multiple states (e.g.
:enabled
+:hover
) requires nested expressions, compounding this issue. - No type safety. The type safety of the
style
prop doesn't extend to variable references or fallback values since they are embedded in a string. - No auto-completion. Lacking type information, your code editor won't be able to help you much to implement the pattern successfully.
CSS Hooks
This library allows you to harness the power of the fallback trick through a familiar, convenient, and type-safe interface. Here's how it works:
- Defining a hook is as easy as writing a CSS selector or at-rule.
- The underlying variables are created automatically, so you don't need to worry about naming, scoping, or maintaining references.
- The familiar syntax of the
style
prop is extended to allow you to leverage your hooks through conditional style objects. You don't even see the underlying variables! - The type safety of the style object syntax is extended to include hooks.
- Native TypeScript auto-completion makes hooks easy to use, with no need to install any additional editor extensions.
Ready to get started?
Proceed to the Setup guide to get started. Or, if you're in a hurry, check out the Quickstart section.