Rule Builder React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React rule builder for if-this-then-that automations: a trigger, conditions and ordered actions as sentence cards, read back as one sentence.
Pick a trigger from the dashed card, then press any step card to edit it: on a desktop the editor opens under the card, on a phone it rises as a bottom sheet. Edits are drafts: Apply, or Enter in a one-line field, saves them; Cancel, Escape or closing the sheet throws them away, and Apply stays dimmed until the required fields are filled (pressing it anyway points at them). Add conditions from the trigger's fields and choose whether all or any must match; add actions from the catalog and reorder them by dragging the grip (on a phone only the grip drags, the rest of the card scrolls the page), with Alt+Up or Alt+Down on a card, or with Space, the arrows and Space again on the grip. Each card's more button offers Edit, Duplicate, Move up, Move down and Remove (the trigger's offers Change trigger). Up, Down, Home and End move between the cards, Tab reaches a card's grip and more button. The header switch turns the rule on or off, and a stopped rule's cards dim; Test runs onTest and shows each action's outcome under the summary, which reads the whole rule as one sentence.
Rule Builder is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add rule-builder, together with Button, Currency Input, Drawer, Dropdown Menu, Field and 17 more and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add rule-builderRule Builder is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import RuleBuilder from @/components/ui/rule-builder.
import { useState } from "react"
import { FileWarning, Mail, Tag } from "lucide-react"
import {
RuleBuilder,
describeRule,
type RuleActionDef,
type RuleTriggerDef,
type RuleValue,
} from "@/components/ui/rule-builder"
const days = (count: number) => (count === 1 ? "1 day" : `${count} days`)
const triggers: RuleTriggerDef[] = [
{
type: "invoice-overdue",
label: "Invoice overdue",
icon: <FileWarning />,
params: [{ id: "days", label: "After", type: "days", required: true, defaultValue: 30 }],
fields: [
{ id: "amount", label: "Amount", type: "currency" },
{
id: "city",
label: "City",
type: "select",
options: [
{ value: "Austin", label: "Austin" },
{ value: "Portland", label: "Portland" },
],
},
],
summary: (params) => `an invoice is **${days(Number(params.days))}** overdue`,
},
]
const actions: RuleActionDef[] = [
{
type: "send-email",
label: "Send email",
icon: <Mail />,
tone: "info",
params: [
{
id: "template",
label: "Template",
type: "select",
required: true,
options: [{ value: "final-notice", label: "Final notice" }],
},
],
summary: () => "send the **“Final notice”** email to the client",
},
{
type: "add-tag",
label: "Add tag",
icon: <Tag />,
params: [{ id: "tag", label: "Tag", type: "text", required: true }],
summary: (params) => `add the **${params.tag}** tag`,
},
]
export function OverdueRule({
initial,
onSave,
}: {
initial: RuleValue
onSave: (rule: RuleValue, sentence: string) => void
}) {
const [rule, setRule] = useState(initial)
return (
<RuleBuilder
title="Final notice after 30 days"
triggers={triggers}
actionTypes={actions}
value={rule}
onValueChange={(next) => {
setRule(next)
onSave(next, describeRule(next, { triggers, actions }))
}}
onTest={async () => ({ matched: true, message: "Invoice #2026-0371 would get the final notice." })}
labels={{ test: "Dry run", enabled: "On" }}
/>
)
}Props
triggers*RuleTriggerDef[]The trigger catalog: { type, label, description?, icon?, group?, params?, fields?, summary? }. fields are the lib/filters fields the conditions can test (an invoice, a deal, a lead); summary(params) says the trigger in a sentence with values wrapped in **, like "an invoice is **30 days** overdue".
actionTypes*RuleActionDef[]The action catalog: { type, label, description?, icon?, group?, tone?, params?, summary?, multiple? }. tone colors the icon tile; multiple: false allows the action once per rule, so it leaves the picker once used. Named actionTypes because actions is the value's list.
titledefault""ReactNodeThe rule's name in the header. It also names the switch, so a screen reader hears which rule turns on or off.
valueRuleValueThe rule: { trigger: { type, params } | null, conditions: FilterGroup, actions: { id, type, params }[] }. conditions is the lib/filters group a saved view or filter-builder uses, root level only.
defaultValueRuleValueThe starting rule while uncontrolled; an empty rule (createRule()) when left out.
onValueChange(value: RuleValue) => voidEvery applied change: an editor's Apply, a picked trigger, an added, duplicated, moved or removed step, the all / any switch. Drafts in an open editor never reach it.
enabledbooleanControlled state of the header switch; pair it with onEnabledChange.
defaultEnableddefaulttruebooleanWhether the rule starts switched on. A switched off rule's cards dim to 60%.
onEnabledChange(enabled: boolean) => voidFires when the header switch flips.
readOnlydefaultfalsebooleanSummaries only: cards without editors, menus, grips or add buttons, and a switch that shows its state. Test still works.
disableddefaultfalsebooleanDims and locks everything: cards, menus, grips, add buttons, the switch and Test.
onTest(value: RuleValue) => Promise<RuleTestResult>Shows the Test button. While the promise runs the button spins; the result ({ matched, message?, steps?: { actionId, ok, message? }[] }) appears under the summary with each action's outcome, and any edit clears it. A rejection shows labels.testFailed.
allowOrdefaulttruebooleanLets the conditions match any instead of all: an all / any switch appears once there are two conditions. Off, conditions always join with and.
maxActionsdefault0numberThe most actions a rule may have; the add button hides at the limit. A value below the current count removes nothing. 0 means no limit.
validatedefault"touched""always" | "touched" | "never"When issues show on the cards (a red ring and a line saying what is missing): always, only after a step was edited once, or never (the app validates with validateRule).
responsivedefaulttruebooleanBelow 768px the step editors, the trigger, field and action pickers and the card menus open as bottom sheets. Off, phones get the desktop editor under the card.
showSummarydefaulttruebooleanThe dashed box that reads the whole rule as one sentence, values in bold.
summaryPositiondefault"bottom""top" | "bottom"Where the sentence sits: under the header or after the actions.
sizedefault"md""sm" | "md"Step cards 48 / 56px tall with 28 / 32px icon tiles, and sm / md fields in the desktop editors. On touch screens every card is at least 56px.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the section pills (When, If, Then), the rails between them, the action numbers and the switch. neutral uses foreground tints.
colordefault""stringAny CSS color; replaces the tone for the pills, rails, numbers and switch. Leave empty to use the tone.
currencydefault"USD"stringISO 4217 code for currency params and currency fields that do not set their own; amounts read like $5,000.
localedefault""stringFormats numbers, money and dates in cards, fields and the sentence (en-US: $5,000). Empty uses the UIProvider's locale, then the browser's.
labelsRuleBuilderLabelsInputEvery visible and spoken string, merged over DEFAULT_RULE_BUILDER_LABELS: when, if, then, chooseTrigger, chooseAction, chooseField, addCondition, addAction, all, any, matchAll, matchAny, match, noConditions, noActions, edit, duplicate, moveUp, moveDown, remove, change, more, apply, cancel, required, back, enabled, test, testing, matched, notMatched, testFailed, stepOk, stepFailed, dismiss, summary, summaryEmpty, trigger, condition, field, operator, value, from, to, choose, search, noResults, daysUnit, days(count, formatted), unknownTrigger, unknownAction, unknownField, fieldUnavailable, incomplete, missing(label), sentence { when, if, then, and, or, comma, nothing }, operators (per filter operator), dragHandle(label), dragInstructions and announce { added, removed, moved, pickedUp, cancelled, pruned }.
classNamesPartial<Record<"header" | "title" | "enabled" | "test" | "section" | "sectionLabel" | "rail" | "steps" | "step" | "stepButton" | "stepIcon" | "stepNumber" | "stepSummary" | "stepIssue" | "stepMenu" | "stepHandle" | "joiner" | "editor" | "editorFooter" | "combinator" | "add" | "empty" | "summary" | "testResult" | "sheet", string>>Extra classes for the inner parts; step is the card, stepButton the stretched button that opens its editor, sheet the phone editor.
classNamestringExtra classes for the root, merged last. Override the tone vars here, e.g. [--tone-soft:...].
Parts
describeRule
functionThe rule as one plain sentence, the same one the summary box shows: "When an invoice is 30 days overdue, if Amount ≥ $5,000, then …". Only complete conditions are read. Use it for notifications, logs and the automation list. Signature: (value: RuleValue, options: { triggers; actions; locale?; currency?; labels? }) => string.
validateRule
functionWhat keeps the rule from running, as { path: "trigger" | `conditions.${id}` | `actions.${id}`, message }: a missing or unknown trigger, incomplete conditions or conditions on fields the trigger lacks, unknown actions and required params left empty. Signature: (value: RuleValue, options: { triggers; actions; labels? }) => RuleIssue[].
createRule
functionAn empty rule (no trigger, an and group with no conditions, no actions). Call it in handlers or state initializers. Signature: (overrides?: Partial<RuleValue>) => RuleValue.
DEFAULT_RULE_BUILDER_LABELS
constantThe English strings, to spread and override. Type: RuleBuilderLabels.
Examples
Empty rule
createRule(): pick a trigger from the dashed card; required params open its editor first.
Any of the conditions
allowOr with any selected: one of three conditions is enough, and the joiners say or.
Read only in a list
readOnly shows summaries only, for an Automations list; Edit brings the editors back.
Invalid steps
validate="always": an e-mail without a template and a condition without a value, ringed in red.
On a phone
The playground at 390px: every card opens its editor as a bottom sheet with Apply and Cancel.
Tone and color
tone="brand" or info colors the pills, rails, numbers and switch; a custom color replaces the tone.
Built-in labels, euros
No labels passed: the built-in strings, size sm, amounts in euros for en-US.
Dependencies
Files
- components/ui/rule-builder.tsx
FAQ
Is the React Rule Builder component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Rule Builder?
Run npx wingo-ui@latest add rule-builder. It also adds the Wingo UI items it builds on (Button, Currency Input, Drawer, Dropdown Menu, Field and 17 more) and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/rule-builder.
How does Rule Builder behave on phones?
Below 768px the step editors, the trigger, field and action pickers and the card menus open as bottom sheets. Off, phones get the desktop editor under the card. The responsive prop controls this and is on by default.
See also
More React AI components
Articles using this component
- React Workflow Builder: Triggers, Branches and AI Steps
Build it: screens and apps13 min read