Strapi Plugin Cascading Select logo

Strapi Plugin Cascading Select

By Mushfau

Two linked dropdowns where the second is filtered by the first (e.g. Country → City), validated on save, with an optional synced relation.

Latest version

v0.1.1

released September 27, 2026

npm install strapi-plugin-cascading-select

strapi-plugin-cascading-select

A Strapi 5 custom field with two linked dropdowns: choosing an option in the first (parent) dropdown filters the second (child) one. Typical uses are Atoll → Island, Country → City and Category → Subcategory.

  • Options come from your own collection types, so editors maintain the lists in the Content Manager.
  • The child must belong to the selected parent. This is checked on the server for every write (Content Manager, REST, GraphQL, custom code), not only in the admin UI.
  • Optionally, a real relation on the same content type is kept in sync with the selected child, so API consumers can populate it like any other relation.
The field in the Content Manager: Seenu is selected in the Atoll dropdown and the Island dropdown lists only Seenu's islands

Requirements

  • Strapi 5 (tested with 5.55)
  • Two collection types, where the child has a relation to the parent (for example island.atoll, manyToOne → atoll).

Installation

npm install strapi-plugin-cascading-select
// config/plugins.ts
export default () => ({
  'cascading-select': {
    enabled: true,
  },
});

Rebuild the admin panel (npm run build) or restart npm run develop.

Adding the field

In the Content-Type Builder, choose Add another field → Custom → Cascading select.

Cascading select in the Custom tab of the Content-Type Builder field picker

Then set the two collections in the basic settings:

Basic settings of the field: parent collection api::atoll.atoll, child collection api::island.island, optional label fields and relation to parent
SettingRequiredDescription
Parent collectionyesUID of the collection listed in the first dropdown, e.g. api::atoll.atoll
Parent label fieldnoAttribute shown as the label. Defaults to name, title, label or the first text attribute
Child collectionyesUID of the collection listed in the second dropdown, e.g. api::island.island
Child label fieldnoSame as above, for the child
Relation to parentnoRelation on the child pointing to the parent. Detected automatically when there is only one
Synced relation (advanced)noRelation on this content type targeting the child collection, set on every save
Required (advanced)noBoth dropdowns must be filled

The same settings in a schema.json:

"location": {
  "type": "customField",
  "customField": "plugin::cascading-select.cascading-select",
  "required": true,
  "options": {
    "parentUid": "api::atoll.atoll",
    "childUid": "api::island.island",
    "syncRelation": "island"
  }
},
"island": {
  "type": "relation",
  "relation": "manyToOne",
  "target": "api::island.island"
}

Misconfigured fields are reported as warnings on startup, and saving them returns an error that explains the problem.

Stored value

The field is a JSON attribute storing the documentId of each selection:

{ "parent": "ujrhq9ufbx98xzspbvau8snr", "child": "evv7c03mcwv2hrr10etrpc5p" }

Changing the parent in the admin resets the child. On save, the value is normalized: unknown keys are dropped, and an empty selection becomes null.

A custom field cannot itself be a relation. The JSON value alone therefore has no referential integrity, and it can't be populated. Set Synced relation to a relation attribute that targets the child collection, and the plugin writes it from the selected child whenever the document is created or updated.

Advanced settings of the field: Required checked, and Synced relation set to island

Query it like any relation. The parent is derived from the child, so the two can never disagree:

GET /api/businesses?populate[island][populate]=atoll

Hide the synced relation from the edit view (Content Manager → Configure the view), since the plugin overwrites manual changes to it.

Validation rules

On create, update and clone, for fields at the root, in components and in dynamic zones:

  • the parent must exist, and the child must belong to it (through the parent relation),
  • a child cannot be set without a parent,
  • with Required on, both parent and child must be set. For content types with Draft & Publish, this check runs on publish instead of on save, following how Strapi handles required fields. Drafts can stay incomplete.

Errors are attached to the field, so the Content Manager shows them next to the dropdowns:

Publishing with only the atoll selected shows the error Island is required under the field

Configuration

// config/plugins.ts
export default () => ({
  'cascading-select': {
    enabled: true,
    config: {
      // Maximum number of options loaded in one dropdown (default 1000).
      maxOptions: 1000,
    },
  },
});

Admin API

The input loads its options from two admin routes, which require an authenticated admin user:

  • GET /cascading-select/options/parents
  • GET /cascading-select/options/children?parent=<documentId>

Both take the field settings as query parameters and only serve configurations used by an existing cascading field (other combinations return 403). They cannot be used to read arbitrary collections. Only the documentId and the label field are returned.

Limitations

  • Two levels (parent → child). Deeper chains (Atoll → Island → Ward) are not supported yet.
  • Single selection in each dropdown.
  • Options are loaded in full up to maxOptions; there is no server-side search yet.
  • With i18n, options are listed in the locale being edited. The synced relation follows Strapi's default locale rules for relations.

Development

npm install
npm run build          # build admin + server bundles into dist/
npm run watch:link     # rebuild on change and push to linked apps with yalc
npm run test:ts:front  # type-check admin code
npm run test:ts:back   # type-check server code
npm run verify         # check the package before publishing

A test app lives in playground. It seeds sample Maldivian atolls and islands and has a Business content type that uses the field. To link the plugin into it:

# plugin
npm run watch:link
# playground (another terminal)
npx yalc add --link strapi-plugin-cascading-select && npm install
npm run develop

Issues and contributions

Report bugs and request features in the GitHub issues. Please include your Strapi version, the field settings (or the attribute from schema.json) and the steps to reproduce. Pull requests are welcome; see Development for the local setup.

License

MIT

Submit your content

Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.

Submit
Submit your content