Category Picker
This is the full documentation for Category Picker, a drill-down and searchable modal for choosing an entry from a large hierarchy.
For a short overview, screenshots and related modules, see the project page.
The picker works against any hierarchy Drupal can express as a self-referencing parent entity reference: taxonomy vocabularies, and any other content entity type with a parent field. The same tree can then be reused as a Views exposed filter, as a Facets widget, and over a Search API index on the front end. The module has no non-core dependencies and, for plain taxonomy, uses an optimised TermStorage fast-path rather than an entity query.
How it works

Open the modal at the root level, where the search box, breadcrumb, and first-level categories are displayed. Navigate through multiple levels using the clickable breadcrumb and Back controls to select a leaf, or search by typing a few characters and choose a result shown as its full Root › Branch › Leaf path. The selected row is added without a page reload.
Requirements
- PHP follows whatever your Drupal core version requires; the module adds no constraint of its own.
- No non-core dependencies for the main module. When the target entity type is
taxonomy_term, an optimisedTermStoragefast-path (coreloadTree()/loadChildren()) is used automatically instead of an entity query. category_picker_viewsrequires core Views.category_picker_facetsrequires the contributed Facets module, which in turn needs Search API and an index.
What it ships
| Component | Plugin id | Use |
|---|---|---|
| Form element | category_picker |
'#type' => 'category_picker' in any form |
| Field widget | category_modal |
"Category picker (modal)" on any entity_reference field, single or multi-value |
| Field formatter | entity_reference_hierarchy_path |
renders the value as Root › Branch › Leaf |
Views filter (sub-module category_picker_views) |
category_picker |
exposed filter whose value form is the modal |
Facets widget (sub-module category_picker_facets) |
category_picker_tree |
hierarchical tree facet matching the picker look |
In short: no non-core dependencies, five ready-to-use components (element, widget, formatter, Views filter, Facets widget), six alter hooks for tuning rows, search, breadcrumbs and order, and comfortable handling of vocabularies with tens of thousands of terms.
The form element
The element is reusable in any form. Its value is an int when single, or an int[] when multi-value.
$form['category'] = [
'#type' => 'category_picker',
'#title' => $this->t('Category'),
'#target_type' => 'taxonomy_term', // any entity type with a parent field
'#display_mode' => 'modal', // modal | inline
'#bundles' => ['categories'], // optional; empty = all bundles
'#parent_field' => 'parent', // self-referencing hierarchy field
'#leaf_mode' => 'no_children', // no_children | boolean_field | all
'#leaf_field' => '', // boolean field name for leaf_mode = boolean_field
'#cardinality' => 1, // 1 | -1 | N
'#separator' => ' › ',
'#min_chars' => 2,
'#dialog_width' => 640,
'#dialog_title' => $this->t('Choose a category'),
'#default_value' => 42, // id, or [id, id, …] when multi-value
];The element is self-contained. After every pick or removal it refreshes its own markup through a hidden update submit, so the host form needs no #ajax. On a multi-value element the selected entries render as a native Drupal tabledrag table; drag order is folded into the value on any widget submit, and hook_category_picker_order_alter() fires when it changes.
Theming
Five Twig templates, each with BASE__ENTITYTYPE and BASE__ENTITYTYPE__BUNDLE suggestions:
category-picker-modal.html.twigrenders the dialog shell.category-picker-level.html.twigrenders one drill-down level, or the search results.category-picker-path.html.twigrenders theRoot › Branch › Leafstring.category-picker-back.html.twigrenders the Back control.category-picker-widget.html.twigrenders the field-widget wrapper.
There are two libraries: category_picker/picker for the modal, and category_picker/picker_inline for the inline tree CSS.
Dark mode, RTL and translation
Dark mode
All colours are CSS custom properties (--category-picker-accent, --category-picker-fg, --category-picker-bg and so on), with a light palette by default and a @media (prefers-color-scheme: dark) block that overrides them. The modal switches automatically with the visitor's OS or browser preference: no setting, no extra library, no dark-mode class to add. Override the custom properties in your theme's CSS if you want different colours.
Right-to-left
Layout uses logical CSS properties (text-align: start / end, border-inline-start) rather than hard-coded left/right, so most of the interface mirrors automatically under an RTL language. The chevron, the breadcrumb separator and the Back arrow do not mirror on their own, so they get explicit [dir="rtl"] overrides that flip them.
Translation
Every string the module prints goes through Drupal's translation layer: PHP strings through t() / $this->t(), Twig strings through the |t filter. Category and entity labels, breadcrumbs and ancestor paths are resolved against the active content language rather than the site default, so a page viewed in French shows French term names even when the underlying entity was created in English. The Modal title widget setting is declared as a label in the module's config schema, so it is picked up by core's Configuration Translation for per-language overrides.
Hooks
Every hook receives a PickerContext value object describing the active picker: target type, parent field, bundles, leaf mode, separator and more. See category_picker.api.php for full signatures and example implementations. All hooks fire after entities are loaded and behave the same whether or not the taxonomy fast-path was used; the one exception is hook_category_picker_query_alter(), which the fast-path skips because it builds no entity query.
hook_category_picker_query_alter()alters the level and search entity query.hook_category_picker_row_alter()alters a single drill-down row.hook_category_picker_breadcrumb_alter()alters the breadcrumb trail.hook_category_picker_search_results_alter()adds or removes search matches.hook_category_picker_path_alter()alters the assembled path string.hook_category_picker_order_alter()reacts to a reordered multi-value list.
Accessibility
The modal is Drupal core's AJAX dialog, so it traps focus while open, closes on Escape, and returns focus to the button that opened it. When the dialog appears, focus moves straight to the search box.
- Every row is a real link or a labelled control. Up and Down move a roving focus through the list, so the whole tree is keyboard-operable.
- Screen-reader users hear a live announcement when a new level loads, when a category is picked, and when one is removed.
- The row list uses
role="listbox"withrole="option"andaria-selectedon each row; the breadcrumb is a labellednavelement; the Back control and the search field both carry accessible names; decorative chevrons and separators are hidden from assistive tech. - The "Selected" line is a polite live region, and the radio inside each search result stays in the accessibility tree and keyboard-focusable even though it is visually hidden.
Frontend and non-admin use
The picker is not admin-only. The widget and the #type => 'category_picker' element are plain Form API, so they work on any front-end form (a node add or edit page, a custom form, a Views exposed-filter block on a public page) for registered or anonymous users. The two AJAX endpoints require no category-picker permission: a user only needs access to the form that hosts the field. You can filter which rows a given user sees with hook_category_picker_row_alter() or hook_category_picker_search_results_alter().
- No front-end libraries to add. It rides entirely on Drupal core's own AJAX and dialog JavaScript. Unlike tag-style widgets such as Tagify, there is no third-party JavaScript library to install, patch or keep in sync.
- The whole hierarchy is reachable immediately. The root level appears the moment the modal opens and every branch is one click away. Nothing is hidden behind a "type at least N characters" prompt.
- Search, Back and breadcrumbs in one place. The search box, the Back control and the clickable breadcrumb trail all sit in the modal header, so the editor never loses their place while drilling.
- Comfortable on large trees. One level per screen keeps each list short, and every search result carries its full ancestor path, so there is never a "which Accessories?" moment.

The picker opens on a front-end form, styled by the site theme rather than the admin theme in drilled mode.

The picker opens on a front-end form, styled by the site theme with dark mode in search mode.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion