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>Structured variables
The variables 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 | variables wins |
| Declaration on the element | Element declaration wins |
!important declaration | !important declaration wins |
Values containing ;, {, }, /* or !important are dropped. They could escape the generated rule.
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.
To reset those defaults, pass Preflight through stylesheets, wrapped in a layer so it stays below your utilities:
import preflight from "tailwindcss/preflight.css?inline";
const image = await render(node, {
stylesheets: [`@layer base { ${preflight} }`],
});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>Theme 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.
Declare tokens in a :root rule, or pass them as structured variables.
:root {
--color-brand-500: #5b21b6;
--spacing-gutter: 2.5rem;
}<div tw="bg-brand-500 p-gutter" />Theme tokens follow the CSS cascade. A media query or a selector can override them.
@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-* |
Some things behave differently from Tailwind here:
- Shadows, filters, transforms and animations merge across utilities before the cascade runs, so
--shadow-*,--drop-shadow-*,--text-shadow-*,--blur-*and--animate-*are not read. Gradient stops do read--color-*:from-brand-500works likebg-brand-500. - 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.
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