Build powerful decision trees with a visual node-based editor
🌐 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, 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>
);
}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, 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,
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
}
}}
/>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 tree is saved |
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