Styling
How CSS reaches a node, what the stylesheet engine supports, and the Tailwind paths.
Pick a path by what you already have:
| You have | Use |
|---|---|
| Tailwind classes, compiled by your bundler | stylesheets with the generated CSS |
| Tailwind classes, no build step | className + a future flag |
| Plain CSS | stylesheets or a <style> tag |
| Design tokens to theme with | cssVariables or a :root rule |
| One-off styles on one node | the style prop |
The paths combine. Each section spells out who wins when they collide.
Inline styles
The style prop puts CSSProperties on one node. It overrides the tag default and every matching stylesheet rule at the same importance.
<div style={{ display: "flex", padding: 48, background: "#0f172a" }}>Hello</div>Stylesheets
Pass real CSS through stylesheets and match it by className or id.
import { } from "takumi-js";
const = await (< ="card">Hello</>, {
: 1200,
: 630,
: [`.card { display: flex; padding: 48px; background: #0f172a; color: white; }`],
});The engine handles:
| Selectors | At-rules | Properties |
|---|---|---|
| class, id, descendant | @keyframes, @media, @supports, @layer | custom properties with var(), shorthands, gradients, box-shadow, filter, backdrop-filter, mix-blend-mode, transform |
A render is one static frame, so interactive pseudo-classes like :hover and :focus parse but
never match.
A <style> tag feeds the same engine and gets extracted from the JSX automatically.
<div className="card">
<style>{`.card { display: flex; padding: 48px; }`}</style>
Hello
</div>Tailwind with your bundler
Compile Tailwind with your bundler and pass the generated CSS through stylesheets. Tailwind itself does the compiling, so every directive and class name works; the engine's CSS support still bounds what renders. With Vite, import the stylesheet using ?inline.
import { ImageResponse } from "takumi-js/response";
import stylesheet from "~/styles/global.css?inline";
export function GET() {
return new ImageResponse(
<div className="bg-background text-foreground flex justify-center items-center w-full h-full text-4xl">
Hello Tailwind!
</div>,
{
width: 1200,
height: 630,
stylesheets: [stylesheet],
},
);
}import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});Tailwind without a build step
Turn on the classNameUtilities future flag and className tokens run through a built-in parser. JSX pasted from an app renders without renaming a prop.
import { } from "takumi-js";
const = await (
< ="bg-blue-500 p-4 rounded-lg">
< ="text-white text-2xl font-bold">Hello Tailwind!</>
</>,
{
: 1200,
: 630,
: { : true },
},
);Arbitrary values work. The parser won't cover every Tailwind feature; the parser mapping lists every supported class.
className is a plain prop, so build it dynamically:
import clsx from "clsx";
const isError = true;
<div
className={clsx(
"p-4 rounded",
isError ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700",
)}
>
{isError ? "Something went wrong" : "Success!"}
</div>;The tw prop feeds the same parser without any flag. It is a plain prop too, takes the same dynamic expressions, exists for satori compatibility, and wins a tie against a className utility on the same node.
<div tw="bg-blue-500 p-4 rounded-lg">Hello</div>Preflight
The native parser applies no Tailwind Preflight by default, so elements keep their UA margins: a <h1> gets a 0.67em top margin until you add mt-0. box-sizing still defaults to border-box. The same import line a Tailwind stylesheet starts with turns Preflight on. It drops the UA margins, list markers and heading font tweaks.
const image = await render(node, {
stylesheets: [`@import "tailwindcss";`],
});Utilities in the cascade
Utilities sit in the last declared layer, Tailwind's own order:
| Against a utility | Winner |
|---|---|
| An unlayered stylesheet rule | the rule |
A rule in a named @layer | the utility |
The style prop | the style prop |
Prefix a utility with ! to flip the result: important declarations reverse the layer order, so a ! utility beats an unlayered rule and a normal style declaration.
Tailwind at-rules
A Tailwind source stylesheet drops into stylesheets unchanged. The engine reads these directives and skips the rest:
| At-rule | Reads as | Rejected |
|---|---|---|
@theme | its :root rule. Nested @keyframes register. Modifiers like reference and inline change nothing | prefix() |
@import "tailwindcss" | Preflight, at the top level only | any other import target |
@apply | the utilities' declarations, expanded in place, ! suffix included | variants like md: |
.card {
@apply mt-4 bg-brand-500;
}Other Tailwind directives (@utility, @custom-variant, @source, @plugin, @config) do not parse. Compile those with Tailwind and use the bundler path.
Design tokens
A utility reads a CSS custom property, the way Tailwind compiles it: bg-red-500 resolves var(--color-red-500), p-4 resolves calc(var(--spacing) * 4). The built-in scale sits behind them as the fallback, so tokens are optional until you want your own palette.
Declaring tokens
Three equivalent places. All of them feed every var(), in stylesheets, in <style> tags, and in utilities.
A :root rule, or a Tailwind @theme block pasted as-is:
:root {
--color-brand-500: #5b21b6;
--spacing-gutter: 2.5rem;
}Or the cssVariables option, which compiles into a :root rule. The leading -- is optional.
import { } from "takumi-js";
const = await (< ="bg-brand-500 p-gutter">Hello</>, {
: 1200,
: 630,
: { "--color-brand-500": "#5b21b6", "--spacing-gutter": "2.5rem" },
: { : true },
});cssVariables entries whose value contains ;, {, }, /* or !important, or whose name contains :, ;, { or }, are dropped. They could escape the generated rule.
A nested palette flattens with the cssVariables helper. Keys join with - into one variable name.
import { } from "takumi-js/helpers";
const = ({ : { : { 500: "#5b21b6" } } });
// { "--color-brand-500": "#5b21b6" }Which utilities read a token
The namespace picks which utilities a token reaches:
| Namespace | Utilities |
|---|---|
--color-* | color utilities: bg-*, text-*, border-*, outline-*, decoration-* |
--spacing, --spacing-* | length utilities: p-*, m-*, w-*, gap-*, inset-*. p-4 reads calc(var(--spacing) * 4), p-gutter reads var(--spacing-gutter) |
--container-* | max-w-* |
--text-* | --text-xl sets text-xl, --text-xl--line-height sets its leading |
--font-* | font families, font-sans |
--font-weight-* | font weights, font-bold |
--tracking-* | tracking-* |
--leading-* | leading-* |
--radius-* | rounded-*, including corners and sides |
--aspect-* | aspect-* |
--blur-* | blur-* and backdrop-blur-* presets |
--drop-shadow-* | drop-shadow-* presets |
--shadow-*, --inset-shadow-*, --text-shadow-* | shadow preset shapes. A custom shape carries its own colours, so shadow colour utilities only reach the built-in fallback |
--animate-* | animate-*. An unknown token like animate-wiggle reads var(--animate-wiggle); pair it with its @keyframes |
--breakpoint-* | the sm:–2xl: variants, and new ones like 3xl:. Variants gate before the cascade, so only an unconditional :root declaration moves them |
Overriding tokens
Tokens declared in a stylesheet follow the CSS cascade. A media query or a selector can override them:
@media (prefers-color-scheme: dark) {
:root {
--color-brand-500: #a78bfa;
}
}Tokens passed as cssVariables sit in a rule after every stylesheet:
| Competing declaration | Result |
|---|---|
Equally specific :root rule in CSS | cssVariables wins |
A more specific selector, :root:root | the selector wins |
| Declaration on the element | Element declaration wins |
Where this differs from Tailwind
- Colours reach gradients and shadows:
from-brand-500andshadow-brand-500read--color-brand-500likebg-brand-500does. - Same as Tailwind, but easy to miss: a gradient needs
bg-linear-*,bg-radialorbg-conic. Stops alone paint nothing. --color-red-500: initialfalls back to the built-in red instead of removingbg-red-500.- A bare
roundedkeeps its built-in value;rounded-smand the rest read the variable. --spacingneeds a unit. A bare number makesp-4compute pixels here, where a browser rejects the declaration.- An aliased token stays live.
--color-brand: var(--background)follows a subtree--backgroundoverride. In a browser,@theme inlineexists to arrange that.
Last updated on