Building complex, dynamic forms has never been this simple
🌐 Website • 🎮 Playground • Features • Installation • Quick Start • Examples • 🪄 AI Generation
Treege is a modern React library for creating and rendering interactive decision trees. Built on top of ReactFlow, it provides a complete solution for building complex form flow, decision logic, and conditional workflows with an intuitive visual editor.
- Node-based Interface: Drag-and-drop editor powered by ReactFlow
- 3 Node Types: Input, UI, and Group nodes
- Conditional Edges: Advanced logic with AND/OR operators (
===,!==,>,<,>=,<=) - AI-Powered Generation: Generate decision trees from natural language descriptions using Gemini, OpenAI, DeepSeek, or Claude (Learn more)
- Multi-language Support: Built-in translation system for all labels
- Type-safe: Full TypeScript support
- Mini-map & Controls: Navigation tools for complex trees
- Theme Support: Dark/light mode with customizable backgrounds
- Production Ready: Full-featured form generation and validation system
- 16 Input Types: text, number, select, checkbox, radio, date, daterange, time, timerange, file, address, http, textarea, password, switch, autocomplete, and hidden
- Cross-Platform: Full support for both React Web and React Native with dedicated implementations
- HTTP Integration: Built-in API integration with response mapping and search functionality
- Advanced Validation: Required fields, pattern matching, custom validation functions
- Security: Built-in input sanitization to prevent XSS attacks
- Enhanced Error Messages: Clear, user-friendly error messages for HTTP inputs and validation
- Conditional Logic: Dynamic field visibility based on user input and conditional edges
- Multi-Step Forms: Group nodes are automatically turned into navigable steps with Back/Continue controls, an
onBackbridge to outer flows, and external-button submission viaformId - Edit Mode: Pre-fill with
initialValues(accepts name keys, reactive) to round-trip and edit previously submitted records - Loading State: Built-in
isLoadingprop renders a customizable skeleton while the flow is being fetched, plusisSubmittingto drive the button's loading state from async submits - Fully Customizable: Override any component (form, inputs, inputLabel, ui, step, submitButton, submitButtonWrapper, loadingSkeleton)
- Optional Dependencies: Graceful degradation when optional packages like
react-native-document-pickeraren't installed - Theme Support: Dark/light mode out of the box
- Google API Integration: Address autocomplete support
- Modular: Import only what you need (editor, renderer, or both)
- Modern Stack: React 18/19, TailwindCSS 4, TypeScript 5
- Well-typed: Comprehensive TypeScript definitions
- Production Ready: Battle-tested and actively maintained
# bun
bun add treege
# npm
npm install treege
# pnpm
pnpm add treege
# yarn
yarn add treegeCreate and edit decision trees visually:
import { TreegeEditor } from "treege/editor";
import type { Flow } from "treege";
function App() {
const [flow, setFlow] = useState<Flow | null>(null);
const handleSave = (updatedFlow: Flow) => {
setFlow(updatedFlow);
console.log("Decision tree saved:", updatedFlow);
};
return (
<TreegeEditor
flow={flow}
onSave={handleSave}
/>
);
}Render interactive forms from decision trees:
import { TreegeRenderer } from "treege/renderer";
import type { Flow, FormValues } from "treege";
function App() {
const flow: Flow = {
id: "flow-1",
nodes: [
{
id: "start",
type: "input",
data: {
name: "username",
label: "Enter your username",
required: true
}
}
],
edges: []
};
const handleSubmit = (values: FormValues) => {
console.log("Form submitted:", values);
};
return (
<TreegeRenderer
flow={flow}
onSubmit={handleSubmit}
/>
);
}import { TreegeEditor } from "treege/editor";
import { TreegeRenderer } from "treege/renderer";
import { useState } from "react";
function App() {
const [flow, setFlow] = useState(null);
const [mode, setMode] = useState<"edit" | "preview">("edit");
return (
<div>
<button onClick={() => setMode(mode === "edit" ? "preview" : "edit")}>
{mode === "edit" ? "Preview" : "Edit"}
</button>
{mode === "edit" ? (
<TreegeEditor flow={flow} onSave={setFlow} />
) : (
<TreegeRenderer flow={flow} onSubmit={console.log} />
)}
</div>
);
}Use the editor's onChange (debounced) to render the form live next to the editor — no Save click needed. Because onChange isn't gated on having input nodes, the preview also reflects an emptied canvas after Clear. Omit onSave to hide the Save button entirely when you rely solely on live updates:
import { TreegeEditor } from "treege/editor";
import { TreegeRenderer } from "treege/renderer";
import { useState } from "react";
import type { Flow } from "treege";
function App() {
const [flow, setFlow] = useState<Flow | null>(null);
return (
<div style={{ display: "flex" }}>
<TreegeEditor onChange={setFlow} /> {/* live, no Save button */}
<TreegeRenderer flow={flow} onSubmit={console.log} />
</div>
);
}Treege provides multiple import paths for optimal bundle size:
// Import everything (editor + renderer + types)
import { TreegeEditor, TreegeRenderer } from "treege";
// Import only the editor
import { TreegeEditor } from "treege/editor";
// Import only the web renderer
import { TreegeRenderer } from "treege/renderer";
// Import only the React Native renderer
import { TreegeRenderer } from "treege/renderer-native";Treege 3.0 includes full React Native support with a dedicated renderer implementation.
# Install Treege
npm install treege
# Install peer dependencies
npm install react-native
# Optional: Install for file input support
npm install react-native-document-pickerimport { TreegeRenderer } from "treege/renderer-native";
import type { Flow, FormValues } from "treege";
function App() {
const flow: Flow = {
id: "flow-1",
nodes: [
{
id: "name",
type: "input",
data: {
type: "text",
name: "fullName",
label: "Full Name",
required: true
}
},
{
id: "email",
type: "input",
data: {
type: "text",
name: "email",
label: "Email",
required: true,
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
}
}
],
edges: []
};
const handleSubmit = (values: FormValues) => {
console.log("Form submitted:", values);
};
return (
<TreegeRenderer
flow={flow}
onSubmit={handleSubmit}
/>
);
}You can customize the appearance using the style and contentContainerStyle props:
<TreegeRenderer
flow={flow}
onSubmit={handleSubmit}
style={{ flex: 1, backgroundColor: "#f5f5f5" }}
contentContainerStyle={{ padding: 20 }}
/>Override default components with your own React Native components.
Each input renderer is a React component receiving a single props object with two keys:
field— DOM-safe props (id,name,value,placeholder,required,aria-invalid). On the web you can spread them onto an element (<input {...field} />); on React Native pick the ones you need.extra— Treege-specific props:setValue,error,label,helperText,node,InputLabel(the resolved, overridable label component), andmissingDependencies(the unfilled fields this input's dynamic options depend on).
import { Text, TextInput, View } from "react-native";
import { TreegeRenderer } from "treege/renderer-native";
const CustomTextInput = ({ field, extra }) => {
return (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 14, marginBottom: 4 }}>{extra.label}</Text>
<TextInput
value={field.value}
placeholder={field.placeholder}
onChangeText={extra.setValue}
style={{
borderWidth: 1,
borderColor: extra.error ? "red" : "#ccc",
padding: 10,
borderRadius: 8
}}
/>
{extra.error && <Text style={{ color: "red", fontSize: 12 }}>{extra.error}</Text>}
{extra.missingDependencies.length > 0 && (
<Text style={{ color: "#b45309", fontSize: 12 }}>
Please fill in first: {extra.missingDependencies.map((d) => d.label).join(", ")}
</Text>
)}
</View>
);
};
<TreegeRenderer
flow={flow}
components={{
inputs: {
text: CustomTextInput
}
}}
/>The React Native renderer includes default implementations for all input types:
Fully Implemented (Vanilla React Native):
text,number,textarea,passwordcheckbox,switch,hidden
With Optional Dependencies (gracefully degrades if not installed):
file- Requires react-native-document-picker (optional)
Requires Custom Implementation (placeholder provided):
select,radio,autocompletedate,daterange,time,timerangeaddress,http
You can implement these inputs using popular React Native libraries:
- @react-native-picker/picker for
selectandradio - react-native-date-picker for
dateandtimeinputs - @react-native-community/google-places-autocomplete for
address
The React Native renderer shares the same API as the web renderer, with some platform-specific props:
| Prop | Type | Default | Description |
|---|---|---|---|
flow |
Flow | null |
- | Decision tree to render |
onSubmit |
(values: FormValues, meta?: Meta) => void |
- | Form submission handler (meta includes HTTP response data) |
onChange |
(values: FormValues) => void |
- | Form change handler |
validate |
(values, nodes) => Record<string, string> |
- | Custom validation function |
initialValues |
FormValues |
{} |
Pre-fill values to edit a record. Accepts node.id or name keys; reactive (re-seeds if it changes after mount) |
components |
TreegeRendererComponents |
- | Custom component overrides |
language |
string |
"en" |
UI language |
validationMode |
"onSubmit" | "onChange" |
"onSubmit" |
When to validate |
theme |
"light" | "dark" |
"dark" |
Renderer theme |
googleApiKey |
string |
- | API key for address input |
headers |
HttpHeaders |
- | HTTP headers as { name: value }, applied to every request (field-level wins) |
isLoading |
boolean |
false |
Render a loading skeleton instead of the form |
isSubmitting |
boolean |
false |
Force the submit/continue button into its loading state (OR-ed with internal state) |
onBack |
() => void |
- | Called when Back is clicked on the first step; bridges to an outer flow |
style |
ViewStyle |
- | ScrollView style (RN only) |
contentContainerStyle |
ViewStyle |
- | Content container style (RN) |
Treege has three node types: input, ui, and group. Navigation is automatic — group nodes drive step navigation and conditional edges drive branching.
Form input with validation, patterns, and conditional logic.
{
type: "input",
data: {
type: "text",
name: "email",
label: "Email Address",
required: true,
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
errorMessage: "Please enter a valid email"
}
}Supported input types: text, number, textarea, password, select, radio, checkbox, switch, autocomplete, date, daterange, time, timerange, file, address, http, hidden
Container for organizing multiple nodes together. Groups also drive multi-step forms: at runtime each group of visible nodes becomes a navigable step (Back/Continue). Child nodes belong to a group via their parentId.
{
type: "group",
data: {
label: "Personal Information"
}
}Display-only elements for visual organization and content display.
{
type: "ui",
data: {
type: "title", // or "divider"
label: "Welcome to the form"
}
}Supported UI types:
title- Display headings and titlesdivider- Visual separator between sections
Create dynamic flow with conditional logic:
{
type: "conditional",
data: {
conditions: [
{
field: "age",
operator: ">=",
value: "18"
},
{
field: "country",
operator: "===",
value: "US"
}
],
logicalOperator: "AND"
}
}Supported operators: ===, !==, >, <, >=, <=
Treege supports multiple languages out of the box:
{
type: "input",
data: {
label: {
en: "First Name",
fr: "Prénom",
es: "Nombre"
}
}
}Override default input renderers with your own. A renderer is a React component
receiving a single props object: field (DOM-safe props, spreadable onto an
element) and extra (setValue, error, label, helperText, node,
InputLabel, missingDependencies).
import { TreegeRenderer } from "treege/renderer";
const CustomTextInput = ({ field, extra }) => {
return (
<label>
{extra.label}
<input
{...field}
className="my-custom-input"
onChange={(e) => extra.setValue(e.target.value)}
/>
{extra.error && <span className="error">{extra.error}</span>}
</label>
);
};
<TreegeRenderer
flow={flow}
components={{
inputs: {
text: CustomTextInput
}
}}
/>All default inputs render their label through a single shared component, DefaultInputLabel. Override it once via components.inputLabel to restyle every field label at once. It receives { label, required, htmlFor } (web) and renders nothing when the field has no label — so the technical name key never leaks into the form. Each input also exposes the resolved label as extra.InputLabel, so custom inputs can reuse it:
<TreegeRenderer
flow={flow}
components={{
inputLabel: ({ label, required, htmlFor }) =>
label ? (
<label htmlFor={htmlFor} className="my-label">
{label}
{required && <span className="text-red-500"> *</span>}
</label>
) : null,
}}
/>Accessibility is preserved even without a visible label: the default inputs fall back to aria-label={label || node.data.name} on the control itself.
Add custom validation logic:
<TreegeRenderer
flow={flow}
validate={(values, visibleNodes) => {
const errors = {};
if (values.password !== values.confirmPassword) {
errors.confirmPassword = "Passwords must match";
}
return errors;
}}
/>Control when validation occurs:
// Validate only on submit (default)
<TreegeRenderer validationMode="onSubmit" />
// Validate on every change
<TreegeRenderer validationMode="onChange" />Use the HTTP input type to fetch and map data from APIs:
{
type: "input",
data: {
type: "http",
name: "country",
label: "Select your country",
httpConfig: {
method: "GET",
url: "https://api.example.com/countries",
responsePath: "$.data.countries", // JSONPath to extract data
mapping: {
label: "name",
value: "code"
},
searchParam: "query", // Enable search functionality
fetchOnMount: true
}
}
}Configure the renderer globally using the TreegeRendererProvider:
import { TreegeRendererProvider } from "treege/renderer";
function App() {
return (
<TreegeRendererProvider
language="fr"
googleApiKey="your-google-api-key"
components={{
// Your custom components
}}
>
<TreegeRenderer flow={flow} />
</TreegeRendererProvider>
);
}When the flow is being fetched asynchronously, pass isLoading to render a skeleton in place of the form:
function App() {
const { data: flow, isPending } = useQuery(/* ... */);
return <TreegeRenderer flow={flow ?? null} isLoading={isPending} onSubmit={console.log} />;
}Customize the skeleton via components.loadingSkeleton:
<TreegeRenderer
flow={flow}
isLoading={isPending}
components={{
loadingSkeleton: () => <MyCustomSkeleton />,
}}
/>To edit a record that was already filled in, pass it to initialValues. The keys can be either node.id or the same name-based keys you receive from onSubmit/onChange, so you can round-trip the submitted object directly — no remapping needed:
const handleSubmit = (values) => save(values); // e.g. { firstName: "Alice", email: "a@b.com" }
// later, to edit the saved record:
<TreegeRenderer flow={flow} initialValues={savedRecord} onSubmit={handleSubmit} />initialValues is reactive: if it changes after mount (e.g. an async-fetched record resolves later), the form is re-seeded automatically — no key prop or isLoading gate required. Passing a new object of identical content does not reset the form, so in-progress edits are preserved.
const { data } = useQuery(/* ... */);
<TreegeRenderer flow={flow} initialValues={data ?? {}} onSubmit={handleSubmit} />Pass isSubmitting to keep the submit/continue button in its loading state (spinner + disabled) while an async onSubmit resolves on your side. It's OR-ed with the renderer's own internal submitting state (e.g. during an HTTP submitConfig call):
<TreegeRenderer flow={flow} isSubmitting={mutation.isPending} onSubmit={handleSubmit} />When a flow contains Group nodes, the renderer automatically splits the form into navigable steps — each contiguous slice of visible nodes sharing the same group becomes one step, with built-in Back/Continue controls (Continue turns into Submit on the last step). Branching via conditional edges recomputes the steps on the fly.
Override the default step layout via components.step:
<TreegeRenderer
flow={flow}
components={{
step: ({ label, children, canGoBack, isLastStep, canContinue, isSubmitting, onBack, onContinue }) => (
<section>
<h2>{label}</h2>
{children}
{canGoBack && <button onClick={onBack}>Back</button>}
<button disabled={!canContinue || isSubmitting} onClick={onContinue}>
{isSubmitting ? "Submitting…" : isLastStep ? "Submit" : "Continue"}
</button>
</section>
),
}}
/>Use canGoBack (not !isFirstStep) to decide whether to show the Back control: it's true on any step past the first, and also on the first step when an onBack prop is provided.
When the renderer is embedded in a surrounding wizard (e.g. a modal with its own steps), use onBack to step back out of the renderer's first step, and formId to drive submission from an external button:
<TreegeRenderer
flow={flow}
formId="treege-form"
onBack={() => modal.goToPreviousStep()} // Back on step 0 → previous modal step
onSubmit={handleSubmit}
/>
// A submit button living outside the renderer (web): only submits on the last
// step — on earlier steps it advances, like the built-in Continue button.
<button type="submit" form="treege-form">Save</button>Use the useTreegeRenderer hook to drive the form yourself (headless mode). It takes the same configuration as TreegeRenderer and returns the full form state and control methods:
import { useTreegeRenderer } from "treege/renderer";
function CustomForm({ flow }) {
const {
formValues,
setFieldValue,
handleSubmit,
formErrors,
visibleNodes,
isSubmitting,
currentStep,
goToNextStep,
goToPreviousStep,
} = useTreegeRenderer({
flow,
onSubmit: (values) => console.log("Submitted:", values),
});
return (
<div>
<button onClick={() => setFieldValue("email", "test@example.com")}>
Prefill Email
</button>
<button onClick={handleSubmit} disabled={isSubmitting}>
Submit
</button>
</div>
);
}The
useTreegeRendererreturn type is exported asUseTreegeRendererReturnfor TypeScript consumers building custom components.
Check out the /example directory for complete examples:
# Run the web example app (Vite, opens /example)
bun run example
# Run the React Native example app (Expo)
bun run example:nativeOnce the development server is running, you can access these examples:
-
Default Example: http://localhost:5173/
- Basic demonstration of Treege functionality
-
Demo Example: http://localhost:5173/example
- Full featured demo showcasing the library capabilities
-
All Inputs Example: http://localhost:5173/example-all-inputs
- Comprehensive showcase of all 16 input types
-
Custom Input Example: http://localhost:5173/example-custom-input
- Demonstrates how to create and integrate custom input components
-
TreegeRendererProvider Example: http://localhost:5173/example-treege-renderer-provider
- Shows global configuration with TreegeRendererProvider
| Prop | Type | Default | Description |
|---|---|---|---|
flow |
Flow | null |
null |
Initial decision tree |
onSave |
(flow: Flow) => void |
- | Callback when the user saves the tree. The Save button is only rendered when this prop is provided, and is no longer disabled on an empty canvas (so a cleared flow can be saved) |
onChange |
(flow: Flow) => void |
- | Called (debounced ~150 ms) on every canvas change with the current flow. Use it for live preview / autosave. Unlike onSave it isn't gated on having input nodes, so it also reports an emptied canvas after Clear, and it does not strip sensitive headers (live consumers need the real flow) |
onExportJson |
() => { nodes: Node[]; edges: Edge[] } |
- | Callback for exporting JSON data |
language |
string |
"en" |
UI language |
theme |
"light" | "dark" |
"dark" |
Editor theme |
aiConfig |
AIConfig |
- | AI configuration for tree generation (see AI Generation) |
className |
string |
- | Additional CSS class names for custom styling |
extraMenuItems |
ExtraMenuItem[] |
- | Extra entries appended to the actions panel "more" dropdown |
openApi |
OpenApiDocument | string |
- | OpenAPI 3.x source used to power URL/route suggestions and the Authorize flow. Accepts a pre-parsed document or a URL string (the editor fetches it on mount and toasts on failure) |
baseUrl |
string |
- | Base URL the tree runs against. HTTP/options-source urls are stored relative to it, shown as a read-only prefix, and used to resolve the "Detect fields" probe. Pass the same value as TreegeRenderer's baseUrl |
headers |
HttpHeaders |
- | Global HTTP headers applied to in-editor requests (e.g. the "Detect fields" button). Pass the same value you give to TreegeRenderer so editor previews use the same auth/headers as runtime |
onAuthorize |
(headers: HttpHeaders) => void |
- | Called when the user submits the Authorize dialog. Forward the resulting headers to TreegeRenderer (or TreegeRendererProvider) so every form request is authenticated |
onHeadersChange |
(headers: HttpHeaders) => void |
- | Called when the user edits headers in the built-in "Global headers" dialog. The component is controlled — update your headers state in response and pass the new object back via the headers prop |
| Prop | Type | Default | Description |
|---|---|---|---|
flow |
Flow | null |
- | Decision tree to render |
onSubmit |
(values: FormValues, meta?: Meta) => void |
- | Form submission handler (meta includes HTTP response data) |
onChange |
(values: FormValues) => void |
- | Form change handler |
validate |
(values, nodes) => Record<string, string> |
- | Custom validation function |
initialValues |
FormValues |
{} |
Pre-fill values to edit a submitted record. Accepts node.id or name keys (same shape as onSubmit); reactive — re-seeds if it changes after mount (see below) |
components |
TreegeRendererComponents |
- | Custom component overrides |
language |
string |
"en" |
UI language |
validationMode |
"onSubmit" | "onChange" |
"onSubmit" |
When to validate |
theme |
"light" | "dark" |
"dark" |
Renderer theme |
googleApiKey |
string |
- | API key for address input |
headers |
HttpHeaders |
- | HTTP headers as { name: value }, applied to every request (field-level wins) |
isLoading |
boolean |
false |
Render a loading skeleton instead of the form (see below) |
isSubmitting |
boolean |
false |
Force the submit/continue button into its loading state (spinner + disabled). OR-ed with the internal submitting state — useful with an async onSubmit |
onBack |
() => void |
- | Called when Back is clicked on the first step; bridges back-navigation to an outer flow (e.g. a parent modal). Shows a Back button on step 0 when provided |
formId |
string |
- | Sets the <form> id so a submit button outside the renderer can target it via the native form attribute. Web only; submits only on the last step in multi-step flows |
className |
string |
- | Additional CSS class names for custom styling |
# Install dependencies
bun install
# Start dev server
bun run dev
# Build library
bun run build
# Run linter and type check
bun run lint
# Preview build
bun run preview- React - UI library
- TypeScript - Type safety
- TailwindCSS 4 - Styling
- ReactFlow - Node-based UI
- Vite - Build tool
Contributions are welcome! Please feel free to submit a Pull Request.
MIT
Created and maintained by Mickaël Austoni