@sonata-innovations/fiber-fbre
v5.0.0
Published
Fiber Render Engine — renders Flow JSON forms with conditional logic, validation, and screen transitions
Maintainers
Readme
@sonata-innovations/fiber-fbre
Fiber Render Engine — consumes Flow JSON and renders data collection forms. Handles conditional logic, validation, calculations, screen transitions, theming, and outputs collected FlowData back to the parent application. Renders 30 component types across ten form styles; zero UI-framework dependency (custom CSS only).
Quick Start
Local Mode
Pass a Flow object directly:
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";
function App() {
return (
<FBRE
flow={myFlow}
screenIndex={0}
onFlowComplete={(data) => console.log(data)}
/>
);
}Remote Mode
Fetch a published flow from a Fiber API:
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";
function App() {
return (
<FBRE
flowId="your-flow-id"
apiEndpoint="https://your-api.example.com/api/v1"
apiKey="your-api-key"
onFlowComplete={(data) => console.log(data)}
/>
);
}Server-Driven Mode
Session-based rendering where the server evaluates conditions and validation:
import { FBRE } from "@sonata-innovations/fiber-fbre";
import "@sonata-innovations/fiber-fbre/styles";
function App() {
return (
<FBRE
sessionEndpoint="https://your-api.example.com/api/v1/public/sessions"
flowId="your-flow-id"
onFlowComplete={(data) => console.log(data)}
/>
);
}Installation
npm install @sonata-innovations/fiber-fbreThen import the CSS once in your app entry point — without it the form renders unstyled:
import "@sonata-innovations/fiber-fbre/styles";Peer dependencies: react ^18.0.0 || ^19.0.0, react-dom ^18.0.0 || ^19.0.0
Runtime dependencies: zustand, @sonata-innovations/fiber-types, @sonata-innovations/fiber-shared
Documentation
This README covers evaluation and day-to-day API lookup. Deeper guides ship inside the package under docs/ (and an AGENTS.md routing guide at the package root), and are browsable in the public docs mirror at github.com/sonata-innovations/fiber-docs.
| Topic | In package | Mirror |
|-------|-----------|--------|
| Concepts overview | docs/fiber-concepts.md | fiber-concepts.md |
| FBRE integration guide | docs/integration/fbre.md | integration/fbre.md |
| Theming reference | docs/features/fbre-theming.md | features/fbre-theming.md |
| Confirmation screen | docs/features/confirmation-screen.md | features/confirmation-screen.md |
Schema references (Flow input and FlowData output) ship in the @sonata-innovations/fiber-types package under docs/schema/, and are mirrored at:
- schema/flow-schema.md · schema/flow-quick-reference.md
- schema/flow-data-schema.md · schema/flow-schema.json
API
<FBRE /> Props
FBRE supports three rendering modes, selected by which props you pass. The modes are mutually exclusive.
Local Mode
Render a Flow object you already have in memory.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| flow | Flow | Yes | Flow JSON object |
| data | FlowData | No | Pre-populated form data (not reactive — read once at mount) |
| screenIndex | number | No | Initial screen index (default 0) |
| themeDefaults | ThemeConfig | No | Theme defaults merged under flow.config.theme — the flow wins |
| theme | ThemeConfig | No | Theme overrides merged over flow.config.theme — the prop wins |
| navigation | NavigationConfig | No | Override navigation settings (merged over flow.config.navigation) |
| controls | ControlsConfig | No | Override controls settings (merged over flow.config.controls) |
| context | Record<string, string \| boolean \| number> | No | External context values for condition evaluation and calculations |
| storeRef | MutableRefObject<StoreApi<FBREStoreState> \| null> | No | Ref to access the Zustand store |
| onFlowComplete | (data: FlowData) => void \| ConfirmationResult \| Promise<void \| ConfirmationResult> | Yes | Called when the user completes the flow. Return/resolve a ConfirmationResult to override the configured confirmation message; a rejected Promise surfaces a completion error |
| onScreenChange | (index: number, data: FlowData) => void | No | Called on screen navigation |
Remote Mode
Fetch a published flow from a Fiber API by ID.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| flowId | string | Yes | ID of the published flow |
| apiEndpoint | string | Yes | Base URL of the Fiber API |
| apiKey | string | No | API key for authentication |
| data | FlowData | No | Pre-populated form data (not reactive) |
| screenIndex | number | No | Initial screen index (default 0) |
| theme | ThemeConfig | No | Override theme settings |
| navigation | NavigationConfig | No | Override navigation settings |
| controls | ControlsConfig | No | Override controls settings |
| context | Record<string, string \| boolean \| number> | No | External context values for condition evaluation and calculations |
| storeRef | MutableRefObject<StoreApi<FBREStoreState> \| null> | No | Ref to access the Zustand store |
| onFlowComplete | (data: FlowData) => void \| ConfirmationResult \| Promise<void \| ConfirmationResult> | Yes | Called when the user completes the flow (see Local Mode) |
| onScreenChange | (index: number, data: FlowData) => void | No | Called on screen navigation |
Server-Driven Mode
Session-based rendering. The server evaluates conditions and validation; the client renders one screen at a time. On completion it resets the submit button and shows a terminal confirmation (from config.confirmation, else a generic "Thank you"; an explicit show: false renders nothing); onFlowComplete still fires with the server result if you want to drive your own post-completion UI.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| sessionEndpoint | string | Yes | Session API base URL |
| flowId | string | Yes | ID of the flow to start a session for |
| apiKey | string | No | API key for authentication |
| theme | ThemeConfig | No | Override theme settings |
| context | Record<string, string \| boolean \| number> | No | External context values for condition evaluation and calculations |
| onFlowComplete | (data: FlowData) => void | Yes | Called when the user completes the flow |
| onScreenChange | (screenNumber: number) => void | No | Called on screen navigation (receives screen number, not index + data) |
Imperative Access
Access the store directly for programmatic control:
import { useFBREStore, useFBREStoreApi, useFBREApi } from "@sonata-innovations/fiber-fbre";
// Inside a child of <FBRE />
const flowData = useFBREStore((s) => s.getFlowData());
const storeApi = useFBREStoreApi();
const data = storeApi.getState().getFlowData();
// In remote mode — access API config, flowId, tenantId
const apiContext = useFBREApi();getMaxScreenIndex() on the store returns the maximum screen index (screen count − 1). (Renamed from getMaxScreenCount, whose name implied a count.)
Events
import { addFBREEventListener, removeFBREEventListener } from "@sonata-innovations/fiber-fbre";
const handler = (id, data) => console.log("file uploaded:", id, data);
addFBREEventListener("file-upload", handler);
removeFBREEventListener("file-upload", handler);Note: the file-upload event bus is module-global — a listener fires for uploads from every <FBRE /> instance on the page, not just one. Filter by the emitted component id if you run multiple instances.
Exports
// Component
import { FBRE } from "@sonata-innovations/fiber-fbre";
// Store hooks
import { useFBREStore, useFBREStoreApi, useFBREApi } from "@sonata-innovations/fiber-fbre";
// Event system
import { addFBREEventListener, removeFBREEventListener } from "@sonata-innovations/fiber-fbre";
// Brand-font loading (FBRE calls ensureFontLoaded itself; exported for hosts
// that want the face registered before first paint)
import { ensureFontLoaded, fontFamilyStack } from "@sonata-innovations/fiber-fbre";
// Error classes
import { ApiError, TimeoutError } from "@sonata-innovations/fiber-fbre";
// Types — props, store, and the full Flow/FlowData/Component type surface
import type {
FBREProps,
FBRELocalProps,
FBRERemoteProps,
FBREServerDrivenModeProps,
FBREStoreState,
FBREApiConfig,
Flow,
FlowScreen,
FlowConfiguration,
ThemeConfig,
NavigationConfig,
ControlsConfig,
FontFamilyConfig,
ConfirmationConfig,
ConfirmationResult,
Component,
ComponentProperties,
FlowData,
ScreenData,
ComponentData,
FileUploadData,
FlowConditionConfig,
ConditionOperator,
// …plus per-component property types and condition/rule types
} from "@sonata-innovations/fiber-fbre";Flow schema, conditions, validation, calculations
The Flow JSON hierarchy (Flow → Screen → Component), all 30 component types, the 19 condition operators, the 17 validators, calculation formulas, and inline markup are documented in the schema references — docs/schema/ in @sonata-innovations/fiber-types, mirrored at schema/flow-schema.md (with a quick reference).
A few runtime facts worth knowing up front:
- No auto-migration. The flat
required/regexproperties were removed from the schema in@sonata-innovations/fiber-types4.0.0 and are ignored if present — an un-migrated field renders with no required marker and no enforcement. Pre-migrate flows tovalidation.rules(andconditions) before passing them to FBRE. - Calculation formulas use single braces to reference component values:
{uuid}. They support arithmetic, comparisons,IF(), aggregation (SUM/COUNT/AVG/MIN/MAXover repeaters), and update reactively. - Condition and validation results live on the store (
conditionResults,validationErrors), not in the Flow JSON. Components hidden by a condition are excluded from FlowData and don't block screen validity.
Confirmation Screen
A terminal "thank you" screen shown after submission, configured on the flow (config.confirmation) rather than as a Screen. It replaces the final screen and hides the controls/stepper; title/body support reference markup (collected fields, calculations, and context values by name). onFlowComplete decides when it shows — return void for immediate, a Promise to keep the submit button in-flight until it settles, or a ConfirmationResult ({ title?, body? }) to override the configured message (e.g. with a server reference number).
In local and remote modes there is no generic fallback: a flow with no config.confirmation completes without any visible acknowledgement, so configure one if the page should acknowledge the submit. (Server-driven mode does fall back, because the renderer there is often the whole page.) A flow can only be submitted once — after a successful completion the controls are disabled even when no confirmation renders, and a rejected promise re-enables them for a retry. Full guide: features/confirmation-screen.md.
Style Families & Advance Behaviors
There is no presentation mode — the look is the style, and the two advance behaviors are navigation flags.
Four of the ten styles form the focused family: centered-minimal, stacked-cards, soft-float, bold-statement. On top of their own look they center content in a narrow column, fade/scale/stagger components in on transitions (respecting prefers-reduced-motion), and enlarge type and tap targets. The other six — clean, outlined, refined-clean, airy-clean, soft-outlined, defined-outlined — are the form family. The vocabulary is flat: any style works on any flow. FBRE derives the family and emits it as data-style-family; styleFamily() and FOCUSED_STYLES are exported for grouping a style picker.
navigation.autoAdvance (default off) advances ~500ms after a single-select choice. navigation.advanceOnEnter (default on) advances on Enter in a text or number field. Both only fire when a screen renders exactly one interactive component (display-only and condition-hidden components don't count), never on the last screen, and never on an invalid screen; Enter-to-advance also stands down inside an open popup, so the colour picker's hex field commits on Enter instead of skipping the screen.
FlowData output, conditions, validation, calculations, transitions, and dark mode are unaffected by any of this. Full guide: features/style-families.md.
Upgrading from 3.x:
config.modeand themodeprop are gone. A"conversational"flow already carried a focused style, so its look is unchanged — addnavigation: { autoAdvance: true }to keep the auto-advance.
Theming
FBRE's appearance is driven entirely by --fbre-* CSS custom properties. Two independent axes: theme.style controls shape (borders vs. underlines, fills, spacing), and the palette controls colors. There are three ways to set the palette, and they layer — each overrides the one before, so you specify only what you want to change:
colorSchemepreset —theme.colorScheme("light"default |"dark") seeds the entire token set with a coherent palette and setsdata-modeon the container. (Replaces the formerdarkModeboolean; there is no runtime fallback — migrate stored flows.)- Palette knobs —
theme.{color, background, surface, text, border, radius, fontFamily, error, success, warning}set their primary token plus the tokens derived from it (e.g.surfacealso drives input fills and hover/alt surfaces). Any subset; unset knobs fall through to the preset. - Raw
--fbre-*CSS — override any token directly (including ones with no knob, like--fbre-star-filled) by scoping rules to your wrapper.
<FBRE
flow={myFlow}
theme={{ colorScheme: "dark", color: "#bcd3cd", surface: "#243244", radius: "3px" }}
onFlowComplete={handleComplete}
/>Brand fonts. theme.fontFamily also accepts { family, src, faces?, stack? }. Given sources, FBRE registers the @font-face in the owning document rather than assuming the host page already loaded the family — which is the only thing that works inside a shadow root, where a face declared in the root's stylesheet is never registered. A plain string stays correct when the host page loads the font itself.
Defaults worth noting: --fbre-radius is 4px and --fbre-font is "Segoe UI", system-ui, -apple-system, sans-serif. The complete token catalog with light/dark defaults is in features/fbre-theming.md.
Screen Transitions
Animate screen changes by setting navigation.transition in the flow config:
{ "config": { "navigation": { "transition": "slideFade" } } }| Type | Effect | Direction-aware? |
|------|--------|-----------------|
| "none" | Instant swap (default) | — |
| "slide" | Full horizontal slide | Yes |
| "fade" | Crossfade | No |
| "slideFade" | 30px slide + opacity | Yes |
| "rise" | Vertical rise/sink | Yes |
| "scaleFade" | Scale + opacity | No |
Direction-aware transitions reverse when navigating backward; buttons are disabled mid-animation to prevent overlap. Customize timing via --fbre-transition-duration / --fbre-transition-easing. @media (prefers-reduced-motion: reduce) sets the duration to 0ms automatically.
FlowData Output
getFlowData() returns the collected form data, mirroring the Flow structure (Flow → ScreenData → ComponentData, with components?: ComponentData[][] for groups and repeaters). Display-only components — header, text, divider, callout, table — are excluded automatically; computed fields are included. Full shape and field semantics: schema/flow-data-schema.md.
Responsive Sizing
FBRE uses CSS container queries to automatically scale controls in narrow containers (under 400px). Buttons, stepper dots, and toggle switches shrink proportionally — no configuration needed; just embed in a smaller container and the compact sizing kicks in.
Multiple Instances
Each <FBRE /> creates its own isolated Zustand store, so multiple instances run simultaneously on the same page without state conflicts. (The one exception is the module-global file-upload event bus noted under Events.)
<FBRE flow={flowA} onFlowComplete={handleA} />
<FBRE flow={flowB} onFlowComplete={handleB} />License
MIT
