Category Filter
Category picker
Introduction
A drill-down + searchable modal for choosing an entry from a large hierarchy — taxonomy terms, or any content entity type that has a self-referencing parent entity-reference field.
Requirements
- Drupal 10.3+ or Drupal 11.
- No non-core dependencies for the main module. The taxonomy fast-path is used automatically when the target entity type is
taxonomy_term. - Submodules:
category_picker_viewsrequires core Views;category_picker_facetsrequires the contributed Facets module.
Installation
Install as you would any Drupal module (composer require drupal/category_picker, then enable at Admin » Extend). Enable the category_picker_views and/or category_picker_facets submodules only if you need those integrations.
Configuration
- Add or edit an
entity_referencefield pointing at a hierarchical target (taxonomy vocabulary, or any entity type with a self-referencing parent field). - At Manage form display, set the widget to Category picker (modal) and adjust its settings (display mode, hierarchy field, which categories are selectable, separator, search threshold, modal size/title).
- Optionally, at Manage display, set the formatter to Ancestor path (breadcrumb).
- For the Views filter: add the “… (category picker modal)” filter to a view. For the Facets widget: set a facet's widget to Category tree (category picker) and enable Facets' Transform entity ID to label and Hierarchy processors.
There is no global settings page — everything is per field / per view / per facet.
What it ships
Component Plugin id Use Form elementcategory_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 (submodule category_picker_views)
category_picker
exposed filter whose value form is the modal
Facets widget (submodule category_picker_facets)
category_picker_tree
hierarchical tree facet
The form element
<?php
$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 — the host form needs no #ajax. Value is an int (single) or int[] (multi).
On a multi-value element the selected entries render as a native Drupal tabledrag table (drag handle, hidden _weight per row, “Show row weights” toggle) styled by the admin theme, and the modal opener sits in a <div class="field-actions"> labelled Add another item — the same chrome core uses for its own multi-value fields. Drag order is folded into the value on any widget submit; hook_category_picker_order_alter() fires when it changes.
The field widget
Attach any entity_reference field to a hierarchical target, then on Manage form display pick Category picker (modal). Target bundles and sort come from the field's own reference settings. Widget settings: display mode (pop-up dialog, or an inline tree always visible under the field), hierarchy field, which categories can be picked (leaf mode / leaf field), path separator, search minimum characters, modal width and title. When leaf mode is “only categories flagged as selectable” the leaf-field name is validated on save — it must be a real boolean field on the target entity type. The Modal title setting names the dialog only; on a multi-value field the opener button always reads “Add another item”.
If the module is uninstalled the field falls back to Drupal's default entity-reference widget.
The Facets tree widget
Enable category_picker_facets, then set the facet's widget to Category tree (category picker). For names instead of raw ids, also enable the facet's Transform entity ID to label (translate_entity) processor and the Hierarchy processor.
This widget renders through core Facets' own LinksWidget and theme hooks (facets_result_item, facets_item_list, …) — it does not use any of this module's category_picker_* theme hooks or template suggestions below, and never touches the form element's selected-values table. To restyle it, follow Facets' own theming/template-override conventions, not this module's.
Multilingual
The form element, widget, and entity_reference_hierarchy_path formatter follow the active content language: every label, the drill-down and breadcrumb, the selected-row / chip labels, search matching, and label sorting are resolved through EntityRepository::getTranslationFromContext(). Search only matches a category whose label in the active language contains the term — a hit found solely through another translation is not shown. Modal/AJAX responses and the path formatter carry languages:language_content (+ languages:language_interface) cache contexts, so each language caches independently.
Entity types that are not translatable (or a hierarchy with translation disabled) are unaffected — resolution is a no-op there.
The Facets tree widget is separate: it still needs Facets' own Transform entity ID to label (translate_entity) processor for translated names, as noted above.
Hooks
See category_picker.api.php for full docs. All take the read-only PickerContext as context.
hook_category_picker_query_alter(QueryInterface $query, PickerContext $context, ?int $parent_id)— alter the entity query for one level/search. Not invoked for taxonomy_term targets with no bundle restriction (they use TermStorage's fast tree-loading path instead of building a query).hook_category_picker_row_alter(array &$row, EntityInterface $entity, PickerContext $context)— alter one drill-down row's data (label, selectability, path, …).hook_category_picker_breadcrumb_alter(array &$crumbs, ?int $parent_id, PickerContext $context)— alter the breadcrumb trail.hook_category_picker_search_results_alter(array &$entities, string $search_string, PickerContext $context)— alter/filter the matched entities for a search.hook_category_picker_path_alter(string &$path, array $ancestors, PickerContext $context)— alter the assembled “Root › … › Leaf” path string.
The four row/breadcrumb/search/path hooks all run on already-loaded entities, so unlike the query hook they apply identically regardless of the taxonomy fast path.
Theming
Every render layer has a themeable hook + .html.twig template, each with hook_theme_suggestions_HOOK_alter() suggestions of the form HOOK__ENTITYTYPE and HOOK__ENTITYTYPE__BUNDLE (most specific wins):
category_picker_modal— the dialog shell (search bar, back slot, body, actions).category_picker_level— one drill-down level or a flat search-result list.category_picker_back— the independently-swapped “back one level” link.category_picker_path— theHierarchyPathFormatterread-only path display.
The multi-value widget's selected-values list is a native Drupal #type => 'table' with tabledrag reordering (no module theme hook) so it inherits the admin theme's styling; drag order is folded in via the row _weight selects on any widget submit.
Standards
phpcs --standard=Drupal,DrupalPractice, phpstan (level per drupal.org), eslint/stylelint with core's config. Tests under tests/src/.
Maintainers
Current maintainers are listed on the project page. Issues and merge requests are welcome in the project's issue queue.
Depends on
Dependencies of the latest stable release
No dependencies recorded for this project.
Required by
Tracked projects that depend on this one
No tracked projects depend on this one yet.
Activity
Releases
| Version | Type | Core | Notes | Release date | |
|---|---|---|---|---|---|
| 1.0.0 | Stable | 10–11 | Sep 3, 2026 |