@sonata-innovations/fiber-fbre
v2.0.4
Published
Fiber Render Engine — renders Flow JSON forms with conditional logic, validation, and screen transitions
Downloads
437
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 | Yes | Called when the user completes the flow |
| 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 | Yes | Called when the user completes the flow |
| 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,
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";Flow JSON Schema
Flow
├── uuid: string
├── metadata: { name?, description?, ...}
├── config?: { mode?, theme?: { color?, darkMode?, style? }, navigation?: { transition?, allowInvalidTransition? }, controls?: { show?, layout?, showStepper? }, summary? }
└── 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 - 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 uses CSS custom properties. Override on the container:
.fbre-container {
--fbre-theme-color: #1976d2;
--fbre-theme-light: #e3f2fd;
--fbre-theme-dark: #1565c0;
--fbre-error: #d32f2f;
--fbre-text: #212121;
--fbre-text-secondary: #666;
--fbre-border: #ccc;
--fbre-bg: #fff;
--fbre-radius: 4px;
--fbre-font: "Segoe UI", system-ui, sans-serif;
}theme.color from flow.config.theme.color is applied automatically via inline style.
Dark Mode
Enable dark mode via the theme prop or config:
// Via theme prop (takes precedence)
<FBRE flow={myFlow} theme={{ darkMode: true }} onFlowComplete={handleComplete} />
// Via flow config
const flow = {
...myFlow,
config: { ...myFlow.config, theme: { darkMode: true } }
};
<FBRE flow={flow} onFlowComplete={handleComplete} />Dark mode applies data-mode="dark" on the container and overrides all CSS custom properties with a dark palette. Custom theme.color values are preserved — the inline style takes precedence over the dark mode default.
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} />