Typed, headless CSS-in-JS for Hono JSX (server-only).
@dldc/hono-css lets you write CSS as plain (typed) objects — with full autocompletion and type-checking on every property and value — and
turn them into real CSS strings and Hono CSS classes. It is headless: it describes what a property accepts (a datatype) and
serializes a JS object into CSS, without any runtime style-injection logic of its own. Rendering is delegated to
hono/css — the css() helper returns a class name and registers the style, which you then output
with the <Style /> component in your layout:
import { Style } from "hono/css";
import { Layout } from "./layout";
export const App = () => (
<Layout>
<Style /> {/* renders every registered style */}
<div className={myClass}>…</div>
</Layout>
);css.raw() is the escape hatch: it returns a plain CSS string (no class registration), for when you want to inline or assemble CSS
yourself.
It ships with:
common— a hand-written database of ~100 CSS datatypes and ~200 properties, plus ready-made value mappers and static value tables.presets/tailwind— a preset that wires thecommondatatypes to TailwindCSS's default design tokens (colors, spacing, typography, borders, shadows), socss.raw({ backgroundColor: "blue-600", padding: "4" })just works with Tailwind-style values.
This is a Deno/JSR package. Add it to your deno.json:
It relies on
hono/cssat runtime, so it's intended for server-rendered Hono apps.
The library is built around a few small concepts that compose together:
A datatype describes the set of values a CSS property can take. It's a list of mappers (checked in order) plus an optional table of static values.
interface CssDatatype {
mappers: CssDatatypeMapper[];
staticValues?: Record<string, string> | string[];
}A mapper receives the raw JS value, and either converts it to a CSS string or defers to the next mapper (via next) if it doesn't
handle that value.
interface CssDatatypeMapper {
mapper: (value: unknown, next: CssNextMapper) => string;
types: string[]; // used by the code generator to emit the TS union
}Common mappers (in common/mappers.ts, namespace cm.*) include: raw ([any value]), cssVar / cssVarRef, cssExpr, px, em,
rem, percent, number, anyString, and token mappers built with datatypeMapperFromTokens.
The properties map maps each camelCase JS property name to a CSS property (which can expand to several, e.g. borderRadiusStart →
border-start-start-radius
border-end-start-radius) and the datatype that validates its values.
type CssPropertiesMap = Record<string, {
toCss: string | string[];
datatype?: CssDatatype; // if omitted, any string is passed through as-is
}>;A config ties the properties map, the variable datatype, and the codegen output together. createEngine(config) is the runtime that
serializes a CSS object into a CSS string.
const engine = createEngine(
defineConfig({
outdir: "./css",
cssProperties, // CssPropertiesMap
cssVariableType, // CssDatatype (used for `vars`)
codegen: { types: { injectHead: [...] } },
}),
);
engine.cssRaw({ display: "flex", padding: 4 });
// "display: flex;\npadding: calc(var(--spacing)*4);"Because every property and value is known ahead of time, the library generates two files from your config so you get full type-checking at the call site:
types.gen.ts— aCssObjtype whose properties and value unions are derived fromcssProperties(plus yourinjectHeadtype imports).css.gen.ts— acssobject bound to your config's engine.
Generate them with a small codegen.ts script that imports the codegen helper and your config:
// codegen.ts
import { codegen } from "@dldc/hono-css/codegen";
import config from "./css.config.ts";
await codegen("./css.config.ts", config);deno run -A ./codegen.tsCreate a config that uses the common datatypes/properties:
// css.config.ts
import { defineConfig } from "@dldc/hono-css";
import { commonVariableType, createCommonProperties } from "@dldc/hono-css/common/properties";
import { createCommonDatatypes } from "@dldc/hono-css/common/datatypes";
const datatypes = createCommonDatatypes({
mappers: { scale: [spacingMapper] }, // your spacing token mapper
});
export default defineConfig({
outdir: "./css",
cssProperties: createCommonProperties(datatypes),
cssVariableType: commonVariableType,
});Then import the generated css and write styles as typed objects:
import { css } from "./css/css.gen.ts";
const styles = css.raw({
display: "flex",
gap: "4",
paddingY: "6",
backgroundColor: "neutral-100",
selectors: { "&:hover": { opacity: 0.8 } },
});The generated css and globalCss objects each expose three render methods, for six total ways to output CSS:
| API | Returns | Notes |
|---|---|---|
css(...objs) |
Promise<string> |
Registers the class with hono/css |
css.raw(...objs) |
string |
Plain CSS string, no registration |
css.debug(...objs) |
Promise<string> |
Logs the CSS, then registers |
globalCss / globalCss.raw / globalCss.debug |
— | Global styles (selectors, supports, layers) |
Properties use camelCase names. The object also supports special keys:
css.raw({
// plain CSS properties (typed via codegen)
display: "flex",
gap: "4",
// CSS variables — values go through the config's cssVariableType
vars: { "--space": "1rem" },
// custom properties emitted as-is
custom: { "scroll-behavior": "smooth" },
// nested selectors
selectors: { "&:hover": { opacity: 0.5 } },
// at-rules
media: { "@media (min-width: 640px)": { display: "grid" } },
supports: { "@supports (display: grid)": { position: "relative" } },
});globalCss additionally supports @layer:
globalCss.raw({
layers: { "@layer base": { selectors: { "*": { boxSizing: "border-box" } } } },
});Tokens are represented as CssVar objects created with cssVar(name, rootValue?):
import { cssVar } from "@dldc/hono-css";
const blue = cssVar("color-blue-500", "#3b82f6");
blue.name; // "--color-blue-500"
blue.var; // "var(--color-blue-500)"
String(blue); // "var(--color-blue-500)"A CssVar can be used anywhere a CSS value is expected (it's handled by the cssVarRef mapper), and token maps drive the Tailwind preset.
cssVar's second argument is the token's initial value, but it isn't injected automatically — you have to declare it yourself so the
variable is actually defined. The cssVarsToRootVars(tokens) helper turns a list of CssVars into the vars record for a :root, :host
block (skipping tokens that have no rootValue):
import { cssVarsToRootVars } from "@dldc/hono-css";
const myVars = [
cssVar("color-blue-500", "#3b82f6"),
cssVar("spacing", "0.25rem"),
];
globalCss.raw({
selectors: {
":root, :host": {
vars: cssVarsToRootVars(myVars),
},
},
});
// :root, :host {
// --color-blue-500: #3b82f6;
// --spacing: 0.25rem;
// }You can combine it with extra, derived variables:
vars: {
...cssVarsToRootVars(myVars),
"--default-font-family": "var(--font-sans)",
},The common/ folder provides a reusable database of CSS knowledge:
createCommonDatatypes(options)— builds ~100 datatypes. Options:mappers— inject design-token mappers into whole families of properties (color,scale,fontSize,fontWeight,lineHeight,fontFamily,letterSpacing,borderRadius,borderWidth,shadow).additionalMappers— inject mappers into any individual datatype (plus aglobalslot applied everywhere).allowBracketRaw(defaulttrue) — allow[arbitrary value].strict(defaultfalse) — disable the permissiveanyStringmapper.
createCommonProperties(datatypes)— builds the ~200-property map.commonVariableType— theCssDatatypefor CSS variable values.cm.*— the ready-made value mappers.s.*— static value tables.
createTailwindPreset() composes common with TailwindCSS's default design tokens, returning a ready-to-use
{ datatypes, properties, cssVariableType }:
// css.config.ts
import { defineConfig } from "@dldc/hono-css";
import { createTailwindPreset } from "@dldc/hono-css/presets/tailwind";
const preset = createTailwindPreset();
export default defineConfig({
outdir: "./css",
cssProperties: preset.properties,
cssVariableType: preset.cssVariableType,
codegen: {
types: {
injectHead: [
`import type * as tokens from "@dldc/hono-css/presets/tailwind/tokens";`,
`import type { CssVarValue, RawValue } from "@dldc/hono-css/common/mappers";`,
],
},
},
});After generating, Tailwind-style values type-check and serialize:
import { css } from "./css/css.gen.ts";
css.raw({
backgroundColor: "blue-600", // var(--color-blue-600)
paddingY: "2", // var(--spacing-2)
borderRadius: "lg", // var(--radius-lg)
fontSize: "sm", // var(--font-size-sm)
fontWeight: "semibold", // var(--font-weight-semibold)
boxShadow: "md", // var(--shadow-md)
});The tokens are also exported directly from @dldc/hono-css/presets/tailwind/tokens (colors, spacing, fontSize, fontWeight,
lineHeight, fontFamily, letterSpacing, borderRadius, borderWidth, boxShadow) if you want to extend or inspect them.
A mapper is just a function that either owns a value or defers. This is how you plug your own design system in:
import type { CssDatatypeMapper } from "@dldc/hono-css";
const spacingMapper: CssDatatypeMapper = {
mapper(value, next) {
if (typeof value === "number") {
return value === 0 ? "0" : `calc(var(--spacing)*${value})`;
}
return next(value);
},
types: ["number"],
};Use datatypeMapperFromTokens(values, typeName) to turn a Record<string, CssVar> into a token-lookup mapper, or write one from scratch as
above.
src/ Core runtime: engine, config, cssVar, codegen, templates
common/ CSS knowledge: datatypes, mappers, properties, static values
presets/ Ready-made configs (tailwind)
examples/ Runnable examples (basic, tailwind) — each has a codegen.ts that calls `codegen()`
tests/ Test suite
deno task check # fmt + check + lint
deno task test:run # run tests
deno task example:basic:run # regenerate basic example codegen
deno task example:tailwind:run # regenerate tailwind example codegen
{ "imports": { "@dldc/hono-css": "jsr:@dldc/hono-css@^1.0.0" } }