Appearance
FormDSL CSS Theming Reference
For LLMs generating styled FormDSL forms. This document covers the complete CSS theming system for
@formdsl/solidand@formdsl/react. Both frameworks share the same stylesheet.
Quick Start
tsx
// 1. Import the stylesheet (once, in your app entry)
import '@formdsl/solid/styles'
// or for React:
import '@formdsl/react/styles'
// 2. Override variables in your own CSS
.form {
--fb-primary: #e11d48;
--fb-radius: 0;
}That's it. Every --fb-* variable is overridable, and user styles automatically win over FormDSL defaults thanks to the CSS layer system.
CSS Layer System
FormDSL uses three ordered CSS layers:
@layer normalize, formbase.reset, formbase.base, formbase.theme;| Layer | Purpose |
|---|---|
formbase.reset | Normalize form elements (box-sizing, font inheritance) |
formbase.base | Structural styles (layout, spacing, grid, flex) |
formbase.theme | Visual styles (colors, borders, shadows, radii) |
Key rule: Any CSS you write outside a @layer block automatically overrides all three layers. You never need !important.
css
/* This wins over everything in formbase.* layers */
.form .field__input {
border: 2px solid navy;
}Theme Variables
All variables are defined on .form and .form-layout inside @layer formbase.theme. Override them on .form, a wrapper class, or :root.
Colors
| Variable | Light Default | Description |
|---|---|---|
--fb-primary | var(--blue-6) | Primary brand color (buttons, links, accents) |
--fb-primary-hover | var(--blue-7) | Primary hover state |
--fb-primary-active | var(--blue-8) | Primary active/pressed state |
--fb-error | var(--red-6) | Error state color |
--fb-error-light | var(--red-1) | Error background tint |
--fb-warning | var(--yellow-6) | Warning color |
--fb-warning-light | var(--yellow-1) | Warning background tint |
--fb-success | var(--teal-6) | Success color |
--fb-success-light | var(--teal-1) | Success background tint |
--fb-info | var(--blue-6) | Info color |
--fb-info-light | var(--blue-1) | Info background tint |
Text
| Variable | Light Default | Description |
|---|---|---|
--fb-text | var(--gray-8) | Body text |
--fb-text-muted | var(--gray-6) | Descriptions, placeholders, secondary text |
--fb-text-heading | var(--gray-9) | Section titles, headings |
Surfaces & Borders
| Variable | Light Default | Description |
|---|---|---|
--fb-surface | var(--gray-0) | Input backgrounds, cards, panels |
--fb-surface-alt | var(--gray-1) | Alternating/highlighted surfaces |
--fb-border | var(--gray-4) | Input borders, dividers |
--fb-border-light | var(--gray-3) | Subtle borders (section outlines, repeat items) |
Spacing
All spacing maps to Open Props size tokens.
| Variable | Open Props | Approx. Value |
|---|---|---|
--fb-space-xs | var(--size-1) | 0.25rem |
--fb-space-sm | var(--size-2) | 0.5rem |
--fb-space-md | var(--size-3) | 0.75rem |
--fb-space-lg | var(--size-5) | 1.25rem |
--fb-space-xl | var(--size-7) | 1.75rem |
Typography
| Variable | Default | Description |
|---|---|---|
--fb-font | var(--font-sans) | Font family |
--fb-font-size | var(--font-size-1) | Base font size (~1rem) |
--fb-font-size-sm | var(--font-size-0) | Small text (~0.875rem) |
--fb-font-size-lg | var(--font-size-2) | Large text (~1.125rem) |
--fb-font-weight-normal | var(--font-weight-4) | Normal weight (400) |
--fb-font-weight-medium | var(--font-weight-5) | Medium weight (500) |
--fb-font-weight-bold | var(--font-weight-6) | Bold weight (600) |
Radius, Focus & Transitions
| Variable | Default | Description |
|---|---|---|
--fb-radius | var(--radius-2) | Default border radius (~0.375rem) |
--fb-radius-sm | var(--radius-1) | Small radius (~0.25rem) |
--fb-focus-ring | 0 0 0 3px var(--blue-3) | Focus ring box-shadow |
--fb-focus-ring-error | 0 0 0 3px var(--red-3) | Error focus ring |
--fb-transition | 150ms var(--ease-2) | Default transition timing |
Dark Mode
FormDSL supports dark mode via two mechanisms:
1. System Preference (automatic)
Uses @media (prefers-color-scheme: dark) with an opt-out:
css
/* Active when system is dark AND html doesn't have data-theme="light" */
:root:not([data-theme="light"]) .form { /* dark overrides */ }2. Manual Toggle
Set data-theme="dark" on <html>:
html
<html data-theme="dark">css
:root[data-theme="dark"] .form { /* dark overrides */ }Dark Mode Variable Values
| Variable | Dark Value |
|---|---|
--fb-primary | var(--blue-4) |
--fb-primary-hover | var(--blue-3) |
--fb-primary-active | var(--blue-2) |
--fb-error | var(--red-4) |
--fb-error-light | color-mix(in srgb, var(--red-5) 15%, var(--gray-8)) |
--fb-warning | var(--yellow-4) |
--fb-warning-light | color-mix(in srgb, var(--yellow-5) 15%, var(--gray-8)) |
--fb-success | var(--teal-4) |
--fb-success-light | color-mix(in srgb, var(--teal-5) 15%, var(--gray-8)) |
--fb-info | var(--blue-4) |
--fb-info-light | color-mix(in srgb, var(--blue-5) 15%, var(--gray-8)) |
--fb-text | var(--gray-2) |
--fb-text-muted | var(--gray-4) |
--fb-text-heading | var(--gray-1) |
--fb-surface | var(--gray-8) |
--fb-surface-alt | var(--gray-7) |
--fb-border | rgba(255, 255, 255, 0.15) |
--fb-border-light | rgba(255, 255, 255, 0.1) |
--fb-focus-ring | 0 0 0 3px color-mix(in srgb, var(--blue-4) 40%, transparent) |
--fb-focus-ring-error | 0 0 0 3px color-mix(in srgb, var(--red-4) 40%, transparent) |
Built-in Themes
default
The standard theme. Clean, modern look with bordered sections, rounded inputs, and blue primary color.
tsx
<Form id="my-form" theme="default" ...>printed
Paper-like aesthetic with strong contrast and minimal chrome. Activated via the theme prop:
tsx
<Form id="my-form" theme="printed" ...>CSS class: .form--theme-printed
Key differences from default:
- Inputs: underline-only (no full border), light indigo background
- Sections: dark horizontal dividers, no border/background
- Section titles: uppercase, letter-spaced, dotted underline
- Radius: all 0 (sharp corners)
- Primary color: indigo (
var(--indigo-7)) - Text: maximum contrast (no greys for muted text)
- Buttons: sharp-cornered, solid borders
- Sidebar: sharp right border, no rounding
Section Variants
Set via the variant prop on <Section>:
| Variant | Class | Effect |
|---|---|---|
default | .form__section | Bordered card with alt-surface background |
plain | .form__section--plain | No border, no background, no padding |
compact | .form__section--compact | Reduced padding, smaller title |
highlighted | .form__section--highlighted | Left border accent + tinted primary background |
tsx
<Section title="Notes" variant="highlighted">Horizontal Field Layout
Set fieldLayout="horizontal" on a <Section> to place labels and inputs side by side (35% / 65% grid). Falls back to stacked on mobile.
tsx
<Section title="Details" fieldLayout="horizontal">CSS class: .form__section--horizontal
Form Modes
Compact Mode
Reduced spacing throughout the form. Set via compact prop on <Form>:
tsx
<Form id="my-form" compact ...>CSS class: .form--compact
Effects:
- Row grid columns shrink to
minmax(min(100%, 120px), 1fr) - Field wrappers constrain to
max-contentwidth
Form Width Presets
Set via width prop on <Form>:
tsx
<Form id="my-form" width="lg" ...>| Value | Width |
|---|---|
'xs' | 480px |
'sm' | 640px |
'md' | 800px (default) |
'lg' | 1000px |
'xl' | 1200px |
Preview Mode
Read-only display. Inputs become static text, interactivity is disabled.
CSS class: .form--preview
Effects:
- Inputs lose borders and background
- Answered fields highlighted with
var(--yellow-1)background - Pointer events disabled on interactive elements
Key CSS Selectors
Form Root
| Selector | Element |
|---|---|
.form | Main form container |
.form-layout | Full-page layout wrapper (flex column, min-height 100vh) |
.form-layout__header | Sticky top header bar |
.form-layout__body | Main content area (flex row with sidebar) |
.form-layout__content | Form content column |
.form-layout__footer | Bottom footer bar |
Sections
| Selector | Element |
|---|---|
.form__section | Section container |
.form__section-title | Section heading (has primary-colored bottom border) |
.form__section-body | Section content wrapper |
.form__section-icon | Optional icon before section title |
Fields & Inputs
| Selector | Element |
|---|---|
.field | Field wrapper (label + input + error) |
.field__label | Field label text |
.field__required | Required asterisk (*) |
.field__description | Help text below label |
.field__input | Text/email/number/date inputs, textareas |
.field__input--textarea | Textarea variant |
.field__input--select | Select dropdown |
.field__input--error | Input in error state |
.field__error | Error message text |
.field__input-wrapper | Container for input + prefix/suffix adornments |
.field__input-adorn | Prefix or suffix adornment |
.field__checkbox-label | Boolean checkbox + label row |
.field__radio-group | Radio option container |
.field__radio-option | Single radio option row |
.field__checkbox-group | Multi-checkbox container |
.field__checkbox-option | Single checkbox option row |
.field__tag-pill | Tag/multi-select pill |
Buttons & Actions
| Selector | Element |
|---|---|
.form__actions | Bottom action bar (submit/save buttons) |
.form__submit | Primary submit button |
.form-header__submit | Header submit button (sidebar/stepper layout) |
.form-header__cta | Header secondary CTA button |
.form-nav-actions | Stepper/tabs bottom navigation bar |
.form-nav-actions__back | Back button |
.form-nav-actions__next | Next/continue button |
.form-nav-actions__save | Save button (in nav bar) |
.form-nav-actions__submit | Submit button (in nav bar, last section) |
Banners & Cards
| Selector | Element |
|---|---|
.banner | Alert banner container |
.banner--info | Info banner (blue) |
.banner--warning | Warning banner (yellow) |
.banner--error | Error banner (red) |
.banner--success | Success banner (green) |
.card | Card container (shadow, padding) |
.info-box | Inline info/warning box |
.info-box--info | Info variant |
.info-box--warning | Warning variant |
Repeat Groups
| Selector | Element |
|---|---|
.form__repeat | Repeat group container |
.form__repeat-item | Single repeat item card |
.form__repeat-item-header | Item header (label + actions) |
.form__repeat-item-remove | Remove item button |
.form__repeat-add | "Add item" button (dashed border) |
.form__repeat-undo | Undo-remove notification bar |
Navigation
| Selector | Element |
|---|---|
.form-sidebar | Sidebar navigation panel |
.form-sidebar__item | Sidebar section link |
.form-sidebar__item--active | Active section (blue tint) |
.form-sidebar__item--complete | Completed section (green indicator) |
.form-sidebar__progress-ring | Circular progress indicator |
.form-stepper | Stepper navigation bar |
.form-stepper__step | Single step item |
.form-stepper__step--current | Current step (blue circle) |
.form-stepper__step--completed | Completed step (green circle) |
.form-stepper__number | Step number circle |
.form-stepper__title | Step label text |
.form-tabs | Tab navigation container |
.form-tabs__tab | Single tab button |
.form-tabs__tab--active | Active tab (gray background, border) |
.mobile-nav | Mobile dropdown navigation (visible at <= 640px) |
.mobile-nav__trigger | Current section dropdown trigger |
.mobile-nav__dropdown | Dropdown menu overlay |
Layout Helpers
| Selector | Element |
|---|---|
.form__row | Grid row (auto-fit columns) |
.form__row--gap-sm | Row with small gap |
.form__row--gap-lg | Row with large gap |
.form__stack | Vertical flex container |
.form__divider | Horizontal rule |
.form-progress | Progress bar container |
.form-progress__bar | Progress bar fill |
Override Recipes
Brand Color Swap
css
.form {
--fb-primary: #e11d48;
--fb-primary-hover: #be123c;
--fb-primary-active: #9f1239;
--fb-focus-ring: 0 0 0 3px rgba(225, 29, 72, 0.3);
}Custom Font Family
css
.form,
.form-layout {
--fb-font: 'Inter', system-ui, sans-serif;
}Rounded vs Sharp Corners
css
/* Fully rounded */
.form {
--fb-radius: 9999px;
--fb-radius-sm: 9999px;
}
/* Sharp (no rounding) */
.form {
--fb-radius: 0;
--fb-radius-sm: 0;
}Dense Spacing
css
.form {
--fb-space-xs: 0.125rem;
--fb-space-sm: 0.25rem;
--fb-space-md: 0.5rem;
--fb-space-lg: 0.75rem;
--fb-space-xl: 1rem;
}Scoped Theme (Class on a Wrapper)
css
.my-embedded-form .form {
--fb-primary: #7c3aed;
--fb-surface: #faf5ff;
--fb-surface-alt: #f3e8ff;
--fb-border: #c4b5fd;
--fb-border-light: #ddd6fe;
}tsx
<div class="my-embedded-form">
<Form id="my-form" ...>
...
</Form>
</div>Dark-Mode-Only Overrides
css
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .form {
--fb-primary: #a78bfa;
--fb-surface: #1e1b4b;
}
}
:root[data-theme="dark"] .form {
--fb-primary: #a78bfa;
--fb-surface: #1e1b4b;
}Custom Section Title Style
css
.form .form__section-title {
border-bottom: none;
background: var(--fb-primary);
color: white;
padding: var(--fb-space-sm) var(--fb-space-md);
border-radius: var(--fb-radius) var(--fb-radius) 0 0;
margin: 0 0 var(--fb-space-md);
}Muted Submit Button
css
.form .form__submit {
background: var(--fb-text);
border-radius: 0;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.form .form__submit:hover {
background: var(--fb-text-heading);
}Responsive Behavior
FormDSL uses a single breakpoint at 640px.
What changes at <= 640px:
| Component | Behavior |
|---|---|
.form | Padding reduces to --fb-space-sm |
.form__row | Collapses to single column (grid-template-columns: 1fr) |
.form-sidebar | Hidden; replaced by .mobile-nav dropdown |
.form-layout__header | Stacks vertically, buttons go full-width |
.form-stepper | Circle + connector sizes shrink |
.form-tabs | Tabs get smaller padding, scroll horizontally |
.form__section--horizontal | Falls back to stacked (label above input) |
Forcing Single Column on Desktop
css
.form .form__row {
grid-template-columns: 1fr;
}Tips & Anti-patterns
Do:
- Override
--fb-*variables instead of writing raw property overrides - Put custom CSS outside any
@layerto automatically win specificity - Override both
--fb-primary-hoverand--fb-primary-activewhen changing--fb-primary - Set variables on both
.formand.form-layoutwhen customizing layout-level colors - Duplicate dark-mode overrides in both the
@mediablock and[data-theme="dark"]rule for full coverage
Don't:
- Use
!important— the layer system makes it unnecessary - Override variables on
:rootif you have multiple forms with different themes on one page (scope to a wrapper class instead) - Forget that spacing variables affect the entire form — changing
--fb-space-mdaffects fields, rows, sections, and banners - Override structural
.baselayer properties (grid, flex, display) with variable changes — variables only control the.themelayer values - Target internal class names that aren't listed above — they may change between versions
Stylesheet Entry Point
The SDK bundles all styles through a single import:
ts
// SolidJS
import '@formdsl/solid/styles'
// React
import '@formdsl/react/styles'This loads (in order):
form.css— variables, reset, base layout, themesidebar.css— sidebar navigation + header layoutstepper.css— stepper navigation + nav actionstabs.css— tab navigationreview.css— submission review modemobile.css— mobile navigation dropdownthemes/printed.css— printed theme overrides
All styles are scoped to FormDSL class names (.form, .field__*, .form-sidebar, etc.) and will not leak into your application.