@formancy/themes
v0.13.0
Published
Reference themes for formancy renderers — four unrelated looks targeting data-formancy-part hooks — with the builders' stylesheet and the admin's palette. CSS only.
Readme
@formancy/themes
Four reference themes, the builders' stylesheet and the admin's palette. CSS only — no JavaScript, no build step, no dependency on any renderer.
What they are for
The renderers ship zero CSS. That claim is easy to make and hard to trust, so these files exist to test it: the same markup and the same engine, dressed as four products that do not look related.
| | Blueprint | Dusk | Pop | Paper | | --- | --- | --- | --- | --- | | Feels like | a technical drawing | a dark app | a sticker sheet | a printed page | | Ground | white | dark | cream or white | paper or white | | Controls | boxes, 3px corners | inset wells, 10px corners | outlined, hard shadow | a line to write on | | Type | IBM Plex Sans | Space Grotesk | Archivo, heavy | Fraunces, spaced small capitals for labels | | Choices | drawn boxes and circles | lit wells | chips | hairline boxes, italic when chosen | | Invalid shown as | a bar on the leading edge | a ring around the control | a pink sticker | a vermilion line and an italic note | | Required shown as | an asterisk | the word "required" | a star | a footnote asterisk | | Active wizard step | a numbered, underlined word | a lit dot on a track | a yellow badge | a roman numeral, underlined |
If supporting any of them had required changing a component, the headless claim would be false. None did.
Use
import '@formancy/themes/blueprint.css'
import '@formancy/themes/dusk.css'
import '@formancy/themes/pop.css'
import '@formancy/themes/paper.css'<div data-formancy-theme="blueprint">…your form…</div>Every theme is scoped under [data-formancy-theme], so a page can load all of
them and switch by changing one attribute — which is what the playground and
the landing page do. workbench.css and room.css, below, are not themes and
are not scoped: each sets its variables on :root, for the whole page.
Each theme sets its own text colour, colour scheme and box-sizing on its
subtree, so it does not depend on the host page's reset. It does not paint the
page behind the form: Pop and Paper look best on the ground colour they
declare as --fm-ground, and fine on white.
Fonts are referenced, not bundled, and every theme falls back to a system stack.
formancy.ai serves its faces itself from Fontsource's packages rather than from a
font service, so opening a page tells no other site who opened it. Fontsource names
a variable face <Family> Variable, so Pop asks for Archivo then
Archivo Variable, and Paper for Fraunces then Fraunces Variable: either
spelling of the face, whichever way a page loads it.
The builders: workbench.css
workbench.css dresses both builders — the tree, the palettes, the inspector, the logic
panel, the prompt pane and the relay beside it, the scenarios, blocks, a datagrid's columns
and the translations — through the data-formancy-part hooks they emit, and every part
they emit has a rule here.
It is not a form theme and nothing depends on it. Change how it looks through its tokens
rather than with rules of your own:
| Token | What it colours |
|---|---|
| --wb-ink, --wb-muted | text, and quiet text |
| --wb-bench, --wb-panel, --wb-surface | the ground, a pane, a control or a card |
| --wb-rule | lines and borders |
| --wb-signal | the selection, focus and the primary action |
| --wb-derived | what the builder worked out rather than what was typed: a type, a path |
| --wb-invalid, --wb-ok, --wb-caution | broken, fine, and costing something |
| --wb-sans, --wb-mono | the two faces |
The room: room.css
room.css is the palette formancy.ai's self-hosted admin is drawn in — a dark ground, glass
panels, violet for what a person acts on and teal for what the system did — so that an
application meant to be the same room imports it rather than copying the values. The admin
imports it the same way. It is custom properties on :root and color-scheme: dark, and
nothing else: no rule for any element, and no face loaded. Your own stylesheet reads them.
import '@formancy/themes/workbench.css'
import '@formancy/themes/room.css' // after the workbench| Variable | What it is |
|---|---|
| --ground | the page, behind everything |
| --glass | a panel over the ground, which shows it through |
| --text, --muted | text, and quiet text |
| --edge, --edge-strong | a line, and a line that bounds a panel |
| --client | violet: what the person acts on |
| --server | teal: what the system did |
| --invalid, --ok, --warn | broken, fine, and half-way |
| --display | the face for headings: Archivo, then IBM Plex Sans |
It also sets every colour of workbench.css to the room's — --wb-ink, --wb-bench,
--wb-panel, --wb-surface, --wb-rule, --wb-muted, --wb-signal, --wb-derived,
--wb-invalid, --wb-ok and --wb-caution — and leaves its faces as the workbench sets
them. Those take effect only after workbench.css: both say :root, so whichever loads later
wins, and loaded first the builder keeps its light panes in the dark room.
The faces are not loaded. --display names Archivo and IBM Plex Sans, and the workbench's
faces are IBM Plex; a page loads them itself, as the admin does from Fontsource, or falls back
to the system's.
The names are what you write var() around, so they are this package's API, and a value is
the room: a change to one changes every page that imports it, and the changelog says so.
Right to left, and your bundler
Every file here that lays anything out — the four themes, and workbench.css, which styles
the two builders — says which side a thing is on in reading order: logical properties, and
:dir(rtl) where CSS has no logical form, as for an icon's background-position or the bar
an inset shadow draws down one side of a selected item. Set dir="rtl" on the page, or on
anything above the form or the builder, and the layout follows.
That holds only while :dir() reaches the browser as written. A bundler targeting
browsers older than the ones :dir() shipped in rewrites it. Vite's default target had
Lightning CSS turn every :dir(rtl) into :is(:lang(ar), :lang(he), …), which follows
the page's language instead: dir="rtl" on a page whose lang is not on that list
changes nothing, and the builder's marks stay on the left. Measured in this repository's
own playground. Set the CSS target to the browsers these stylesheets are written for —
each the first release with both :dir() and color-mix():
// vite.config.ts
export default defineConfig({
build: { cssTarget: ['chrome120', 'edge120', 'firefox113', 'safari16.4', 'ios16.4'] },
})A stylesheet loaded with a plain <link> is not rewritten and needs nothing.
Writing your own
Copy one and change it. The surface is data-formancy-part attributes, and every
part either renderer emits that has anything to draw is styled in every form theme
shipped here — apps/docs/src/themes.test.ts fails on one that is not — so a copy
already names them all. These are where to start, not the whole list:
- fields:
field(withdata-state="invalid|valid"),label,error,required-hint,radio-option,checkbox-option,static - repeaters:
repeater,repeater-legend,row - layouts:
layout-row,layout-column,layout-section,layout-section-heading,layout-table(withdata-columns),layout-tabs,tablist,tab,tabpanel - wizard and submit:
stepper,wizard-nav,submit - errors:
error-summary,error-summary-heading - rich text:
richtext-toolbar,richtext-button,richtext-preview,richtext,visually-hidden - files:
file-list,file-item,file-status,file-unavailable
visually-hidden has to be hidden by every theme — it is the only name the
rich text toolbar's buttons have, so it is moved off screen, never removed.
Controls are plain input, select and textarea inside [data-formancy-part="field"].
Nothing is a class, so nothing collides with your design system.
