Takumi

Styling

How CSS reaches a node, what the stylesheet engine supports, and the Tailwind paths.

Four ways to put CSS on a node:

  • style prop: inline CSSProperties on one node. Overrides the tag default.
  • tw prop: Tailwind classes on one node.
  • <style> tags: CSS that travels inside the JSX.
  • stylesheets option: full stylesheets matched against className and id.

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:

SelectorsAt-rulesProperties
class, id, descendant@keyframes, @media, @supportscustom 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 declarationResult
Equally specific :root rule in CSSvariables wins
Declaration on the elementElement 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.

og.tsx
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],
    },
  );
}
vite.config.ts
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:

NamespaceUtilities
--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-500 works like bg-brand-500.
  • A gradient needs bg-linear-*, bg-radial or bg-conic. Stops alone paint nothing, as in Tailwind.
  • --color-red-500: initial falls back to the built-in red instead of removing bg-red-500.
  • A bare rounded keeps its built-in value; rounded-sm and the rest read the variable.
  • --spacing needs a unit. A bare number makes p-4 compute 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

On this page