-

Condition Builder

PreviousNext

A compact rule builder for workflow branches, filters, routing, and guardrails.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/condition-builder.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/condition-builder.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/condition-builder.json

Component

components/ui/condition-builder.tsx
"use client"

import * as React from "react"
import { PlusIcon, Trash2Icon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export interface ConditionRule {
  id: string
  field: string
  operator: string
  value: string
}

export interface ConditionBuilderOption {
  label: string
  value: string
}

export interface ConditionBuilderProps extends React.ComponentProps<"div"> {
  value: ConditionRule[]
  onValueChange?: (value: ConditionRule[]) => void
  fields: ConditionBuilderOption[]
  operators?: ConditionBuilderOption[]
  disabled?: boolean
  addText?: string
}

const defaultOperators: ConditionBuilderOption[] = [
  { label: "Equals", value: "equals" },
  { label: "Does not equal", value: "not_equals" },
  { label: "Contains", value: "contains" },
  { label: "Greater than", value: "gt" },
  { label: "Less than", value: "lt" },
  { label: "Exists", value: "exists" },
]

function createRule(fields: ConditionBuilderOption[]): ConditionRule {
  return {
    id: crypto.randomUUID(),
    field: fields[0]?.value ?? "",
    operator: "equals",
    value: "",
  }
}

export function ConditionBuilder({
  value,
  onValueChange,
  fields,
  operators = defaultOperators,
  disabled = false,
  addText = "Add condition",
  className,
  ...props
}: ConditionBuilderProps) {
  function updateRule(index: number, patch: Partial<ConditionRule>) {
    onValueChange?.(
      value.map((rule, ruleIndex) =>
        ruleIndex === index ? { ...rule, ...patch } : rule
      )
    )
  }

  return (
    <div className={cn("space-y-2", className)} {...props}>
      {value.map((rule, index) => (
        <div
          key={rule.id}
          className="border-border bg-card grid gap-2 rounded-lg border p-2 sm:grid-cols-[minmax(0,1fr)_160px_minmax(0,1fr)_auto]"
        >
          <Select
            value={rule.field}
            onValueChange={(field) => updateRule(index, { field })}
            disabled={disabled}
          >
            <SelectTrigger className="h-9">
              <SelectValue placeholder="Field" />
            </SelectTrigger>
            <SelectContent>
              {fields.map((field) => (
                <SelectItem key={field.value} value={field.value}>
                  {field.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select
            value={rule.operator}
            onValueChange={(operator) => updateRule(index, { operator })}
            disabled={disabled}
          >
            <SelectTrigger className="h-9">
              <SelectValue placeholder="Operator" />
            </SelectTrigger>
            <SelectContent>
              {operators.map((operator) => (
                <SelectItem key={operator.value} value={operator.value}>
                  {operator.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Input
            value={rule.value}
            onChange={(event) =>
              updateRule(index, { value: event.target.value })
            }
            placeholder="Value"
            disabled={disabled || rule.operator === "exists"}
            className="h-9"
          />
          <Button
            type="button"
            variant="ghost"
            size="icon"
            className="size-9 rounded-md"
            disabled={disabled}
            onClick={() =>
              onValueChange?.(
                value.filter((_, ruleIndex) => ruleIndex !== index)
              )
            }
            aria-label="Remove condition"
          >
            <Trash2Icon className="size-4" />
          </Button>
        </div>
      ))}
      <Button
        type="button"
        variant="outline"
        size="sm"
        className="h-8 rounded-md"
        disabled={disabled}
        onClick={() => onValueChange?.([...value, createRule(fields)])}
      >
        <PlusIcon className="size-4" />
        {addText}
      </Button>
    </div>
  )
}

Usage

import { ConditionBuilder } from "@/components/ui/condition-builder"
 
export function Example() {
  return (
    <ConditionBuilder
      value={[]}
      fields={[{ label: "Confidence", value: "confidence" }]}
    />
  )
}