Advertising sustains the DA. Ads are hidden for members. Join today

Contributed module documentation

Category Picker

Last updated on
4 September 2026

This page has not yet been reviewed by Contributed module documentation maintainer(s) and added to the menu.

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

Specific version(s)

Category Picker requires Drupal 10.3 or newer, or any Drupal 11 release.

  • 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 optimised TermStorage fast-path (core loadTree() / loadChildren()) is used automatically instead of an entity query.
  • category_picker_views requires core Views.
  • category_picker_facets requires 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.twig renders the dialog shell.
  • category-picker-level.html.twig renders one drill-down level, or the search results.
  • category-picker-path.html.twig renders the Root › Branch › Leaf string.
  • category-picker-back.html.twig renders the Back control.
  • category-picker-widget.html.twig renders 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" with role="option" and aria-selected on each row; the breadcrumb is a labelled nav element; 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

Page status: No known problems

You can: