Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@dldc/hono-css

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 the common datatypes to TailwindCSS's default design tokens (colors, spacing, typography, borders, shadows), so css.raw({ backgroundColor: "blue-600", padding: "4" }) just works with Tailwind-style values.

Installation

This is a Deno/JSR package. Add it to your deno.json:

{
  "imports": {
    "@dldc/hono-css": "jsr:@dldc/hono-css@^1.0.0"
  }
}

It relies on hono/css at runtime, so it's intended for server-rendered Hono apps.


How it works

The library is built around a few small concepts that compose together:

1. Datatypes

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[];
}

2. Mappers

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.

3. Properties map

The properties map maps each camelCase JS property name to a CSS property (which can expand to several, e.g. borderRadiusStartborder-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
}>;

4. Config & the engine

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);"

5. Code generation

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 — a CssObj type whose properties and value unions are derived from cssProperties (plus your injectHead type imports).
  • css.gen.ts — a css object 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.ts

Quick start

Create 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)

The CSS object syntax

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" } } } },
});

Design tokens (CSS variables)

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.

Injecting the root values

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 module

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 a global slot applied everywhere).
    • allowBracketRaw (default true) — allow [arbitrary value].
    • strict (default false) — disable the permissive anyString mapper.
  • createCommonProperties(datatypes) — builds the ~200-property map.
  • commonVariableType — the CssDatatype for CSS variable values.
  • cm.* — the ready-made value mappers.
  • s.* — static value tables.

Tailwind preset

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.


Custom mappers

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.


Repository layout

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

Tasks

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

About

Type-safe css object function based on hono's css feature

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages