Helm Docs.
Tool
Helm
Helm is currently in beta
The layout stored in project config may still change shape between beta releases, so try it on a staging site before a production one.
- Craft CMS 5.10+
- PHP 8.2+
The beta needs an explicit version constraint. Without one, Composer skips it as a non-stable release:
Shell
composer require bensomething/craft-helm:^1.0.0-beta
php craft plugin/install helm
- Go to Settings → Helm (or hit Edit layout on the dashboard) to open the layout designer.
- Add tabs and drag in elements:
- The Widgets tab — Widget Template (renders a site template, with an optional title), Recent Entries, My Drafts, New Users, Quick Post (opens the new-entry editor slideout), and Native Widget, which hosts any installed dashboard widget — core (Feed, Updates, Quick Post, …) or plugin-provided (e.g. Commerce's stats widgets), with the widget's own settings. Per-instance settings via the gear icon, including a View Mode (Grouped / In a pane / Inline) like the Content Block field's.
- The UI Elements tab — Craft's built-in Heading, Tip, Warning, Markdown, Horizontal Rule, and Line Break. (Craft's own Template element is hidden for dashboard layouts — Widget Template covers it, adding the widget pane, a title, and a
viewervariable instead of a nullelement.)
- Use each element's Current User Condition (in its settings slideout) to scope it to specific user groups. Whole tabs can be conditioned too.
- Save. The layout is stored in project config (
helm.dashboard), so it deploys across environments like any other schema change.
Visiting /admin/dashboard (the Dashboard nav item, and the default post-login redirect) now renders the Helm dashboard.
- The core
dashboardCP route is overridden viaEVENT_REGISTER_CP_URL_RULES. - The layout is a regular
craft\models\FieldLayoutwith a plugin-owned type (bensomething\helm\models\Dashboard), persisted to project config the same way core persists the user field layout. - At render time, each tab and element is filtered with
showInForm(), which evaluates its Current User Condition against the logged-in user. Elements render viaformHtml(null)— there is no backing element.
There are three tiers, by widget type:
- Site-specific, display-only → the Widget Template element. Add a Twig file to your site's
templates/folder containing just the content (no pane markup needed), then add a Widget Template in the designer, pointing at it, with an optional per-instance title. The template receives aviewervariable (the logged-in user). No PHP involved. - Site-specific with settings or heavy logic → a
BaseWidgetclass in your site module. Registers via the same event as below; lives and deploys with the site repo. - Reusable across sites → a
BaseWidgetclass in this plugin (or its own plugin).
Extend bensomething\helm\fieldlayoutelements\BaseWidget and register it for dashboard layouts (from a plugin or a site module's init()):
PHP
use bensomething\helm\fieldlayoutelements\BaseWidget;
class MyWidget extends BaseWidget
{
// Public properties are serialized into the layout config automatically
public int $limit = 5;
protected function selectorLabel(): string
{
return 'My Widget';
}
public function hasSettings()
{
return true;
}
protected function settingsHtml(): ?string
{
return \craft\helpers\Cp::textFieldHtml([
'label' => 'Limit',
'name' => 'limit',
'type' => 'number',
'value' => $this->limit,
]);
}
protected function widgetHtml(): ?string
{
return '<p>Hello from my widget.</p>';
}
}
PHP
use bensomething\helm\models\Dashboard;
use craft\events\DefineFieldLayoutElementsEvent;
use craft\models\FieldLayout;
use yii\base\Event;
Event::on(FieldLayout::class, FieldLayout::EVENT_DEFINE_UI_ELEMENTS, function(DefineFieldLayoutElementsEvent $event) {
if ($event->sender->type === Dashboard::class) {
$event->elements[] = MyWidget::class;
}
});
BaseWidget gives you width control, a titled pane wrapper, and error isolation (a widget that throws renders an error box instead of breaking the dashboard).
- Custom fields can't be added. The designer's field library is emptied for dashboard layouts (via
EVENT_DEFINE_CUSTOM_FIELDS), so only UI elements are on offer. As a fallback, any field that somehow ends up in a layout is skipped at render time, fields need a backing element to edit. - Native widgets run outside their native shell. The adapter fakes what widget JS expects — a
#widget{id}container, a.bodydiv, and aCraft.Widgetstub — and wraps each widget's JS in a try/catch. A widget that assumes more of the dashboard than that will log a warning to the console and render its server-side body without the interactive parts; it won't take the rest of the dashboard down with it. Most core and plugin widgets are fine, but check any given one on a staging site before relying on it. - A native widget is one shared instance, not one per user. Craft's own widgets are per-user by design, in a Helm layout, every viewer sees the same configured instance. Helm doesn't add permission checks of its own, so whatever a widget does or doesn't filter by permission is what your viewers get. Scope it with a Current User Condition if that matters. (Craft's one-per-dashboard limit is also ignored, deliberately: that's a per-user check with no meaning in a shared layout.)
- Template UI elements receive
element: null. Don't rely on theelementvariable in dashboard templates. - Element Conditions are meaningless here (there's no element), only Current User Conditions have any effect.
- Saving the layout requires an admin with
allowAdminChangesenabled, like any project config change.