build69
build69  /  Components  /  Forms & Flows  /  ConditionBuilder

ConditionBuilder

v1.0.9Updated 19 Oct 2026

Predicate builder: repeating rows of field / operator / value joined by and / or, with add and remove per row and one level of grouping (fb_3e0ed14a). The control behind "only send this automation when Department is HR and Status is not Draft". Rules are DATA - a JSON-serialisable ConditionRuleSet, controlled or uncontrolled - and ship with a dependency-free evaluator (evaluateConditionRuleSet) that runs the saved rule against a record, on the server if that is where the automation runs.

Preview

Live · 12 stories
DefaultOpen ↗
▸Why this is recommendedconditionrulefilterquery+7

The default, out-of-the-box ConditionBuilder.

Recommended for+14
  • ✓Only send this automation when field X is / is not Y
  • ✓Build a filter or segment out of field, operator and value rows joined by and / or
  • ✓Let an admin write approval rules without writing code
  • ✓An if-this-then-that condition editor on a settings screen
  • ✓Saved views or smart lists a user defines themselves
Tags+6
conditionrulefilterquerypredicateautomationand-orbuildercriteriasegmentdefault
Features+4
  • Rules are data: ConditionRuleSet = { joiner, groups: [{ id, joiner, conditions: [{ id, field, operator, value }] }] } - controlled value + onChange, or uncontrolled defaultValue
  • One level of nesting: conditions share their group joiner, groups share the set joiner, so (A and B) or (C and D) is expressible and the stored shape stays a fixed two-level object, not a recursive tree
  • The field's type decides the operators: text (is / contains / starts with / is empty), number (greater than / between), date (before / after / on or before), boolean (is true / is false), select and multiselect (is any of / has all of / is none of)
  • The operator's arity decides the value editor: none for is empty, one for most, TWO for is between, a multi-select for is any of
  • Value editors are this catalogue's own controls - Input, NumberInput, DatePicker, Select, MultiSelect - never hand-rolled
  • Every control is named with its row number and its part (Condition 2 operator), so a screen reader never meets a third anonymous combo box in a repeating row
  • Removing a row moves focus to the row that took its place (or the Add button) and announces itself through a polite live region
  • The and / or joiner is a real radio group (arrow keys, Space), shown once per group and echoed as text after
  • States: empty, disabled (controls stay, edits blocked), readOnly (the rule as sentences, no controls), an invalid row wired to its own message, and maxConditions / maxGroups ceilings that say why Add is off
  • Pure helpers with no eval and no dynamic code: evaluateConditionRuleSet / evaluateCondition / filterByConditionRuleSet, conditionRuleSetIssues for the Save button, conditionValueText for a saved-rule summary - importable from lib/condition-rules so a server component can CALL them
  • A field id reaches own properties only, so a stored rule cannot walk the prototype chain of the record it is evaluated against
  • Dates are stored as ISO YYYY-MM-DD strings and read by their digits, so a rule means the same day in every timezone and the whole set survives JSON round-tripping
  • i18n labels prop (ConditionBuilderLabels - operator names, placeholders, per-row aria-labels, announcements; {index} / {group} / {max} templates)
Not recommended for−2
  • ✕Designing a form (use FormBuilderCanvas)
  • ✕Laying out the steps of a workflow around the condition (use RuleStep)
  • ✕Quick one-click filters above a table (use FilterBar)
  • ✕Free-text search (use SearchInput)
  • ✕Arbitrary boolean algebra deeper than one level of grouping - the shape is deliberately two levels

Install

Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.

get_registry_item({ name: "ConditionBuilder" })

Usage

import { ConditionBuilder, evaluateConditionRuleSet, type ConditionRuleSet } from '@/components/patterns/condition-builder';

const FIELDS = [
  { id: 'department', label: 'Department', type: 'select', options: [{ value: 'hr', label: 'People and Culture' }] },
  { id: 'salary', label: 'Monthly salary', type: 'number' },
];

const [rules, setRules] = React.useState<ConditionRuleSet>({ joiner: 'and', groups: [] });

<ConditionBuilder fields={FIELDS} value={rules} onChange={setRules} aria-label="Send this email when" />;

// later, where the automation runs (import from lib/condition-rules on the server):
const shouldSend = evaluateConditionRuleSet(rules, FIELDS, employee);

Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.

Props

PropTypeRequired
fields

The fields conditions may be built on: { id, label, type, options?, operators?, placeholder?, min?, max?, step?, unit? }. type is text | number | date | boolean | select | multiselect

ConditionField[]yes
value

Controlled rule set

ConditionRuleSet—
defaultValue

Uncontrolled initial rule set

ConditionRuleSet—
onChange

Fires with the FULL next rule set after every edit

(value: ConditionRuleSet) => void—
allowGroups

Offer Add group, i.e. the one level of nesting. Default true; false keeps the stored shape unchanged so it can be turned on later

boolean—
maxConditions

Maximum conditions across every group. Default unlimited

number—
maxGroups

Maximum groups. Default unlimited

number—
disabled

Block every edit while keeping the rows visible, e.g. while saving

boolean—
readOnly

Render the rule as a static sentence per row - how a saved automation reads on a detail page. Distinct from disabled

boolean—
showValidation

Paint a row whose operator needs a value and has none as invalid. Default true. A row with no field yet is never painted

boolean—
size

Row control height. Two sizes only, because the Select trigger the row is built around has no larger one

'sm' | 'md'—
emptyState

Replaces the built-in message when there are no conditions

React.ReactNode—
footerEnd

Extra controls beside Add condition

React.ReactNode—
generateId

Id factory for new rows. Default: a useId-scoped counter

() => string—
labels

Override any built-in string (operator names, placeholders, per-row aria-labels, live-region announcements, limit hints) for i18n

Partial<ConditionBuilderLabels>—

Related