Design System
A Modern board is styled through a public contract: CSS variables, class hooks and data attributes. Everything on this page is public API. Anything not listed here is internal and can change in any release.
The contract applies to Modern boards only. Classic boards keep their flowforge-* classes and are not affected by anything below.
Layering
Flowforge ships plain CSS. It contains no Tailwind utilities and no @apply, so your app needs no @source line and a Tailwind prefix() cannot break it.
The stylesheet opens with the layer order Filament uses, and puts every rule in one sublayer:
@layer properties, theme, base, components, utilities;
@layer components.flowforge {
/* every rule */
}
Your unlayered CSS always beats flowforge's. Your layered CSS wins when it sits in components itself or in a later layer such as utilities.
Filament's own component styles sit in components, which outranks the components.flowforge sublayer. Flowforge uses !important in the few places it must beat them. See Properties set with !important.
Filament panels get the stylesheet in the page head. A standalone board links it in its own markup. You do not register or import it.
Variables
Flowforge never sets a public --ff-* variable. Each rule reads the variable through a private alias with a default, so a value you set at any scope wins. That scope can be :root, .ff-board or one card.
The dark column shows where the default changes under Filament's .dark class. A single value applies to both modes.
| Variable | Tier | Default (light / dark) | Since | Used for |
|---|---|---|---|---|
--ff-surface | semantic | var(--gray-50) / var(--gray-950) | 4.2.0 | Background of the sticky column-header strip in the lane view. Set it to the page background. |
--ff-surface-sunken | semantic | var(--gray-100) / color-mix(in oklab, #fff 5%, transparent) | 4.2.0 | Background of a column tray and of a lane cell. --ff-tray-bg overrides it. |
--ff-surface-raised | semantic | #fff / var(--gray-900) | 4.2.0 | Background of a card and of the filters panel. --ff-card-bg overrides it for cards. |
--ff-border | semantic | color-mix(in oklab, var(--gray-950) 5%, transparent) / color-mix(in oklab, #fff 10%, transparent) | 4.2.0 | Ring around a card and around the filters panel. --ff-card-ring overrides it for cards. |
--ff-text | semantic | var(--gray-950) / #fff | 4.2.0 | Column titles, lane titles, card titles and the add row on hover. |
--ff-text-muted | semantic | var(--gray-500) / var(--gray-400) | 4.2.0 | Secondary text and icons: the group-by label, column icons, counts, summaries, tags, card meta, the add row, the avatar overflow, lane counts and icons, and the header subheading. |
--ff-accent | semantic | var(--primary-500) | 4.2.0 | Outline of an accepting drop target, the drop gap border and the loading text. |
--ff-accent-tint | semantic | var(--primary-50) / color-mix(in oklab, var(--primary-500) 10%, transparent) | 4.2.0 | Fill of a tray or lane cell that accepts the card. |
--ff-danger | semantic | var(--danger-500) | 4.2.0 | Outline of a refusing tray or lane cell, and the limit hint. |
--ff-warning | semantic | var(--warning-600) / var(--warning-400) | 4.2.0 | Count of a full column. |
--ff-radius | semantic | 0.75rem | 4.2.0 | Card radius. The tray and lane cell radius is this plus 0.25rem. |
--ff-gap | semantic | 1.25rem | 4.2.0 | Space between columns, on the flat board and in the lane grid. |
--ff-ease | semantic | cubic-bezier(0.16, 1, 0.3, 1) | 4.2.0 | Easing of tray and cell fills, add row color, card shadow and card action fades. |
--ff-duration | semantic | 200ms | 4.2.0 | Duration of the same transitions. |
--ff-column-width | component | 300px | 4.2.0 | Width of a column, and of each column in the lane grid. |
--ff-tray-bg | component | var(--ff-surface-sunken) | 4.2.0 | Background of a column tray and of a lane cell. |
--ff-tray-radius | component | calc(var(--ff-radius) + 0.25rem) | 4.2.0 | Corner radius of a tray and of a lane cell. |
--ff-card-bg | component | var(--ff-surface-raised) | 4.2.0 | Card background. |
--ff-card-ring | component | var(--ff-border) | 4.2.0 | Card ring color. |
--ff-card-radius | component | var(--ff-radius) | 4.2.0 | Card corner radius. |
--ff-card-shadow | component | 0 1px 2px 0 rgb(0 0 0 / 0.05) | 4.2.0 | Card shadow list. It sets the resting and the hover shadow together. |
--ff-card-padding | component | 0.75rem | 4.2.0 | Space inside a card. |
--ff-drag-tilt | component | 2.5deg | 4.2.0 | Rotation of the card while it is dragged. |
--ff-drag-shadow | component | 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) | 4.2.0 | Shadow list of the dragged card. |
--ff-placeholder-border | component | var(--ff-accent) | 4.2.0 | Border of the drop gap. |
Flowforge also reads the Filament palette: --gray-*, --primary-*, --danger-* and --warning-*. It never sets them.
Notes that apply to several variables:
- The shadow variables take a shadow list. Use
0 0 #0000for no shadow, nevernone. Flowforge combines the value with a ring, andnonemakes the whole declaration invalid. --ff-surfacepaints the sticky lane-header strip behind the column headers. Set it to the background your page uses, so cards scroll under the strip without showing through.--ff-card-shadowsets the resting shadow and the hover shadow together.- The dragged card is a clone on
body. Flowforge copies the variables above from the card you picked up, so a value set at any scope reaches the clone.
Hooks
Hooks are class names. Modern markup carries ff-* hooks only. The flowforge-* classes of classic boards never appear in Modern views, so a rule written for one board does not reach the other.
| Hook | Element |
|---|---|
ff-board | The board root |
ff-columns | The row that holds the columns of a board without lanes. Set its padding to space the columns from the page edge |
ff-column | One column: its header and its tray. In the lane view it holds only the column header |
ff-column-header | The row with the title, count, summary and actions |
ff-count | The card count in a column header |
ff-summary | The column summary text |
ff-tray | The surface that holds a column's cards. The lane view has no tray, its cells are the surface |
ff-card | One card |
ff-card-actions | The action menu in a card's corner |
ff-add-row | The add-card row, the "Show more cards" button in a lane cell and the "Show more lanes" button |
ff-lanes | The root of the lane view |
ff-lane-header | The header of one lane. It stays at the left edge while the lanes scroll sideways, and a click collapses or expands the lane |
ff-cell | One lane and column intersection, a drop target |
ff-limit-hint | The hint shown over a full column's tray during a drag. The lane view does not render it |
ff-card-tag | One tag from CardTag |
ff-card-meta | The row of entries from CardMeta |
ff-card-avatar | One avatar from CardAvatar |
Other ff-* classes exist for layout. They are internal.
States
Flowforge reports state through data attributes. SortableJS toggles classes only, so flowforge-modern.js mirrors its states into these attributes.
| Attribute | On | Meaning |
|---|---|---|
data-dragging | The dragged card clone | A drag is in progress |
data-placeholder | The drop gap | Where the card will land |
data-drop="active" or data-drop="blocked" | A column, or a lane cell | The hovered target accepts or refuses the card |
data-full | A column | The column is at its limit |
data-collapsed | A column, or a lane header | The column shows as a collapsed rail, or the lane's cells are hidden |
data-pending | A card | Its move is saving |
The dragged clone sits on body, outside .ff-board. A rule that must reach it starts at .ff-card[data-dragging], and the visible card is its .ff-card-body child.
Customizing
Set variables in your theme and add hook rules beside them:
:root { --ff-accent: var(--primary-600); --ff-radius: .5rem; --ff-drag-tilt: 1.5deg; }
.dark { --ff-card-bg: var(--gray-900); }
.ff-column[data-drop="blocked"] .ff-tray { outline: 2px dashed var(--ff-danger); }
The first line sets three variables for every board. The second sets a dark mode value. The third is a hook rule: it outlines a column that refuses the card you are dragging.
A variable set on .ff-board applies to that board only. A variable set on one card applies to that card only.
Tailwind apps
Tailwind v4 apps can map variables to utilities with @theme inline:
@theme inline {
--color-ff-surface: var(--ff-surface, var(--gray-50));
--color-ff-accent: var(--ff-accent, var(--primary-500));
}
Flowforge never sets --ff-* variables, so each mapped variable needs the fallback shown, which matches the light default in the variable table. Set the variable under .dark for a dark value. inline keeps each utility reading the variable at use time, so your overrides still apply.
Card components
CardTitle, CardTag, CardMeta and CardAvatar are styled by the Modern stylesheet and designed for Modern boards. On a classic board they render ff-card-* markup, and their look is not guaranteed: after a wire:navigate visit to a Modern board the Modern stylesheet stays in the page head and can style them. See Card Components.
Properties set with !important
Flowforge sets these properties with !important, to beat Filament component styles that outrank its sublayer. To override one, put your own !important rule in an earlier layer, because cascade layers reverse the order for !important. An unlayered or later-layer !important rule loses to flowforge's layered one.
@layer base {
.ff-card-meta .fi-badge { color: red !important; }
}
| Selector | Properties |
|---|---|
.ff-card-meta .fi-badge | padding, font-weight, color, background, box-shadow |
.ff-card-meta .fi-in-text-item | font-size, color |
.ff-filters | overflow, background-color, box-shadow |
.ff-filters-toolbar | padding-inline |
.ff-filters-panel--above | border-bottom-width |
.ff-filters-panel--before, .ff-filters-panel--after | background-color, border-radius, box-shadow |
.ff-filters-indicators | padding-inline |
The .ff-filters* classes are internal. They are listed because they carry !important, and they can change in any release.
Versioning
- Adding a variable, hook or state is a minor release.
- Renaming or removing one happens in a major release only. The old name keeps working for that major through a fallback,
var(--ff-new, var(--ff-old)), and this page marks it deprecated.
The Since column in the variable table records the release that added each variable.