BuzzForm
BuzzFormDocs

Conditional Logic

Control field visibility, disabled state, and read-only mode using declarative conditions.

Conditional Logic

BuzzForm uses a declarative AST for conditional logic — no function callbacks. Conditions work for condition, hidden, disabled, readOnly, and required.

Condition Types

Boolean Literal

Simplest form — always true or false:

{ type: "text", name: "city", disabled: true }

Atomic Condition

Check a field value against a condition:

{
  type: "text",
  name: "phone",
  label: "Phone Number",
  // Only enabled when contactMethod is "phone"
  disabled: { $data: "/contactMethod", neq: "phone" },
}

Array of Conditions (Implicit AND)

Multiple conditions that must all be true:

{
  type: "text",
  name: "companyEmail",
  // Only shown when accountType is "business" AND hasCompany is true
  condition: [
    { $data: "/accountType", eq: "business" },
    { $data: "/hasCompany", eq: true },
  ],
}

Explicit AND / OR

Group conditions logically:

{
  type: "text",
  name: "discountCode",
  // Show when role is "admin" OR "manager"
  condition: {
    $or: [
      { $data: "/role", eq: "admin" },
      { $data: "/role", eq: "manager" },
    ],
  },
}

Negation

Invert a condition with not: true:

{
  type: "text",
  name: "otherReason",
  label: "Please specify",
  // Show when reason is NOT "other"
  condition: { $data: "/reason", eq: "other", not: true },
}

Inline Functions (Escape Hatch)

If your schema is defined in code (not JSON), you can use a JavaScript function for any condition. This receives the unified ExprContext:

{
  type: "text",
  name: "managerName",
  // Simple JS logic instead of AST
  condition: ({ data, context }) => data.role === "Manager" && context.isEnterprise,
}

Custom Functions ($fn)

For complex logic that you want to keep serializable, register functions via registries.fns. You can provide these globally via <FormProvider> or directly to a specific form:

const myRegistries = { 
  fns: {
    isEligible: ({ data }) => data.age >= 18 && data.status === "active",
  }
};

// Global registration
<FormProvider registries={myRegistries}>...</FormProvider>

// OR: Per-form registration
<Form registries={myRegistries} ... />

Once registered, reference the function via $fn in your schema:

{
  type: "text",
  name: "license",
  condition: { $fn: "isEligible" },
}

Comparison Operators

Atomic conditions support these operators:

OperatorDescriptionExample
eqEquals{ $data: "/status", eq: "active" }
neqNot equals{ $data: "/status", neq: "draft" }
gtGreater than{ $data: "/age", gt: 18 }
gteGreater than or equal{ $data: "/age", gte: 18 }
ltLess than{ $data: "/age", lt: 65 }
lteLess than or equal{ $data: "/age", lte: 65 }
containsString contains{ $data: "/name", contains: "test" }
startsWithString starts with{ $data: "/code", startsWith: "ABC" }
endsWithString ends with{ $data: "/email", endsWith: "@company.com" }

Using $data and $context

Referencing Form Data

Use JSON Pointer format (paths start with /):

const contactSchema = defineSchema({
  fields: [
    {
      type: "select",
      name: "subject",
      label: "Subject",
      options: ["General", "Support", "Sales", "Other"],
    },
    {
      type: "text",
      name: "otherSubject",
      label: "Please Specify",
      // Only shown when subject is "Other"
      condition: { $data: "/subject", eq: "Other" },
    },
  ],
});

Important: When using condition, you don't need to repeat the condition in required. The field is only present when the condition is met:

// ✅ Good - condition controls visibility, required applies when visible
{
  type: "text",
  name: "otherSubject",
  condition: { $data: "/subject", eq: "Other" },
  required: true, // Only required when shown
}

// ❌ Redundant - don't do this
{
  type: "text",
  name: "otherSubject",
  condition: { $data: "/subject", eq: "Other" },
  required: { $data: "/subject", eq: "Other" }, // Unnecessary repetition
}

Path examples:

  • /username — root-level field
  • /address/city — nested field in a group
  • /items/0/name — specific array item

Referencing External Context

Use $context to access data passed via contextData:

const form = useForm({
  schema,
  contextData: {
    userRole: "admin",
    isLocked: false,
    maxItems: 10,
  },
});

// In your schema:
{
  type: "text",
  name: "adminNotes",
  label: "Admin Notes",
  // Only shown for admin users
  condition: { $context: "/userRole", eq: "admin" },
  // Disabled when form is locked
  disabled: { $context: "/isLocked", eq: true },
}

Condition vs Hidden

Both control visibility but behave differently:

PropWhen FalseValidationState
conditionField unmountedSkippedRemoved
hiddenField hiddenStill runsRetained

Use condition when you want to completely remove the field. Use hidden when you want to keep the value but hide it visually.

Examples

Contact Form with Conditional Fields

Building on the Quick Start example:

const contactSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "name",
      label: "Full Name",
      required: true,
    },
    {
      type: "email",
      name: "email",
      label: "Email",
      required: true,
    },
    {
      type: "select",
      name: "subject",
      label: "Subject",
      options: ["General", "Support", "Sales", "Other"],
      required: true,
    },
    {
      type: "textarea",
      name: "message",
      label: "Message",
      required: true,
    },
    // Only shown when subject is "Other"
    {
      type: "text",
      name: "otherSubject",
      label: "Please Specify",
      condition: { $data: "/subject", eq: "Other" },
      required: true,
    },
    // Only shown for Support subject
    {
      type: "text",
      name: "ticketId",
      label: "Support Ticket ID",
      condition: { $data: "/subject", eq: "Support" },
    },
  ],
});

Dependent Dropdowns

const contactSchema = defineSchema({
  fields: [
    {
      type: "select",
      name: "country",
      label: "Country",
      options: ["US", "UK", "Canada"],
    },
    {
      type: "select",
      name: "region",
      label: "Region / State",
      options: regions,
      // Disabled until country is selected
      disabled: { $data: "/country", eq: "" },
    },
  ],
});

Show/Hide Based on Selection

const contactSchema = defineSchema({
  fields: [
    {
      type: "radio",
      name: "contactMethod",
      label: "Preferred Contact",
      options: ["email", "phone", "mail"],
    },
    {
      type: "email",
      name: "email",
      label: "Email Address",
      condition: { $data: "/contactMethod", eq: "email" },
      required: true,
    },
    {
      type: "text",
      name: "phone",
      label: "Phone Number",
      condition: { $data: "/contactMethod", eq: "phone" },
      required: true,
    },
    {
      type: "text",
      name: "address",
      label: "Mailing Address",
      condition: { $data: "/contactMethod", eq: "mail" },
      required: true,
    },
  ],
});

Complex Conditions

const orderSchema = defineSchema({
  fields: [
    {
      type: "number",
      name: "orderTotal",
      label: "Order Total",
    },
    {
      type: "checkbox",
      name: "isVip",
      label: "VIP Customer",
    },
    {
      type: "text",
      name: "discountCode",
      label: "Discount Code",
      // Show when order total >= 100 AND customer is not VIP
      condition: {
        $and: [
          { $data: "/orderTotal", gte: 100 },
          { $data: "/isVip", eq: false },
        ],
      },
    },
  ],
});

Required Conditionally

const contactSchema = defineSchema({
  fields: [
    {
      type: "select",
      name: "contactMethod",
      label: "Contact Method",
      options: ["email", "phone"],
    },
    {
      type: "email",
      name: "email",
      label: "Email Address",
      // Required only when email is selected
      required: { $data: "/contactMethod", eq: "email" },
    },
    {
      type: "text",
      name: "phone",
      label: "Phone Number",
      // Required only when phone is selected
      required: { $data: "/contactMethod", eq: "phone" },
    },
  ],
});

Note: In this case, using condition might be better than required since you only want one contact method:

{
  type: "email",
  name: "email",
  condition: { $data: "/contactMethod", eq: "email" },
  required: true, // Only required when shown
}

resolveExpr Helper

Test conditions outside of forms:

import { resolveExpr } from "@buildnbuzz/form-react";

const condition = { $data: "/role", eq: "admin" };
const ctx = { data: { role: "admin" } };

const isVisible = resolveExpr<boolean>(condition, ctx);
// true

On this page