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.

Requirements

  • Craft CMS 5.10+
  • PHP 8.2+

Installation

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

Usage

  1. Go to Settings → Helm (or hit Edit layout on the dashboard) to open the layout designer.
  2. 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 viewer variable instead of a null element.)
  3. Use each element's Current User Condition (in its settings slideout) to scope it to specific user groups. Whole tabs can be conditioned too.
  4. 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.

How it works

  • The core dashboard CP route is overridden via EVENT_REGISTER_CP_URL_RULES.
  • The layout is a regular craft\models\FieldLayout with 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 via formHtml(null) — there is no backing element.

Writing your own widgets

There are three tiers, by widget type:

  1. 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 a viewer variable (the logged-in user). No PHP involved.
  2. Site-specific with settings or heavy logic → a BaseWidget class in your site module. Registers via the same event as below; lives and deploys with the site repo.
  3. Reusable across sites → a BaseWidget class 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).

Caveats

  • 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 .body div, and a Craft.Widget stub — 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 the element variable 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 allowAdminChanges enabled, like any project config change.