[ Web Proxy ]
URL:
Viewing: https://developer.wordpress.org/block-editor/explanations/architecture/dashboard-widgets/ [Back]  [Original]

The Dashboard Widget System Block Editor Handbook | Developer.WordPress.org Skip to content
WordPress.org

The Dashboard Widget System

The widget system is experimental and ships behind the `gutenberg-dashboard-widgets` experiment. APIs and file conventions may change.

This document explains how the pieces of the dashboard widget system relate to each other: the authoring convention, the build pipeline, the server-side registry, the client contract published as @wordpress/widget-primitives, and the hosts that render widgets.

Overview

A widget passes through several stages, each owned by a different part of the codebase:

The widget pipeline: from the widgets folder, through the build and the server registry, to the client package and hosts [The widget pipeline: from the widgets folder, through the build and the server registry, to the client package and hosts]

Each stage hands the next a single artifact and nothing else: a folder convention, then the build manifest (build/widgets/registry.php), then the in-memory registry (WP_Widget_Type_Registry), then a REST record, then a WidgetType. No stage reaches into how another works.

That separation lets each stage evolve independently. It is also why the client contract lives in its own package.

Authoring: a widget is a folder

A widget is a directory under widgets/, discovered by convention; there is no registration call to write:

widgets/hello-world/
 widget.json        static metadata (name, title, description, help, icon, actions, keywords, category, presentation, textdomain)
 widget.ts          optional metadata module: default-exports attributes, example
 render.tsx         render module: default-exports the React component
 style.module.css   optional, injected at runtime by the build
 report.csv         optional static asset; action `href`s resolve it only where `widgets/` ships

The split between widget.json and widget.ts is deliberate. widget.json is build-time input: plain JSON the pipeline can read without executing code, including the translatable strings (title, description, help, keywords) the server localizes through textdomain.

Unlike the other translatable strings, help is an object: content plus optional links, meant for compact surfaces such as tooltips.

actions: declarative verbs (id, label, optional icon and relevance, plus exactly one fulfillment key). Today the only key is href, a link target, with optional download / openInNewTab. Hosts mount the primitive and place it; the dashboard routes by relevance: 'high' and 'medium' to a persistent footer, the rest to a “More” menu. A relative href that exists under widgets/{name}/ becomes a plugin URL at registration; missing relative non-admin files are dropped. data: and javascript: hrefs are rejected. Prefer absolute URLs for assets that must work in the plugin zip (which does not ship widgets/).

icon: a registered icon name (collection/icon-name), resolved client-side through the site’s Icons API. Malformed names are dropped at registration.

widget.ts is the live half of the metadata: values that only exist in JavaScript, such as the attributes field schema (including optional relevance hints) that hosts feed into DataForm. It is optional: a widget declared entirely by its manifest ships none, and resolves from its record alone.

Its render component receives the widget’s attributes and, optionally, setAttributes:

export default function HelloWorld( { attributes } ) { ... }

Build: from folders to script modules

@wordpress/build (packages/wp-build/) is the generic build tool for packages, routes, and widgets. For widgets specifically, it:

  1. Discovers every directory under widgets/ and reads its widget.json.
  2. Compiles up to two ES script modules per widget: render (from render.*) and widget (from widget.*). Either may be absent; a missing source file produces no module. Each compiled module ships an *.asset.php listing its module dependencies and a cache-busting version.
  3. Emits build/widgets/registry.php, the manifest: one entry per widget with its directory name, metadata, and which modules were built.
  4. Emits build/widgets.php, which at init calls wp_register_script_module() for every built module, with IDs derived from the folder name (<prefix>/widgets/<dir>/render and <prefix>/widgets/<dir>/widget).

The server registry

WP_Widget_Type_Registry (lib/experimental/dashboard-widgets/) is a singleton, hydrated at init from the manifest. Each entry becomes a WP_Widget_Type with name, render_module, widget_module, presentation, category, icon, and the translatable title, description, help, actions, and keywords (localized at registration time using the widget’s textdomain).

The hydration is a deterministic copy, with no filters in between. The widgets/ folder is the single source of widget authorship in this codebase.

The registry is the server’s authoritative list of widget types for the site. Two consumers read it:

  • The REST controller (WP_REST_Widget_Modules_Controller) exposes it at /wp/v2/widget-modules, returning { name, render_module, widget_module, presentation, category, title, description, help, icon, actions, keywords } per record.
  • The dashboard page hooks its dashboard-wp-admin_boot_dependencies filter, a per-page instance of the generic {page-slug}-wp-admin_boot_dependencies, and adds every registered module to its import map as a dynamic dependency. A dynamic dependency is reachable by import() but never executed eagerly.

Registration only makes the modules known to WordPress; loading them is a separate, per-host decision. Dynamic import() against the import map is how the dashboard loads widgets today. A host can load them another way: enqueue a module eagerly (wp_enqueue_script_module()), declare it as a static dependency of its own module, or, outside WordPress, skip the import map and resolve modules through its own ResolveWidgetModule.

The registry exists as a class, rather than having REST read the manifest directly, so the source of widget types stays an implementation detail. Today that source is only the build manifest. A future source, such as a plugin-facing registration API, would target the registry without touching the build pipeline.

The client contract: @wordpress/widget-primitives

Everything after the REST record is the job of @wordpress/widget-primitives, the contract both widget authors and hosts share. Its full surface (the contract types, the discovery hook, and the render component) is covered in the Widget Primitives / Introduction story. In the pipeline it does two things.

useWidgetTypes( records ) takes the host-supplied records, imports each record’s widget_module for the live metadata, and merges it with the record into WidgetType[]. A record without a metadata module resolves from its own fields, as long as it carries a render_module. The record’s presentation, category, title, description, help, actions, and keywords, all sourced from widget.json (with title, description, help, actions, and keywords localized server-side), win over the module’s value. The record’s icon is a registered icon name: the hook resolves it through the application-registered resolver (registerIconResolver), and the resolved element wins over a module’s element. The hook reaches for no store or endpoint; a host such as the dashboard reads its own widgetModule core-data entity (backed by /wp/v2/widget-modules) and passes the records in.

A module’s attributes may also reference field types by name (type: 'location'). The application registers those definitions up front through registerFieldType() (the dashboard route registers its own on boot), and useWidgetTypes resolves every named reference through that registry while building each WidgetType: the registered definition supplies the field’s behavior on top of its DataViews baseType, and hosts receive plain DataViews fields. The widget declaration stays serializable; resolution happens once, at this boundary.

<WidgetRender> then resolves a WidgetType.renderModule through a host-provided ResolveWidgetModule and mounts the component with the attributes / setAttributes contract. On a WordPress page the resolver can be as simple as ( id ) => import( id ); hosts with other loading strategies supply their own.

For what a widget declares through that contract, layer by layer, see Anatomy of a widget type.

Hosts

The dashboard engine (@wordpress/widget-dashboard, mounted by routes/dashboard/) is the host this repository ships today. It shows what a host owns:

  • Registers the widgetModule core-data entity in its page init module, which @wordpress/boot runs before the dashboard renders.
  • Reads the entity’s records and passes them to useWidgetTypes( records ).
  • Owns the layout array and its persistence.
  • Wraps every instance in its own chrome: header, toolbars, error boundary, Suspense fallback.
  • Passes resolveWidgetModule down through its context, overridable for tests and Storybook.

The same WidgetType could be rendered by any other host, and where and how to render belongs entirely to the host. Every host is a consumer of the package, but not every consumer is a host: tests, Storybook, or a picker that only lists widget types consume the same contract without rendering anything.

Why a standalone package

The pipeline above splits in one place: everything up to the REST endpoint is WordPress infrastructure, and everything after WidgetType[] is a host concern. The contract in between is small, stable, and needed by both sides, which is the shape of a package:

  • Widget authors depend on it to type their metadata and render components.
  • Hosts depend on it to discover and mount widgets without knowing how they were built or registered.
  • Neither side needs the other’s dependencies: the package keeps its own footprint minimal (element, plus type-only dataviews); discovery is data-source agnostic, so it no longer depends on core-data/data.

Chapters

Chapter list

Web Proxy Viewer  |  New URL  |  Original Page