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.
released September 27, 2026
npm install strapi-plugin-cascading-selectA 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.
populate it like any other relation.
island.atoll,
manyToOne → atoll).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.
In the Content-Type Builder, choose Add another field → Custom → Cascading select.
Then set the two collections in the basic settings:
| Setting | Required | Description |
|---|---|---|
| Parent collection | yes | UID of the collection listed in the first dropdown, e.g. api::atoll.atoll |
| Parent label field | no | Attribute shown as the label. Defaults to name, title, label or the first text attribute |
| Child collection | yes | UID of the collection listed in the second dropdown, e.g. api::island.island |
| Child label field | no | Same as above, for the child |
| Relation to parent | no | Relation on the child pointing to the parent. Detected automatically when there is only one |
| Synced relation (advanced) | no | Relation on this content type targeting the child collection, set on every save |
| Required (advanced) | no | Both 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.
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.
Query it like any relation. The parent is derived from the child, so the two can never disagree:
GET /api/businesses?populate[island][populate]=atollHide the synced relation from the edit view (Content Manager → Configure the view), since the plugin overwrites manual changes to it.
On create, update and clone, for fields at the root, in components and in dynamic zones:
Errors are attached to the field, so the Content Manager shows them next to the dropdowns:
// config/plugins.ts
export default () => ({
'cascading-select': {
enabled: true,
config: {
// Maximum number of options loaded in one dropdown (default 1000).
maxOptions: 1000,
},
},
});The input loads its options from two admin routes, which require an authenticated admin user:
GET /cascading-select/options/parentsGET /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.
maxOptions; there is no server-side search yet.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 publishingA 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 developReport 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.
MIT
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
