Skip to content
AuthoringStyles

Styles

Style catalogue views with CSS imports, modules, assets and optional PostCSS.

Per-root imported stylesheets and binary assets compile into mokly-generated/. Mokly passes fragment-relative renderer and entry stylesheet links to your renderer; emit them in the document head to make imported CSS visible. Pages receive no automatic links. An optional PostCSS module processes imported CSS before CSS Modules and bundling; Tailwind v4 and autoprefixer setup is below.

Import CSS beside a screen

With imported CSS delivery, import plain CSS from an entry or from a component used by it. Mokly bundles a stylesheet for each entry that reaches CSS and passes its fragment-relative URL to your renderer after any configured stylesheets links. Emit those links in your renderer’s document head; they are not inserted into your HTML automatically. When a renderer imports its own theme stylesheet, its link comes before the entry’s stylesheet. A file delivered through the renderer is removed from the entry’s CSS bundle, even when the entry reaches it through another CSS @import. An entry that re-exports a screen or component defined in a helper delivers that entry’s CSS, not a bundle named after the helper. Independent entries using the same helper each deliver their own CSS.

tsx
import "./button.css";

export function Button() {
  return <button className="button">Continue</button>;
}

Entries that share a component each get their own stylesheet. Pages generate their entry’s stylesheet too, but a page callback receives no stylesheet links; add a relative link to its complete document yourself if needed.

Use CSS Modules and local assets

Name scoped styles *.module.css and import the default class map or a valid-identifier named class. Class names are derived from the file path, not the CSS content or unrelated entries. Same-file and global composes work when the composed class is defined earlier in the same file; forward references and composing from another file fail Build. Exported values start with the owning class, followed by composed names. Classes, IDs, keyframes and their animation references are scoped together; :global(...) keeps a selector global. Counter-style, view-transition, custom-property, grid-area and container names stay global. Apart from local names, Mokly passes module CSS through as authored: prefixes, fallback declarations, modern syntax, comments and url() forms are not rewritten by module scoping. @scope start and limit selectors work with local, global and nested selector forms, including class names containing to. The local names are exported; they can be composed by a later rule. Mokly checks that scoping changes no other authored CSS. The spacing inside a :global() or :local() list matters: whitespace on either side of a comma, including a line break, makes .wrap :global(.x, .y) match .y inside .x inside .wrap. Without whitespace, .wrap :global(.x,.y) matches one element with both classes. Empty list items disappear, but a wrapper with no selector at all fails Build with guidance to add one or remove the wrapper. A trailing comma and space in :global() or :local() can separate the following selector; the same spelling in :is() and similar selectors changes its meaning and fails Build. Remove that trailing comma to keep the selector attached. This follows the plugins used by css-loader and Vite. To match either class, write .wrap :global(.x), .wrap :global(.y) or .wrap :global(:is(.x, .y)) instead. If Build cannot safely scope an escaped selector name, remove leading zeros and write at most five hex digits followed by exactly one space. That space replaces the whitespace that ended the original escape; keep any further spacing or comment after it. If the character needs six hex digits, write the character itself. Use a plain stylesheet if it cannot be written that way. If Build says scoping would change more than local names, move that declaration to a plain stylesheet. Two current cases trigger that check: animation: grow-progress auto linear and quoted keyframe names such as @keyframes "pulse". animation-name: ease can still stay bare when a local keyframe named ease is renamed; use a plain stylesheet for this case until CSS Modules animation handling is improved. Local and remote @imports and relative url() values inside custom properties work the same way in a CSS Module as in plain CSS. For a local image-set() image, write url("./image.png"); Mokly keeps that form and copies the asset. Use ordinary CSS, local classes or custom properties instead of authored :import, :export or @value syntax. Mokly does not read Browserslist for CSS Modules. If you configure consumer PostCSS/autoprefixer, its target settings still apply before module naming.

tsx
import styles from "./card.module.css";

export function Card() {
  return <div className={styles.card}>Overview</div>;
}

For a TypeScript application, add ambient declarations for *.module.css (a default Readonly<Record<string, string>> class map) and *.css (side-effect imports) in your own .d.ts file. Mokly does not generate TypeScript declarations for consumer stylesheets.

css
.card {
  background: url("./cover.webp?size=small#preview") center / cover;
}

Mokly copies local font/image URLs to generated assets, mirrors their paths under mokly-generated/assets/, and preserves query/hash suffixes. Build writes the complete generated tree regardless of Git ignore rules. Check reads the Git index: either track the whole tree or leave it untracked and ignored. Broad ignore rules such as dist/ and node_modules/ can also match mirrored asset paths; account for them when staging the whole tree. For a partly tracked tree, Check lists tracked and untracked paths and gives both remedies. Keep asset filenames and directories URL-safe: no spaces, trailing dots or Windows device names. An npm scope following node_modules may begin with @; Mokly encodes that character in view links and serves the original package asset path. Use a stylesheet-relative url(), never /root/asset.png. Inside image-set() and -webkit-image-set(), wrap a local asset in url(): image-set(url("./cover.webp") 1x). Quoted local filenames cannot be copied or validated and fail Build; quoted HTTP(S), // and data: sources remain external. Remote HTTP(S), //, data: and #fragment URLs are unchanged. Link your own separately authored public assets normally. An imported url() asset that is already a public file under mockupsDir fails rather than silently hiding its original route; keep it separate or move its source outside mockupsDir. Remote CSS @imports stay external and are not fetched or inventoried; valid prelude imports appear before local rules in the bundle. CSS @imports of packages select the style export condition or style main field ahead of your JavaScript conditions and main fields, so @import "tailwindcss" resolves to CSS even with custom module resolution.

Add Tailwind v4 and autoprefixer

Install PostCSS and the plugins in the consumer repository, then name the config-relative module in mokly.config.ts. Mokly does not install, discover or pin these plugins for you.

sh
npm install -D postcss tailwindcss @tailwindcss/postcss autoprefixer
ts
import { defineConfig } from "@mokly/mokly";

export default defineConfig({
  roots: [{ dir: "src" }],
  mockupsDir: "docs/mockups/generated",
  postcss: "postcss.config.mjs",
});
js
// postcss.config.mjs
import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";

export default {
  plugins: [
    tailwindcss({ base: import.meta.dirname, optimize: false }),
    autoprefixer({ overrideBrowserslist: ["Safari 14"] }),
  ],
};

Mokly bundles local PostCSS config imports for reloading, but loads package plugins unbundled from your repository so their native bindings and package-relative files continue to work. Tailwind’s default base is the working directory of the Mokly process, and its default optimize changes with NODE_ENV; pin both as above for stable bytes. Also use source(none) and explicit @source paths below to avoid scanning the public mockups directory (even when launched from another cwd). Tailwind recursively inlines local @imports from disk, so a nested import of renderer-owned CSS in an entry can bypass Mokly’s pruning and fails Build when Tailwind reports it. Import shared CSS only from the renderer or directly from the entry stylesheet so Mokly can prune it. For Tailwind context in a component or module (@apply), use @reference instead of @import: when the renderer already delivers Tailwind, a direct entry @import "tailwindcss" is pruned.

For styles/catalogue.css beneath the repository root, opt into exactly the sources whose class names belong in this catalogue. Import the CSS from an entry or its component and use its generated stylesheet. Import the theme and utilities layers separately to leave existing screen defaults intact: the full tailwindcss import also includes a CSS reset. The example sets overrideBrowserslist: ["Safari 14"] on autoprefixer so user-select: none produces a WebKit prefix.

css
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
@source "../src";

.control {
  user-select: none;
}

If you intentionally use Tailwind’s automatic discovery instead, use @source not "../docs/mockups"; to exclude direct scans in this layout. Tailwind can still report a broad scan of the parent docs/ directory whose glob reaches mockupsDir; when the whole docs/ tree can be excluded, use @source not "../docs"; instead. Otherwise keep the source(none) example above and explicitly include only authored trees. The build fails if a plugin scans any otherwise-public file under mockupsDir, rather than quietly hiding them. @source paths are relative to the stylesheet, not the shell’s working directory.

Keep runtime styles in the renderer

Runtime CSS-in-JS such as styled-components and Emotion still belongs to the consumer renderer: wrap screens in the same providers and collect any runtime-generated styles in the returned complete document. This integration is still needed even when you import CSS. For styled-components in a "type": "module" repository, moduleResolution.mainFields can be set to ["module", "main"] to prefer a package’s ESM entry if your consumer requires it; esbuild’s Node default prefers main instead, and Mokly does not set these fields for your app.

Other styling workflows

Sass, Less, Stylus, non-PostCSS build-time CSS-in-JS transforms, Vite config and Vite plugins are outside this workflow. JavaScript asset imports with a file loader cannot supply portable catalogue URLs: use dataurl or binary for those imports instead. moduleResolution.loaders[".css"] = "empty" deliberately opts out of both plain CSS and CSS Modules; ".module.css": "empty" opts out only of CSS Modules.