Grav 2.0 Developer Guide: Admin Next Panels & Menu Bar

Floating widgets, context panels, and menu bar actions, the lightweight ways to add UI

9 mins

Not every plugin needs a whole admin page. Sometimes you just want a button in the toolbar, or a little assistant in the corner, or a panel that pops out next to whatever you're editing. AI Pro drops a chat assistant into the bottom-right of every admin screen. Revisions Pro slides a revision-history panel in beside the page you're editing. Warm Cache adds one button to the top bar that warms your cache and gets out of the way.

These are the lightweight Admin Next integration points, and there are three of them. They get confused for each other a lot, so here's the quick map before we dig in:

  • Floating widgets are the always-available tools in the bottom-right corner. Global. They don't care what page you're on.
  • Context panels slide in from the side and do know what you're editing. They're attached to the editor.
  • Menu bar items are action buttons in the top toolbar. One click, one action.

Pick by what the thing needs to know and where it should live. Let's take them in turn.

Floating Widgets

A floating widget is the little circle in the bottom-right corner that opens a panel when you click it. AI Pro and AI Translate are both widgets. They're available on every admin screen, they float above whatever you're doing, and they're a good fit for a tool you want within reach all the time without it ever taking over the page.

You register one with onApiFloatingWidgets. Here's AI Pro's, near enough:

public function onApiFloatingWidgets(Event $event): void
{
    $widgets = $event['widgets'] ?? [];
    $widgets[] = [
        'id'                => 'ai-pro-chat',
        'plugin'            => 'ai-pro',
        'label'             => 'AI Assistant',
        'icon'              => 'robot',         // Font Awesome name (plain, no fa- needed)
        'gradient'          => 'linear-gradient(135deg, #6366f1, #8b5cf6)',
        'priority'          => 10,
        'width'             => 420,
        'height'            => 580,
        'useStandardHeader' => false,           // we draw our own header inside the widget
        'authorize'         => ['api.ai-pro', 'admin.ai-pro'],
    ];
    $event['widgets'] = $widgets;
}

The keys worth knowing: gradient colors the floating button and the panel header, width and height size the panel, useStandardHeader lets you opt out of Admin Next's title bar and draw your own, showFab controls whether the round button shows at all, and autoLoad loads your script on page init instead of waiting for a click (AI Translate uses that so it can hook into the editor early). As with the sidebar, authorize gates visibility on the server and gets stripped out before the list reaches the browser.

One thing to get right: floating-widget icons are Font Awesome, and you can pass either the plain name (robot) or the full class (fa-solid fa-robot). Context panels use Lucide instead, so keep the two straight.

The widget itself is a web component at admin-next/widgets/{slug}.js, the same contract as every other Admin Next component. Admin Next hands you the tag name and you close yourself by firing a close event:

const TAG = window.__GRAV_WIDGET_TAG;   // "grav-ai-pro--widget"

class AiProWidget extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this._render();
  }

  _close() {
    this.dispatchEvent(new CustomEvent('close'));   // Admin Next animates it shut
  }

  _render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: flex; flex-direction: column; height: 100%;
                color: var(--foreground); background: var(--background); }
      </style>
      <header><span>AI Assistant</span><button id="x">&times;</button></header>
      <div class="body">...</div>
    `;
    this.shadowRoot.getElementById('x').addEventListener('click', () => this._close());
  }
}

customElements.define(TAG, AiProWidget);

You get the usual API globals (window.__GRAV_API_SERVER_URL, __GRAV_API_PREFIX, __GRAV_API_TOKEN) for talking to your endpoints, and __GRAV_DIALOGS for confirmations.

Here's the catch that the marketing copy tends to gloss over: a floating widget does not know what page you're editing. Admin Next doesn't inject the current route into it. It's a global tool, full stop. If your widget needs to act on the current content, you read it yourself. AI Translate, for example, reads window.__GRAV_CONTENT_LANG to learn the page's language, and you can read window.location to figure out the route. That works fine. But if knowing the current page is the whole point of your tool, you probably don't want a floating widget at all. You want a context panel.

Context Panels

A context panel is the other kind of panel, and it's the one that's genuinely aware of what you're doing. It slides in from the side, it's triggered by a button in the editor's toolbar, and it only appears in the editors you say it should. Revisions Pro is the canonical example: open a page, click the history icon, and a panel slides in showing that exact page's revisions. Move to another page and it updates.

Registration is onApiContextPanels:

public function onApiContextPanels(Event $event): void
{
    $panels = $event['panels'] ?? [];
    $panels[] = [
        'id'            => 'revisions-pro',
        'plugin'        => 'revisions-pro',
        'label'         => 'Revision History',
        'icon'          => 'history',                // Lucide icon name (not Font Awesome)
        'contexts'      => ['pages', 'config'],      // which editors show the toolbar button
        'priority'      => 10,
        'width'         => 900,
        'badgeEndpoint' => '/revisions-pro/badge',   // optional, returns { count: N }
    ];
    $event['panels'] = $panels;
}

contexts is the key difference from a widget. It's the list of editors where your toolbar button appears: pages, config, plugins, themes. The badgeEndpoint is a nice touch if you want a count on the button (Revisions Pro shows how many revisions a page has); it's hit with the current context and returns { count: N }.

The panel component lives at admin-next/panels/{slug}.js, and this is where the context-awareness actually lands. Admin Next sets route, lang, and type as HTML attributes on your element, and it updates them as the user moves around. Declare them as observed attributes and refetch when they change:

const TAG = window.__GRAV_PANEL_TAG;   // "grav-revisions-pro--panel"

class RevisionsPanel extends HTMLElement {
  static get observedAttributes() { return ['route', 'lang', 'type']; }

  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this._render();
    if (this.getAttribute('route')) this._fetchRevisions();
  }

  attributeChangedCallback() {
    // The editor changed what's open. Refetch for the new route.
    if (this.getAttribute('route')) this._fetchRevisions();
  }

  async _fetchRevisions() {
    const route = this.getAttribute('route');
    // ... fetch /revisions-pro/list?route=... and render
  }
}

customElements.define(TAG, RevisionsPanel);

So the rule of thumb is simple. If the tool stands on its own, make it a floating widget. If the tool is about this page right now, make it a context panel and let Admin Next feed you the route.

Menu Bar Items

The last one is the simplest. A menu bar item is an action button in the top toolbar. One click runs one action. Warm Cache adds a button that warms the cache; Git Sync and Rsync add buttons to push and pull. That's the shape of it.

I want to be straight about something the original outline got wrong: menu bar items are actions, not links. There's no navigation item, no external URL, no dropdown of sub-links. The single thing a menu bar item does is fire a server-side action when you click it. If you want navigation, that's a sidebar entry or a page action, not the menu bar.

It takes two handlers, one to register the button and one to do the work:

public static function getSubscribedEvents(): array
{
    return [
        'onApiMenubarItems'  => ['onApiMenubarItems', 0],
        'onApiMenubarAction' => ['onApiMenubarAction', 0],
    ];
}

public function onApiMenubarItems(Event $event): void
{
    $items = $event['items'] ?? [];
    $items[] = [
        'id'        => 'warm-cache',
        'plugin'    => 'warm-cache',
        'label'     => 'Warm Cache',
        'icon'      => 'fa-gauge-high',                  // Font Awesome class
        'action'    => 'warm',                           // identifies the handler below
        'confirm'   => 'Warm the cache now?',            // optional confirmation prompt
        'authorize' => ['admin.configuration', 'admin.super'],
    ];
    $event['items'] = $items;
}

public function onApiMenubarAction(Event $event): void
{
    // Guard clause: only handle your own plugin and action
    if ($event['plugin'] !== 'warm-cache' || $event['action'] !== 'warm') {
        return;
    }

    [$status, $message] = static::warmCache();

    $event['result'] = [
        'status'  => $status,        // 'success' or 'error'
        'message' => $message,       // shown to the user as a toast
    ];
}

The flow is: the user clicks, Admin Next shows your confirm text if you set one, then it POSTs the action. The API fires onApiMenubarAction, your handler does the work and sets $event['result'], and Admin Next turns that result into a success or error toast. The message you return is the message the user sees, so write it for them, not for your logs.

Menu bar icons are Font Awesome, like floating widgets. And the guard clause matters: every plugin's handler runs on every menu bar action, so always check both $event['plugin'] and $event['action'] before you do anything.

Which One Do I Want?

If you're not sure which of the three fits, this is the whole decision:

You want... Use Knows the current page?
A tool reachable from anywhere (chat, calculator, helper) Floating widget No, it's global
Something tied to what's being edited (history, info, linting) Context panel Yes, gets route/lang/type
A one-shot action from the top bar (warm cache, sync, rebuild) Menu bar item No, it's a fire-and-forget action

A few things that apply across all three:

  • They load on every relevant screen, so keep them light. A floating widget in particular is mounted everywhere. Don't pull in a heavy bundle for a small tool.
  • Gate with authorize. Floating widgets and menu bar items both take it, and it's checked server-side. Don't rely on hiding things in the UI.
  • Read the theme tokens (var(--foreground), var(--background), var(--border), var(--primary)) so your widget or panel follows light and dark mode for free.
  • Mind the icon sets. Font Awesome for floating widgets and menu bar items, Lucide for context panels. Mixing them up is the single most common "why is my icon missing" question.

Reference Implementations

All three are worth reading next to this post:

  • AI Pro and AI Translate are floating widgets. AI Pro is a self-contained assistant with its own header (useStandardHeader: false); AI Translate uses autoLoad and reads __GRAV_CONTENT_LANG to work against the current page's language. Their components live at admin-next/widgets/{slug}.js.
  • Revisions Pro is the context panel to study. It declares contexts, ships a badgeEndpoint, and reacts to the route/lang/type attributes to show the right history. Component at admin-next/panels/revisions-pro.js.
  • Warm Cache, Git Sync, and Rsync are menu bar actions, the simplest of the bunch and a good place to start.

Quick Links and Help

And that wraps the Grav 2.0 developer series. Between compatibility flags, API endpoints, custom fields, custom pages, and these three lightweight points, you've got every hook you need to make a plugin feel completely at home in Admin Next. Most plugins won't use all of them, and that's fine. Reach for the lightest one that does the job, ship it, and come tell us in Discord what you built.

Andy


That's the full Grav 2.0 developer series. Start from the top with Grav 2.0 for Plugin Developers.

Grav Premium
Turbo-charge your Grav site - from the creators of Grav