Higher Quality, Stronger Performance, Increased Stability, Better Developer Experience, discover everything we've shipped recently!

Strapi plugin logo for Conditional Field Builder

Conditional Field Builder

A Strapi v5 custom field: pick an option from a dropdown and render a tailored set of conditional sub-fields.

Conditional Field Builder

A Strapi v5 custom field that turns one dropdown into a schema-less, per-option form — stored as a single JSON value.

npm version npm downloads license Strapi v5 TypeScript

Install · Quick start · Examples · Field reference · API usage


Why this plugin?

Strapi v5.17 introduced Conditional Fields — but they only toggle visibility of fields already declared in your schema. This plugin solves the other half of the problem: letting one field morph into a different shape per option, without ever touching the content-type schema.

Capabilityv5.17 nativeThis plugin
Hide/show fields that already exist in the schema
Declare new fields per option without schema changes
Bundle discriminator + dynamic data into one JSON value
13 sub-field types inside a single field
JSON-driven configuration in the Content-Type Builder

Highlights

  • Strapi v5 native — built on the official Custom Field API (type: 'json')
  • 13 field typestext, textarea, number, email, password, select, checkbox, radio, date, time, datetime, boolean, range
  • Type-safe — TypeScript end-to-end, React 18
  • @strapi/design-system — looks and feels like the rest of the admin
  • Built-in validation — required, min/max, step, choices
  • i18n — English, French, Arabic out of the box (drop a JSON file to add more)
  • Plugin Settings page — store a reusable default template for your team
  • Unit tested — pure validation utilities covered with Vitest

Install

npm  i  strapi-plugin-conditional-field-builder
# or
yarn add strapi-plugin-conditional-field-builder
# or
pnpm add strapi-plugin-conditional-field-builder

Enable it in config/plugins.ts (or .js):

export default ({ env }) => ({
  'conditional-field-builder': {
    enabled: true,
  },
});

Rebuild the admin and start Strapi:

npm run build && npm run develop
Install from source (local development / forks)
git clone https://github.com/AhmadAl-Ghalban/strapi-plugin-conditional-field-builder.git \
  ./src/plugins/conditional-field-builder
cd ./src/plugins/conditional-field-builder
npm install && npm run build
// config/plugins.ts
export default ({ env }) => ({
  'conditional-field-builder': {
    enabled: true,
    resolve: './src/plugins/conditional-field-builder',
  },
});

Quick start

1. Add the field

In the Content-Type BuilderAdd another fieldCustomConditional Dropdown.

2. Configure options as JSON

In the field's Options panel, describe the options and the sub-fields each one should render:

[
  {
    "label": "Text",
    "value": "text",
    "fields": [
      { "name": "title",       "type": "text",     "required": true },
      { "name": "description", "type": "textarea" }
    ]
  },
  {
    "label": "Date",
    "value": "date",
    "fields": [
      { "name": "date", "type": "date" },
      { "name": "time", "type": "time" }
    ]
  },
  {
    "label": "Range",
    "value": "range",
    "fields": [
      { "name": "min", "type": "number", "min": 0 },
      { "name": "max", "type": "number", "max": 100 }
    ]
  }
]

3. Author content

┌─────────────────────────────────────────────────┐
│ My Field *                                      │
│ ┌─────────────────────────────────────────────┐ │
│ │  Date                                    ▾  │ │
│ └─────────────────────────────────────────────┘ │
│                                                 │
│  Date   [ 2026-01-01      ]                     │
│  Time   [ 10:00           ]                     │
└─────────────────────────────────────────────────┘

4. Stored value

{
  "selectedOption": "date",
  "data": {
    "date": "2026-01-01T00:00:00.000Z",
    "time": "2026-01-01T10:00:00.000Z"
  }
}

Examples

Contact methods — select + email + phone + message
[
  {
    "label": "Email",
    "value": "email",
    "fields": [
      { "name": "address", "label": "Email address", "type": "email", "required": true },
      { "name": "subject", "type": "text" }
    ]
  },
  {
    "label": "Phone",
    "value": "phone",
    "fields": [
      { "name": "country", "type": "select", "required": true,
        "choices": [
          { "label": "Saudi Arabia (+966)", "value": "+966" },
          { "label": "United States (+1)",  "value": "+1"   },
          { "label": "United Kingdom (+44)","value": "+44"  }
        ]
      },
      { "name": "number", "type": "text", "required": true, "placeholder": "5XXXXXXXX" }
    ]
  },
  {
    "label": "Message",
    "value": "message",
    "fields": [
      { "name": "body", "type": "textarea", "required": true, "placeholder": "Write your message…" }
    ]
  }
]
Product variants — physical vs digital
[
  {
    "label": "Physical product",
    "value": "physical",
    "fields": [
      { "name": "price",    "type": "number", "min": 0, "step": 0.01, "required": true },
      { "name": "stock",    "type": "range",  "min": 0, "max": 1000,  "step": 1 },
      { "name": "shipping", "type": "radio",
        "choices": [
          { "label": "Standard", "value": "standard" },
          { "label": "Express",  "value": "express"  }
        ]
      },
      { "name": "giftWrap", "label": "Gift wrap available", "type": "checkbox" }
    ]
  },
  {
    "label": "Digital product",
    "value": "digital",
    "fields": [
      { "name": "price",       "type": "number",  "min": 0, "step": 0.01, "required": true },
      { "name": "downloadUrl", "type": "text",    "required": true, "placeholder": "https://…" },
      { "name": "drm",         "label": "DRM protected", "type": "boolean" }
    ]
  }
]
Event scheduling — all-day / timed / recurring
[
  {
    "label": "All-day event",
    "value": "allDay",
    "fields": [
      { "name": "day",   "type": "date", "required": true },
      { "name": "notes", "type": "textarea" }
    ]
  },
  {
    "label": "Timed event",
    "value": "timed",
    "fields": [
      { "name": "startsAt", "type": "datetime", "required": true },
      { "name": "endsAt",   "type": "datetime", "required": true }
    ]
  },
  {
    "label": "Recurring slot",
    "value": "recurring",
    "fields": [
      { "name": "weekday", "type": "select", "required": true,
        "choices": [
          { "label": "Monday",    "value": "mon" },
          { "label": "Tuesday",   "value": "tue" },
          { "label": "Wednesday", "value": "wed" },
          { "label": "Thursday",  "value": "thu" },
          { "label": "Friday",    "value": "fri" }
        ]
      },
      { "name": "time", "type": "time", "required": true }
    ]
  }
]
CTA block — link / newsletter form / embedded video
[
  {
    "label": "Link button",
    "value": "link",
    "fields": [
      { "name": "label",        "type": "text", "required": true },
      { "name": "href",         "type": "text", "required": true, "placeholder": "/about or https://…" },
      { "name": "openInNewTab", "type": "boolean" }
    ]
  },
  {
    "label": "Newsletter form",
    "value": "form",
    "fields": [
      { "name": "headline",    "type": "text" },
      { "name": "placeholder", "type": "text", "placeholder": "you@example.com" },
      { "name": "submitLabel", "type": "text" }
    ]
  },
  {
    "label": "Embedded video",
    "value": "video",
    "fields": [
      { "name": "url",      "type": "text", "required": true },
      { "name": "autoplay", "type": "checkbox" }
    ]
  }
]

Field reference

Each entry in fields[] accepts:

PropertyTypeApplies toNotes
namestringallRequired. Key inside data
labelstringallDefaults to name
typestringallSee list below
requiredbooleanallEnforces required-field validation
placeholderstringtext-like inputs
min / maxnumbernumber, rangeNumeric bounds
stepnumbernumber, rangeNumeric step
choices[{label, value}]select, radioOption list
defaultValueanyallReserved for form-style initialization

Supported types text · textarea · number · email · password · select · checkbox · radio · date · time · datetime · boolean · range


Querying the stored value

The value is persisted as plain JSON, so you can use it directly from REST, GraphQL, or any service:

// Find pages whose "cta" block is a video
const entries = await strapi.documents('api::page.page').findMany({
  filters: { cta: { selectedOption: { $eq: 'video' } } },
});
// Render based on the discriminator
switch (page.cta.selectedOption) {
  case 'link':  return <Button href={page.cta.data.href}>{page.cta.data.label}</Button>;
  case 'form':  return <Newsletter {...page.cta.data} />;
  case 'video': return <Video src={page.cta.data.url} autoplay={page.cta.data.autoplay} />;
}

Architecture

strapi-plugin-conditional-field-builder/
├─ server/src/
│   ├─ register.ts                       # registers the custom field on the server
│   └─ bootstrap.ts
├─ admin/src/
│   ├─ index.ts                          # registers field + settings link
│   ├─ types.ts                          # shared TypeScript types
│   ├─ components/
│   │   ├─ ConditionalDropdownInput.tsx  # main Content Manager input
│   │   ├─ DynamicFieldRenderer.tsx      # per-type field renderer
│   │   └─ OptionsJsonInput.tsx          # CTB JSON config helper
│   ├─ pages/
│   │   └─ SettingsPage.tsx              # plugin settings page
│   ├─ utils/
│   │   ├─ validation.ts                 # pure validation + parsing helpers
│   │   └─ __tests__/validation.test.ts
│   └─ translations/{en,fr,ar}.json
└─ package.json

Why type: 'json'? The persisted value bundles a discriminator (selectedOption) with a heterogeneous data map. JSON is the natural backing for that shape — and it keeps the content-type schema stable as authors evolve their options over time.


Validation API

utils/validation.ts exposes pure functions used by the input and the tests:

FunctionPurpose
validateField(field, value)Validate a single sub-field
validateValue(value, options, required)Validate the full shape
parseOptions(raw)Accepts an array or a JSON string
parseValue(raw)Accepts an object or a JSON string

Scripts

npm run build           # build the plugin
npm run watch           # watch & rebuild
npm run watch:link      # build + symlink into a host project
npm test                # run unit tests (Vitest)
npm run test:ts:front   # TypeScript check (admin)
npm run test:ts:back    # TypeScript check (server)

Internationalisation

Translations live under admin/src/translations/<locale>.json. Add a new locale by dropping a file in that folder — it's picked up automatically by registerTrads.


Settings page

Settings → Conditional Dropdown lets administrators store a default options template (JSON) that authors can copy into the Content-Type Builder. The value is persisted in localStorage; swap in a backend route if you need cross-admin sharing.


Contributing

Issues and PRs are very welcome.

  • Bugs / feature requests: open an issue
  • Pull requests: fork → branch → PR against main

Maintainer


License

MIT © Ahmad Al-Ghalban

Install now

npm install strapi-plugin-conditional-field-builder

STATS

No GitHub star yet223 weekly downloads

Last updated

8 days ago

Strapi Version

5.47.0 and above

Author

github profile image for AhmadAl-Ghalban
AhmadAl-Ghalban

Useful links

Create your own plugin

Check out the available plugin resources that will help you to develop your plugin or provider and get it listed on the marketplace.