Skip to main content

Styling

Use theme tokens for color, type, and radius; the appearance prop for Tailwind; or stable fillo-* classes for plain CSS.

Which stylesheet

Every renderer ships its own CSS — import one file before anything else here applies. Pick the import that matches your setup:

two artifacts
// Choose exactly one. Modern CSS and Tailwind v4:
import "@usefillo/react/styles.css";

// Tailwind v3 or a reset-heavy host instead:
// import "@usefillo/react/styles.unlayered.css";
// The same two choices exist on @usefillo/dom.
Your setupImportWhy
Tailwind v4, or modern layered CSS@usefillo/react/styles.cssDefaults live in @layer components, so any unlayered rule or utility of yours wins automatically.
Tailwind v3, or reset-heavy global CSS@usefillo/react/styles.unlayered.cssSame rules without cascade layers. Use it when layered defaults lose to your unlayered reset and the form renders bare.
Fully customno stylesheetUnstyled but functional markup — style the fillo-* classes and slots yourself.

Three ways to override

With the stylesheet shipped, override in whichever of these three ways matches how much you need to change — they stack, so most apps reach for the first two.

1. Theme tokens { colorScheme, primary, background, text, radius, fontFamily }, via the theme prop or defineForm(). The only one that also styles the hosted /f page.

2. The appearance prop — per-slot class strings plus data-* state attributes, made for Tailwind. React only (<FilloForm> / <FilloProvider>).

3. Plain CSS — stable fillo-* classes and CSS variables; every default rule is single-class specificity, so one class of yours always wins.

By default the renderer inherits the host page’s CSS color-scheme and font. If your theme provider only toggles a class or data attribute, pass its resolved "light" or "dark" mode. Use "auto" only for a deliberately system-driven page. A fixed hex background selects a readable control palette unless you explicitly choose one.

On @usefillo/dom, style with theme tokens and fillo-* classes, plus a components override where you need a field to render differently — the appearance prop itself doesn’t exist there.

theme tokens
<FilloForm
  formId="onboarding"
  theme={{ primary: "#4f46e5", radius: "10px", fontFamily: "inherit" }}
/>

// Code-defined forms carry the theme in defineForm() — it syncs to your
// workspace, so the hosted /f page renders the same tokens:
const feedback = defineForm({
  id: "feedback",
  theme: { primary: "#4f46e5", radius: "10px" },
  // …pages
});

The appearance prop

React only — this prop lives on <FilloForm> and <FilloProvider> (@usefillo/react); see the three ways above for the @usefillo/dom equivalents.

Attach class strings to named slots. They are appended after the built-in fillo-* class and win by cascade order — the default stylesheet lives in a lower cascade layer, so no !important is ever needed. Any slot takes a string or a (state) => string function; fields overrides one field by id. Works the same on <FilloProvider>.

appearance + tailwind
<FilloForm
  formId="onboarding"
  appearance={{
    classNames: {
      root: "mx-auto max-w-xl",
      label: "text-sm font-medium text-zinc-900",
      // data-invalid lives on the field wrapper — mark it `group` so
      // inner slots can react to it:
      field: "group",
      control:
        "rounded-lg border-zinc-300 shadow-xs focus:ring-2 focus:ring-indigo-600 " +
        "group-data-[invalid]:border-red-400",
      option:
        "rounded-lg border-zinc-200 hover:bg-zinc-50 " +
        "data-[selected]:border-indigo-600 data-[selected]:bg-indigo-50",
      error: "text-xs font-medium text-red-600",
      button: (s) =>
        s.variant === "primary"
          ? "rounded-full bg-indigo-600 font-semibold hover:bg-indigo-500"
          : "text-zinc-500",
    },
    fields: {
      // One question diverges; everything else keeps the classes above.
      score: {
        options: "grid grid-cols-11 gap-1.5",
        option: "aspect-square justify-center rounded-md p-0 text-sm",
      },
    },
  }}
/>

Slot reference

Slot names come from FILLO_SLOTS (exported by @usefillo/core) — this table renders from it. Renames are breaking; additions are minors. The “Powered by Fillo” badge is not a slot.

SlotWhat it is
rootThe form element itself. Carries data-state, data-page, and data-last-page.
headerTitle and description wrapper (rendered when showTitle is on).
titleThe form title.
descriptionThe form description under the title.
pageTitleThe current page's heading in multi-page forms.
progressProgress track in multi-page forms.
progressFillThe filled part of the progress track.
blocksContainer of the current page's fields.
fieldOne field's wrapper. Carries data-field, data-kind, data-invalid, data-required.
labelA field label.
fieldDescriptionHelp text under a field label.
controlThe interactive element — inputs, textareas, dropdowns, dropzones.
optionsList wrapper on choice fields (select, multi-select, rating, scale).
optionOne choice row, star, scale step, or toggle. Carries data-selected / data-checked.
optionLabelThe text inside an option row.
errorA field's validation message.
footerThe Back / Next / Submit row.
buttonNavigation and submit buttons. classNames functions get state.variant: "primary" | "ghost".
successThe thank-you screen after submit.
resumeThe saved-progress notice with its Start over action (settings.saveProgress forms).
turnstileThe human-verification widget container (forms that require a human check).
calculatedA calculated field's read-only row. Carries data-field and data-kind; the value inside is .fillo-calculated-value.

State attributes

From FILLO_DATA_ATTRS (exported by @usefillo/core). Tailwind data-[…]: variants must sit on the slot that carries the attribute — put group on the field wrapper to reach inner slots, as in the example above.

AttributeWhereMeaning
data-filloevery slot elementThe slot name, e.g. data-fillo="control".
data-kindfield wrapperThe field kind, e.g. data-kind="email".
data-fieldfield wrapperThe field id — target one question in CSS.
data-invalidfield wrapperPresent while the field has a validation error. Controls also carry aria-invalid.
data-requiredfield wrapperPresent on required fields.
data-optionoption rowThe option id, e.g. data-option="very-good" — target one choice in CSS.
data-selectedoption / star / scale stepPresent on the chosen option row, active star, or active scale step.
data-checkedcheckbox / toggle optionPresent while checked.
data-drag-overupload dropzonePresent while a file is dragged over it.
data-staterootEngine status: idle | submitting | submitted | error.
data-pagerootCurrent page number, 1-based.
data-last-pagerootPresent on the final page.

Plain CSS

No Tailwind? Every element keeps its stable fillo-* class, and theme tokens are ordinary CSS variables. The default stylesheet is :where()-flattened to single-class specificity, so one selector of yours always wins.

plain css
/* Stable class names — renames are breaking, additions are minors. */
.fillo-option { border-radius: 0; }

/* One question, via its data-field id: */
[data-field="quality"] .fillo-options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}

/* Theme tokens are plain CSS variables on any ancestor: */
.fillo-form { --fillo-primary: #4f46e5; --fillo-radius: 10px; }

/* Type scale: the form inherits your page's font by default. Pin or scale
   the whole form with one token (use an absolute value like px) — every
   internal size is proportional: */
.fillo-form { --fillo-font-size: 16px; }

A number field with a prefix or suffix set wraps its input in one extra class, plus a class for each affix span:

ClassWhat it is
.fillo-numberWraps a number field's input when prefix or suffix is set. No wrapper at all on a plain number field.
.fillo-number-prefixThe prefix text before the input, e.g. "$". Rendered only when prefix is set.
.fillo-number-suffixThe suffix text after the input, e.g. " kg" — edge spaces are preserved. Rendered only when suffix is set.

Localize the strings

React only — there’s no equivalent on @usefillo/dom yet. The strings prop (on <FilloForm> and <FilloProvider>) overrides every visitor-facing renderer string — Back/Next/Submit, placeholders, the (optional) suffix, error and success panels — as Partial<FilloStrings>; defaults are exported as DEFAULT_STRINGS. Schema-authored text — labels, descriptions, success copy set in settings — always wins over these defaults.

localized renderer
<FilloForm
  formId="kontakt"
  strings={{
    back: "Zurück",
    next: "Weiter",
    submit: "Absenden",
    submitting: "Wird gesendet…",
    optional: " (optional)",
    successTitle: "Danke!",
    successMessage: "Ihre Antwort wurde gespeichert.",
  }}
/>

The hosted page

The hosted /f page and the dashboard preview render the default renderer with theme tokens only appearance classes and your own CSS never reach it.

Storage the SDK uses

For privacy reviews: the SDK uses localStorage only and writes up to three key families. It sets no cookies. Saved-progress answers stay server-side; a browser response-limit key includes the configured scope-field value, so do not use a sensitive answer as that scope.

KeyPurposeLifetimeContents
fillo:submission:<formId>[::<scope>]Tracks the browser key and completed response for forms using responseLimit by browser, optionally scoped by another field.Persistent until the visitor clears site data.Random key, timestamp, response id. The storage key also includes the scope-field answer when configured.
fillo:sync:<baseUrl>|<pk>|<handle>Code-form sync cache so returning visitors skip the /forms/sync request.1 hour for published forms, 60 seconds for drafts.Form id, slug, status, content hash, timestamp. No PII.
fillo:draft:<formId>Points this browser to a server-side saved-progress draft so the form can resume after reload.Until submit or Start over. A stale reference is cleared after server expiry; clearing site data removes it immediately.Draft id and per-draft bearer token. No answers or respondent profile data.

When classes aren’t enough, swap a field’s markup entirely with a components override, or drop the default layout altogether and render every field yourself.

This page for agents: /docs/styling.md · index at /llms.txt

Updated

Was this page helpful?