Customization
Treege allows you to customize the rendered form components to match your design system.
Custom Components
Override default input components with your own implementations.
Component Props
All custom components receive these props:
export type InputRenderProps<T extends InputType = InputType> = {
/**
* The node data for this input field
*/
node: Node<InputNodeData>;
/**
* Current value of the input field (typed based on input type when T is specified)
*/
value: InputValueTypeMap[T];
/**
* Unique field ID (nodeId)
*/
id: string;
/**
* Field name (resolved using priority: name > label > nodeId)
* Use this for the name and id attributes of the input element
*/
name: string;
/**
* Function to update the input value
* @param value - The new value (typed based on input type when T is specified)
*/
setValue: (value: InputValueTypeMap[T]) => void;
/**
* Validation error message for this field (if any)
*/
error?: string;
/**
* Translated label (already processed with current language)
*/
label?: string;
/**
* Translated placeholder (already processed with current language)
*/
placeholder?: string;
/**
* Translated helper text (already processed with current language)
*/
helperText?: string;
/**
* Missing required fields on form submit (for submit inputs)
*/
missingRequiredFields?: string[];
/**
* Whether the form is currently being submitted (for submit inputs)
*/
isSubmitting?: boolean;
};Example: Custom Text Input
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import type { InputRenderProps } from "treege/renderer"
const CustomTextInput = ({
node,
value,
setValue,
error,
label,
placeholder,
helperText,
id,
name
}: InputRenderProps<"text">) => {
return (
<div className="mb-4">
<label className="block text-sm font-medium mb-1" htmlFor={id}>
{label}
{node.data.required && <span className="text-red-500 ml-1">*</span>}
</label>
<input
id={id}
name={name}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
{helperText && !error && (
<p className="text-gray-500 text-sm mt-1">
{helperText}
</p>
)}
</div>
)
}Using Custom Components
import { TreegeRenderer } from "treege/renderer"
import { CustomTextInput } from "./CustomTextInput"
import { CustomNumberInput } from "./CustomNumberInput"
<TreegeRenderer
flows={flows as Flow}
onSubmit={handleSubmit}
components={{
inputs: {
text: CustomTextInput,
number: CustomNumberInput,
},
}}
/>Important: Using id and name Props
The id and name props are automatically provided to your custom components. Always use them for proper form behavior:
const CustomInput = ({ id, name, value, setValue }: InputRenderProps<"text">) => (
<input
id={id} // Use for accessibility (links label to input)
name={name} // Use for form submission and field identification
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)The name field is resolved with priority: node.data.name > node.data.label > node.id
Custom Submit Button
You can customize the submit button and its wrapper:
<TreegeRenderer
flows={flow}
onSubmit={handleSubmit}
components={{
submitButton: ({ label }) => (
<button type="submit" className="btn-primary">
{label || "Submit"}
</button>
),
submitButtonWrapper: ({ children, missingFields }) => (
<div className="submit-wrapper">
{children}
{missingFields && missingFields.length > 0 && (
<div className="tooltip">
Please fill required fields: {missingFields.join(", ")}
</div>
)}
</div>
),
}}
/>Submit Input with State
For submit type inputs, you get additional props:
const CustomSubmitInput = ({
label,
missingRequiredFields,
isSubmitting
}: InputRenderProps<"submit">) => (
<button
type="submit"
disabled={isSubmitting || (missingRequiredFields?.length ?? 0) > 0}
>
{isSubmitting ? "Submitting..." : label || "Submit"}
</button>
)Custom Validation
Add custom validation logic for specific fields.
Example: Email Validation
<TreegeRenderer
flows={flow}
onSubmit={handleSubmit}
validators={{
email: (value) => {
if (!value) return "Email is required"
if (!value.endsWith("@company.com")) {
return "Must use company email address"
}
}
}}
/>Example: Password Strength
validators={{
password: (value) => {
if (!value) return "Password is required"
if (value.length < 8) return "Must be 8+ characters"
if (!/[A-Z]/.test(value)) return "Need uppercase letter"
if (!/[0-9]/.test(value)) return "Need number"
}
}}Tip: Validators receive the field value and all form values, allowing for cross-field validation.