@sonata-innovations/fiber-fbre
v3.3.0
Published
Fiber Render Engine — renders Flow JSON forms with conditional logic, validation, and screen transitions
Maintainers
Readme
@sonata-innovations/fiber-fbre v2
Fiber Render Engine — consumes Flow JSON and renders data collection forms. Handles conditional logic, validation, screen transitions, and outputs collected FlowData back to the parent application.
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 in your app entry point:
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
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 |
| mode | FlowModeType | No | Override form mode ("standard" | "conversational") |
| screenIndex | number | No | Initial screen index (default 0) |
| theme | ThemeConfig | No | Override theme settings (merged over flow.config.theme) |
| 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 |
| onScreenValidationChange | (index: number, data: any) => void | No | Called when screen validity changes |
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 |
| mode | FlowModeType | No | Override form mode ("standard" | "conversational") |
| 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. 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 |
| onScreenValidationChange | (index: number, data: any) => void | No | Called when screen validity changes |
Server-Driven Mode
Session-based rendering. The server evaluates conditions and validation; the client renders one screen at a time.
| 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 |
| mode | FlowModeType | No | Override form mode ("standard" | "conversational") |
| 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 (note: 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();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);Exports
// Components
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";
// Error classes
import { ApiError, TimeoutError } from "@sonata-innovations/fiber-fbre";
// Types
import type {
FBREProps,
FBRELocalProps,
FBRERemoteProps,
FBREServerDrivenModeProps,
FBREStoreState,
FBREApiConfig,
Flow,
FlowScreen,
FlowConfiguration,
ThemeConfig,
NavigationConfig,
ControlsConfig,
ConfirmationConfig,
ConfirmationResult,
FlowMetadata,
Component,
ComponentProperties,
ComponentDisplayProperties,
ComponentDividerProperties,
ComponentInputProperties,
ComponentOptionProperties,
ComponentSliderProperties,
ComponentSwitchProperties,
ComponentRatingProperties,
ComponentFileUploadProperties,
ComponentGroupProperties,
FlowData,
ScreenData,
ComponentData,
FileUploadData,
FileUploadBase64Data,
FileUploadS3Data,
FlowConditionConfig,
ConditionOperator,
ConditionRule,
ConditionGroup,
ConditionDependency,
} from "@sonata-innovations/fiber-fbre";Confirmation Screen
A terminal "thank you" screen shown after submission, configured on the flow (config.confirmation) — not a Screen. Renders only when show is not false and title/body has content, replacing the final screen and hiding the controls/stepper. title/body support ${...} reference markup (collected fields, calculations, and context values by name).
const flow = {
...myFlow,
config: { ...myFlow.config, confirmation: { show: true, title: "Thank you!", body: "Submitted, ${email}." } },
};
<FBRE flow={flow} onFlowComplete={handleComplete} />onFlowComplete decides when it shows: return void → shows immediately; return a Promise → the submit button stays in-flight until it settles (resolve shows it, reject shows a completion error instead); return/resolve a ConfirmationResult ({ title?, body? }) → overrides the configured message, e.g. with a server reference number.
const handleComplete = async (data: FlowData): Promise<ConfirmationResult> => {
const { referenceId } = await saveToBackend(data);
return { title: "All done!", body: `Reference: ${referenceId}.` };
};Flow JSON Schema
Flow
├── uuid: string
├── metadata: { name?, description?, ...}
├── config?: { mode?, theme?: { color?, colorScheme?, style?, background?, surface?, text?, border?, radius?, fontFamily?, error?, success?, warning? }, navigation?: { transition?, allowInvalidTransition? }, controls?: { show?, layout?, showStepper? }, summary?, confirmation?: { show?, title?, body? } }
└── screens: FlowScreen[]
├── uuid: string
├── label?: string
├── conditions?: FlowConditionConfig
└── components: Component[]
├── uuid: string
├── type: string
├── properties: { ... }
├── conditions?: FlowConditionConfig
└── components?: Component[] (groups and repeaters)Component Types
| Type | Category | Description |
|------|----------|-------------|
| header | Display | Heading text |
| text | Display | Rich text with markup support |
| divider | Display | Horizontal rule with optional label |
| callout | Display | Styled alert/info box with variant coloring (info, success, warning, neutral) |
| table | Display | Static comparison/data table with optional column highlighting |
| inputText | Input | Single-line text field |
| inputTextArea | Input | Multi-line text field |
| inputNumber | Input | Numeric input (supports decimal restriction and start adornment) |
| dropDown | Selection | Native select dropdown |
| dropDownMulti | Selection | Multi-select with tag chips |
| checkbox | Selection | Checkbox group |
| radio | Selection | Radio button group |
| toggleSwitch | Selection | On/off toggle |
| yesNo | Selection | Two large tappable buttons for binary yes/no |
| cardSelect | Selection | Card-based visual selection with image/icon support |
| date | Date & Time | Calendar popup date picker |
| time | Date & Time | Hour/minute time selector |
| dateTime | Date & Time | Combined date + time picker |
| dateRange | Date & Time | Two date pickers for start/end |
| timeRange | Date & Time | Two time pickers for start/end |
| dateTimeRange | Date & Time | Two datetime pickers for start/end |
| fileUpload | Interactive | File picker with size validation |
| rating | Interactive | Star rating with half-star precision |
| slider | Interactive | Range slider with value label |
| colorPicker | Interactive | Saturation/hue picker with hex input |
| computed | Display | Calculated field with formula-based computed values |
| signature | Interactive | Signature pad — draw on canvas, type name, or both |
| group | Container | Component container with layout grid, collapsible |
| repeater | Container | Iterable component container (add/remove iterations, pre-populated rows) |
Conditions
Components and screens can be conditionally shown/hidden using FlowConditionConfig:
{
"action": "show",
"when": {
"logic": "and",
"rules": [
{
"source": "other-component-uuid",
"operator": "equals",
"value": "yes"
}
]
}
}Logic: "and" (all rules must match) or "or" (any rule matches).
Context rules: Set sourceType: "context" on a rule to reference an external context value instead of a component. In this case, source is a context key (not a component UUID).
Operators:
| Category | Operators |
|----------|-----------|
| Equality | equals, notEquals |
| String | contains, notContains, startsWith, endsWith |
| Presence | isEmpty, isNotEmpty |
| Numeric | greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual |
| Set (single) | isOneOf, isNotOneOf |
| Set (multi) | includesAny, includesAll, includesNone |
| Boolean | isTrue, isFalse |
Condition results are stored separately from the Flow JSON in a conditionResults map (keyed by target UUID). Hidden components are excluded from FlowData output.
Validation
Components support a rules-based validation system via properties.validation:
{
"validation": {
"rules": [
{ "type": "required" },
{ "type": "email" },
{ "type": "minLength", "params": { "min": 5 } }
]
}
}17 built-in validators: required, email, phone, url, minLength, maxLength, exactLength, minValue, maxValue, pattern, minSelected, maxSelected, fileType, fileSize, contains, excludes, matchesField
Each validator has a default error message. Custom messages can be set per rule via the message field.
Error display: The first failing message is shown below the field with an "(and N more)" count when multiple rules fail.
Cross-field validation: matchesField enables bidirectional re-evaluation — changing either field re-validates the other.
Migration: Legacy required and regex flat properties are automatically converted to validation rules on load.
Validation errors are stored in validationErrors: Record<string, string[]> on the store (keyed by component UUID). Hidden components (by conditions) do not block screen validity.
Calculations
Flows can include formula-based computed values via FlowCalculation:
{
"calculations": [
{
"uuid": "calc-uuid",
"formula": "{{comp-a-uuid}} + {{comp-b-uuid}} * 0.1",
"label": "Total"
}
]
}- Formulas reference component values via
{uuid}tokens and support standard arithmetic (+,-,*,/) - Aggregation functions:
SUM(),COUNT(),AVG(),MIN(),MAX()over repeater iterations - Scalar
MIN(expr, ...)/MAX(expr, ...)for capping values (e.g.MIN({discount}, 25)) IF(condition, then, else)for conditional formulas (e.g. tiered pricing)- Comparison operators:
>,<,>=,<=,==,!=(return 1/0) - Option metadata is accessible for option-based components
- Results update reactively when source values change
- Access results via
getCalculationResult(uuid)on the store
Markup
Text components and detail fields support inline markup:
[b]bold[/b]→ bold[i]italic[/i]→ italic[l href="url"]link[/l]→ link
Conversational Mode
Set mode: "conversational" to transform FBRE into a one-question-per-screen experience:
// Via JSX prop
<FBRE flow={myFlow} mode="conversational" onFlowComplete={handleComplete} />
// Via flow config
const flow = {
...myFlow,
config: { ...myFlow.config, mode: "conversational" }
};
<FBRE flow={flow} onFlowComplete={handleComplete} />What changes:
- Content is vertically and horizontally centered
- Single-select components (
radio,yesNo,cardSelect,dropDown) auto-advance ~500ms after selection - Pressing Enter on
inputText/inputNumberadvances to the next screen - Auto-advance (both Enter and on-change) only fires when the screen renders exactly one interactive component. Multi-input screens — including those produced by FBTL's per-card page-break toggle — behave like a normal form: the user fills every field and clicks Continue. Display-only components (text, header, callout, divider, table, computed) and components hidden by conditions don't count toward the input total.
- Components animate in with fade + scale + stagger on screen transitions (respects
prefers-reduced-motion) - Option items, Yes/No buttons, card-select cards, and input fields are enlarged for easier tapping
- Each of the 6 existing styles has a tailored conversational presentation (e.g. clean = bordered cards, soft-outlined = pill-shaped options, defined-outlined = inverted selection)
What doesn't change:
- FlowData output is identical
- Conditions, validation, calculations all work the same
- Navigation buttons still work alongside auto-advance
- Works with all 6 transition types and dark mode
Theming
FBRE's appearance is driven entirely by CSS custom properties (the --fbre-* tokens). There are two independent axes — style controls the shape (borders vs. underlines, fills, spacing; set via theme.style), and the palette controls the colors. This section is a complete reference for the palette.
Palette values resolve through four layers, each overriding the one above, so you only specify what you want to change:
1. Light defaults base.css (lowest)
2. Preset: colorScheme: "dark" base.css [data-mode="dark"]
3. Knobs: theme.{surface,text…} inline custom properties
4. Raw CSS: your own --fbre-* rules your stylesheet (highest)Color scheme (presets)
theme.colorScheme ("light" default | "dark") seeds the entire token set with a coherent light or dark palette and sets data-mode on the container. Set it on the theme prop or in the flow config (the prop takes precedence):
<FBRE flow={myFlow} theme={{ colorScheme: "dark" }} onFlowComplete={handleComplete} />Replaces the former
darkMode: boolean:darkMode: true→colorScheme: "dark",darkMode: false/absent →colorScheme: "light". There is no runtime fallback — migrate stored flows before upgrading.
Theme knobs
Layer brand colors on top of the preset with the theme palette knobs. Each knob sets its primary --fbre-* token plus the tokens derived from it (e.g. surface also drives the input fills and hover/alt surfaces, via mode-aware color-mix). Any subset may be set; unset knobs fall through to the preset.
<FBRE
flow={myFlow}
theme={{
colorScheme: "dark",
color: "#bcd3cd", // accent
background: "#1a2a3a", // form ground
surface: "#243244", // input fills / cards
text: "#e8ede9",
border: "#2e4158",
radius: "3px",
fontFamily: '"Inter", system-ui, sans-serif',
}}
onFlowComplete={handleComplete}
/>| Knob | Primary token | Also derives |
| --- | --- | --- |
| color | --fbre-theme-color | light/dark accent tints |
| background | --fbre-bg | — |
| surface | --fbre-surface | input fills, hover/alt/subtle surfaces, control surface |
| text | --fbre-text | secondary/placeholder/disabled, label |
| border | --fbre-border | hover/light/subtle |
| radius | --fbre-radius | — |
| fontFamily | --fbre-font | — |
| error / success / warning | --fbre-{error,success,warning} | their -bg / -border (and error-light) |
Raw CSS (escape hatch)
For tokens not exposed as a knob (e.g. --fbre-star-filled, --fbre-shadow-color) — or for surgical control — override the --fbre-* variables directly. Scope to your wrapper so you out-specify the engine defaults regardless of load order:
.my-form-wrapper .fbre-container {
--fbre-surface: #243244;
--fbre-star-filled: #bcd3cd;
}Knobs are inline (highest specificity among engine-supplied values), so a knob and a raw rule on the same token: the knob wins unless your selector is also high-specificity. Use knobs for the common palette, raw CSS for the long tail.
Token catalog
Every themeable value, with light/dark preset defaults. Tokens marked (derived) are produced from a knob when it's set; you can also set any of them raw.
Accent — --fbre-theme-color (#1976d2 / #42a5f5, knob color), --fbre-theme-light (#e3f2fd / #1a2332, derived), --fbre-theme-dark (#1565c0 / #1e88e5, derived), --fbre-on-primary (#fff / #fff).
Ground & surfaces
| Token | Light | Dark | Knob |
| --- | --- | --- | --- |
| --fbre-bg | #fff | #121212 | background |
| --fbre-surface | #fff | #1e1e1e | surface |
| --fbre-surface-hover | #f5f5f5 | #2a2a2a | (derived) |
| --fbre-surface-alt | #e0e0e0 | #333 | (derived) |
| --fbre-surface-subtle | #e8e8e8 | #2a2a2a | (derived) |
| --fbre-control-surface | #fff | #e0e0e0 | (derived) |
| --fbre-input-bg | var(--fbre-surface) | var(--fbre-surface) | (derived) |
| --fbre-input-bg-focus | var(--fbre-bg) | var(--fbre-bg) | (derived) |
Text — --fbre-text (#212121 / #e0e0e0, knob text), --fbre-text-secondary (#666 / #9e9e9e, derived), --fbre-text-placeholder (#aaa / #666, derived), --fbre-text-disabled (#bbb / #555, derived), --fbre-label (var(--fbre-text-secondary), derived).
Borders — --fbre-border (#ccc / #444, knob border), --fbre-border-hover (#999 / #666, derived), --fbre-border-light (#ddd / #444, derived), --fbre-border-subtle (#eee / #333, derived).
State colors
| Token | Light | Dark | Knob |
| --- | --- | --- | --- |
| --fbre-error | #d32f2f | #ef5350 | error |
| --fbre-error-light | #fdecea | #2c1b1b | (derived) |
| --fbre-success | #2d6a28 | #a8e6a0 | success |
| --fbre-success-bg | #ddffda | #1a3318 | (derived) |
| --fbre-success-border | #b8e6b4 | #2d5a28 | (derived) |
| --fbre-warning | #7a5900 | #ffe082 | warning |
| --fbre-warning-bg | #fff8e1 | #332b00 | (derived) |
| --fbre-warning-border | #ffe082 | #665500 | (derived) |
Controls (no knob — set raw) — --fbre-toggle-track (#ccc / #555), --fbre-star-empty (#ddd / #444), --fbre-star-filled (#ffc107 / #ffca28), --fbre-slider-track (#ddd / #444), --fbre-shadow-color (rgba(0,0,0,.12) / rgba(0,0,0,.4)).
Shape & motion — --fbre-radius (4px, knob radius), --fbre-font ("Segoe UI", system-ui, sans-serif, knob fontFamily), --fbre-layout-gap (4px, varies by style), --fbre-transition-duration (250ms), --fbre-transition-easing (cubic-bezier(0.4, 0, 0.2, 1)).
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 during animation to prevent overlapping transitions.
Customize timing via CSS custom properties:
.fbre-container {
--fbre-transition-duration: 250ms;
--fbre-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
}@media (prefers-reduced-motion: reduce) sets duration to 0ms automatically.
Architecture
| Concern | Implementation |
|---------|---------------|
| UI framework | Zero-dependency custom CSS |
| Styling | CSS custom properties + BEM (fbre- prefix) |
| Display detection | isDisplayComponent() checks type against constant set (header, text, divider, callout, table) |
| State | Zustand store factory, one store per <FBRE /> instance via React Context |
| Re-renders | Zustand selectors — granular, automatic |
| Build | Vite library mode |
| Components | Registry pattern (Map<string, FC>), one file per type |
| Dependencies | react, zustand, @sonata-innovations/fiber-types, @sonata-innovations/fiber-shared |
FlowData Output
getFlowData() returns collected form data. Display-only components (header, text, divider, callout, table) are excluded automatically based on their type.
FlowData
├── uuid: string
├── metadata: Record<string, string>
└── screens: ScreenData[]
├── uuid: string
├── label?: string
└── components: ComponentData[]
├── uuid: string
├── type: string
├── label?: string (omitted when empty)
├── value: string | string[] | number | number[] | boolean | null | FileUploadData
└── components?: ComponentData[][] (groups and repeaters)Build
npm run buildOutput:
dist/
├── index.d.ts # Bundled type declarations
├── fiber-fbre.js # ES module
├── fiber-fbre.cjs # CommonJS
└── fiber-fbre.css # All stylesResponsive 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. Multiple instances can run simultaneously on the same page without state conflicts.
<FBRE flow={flowA} onFlowComplete={handleA} />
<FBRE flow={flowB} onFlowComplete={handleB} />