This page collects the practices that keep a Stillwind project easy to change. It also lists the mistakes that silently produce no CSS. The examples use React.
Keep styles next to their elements
Write the utilities in a style() call on the element that they style. When
several places need the same styles, reuse them in this order:
- Extract a component of your framework, and keep the
style()call on its element. The styles stay next to the markup that they depend on. - Define a custom utility in your stylesheet when a component does not fit, such as for links inside other components’ text. A utility is available in every file of the project, so its effect is wider than a component’s.
- Export a stored
style()result. The styles then move away from the element that uses them.
When you use an imported style, spread it through style():
import { style } from "stillwind/react";
import { card } from "./card";
export function Panel() {
return <section {...style(card)}>…</section>;
}{...style(card)} shows that the element receives Stillwind styles, and you can
add utilities in the same call. A plain {...card} hides this.
Spread style() onto the element
Spread the result of style() onto the element. Do not read className,
class, or style out of the result. The result can contain more than one
prop, and its shape depends on the framework and on the inputs. If code needs a
class string for another purpose, that code does not need style(), and a
migration can leave it unchanged.
Set custom properties in the style() call, such as
style("w-(--progress)", { "--progress": value }). style() returns a style
prop when it needs inline values, so a separate style prop on the same
element can conflict with it. Set the style prop directly only on elements
without a style() call. You can also use style() for an element that has
custom properties and no utilities.
Express states with attributes
Keep the same classes on an element, and let its attributes decide which styles apply:
- For a state with an attribute, such as
disabledoraria-pressed, use the matching variant. - For an option without a matching attribute, set a
data-*attribute and use adata-[…]:variant. - For a value from a continuous range, set a custom property in the
style()call, and read it in a utility such asw-(--progress).
Use a condition such as selected && "bg-blue-50" only when no attribute fits.
Reusable styles shows each approach.
Do not track in JavaScript a state that CSS can detect, such as hover, focus, or the width of a container. Use the matching variant, and keep component state for what the application needs.
Connect elements with refs
When an element’s style depends on another element, such as a hovered parent, a
sibling, a container, or an anchor, connect them with a ref from
style.ref(). Do not use group-*, peer-*, named containers, or hand-written
parent and sibling selectors:
import { style } from "stillwind/react";
export function Card() {
const card = style.ref("card");
return (
<article {...style(card, "rounded-lg p-4")}>
<h2 {...style({ [card("hover")]: "text-blue-600" })}>Project</h2>
</article>
);
}A ref is a variable, so TypeScript reports a misspelled ref, and two components cannot collide on the same name. Anchors for anchor positioning use refs too. Element relationships covers siblings, containers, and anchors.
Define animations in their components
Define keyframes with style.keyframes() and view
transitions with style.viewTransition() in the
code that uses them. Do not add global @keyframes rules or fixed
view-transition-name values. The macros generate unique names, so you can
add, move, or remove a component without checking other code.
Declare refs and animations in the smallest scope
Declare a ref, keyframes, or a view transition in the smallest scope that contains all of its uses, as you would declare any other variable. Place the declaration next to those uses:
- A declaration that one component uses belongs in that component’s function.
- A declaration that a callback uses, such as a
map()callback, belongs in that callback. - A declaration that several components of one module use belongs at the top level of that module.
- When other modules use a declaration, export it from one module, and import it where you need it.
Each declaration creates one name wherever you write it. A smaller scope limits where the variable is visible, and it does not create a new name for each render or call.
Express options with one of a fixed set of values
For a prop that takes one of several values, such as a type or a tone,
use one of these forms. Set the value as a data-* attribute, and write one
variant group for each value:
import { style } from "stillwind/react";
export function Alert({ type }: { type: "info" | "warning" | "error" }) {
return (
<div
data-type={type}
{...style({
"": "rounded-md border p-4",
"data-[type=info]:": "border-sky-300 bg-sky-50",
"data-[type=warning]:": "border-amber-300 bg-amber-50",
"data-[type=error]:": "border-red-300 bg-red-50",
})}
/>
);
}Or select the utilities with an object lookup:
import { style } from "stillwind/react";
export function Alert({ type }: { type: "info" | "warning" | "error" }) {
return (
<div
{...style(
"rounded-md border p-4",
{
info: "border-sky-300 bg-sky-50",
warning: "border-amber-300 bg-amber-50",
error: "border-red-300 bg-red-50",
}[type],
)}
/>
);
}The attribute form keeps the classes static and changes a data-* attribute.
The attribute also shows the value in the browser’s developer tools and in test
queries. The lookup form changes the classes and adds no attribute. In both
forms, each value has its utilities in one place.
The two forms produce different CSS:
- The attribute form compiles into one class. The element always has this class, and the attribute selectors decide which declarations apply.
- The lookup form generates one class for each value. The lookup selects one of these classes at runtime. In this example, the common styles add one more class, which the element always has.
In both forms, the styles for a value usually win over the common styles. In the
attribute form, the attribute selector adds specificity, and the variant order
places the rules after the common styles. In the lookup form, the compiler
places the utilities that a condition selects after the unconditional
utilities. More specific selectors and !important can still change the
result. CSS ordering describes these rules.
Group utilities by variant
When several utilities share a variant, group them under one key instead of repeating the prefix:
style({
"": "rounded-md bg-gray-100 px-4 py-2",
"hover:": "bg-gray-200 text-gray-900",
"focus-visible:": "outline-2 outline-offset-2 outline-accent",
});
// Or write the base utilities as a separate string.
style("rounded-md bg-gray-100 px-4 py-2", {
"hover:": "bg-gray-200 text-gray-900",
"focus-visible:": "outline-2 outline-offset-2 outline-accent",
});The key "" groups utilities without adding a variant. Each group shows the
styles of one state, so readers do not need to collect the prefixed utilities
from a long string.
Keep values in the theme
Define colors, spacing, and breakpoints as @theme tokens, and use the
utilities that they add. Avoid arbitrary values such as text-[#abcdef] and
max-[768px]:. Design systems enables lint
rules that report them.
Choose how to define themes
When the application has only a light and a dark theme, define each color
token with light-dark(), and let color-scheme select the value.
Design systems shows this setup.
When the application can have more themes, light-dark() cannot express them.
Define the tokens as ordinary values, and override them for each theme with an
attribute selector. Set color-scheme in each theme, so that form controls and
scroll bars match:
@theme {
--color-surface: #ffffff;
--color-foreground: #202b45;
}
[data-theme="dark"] {
color-scheme: dark;
--color-surface: #1b263b;
--color-foreground: #e4edff;
}
[data-theme="sepia"] {
color-scheme: light;
--color-surface: #f4ecd8;
--color-foreground: #433422;
}Set data-theme on the root element, or on a container to theme part of the
page. The utilities, such as bg-surface, stay the same in every theme.
When a theme needs styles beyond its tokens, define a variant for it with @custom-variant, and use the variant instead of a hand-written selector:
@custom-variant theme-sepia (&:where([data-theme="sepia"], [data-theme="sepia"] *));Use breakpoints from the theme in JavaScript
When JavaScript needs a breakpoint, get its media query with style.media() instead of repeating the value:
import { style } from "stillwind/react";
const sidebar = window.matchMedia(style.media("sidebar"));A query such as matchMedia("(width >= 52.5rem)") does not follow a later
change of the breakpoint in the theme. Prefer a variant when CSS alone can make the
change. Responsive styles shows more uses.
Define utilities with @apply
Write the body of a custom utility with @apply, and write its conditions as
variants:
@utility link {
@apply text-accent underline-offset-4 hover:underline;
}Do not write selectors or media queries by hand. The variants follow your
theme, and they avoid differences that are easy to miss. For example, the
hover: variant applies only on devices that can hover, while a plain :hover
rule stays active after a tap on a touch screen. Inside a style rule, use
@variant instead of a condition.
Avoid separate CSS
Write styles in style() calls and custom utilities, not in CSS files with
your own class names. A custom utility works with every variant, such as
hover:link, and a fixed class name does not. For a reusable condition, define
a variant with @custom-variant instead of
writing its selector in several rules.
For HTML that you do not control, such as rendered Markdown, define the styles
in a custom utility and apply it to the container. You can also use a Tailwind
plugin such as @tailwindcss/typography.
Run the checks after each change
Run stillwind lint --fix and stillwind fmt after you change styles, and fix
the remaining findings. Add a suppression comment only for a deliberate
exception, and give the reason. See
Checks in the editor and CI.
Mistakes that produce no CSS
The build succeeds in each of these cases, but the styles do not apply. Run
stillwind lint to find most of them.
- A class name outside
style(), such as in aclassNamestring, gets no CSS. - A utility name that your code builds at runtime, such as
`bg-${color}-600`, gets no CSS. Write complete names, such ascolor === "red" ? "bg-red-600" : "bg-blue-600". - A
style()call written directly inside anotherstyle()call contributes nothing. Store the inner style in a variable first. - Custom properties in a base do not carry over to the styles that extend it.
Set them in the
style()call that is spread onto the element. - Classes in the
classfield pass through without generated CSS. - A suppression comment hides a finding only. A misspelled utility still produces no CSS.
The compiler can evaluate the arguments of style() in any order, and it can
skip arguments whose result is not used. Calculate values and perform effects
before you call style().