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>

Tailwind

Bring your stylesheet

Compile Tailwind with your bundler, then pass the CSS through stylesheets. On Vite, import it with the ?inline query.

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, 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.

Utilities lose to any rule from stylesheets, the way @layer utilities loses to unlayered CSS. Mark the utility with ! to win instead.

<div tw="bg-blue-500 p-4 rounded-lg">
  <h1 tw="text-white text-2xl font-bold">Hello Tailwind!</h1>
</div>

Theme tokens

Pass a theme keyed by CSS custom property name to give the parser your own tokens. The namespace picks which utilities the token reaches, the same way it does in Tailwind.

const image = await render(<div tw="bg-brand-500 p-gutter" />, {
  width: 1200,
  height: 630,
  theme: {
    "--color-brand-500": "#5b21b6",
    "--spacing-gutter": "2.5rem",
  },
});
NamespaceUtilities
--color-*every color utility: bg-*, text-*, border-*, gradient stops
--spacing-*every length utility: p-*, m-*, w-*, gap-*
--container-*max-w-*
--text-*font sizes, text-xl
--font-*font families, font-sans
--font-weight-*font weights, font-bold
--tracking-*tracking-*
--leading-*leading-*
--radius-*rounded-*
--shadow-*shadow-*
--drop-shadow-*drop-shadow-*
--text-shadow-*text-shadow-*
--blur-*blur-*
--aspect-*aspect-*
--animate-*animate-*

Tailwind's other namespaces have no utility to reach here: --breakpoint-*, --ease-*, --inset-shadow-*, --perspective-*, --zoom-*, --tab-size-*.

Naming a built-in token replaces it, so --color-red-500 changes what bg-red-500 paints. A value of initial removes a token. --color-*: initial clears the whole namespace, built-in palette included.

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