npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wordpress/widget-dashboard

v0.7.0

Published

Stateless rendering engine for widget dashboards: the WidgetDashboard compound component and its grid-settings kit.

Readme

Widget Dashboard

This prerelease depends on WordPress core-private APIs and is built to run inside WordPress core. It is not yet safe to install and run as a standalone npm dependency from an external plugin.

Stateless rendering engine for widget dashboards. WidgetDashboard renders an editable grid of widget instances behind a consumer-controlled edit mode: drag-to-reorder, resize, a modal inserter, per-widget settings, and command-palette integration.

The engine owns no data. Widget types flow in through the widgetTypes prop (see @wordpress/widget-primitives), the consumer owns the committed layout array, and in-progress edits accumulate in an internal staging layer until the user commits them, at which point onLayoutChange fires with the updated array. Grid placement renders through @wordpress/grid.

For how the widget system fits together (authoring, build, server registry, hosts), see the dashboard widget system architecture document.

Installation

Install the module:

npm install @wordpress/widget-dashboard --save

This package assumes that your code will run in an ES2015+ environment. If you're using an environment that has limited or no support for such language features and APIs, you should include the polyfill shipped in @wordpress/babel-preset-default in your code.

Setup

Component styles are CSS Modules injected at runtime when a component mounts; there is no stylesheet to enqueue or import.

Visual defaults read the design tokens that @wordpress/theme publishes as --wpds-* CSS custom properties. In WordPress screens managed by Gutenberg the tokens stylesheet is loaded centrally and no setup is needed. Elsewhere, install and load it in your application:

npm install @wordpress/theme
import '@wordpress/theme/design-tokens.css';

Usage

import { useState } from '@wordpress/element';
import { WidgetDashboard } from '@wordpress/widget-dashboard';

function Dashboard() {
	const [ layout, setLayout ] = useState( defaultLayout );

	return (
		<WidgetDashboard
			layout={ layout }
			onLayoutChange={ setLayout }
			widgetTypes={ widgetTypes }
		/>
	);
}

<WidgetDashboard> renders <WidgetDashboard.Widgets /> by default. Pass children to compose the dashboard — header, empty state, footer — around the grid:

<WidgetDashboard
	layout={ layout }
	onLayoutChange={ setLayout }
	widgetTypes={ widgetTypes }
>
	<WidgetDashboard.NoWidgetsState>
		<p>{ __( 'No widgets yet.' ) }</p>
	</WidgetDashboard.NoWidgetsState>
	<WidgetDashboard.Widgets />
</WidgetDashboard>

Composition

The dashboard is built from two kinds of parts:

  • Triggers and chrome you arrange. Actions, Widgets, WidgetChrome, NoWidgetsState, and Commands are compound components; compose them as children to place them in your layout.
  • Overlays the engine mounts. The widget inserter, the per-widget settings editor, and the reset confirmation are mounted by the engine and driven by shared UI state. Triggers open them only through that state — the "Add widget" button and the command palette both open the inserter — so there is no overlay to place in the tree.

Omitting children renders the default arrangement. When you pass children, the overlays mount regardless of what you compose.

Properties

layout: DashboardWidget[]

Widget instances to render. Each instance carries a stable uuid, a type reference, optional attributes, and a placement describing its slot in the grid. The consumer owns this state.

onLayoutChange: ( layout: DashboardWidget[] ) => void

Called when the user commits in-progress edits via the Done action. Receives the full layout array as it should be persisted. In-progress mutations (reorder, resize, add, remove, attribute edits) accumulate in the dashboard's internal staging layer and do not fire this callback until commit.

onLayoutReset: () => void

Optional. Reset action surfaced by <WidgetDashboard.Actions /> and the command palette. When omitted, the reset entry points are disabled.

widgetTypes: WidgetType[]

The widget types available to the dashboard. The dashboard never queries a store directly — consumers scope and filter via this prop.

isResolvingWidgetTypes: boolean

Optional. When true, widget types are still loading: instances whose type is not yet in widgetTypes show a loading state instead of a missing state.

editMode: boolean

When true, the grid enables drag and resize. Defaults to false.

onEditChange: ( next: boolean ) => void

Optional. Called when edit mode toggles via WidgetDashboard.Actions (or any consumer-built toggle). When omitted, WidgetDashboard.Actions renders nothing.

resolveWidgetModule: ( moduleId: string ) => Promise< { default: ComponentType } >

Optional. Maps a WidgetType.renderModule id to the React component that renders the widget. Defaults to a dynamic import( /* webpackIgnore */ moduleId ). Override for tests, Storybook, or remote-URL loading.

gridSettings: WidgetGridSettings

Optional. Grid model configuration; see Grid settings. Defaults to DEFAULT_GRID.

children: ReactNode

Optional. Composition slot for the dashboard's triggers and chrome. When omitted, the engine renders the default arrangement: the empty state, the actions, the widgets grid, and the command palette integration. The engine-mounted overlays are present either way.

Compound components

<WidgetDashboard.Widgets />

Iterates layout, renders each entry through <WidgetDashboard.WidgetChrome />, and feeds the resulting tree into the underlying grid (@wordpress/grid).

<WidgetDashboard.WidgetChrome />

Per-instance wrapper. Provides widget identity to the render tree via context and hosts the widget's render module under a Suspense boundary and an error boundary. The instance is read from layout; consumers don't pass it manually.

<WidgetDashboard.NoWidgetsState>

Renders its children only when layout is empty. Pair it with <WidgetDashboard.Widgets /> so the empty state shows up in place of the grid until widgets are added.

<WidgetDashboard.Actions />

Edit-mode toggle: a "Customize" button while editMode is off, and "Add widget", "Cancel", "Done" while it is on. The buttons and the more-actions menu are triggers: "Customize" and "Done" fire onEditChange, "Add widget" opens the inserter, and "Reset to default" opens the reset confirmation. Returns null when the dashboard is mounted without onEditChange, so surfaces that don't expose edit mode can keep Actions in their tree unconditionally. The Customize button also needs the policy to allow customize; Done and Cancel stay available while already customizing.

<WidgetDashboard.Commands />

Command palette integration. It registers the dashboard's commands through @wordpress/commands (customize, add widgets, reset to default) and sets the active command context. It renders nothing, and surfaces wherever the host application mounts the command palette. Ships in the default arrangement; when passing custom children, compose it to keep the integration. The customize command, and the add-widgets command outside edit mode, follow the policy's customize answer.

<WidgetDashboard.Policy>

Governs what users may do on the dashboards below it: whether Customize is offered, which widget types the inserter lists, and which instances can be removed, moved, resized, or edited. Unlike the other compound components, it mounts around <WidgetDashboard>. See Governance.

<Page> from @wordpress/admin-ui exposes an actions slot used across admin screens (DataViews, WidgetDashboard, …). Plug Actions straight into it:

import { Page } from '@wordpress/admin-ui';

<WidgetDashboard
	layout={ layout }
	onLayoutChange={ setLayout }
	widgetTypes={ widgetTypes }
	editMode={ editMode }
	onEditChange={ setEditMode }
>
	<Page
		title={ __( 'My Dashboard' ) }
		actions={ <WidgetDashboard.Actions /> }
	>
		<WidgetDashboard.Widgets />
	</Page>
	<WidgetDashboard.Commands />
</WidgetDashboard>;

<Page> is optional. The compound renders inside any container, so a bare <header> or custom chrome works just as well.

Inserting widgets

The "Add widget" button in <WidgetDashboard.Actions /> opens a modal inserter. It lists the widgetTypes prop as a grid of live previews (each preview renders the type's example attributes through its own render module), supports search, and exposes a "Select" action with bulk support so users can insert one or several widgets in a single layout change. A <WidgetDashboard.Policy> above the dashboard narrows the listing through the insert operation; without one, every entry is offered.

On confirmation, the inserter creates instances (using each type's example.attributes as the initial values) and appends them to the staged layout. The dialog closes after a successful insertion or when the user dismisses it.

Governance

The engine knows which operations a user can perform on it. It does not know who the user is or what the application allows. <WidgetDashboard.Policy> is the seam through which the application answers, with a single callback:

type CanPerformDashboardOperation = (
	request: DashboardOperationRequest
) => boolean;

type DashboardOperationRequest =
	| { operation: 'customize' }
	| { operation: 'reset' }
	| { operation: 'insert'; widgetType: WidgetType }
	| {
			operation: 'remove' | 'move' | 'resize' | 'edit';
			widget: DashboardWidget;
			widgetType?: WidgetType;
	  };

Each request names the operation and carries its subject, so a branch on request.operation narrows the rest of the object. Instance requests carry the placed widget and its type, absent when the type is not registered:

<WidgetDashboard.Policy
	canPerform={ ( request ) => {
		switch ( request.operation ) {
			case 'customize':
				return canEditLayout;
			case 'insert':
				return request.widgetType.category === activeSection;
			case 'remove':
				return ! request.widget.attributes?.pinned;
			default:
				return true;
		}
	} }
>
	<WidgetDashboard { ...props } />
</WidgetDashboard.Policy>

| Operation | Subject | What it gates | | ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | customize | none | The Customize button, the core/dashboard/customize command, the core/dashboard/add-widgets command outside edit mode, and the automatic entry into customize mode on an empty layout. | | reset | none | The Reset to default entry in the overflow menu, the core/dashboard/reset-to-default command, and the confirmation prompt they open. A denied reset is hidden, not disabled. | | insert | widgetType | Whether the inserter offers the type; a rejected type keeps rendering where already placed. The Add widget button and command show only while some registered type is insertable. | | remove | widget, widgetType? | The Remove control in customize mode. | | move | widget, widgetType? | Dragging the tile in customize mode. A denied tile is pinned: it holds its index while the other tiles reorder around it; a change ahead of it can still reflow the cell it lands in. | | resize | widget, widgetType? | The resize handle and the width menu. | | edit | widget, widgetType? | Attribute editing: the inline fields and the settings trigger in the header, the settings surface, and the widget's setAttributes, which is absent when denied so the widget renders read-only. |

The engine resolves the policy once, in its provider, and every surface asks that resolved answer, so further sources join at the same point without touching the surfaces.

The staging layer enforces the same answers. Every mutation is diffed per instance on arrival, whichever trigger staged it, and each change the policy denies is re-asserted before it lands: a removed locked instance returns at its index, a denied move holds its index, a denied resize keeps its spans, a denied edit keeps its attributes, and a new instance of a rejected type is dropped. What the interface hides, the staging layer rejects.

Instance operations carry widgetType only while that type is still in widgetTypes. A plugin that is gone, or a render module that failed to load, leaves the field absent: the engine still asks, because the instance is still on the board, but a policy that keys its locks on the type has nothing to match. Locks that must survive that case belong on the instance itself. The same absence is why a new instance of an unregistered type is not dropped: there is no type to ask about.

Rules of the contract:

  • Return true for operations you do not govern. Policies compose restrictively, so a default false would deny every operation added later.
  • Mount it around <WidgetDashboard>, not inside. The engine mounts the inserter outside the children subtree, so a policy placed inside children has no effect. One provider can cover several dashboards.
  • Nested policies only narrow. An operation is allowed when every enclosing policy allows it; an inner policy cannot re-grant what an outer one denied. Without a policy, every operation is allowed.
  • The callback is called during render. Keep it synchronous, and memoize it when it derives from state; a new function re-evaluates the dashboard, even with the inserter open.

The policy governs the interface: it decides what the dashboard offers, not what the server accepts. A host that must enforce permissions does so where the layout persists. And it never reaches widget bodies: a widget asks the server about its own entities, and reads the application's decisions only as the presence or absence of what the host lends it.

Grid settings

The dashboard supports two grid models, configured through the gridSettings prop: the 2D packed grid model, where tiles declare explicit spans over uniform rows, and the content-driven masonry model, where heights follow content and resize is horizontal-only. The settings are read-only for the dashboard: there is no in-dashboard editing UI. The consumer owns the values and their persistence.

The exported kit for handling them:

  • WidgetGridSettings — discriminated union of the per-model settings shapes.
  • DEFAULT_GRID — canonical default settings, applied when gridSettings is omitted.
  • normalizeGridSettings( settings, defaultRowHeight ) — coerces legacy freeform row heights to the nearest preset. Run it over stored payloads before passing them in.
  • ROW_HEIGHT_PRESETS / DEFAULT_ROW_HEIGHT — the row-height presets (small, medium, large) that rowHeight values normalize to.
  • WIDGET_DASHBOARD_COLUMN_COUNT — column count used on wide containers when the host sets no gridSettings.columns. A default, not a ceiling: a finite columns is floored, with a floor of 1, and rendered as asked. The effective count steps down from container width: the count at 960px and above, min( 2, count ) below that, one column below 600px.
<WidgetDashboard
	layout={ layout }
	onLayoutChange={ setLayout }
	widgetTypes={ widgetTypes }
	gridSettings={ { model: 'masonry' } }
/>

Tile spacing

The tile chrome is a Card at the Card's default density. Hosts can tighten or relax it by setting two custom properties at :root:

:root {
	--wp-widget-dashboard-tile-padding: var( --wpds-dimension-padding-lg );
}

--wp-widget-dashboard-tile-padding controls the padding of the tile surface. --wp-widget-dashboard-tile-header-gap controls the space between the tile header and the body; it follows the tile padding unless set apart. Use --wpds-* spacing tokens as values. The floating header of full-bleed tiles and the picker previews follow the same properties.

:root rather than a dashboard wrapper matters for the picker: it mounts in a dialog under document.body, which a wrapper's custom properties never reach.

How this host translates the contract

This engine is one host implementation of @wordpress/widget-primitives. It maps contract fields to host-owned UI as follows.

Identity and help

When a widget type declares help, the tile chrome surfaces its content and optional links in a infotip beside the title.

Attribute editing

A widget declares importance per attribute through relevance ('high' | 'medium' | 'low', absent means 'low'). The declaration states importance, not placement; this host maps it to two surfaces:

  • Prominent surface: relevance: 'high' fields render as bare inline controls in the tile header, for in-context edits.
  • Settings surface: the full schema, opened from the settings trigger. The trigger shows only when some attribute is not promoted; otherwise it would repeat the prominent surface.

The prominent surface holds only while it fits. The header measures the space it can grant its toolbar; when the promoted fields' natural width exceeds it, they collapse into a dropdown holding them as a form. The settings trigger is not part of the collapse: it stays in the toolbar whenever non-promoted attributes exist. The presentation follows the measurement both ways: widen the tile and the fields return inline.

Edits on any surface stage through the engine's internal layer and reach onLayoutChange on commit. Prominent-surface and dropdown edits publish on a shared auto-save debounce; the settings surface publishes on Save.

Actions

Actions carry the same relevance scale, mapped to surfaces of decreasing prominence:

  • Footer, leading: relevance: 'high' actions render as text links in a persistent strip under the widget body, a declared icon riding as prefix.
  • Footer, trailing: relevance: 'medium' actions render as compact affordances: icon-only links when the action declares an icon, text links otherwise.
  • More menu: the rest collapse into the three-dots menu in the tile header.

Every affordance is a real anchor. Full-bleed widgets have no footer, so all of their actions stay in the menu.

Authoring widgets

Widget render modules receive only what they need to render and edit:

interface WidgetRenderProps< Item = unknown > {
	attributes: Item;
	setAttributes?: ( next: Partial< Item > ) => void;
}

setAttributes flows back through the staging layer and reaches onLayoutChange on commit. Removal, badges, and error chrome are not part of this contract — those belong to the consumer.

Types

  • DashboardWidget — a placement of a widget on the dashboard. Carries uuid, type, attributes, placement.
  • WidgetGridSettings — grid model configuration; see Grid settings.
  • DashboardOperationRequest / CanPerformDashboardOperation — the policy contract; see Governance. DashboardInstanceOperation and DashboardInstanceOperationRequest name the per-instance half.

The widget contract types (WidgetName, WidgetType, WidgetRenderProps, ResolveWidgetModule) are defined in @wordpress/widget-primitives and imported from there directly; this engine does not re-export them.

Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to npm and used by WordPress as well as other software projects.

To find out more about contributing to this package or Gutenberg as a whole, please read the project's main contributor guide.