Styling
How CSS reaches a node, what the stylesheet engine supports, and the Tailwind paths.
Four ways to put CSS on a node:
styleprop: inlineCSSPropertieson one node. Overrides the tag default.twprop: Tailwind classes on one node.<style>tags: CSS that travels inside the JSX.stylesheetsoption: full stylesheets matched againstclassNameandid.
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 | 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>CSS variables
The cssVariables option declares CSS custom properties without writing CSS. Each entry becomes one :root declaration. The leading -- is optional.
import { } from "takumi-js";
const = await (< ="card">Hello</>, {
: 1200,
: 630,
: [`.card { color: var(--brand) }`],
: { "--brand": "#5b21b6" },
});Every var() reads them, in stylesheets, in <style> tags, and in tw.
| Competing declaration | Result |
|---|---|
Equally specific :root rule in CSS | cssVariables wins |
| Declaration on the element | Element declaration wins |
The generated rule comes after every stylesheet, so a media query at equal specificity still loses. Override an entry with a more specific selector, such as :root:root.
Values containing ;, {, }, /* or !important 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" }Tailwind
Bring your stylesheet
Compile Tailwind with your bundler. Pass the generated CSS through stylesheets. With Vite, import it 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()],
});Native parser
The tw prop runs a built-in parser with no build step. It won't cover every Tailwind feature.
- Arbitrary values work.
- See the parser mapping for every supported class.
tw has 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 classNameUtilities future flag feeds className tokens to this parser too.
Tailwind at-rules
The engine parses Tailwind source stylesheets as-is. It reads these directives:
| At-rule | Reads as | Rejected |
|---|---|---|
@theme | its :root rule. Nested @keyframes register. Modifiers like reference and inline change nothing | prefix() |
@import "tailwindcss" | Preflight: drops the UA margins, list markers and heading font tweaks | any other import target |
@apply | the utilities' declarations, expanded in place, ! suffix included | variants like md: |
Other Tailwind directives (@utility, @custom-variant, @source, @plugin, @config) do not parse. Compile those with Tailwind, then bring the stylesheet.
The import line goes through stylesheets:
const image = await render(node, {
stylesheets: [`@import "tailwindcss";`],
});@apply expands inside stylesheet rules:
.card {
@apply mt-4 bg-brand-500;
}Unlayered stylesheet rules override tw utilities, and rules in a named @layer lose to them. That is Tailwind's own order: utilities are the last declared layer. Prefix a utility with ! to override an unlayered rule.
<div tw="bg-blue-500 p-4 rounded-lg">
<h1 tw="text-white text-2xl font-bold">Hello Tailwind!</h1>
</div>Utilities read CSS variables
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.
Declare tokens in a :root rule, in a Tailwind @theme block pasted as-is, or pass them as the cssVariables option.
:root {
--color-brand-500: #5b21b6;
--spacing-gutter: 2.5rem;
}<div tw="bg-brand-500 p-gutter" />Tokens declared in a stylesheet follow the CSS cascade. A media query or a selector can override them. Tokens passed as cssVariables sit after every stylesheet, so overriding one takes a more specific selector.
@media (prefers-color-scheme: dark) {
:root {
--color-brand-500: #a78bfa;
}
}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 |
Some things behave differently from Tailwind here:
- Colours reach gradients and shadows:
from-brand-500andshadow-brand-500read--color-brand-500likebg-brand-500does. - A gradient needs
bg-linear-*,bg-radialorbg-conic. Stops alone paint nothing, as in Tailwind. --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.
tw is a plain prop, so build it dynamically:
import clsx from "clsx";
const isError = true;
<div tw={clsx("p-4 rounded", isError ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700")}>
{isError ? "Something went wrong" : "Success!"}
</div>;Last updated on