Layout Banners
Overview
A layout banner is a global message the backend adds under the dashboard header: a colored strip carrying a line of text and its link buttons, or a component of your frontend layer. It renders on every page of the dashboard layout — tenant pages and the module back-office pages under /modules alike — and banners stack by order.
Whether a banner shows is decided per request, on the server, while the site layout is built. The answer is part of the server-rendered page, so a banner never flashes in or out after hydration.
Everything lives in @antelopejs/interface-dms/layout-banners.
Register a Banner
RegisterLayoutBanner(info) registers the banner and returns a function removing it. Register at import time, next to the state the banner reports on; the registration is dropped automatically when your module stops.
import {
LayoutBannerVariant,
RegisterLayoutBanner,
} from "@antelopejs/interface-dms/layout-banners";
import { isMaintenanceScheduled } from "./maintenance";
RegisterLayoutBanner({
key: "my-app:maintenance",
variant: LayoutBannerVariant.WARNING,
text: "$my_app.banners.maintenance",
order: 10,
dismissible: true,
visible: async ({ tenantId }) => isMaintenanceScheduled(tenantId),
});
The text can be a composed text — an i18n key and the raw values it names, written in the reader's language — and actions adds link buttons after it, the last one leading:
RegisterLayoutBanner({
key: "my-app:maintenance",
variant: LayoutBannerVariant.WARNING,
text: {
key: "my_app.banners.maintenance_at", // "Maintenance {date}"
params: { date: { type: "datetime", value: "2026-10-12T22:00:00Z" } },
},
actions: [{ label: "$my_app.banners.status", to: "https://status.example.com" }],
});
Resolve the Content per Request
When the message names the state that makes the banner show — the invoice that failed, its amount, the day the workspace is suspended — resolve(context) builds the text and the buttons for each request, or answers undefined to hide the banner. It replaces visible: one read decides both.
RegisterLayoutBanner({
key: "saas:past-due",
variant: LayoutBannerVariant.ERROR,
resolve: async ({ tenantId }) => {
const invoice = await openInvoice(tenantId);
if (!invoice) return undefined;
return {
text: {
key: "saas.banner.past_due", // "Invoice {invoice} of {amount} failed, retry {date}"
params: {
invoice: invoice.number,
amount: { type: "money", value: invoice.amountDue, currency: invoice.currency },
date: { type: "date", value: invoice.nextAttemptAt, format: "day" },
},
},
actions: [{ label: "$saas.banner.pay", to: "/settings/workspace/billing?pay=1" }],
};
},
});
The server sends the raw values; the browser formats them for the reader's language, time zone and clock, so the resolver never formats text. A banner like this one needs no frontend component.
Mount a Component
A banner can mount a global component of your frontend layer instead of a text. The DMS still draws the chrome around it — color, icon, close button and ARIA role — and passes props to the component:
RegisterLayoutBanner({
key: "my-app:trial",
variant: LayoutBannerVariant.INFO,
component: "MyAppTrialBanner",
props: { upgradeUrl: "/settings/billing" },
visible: ({ isOwner }) => isOwner,
});
Re-registering a key replaces the banner.
LayoutBannerInfo
| Field | Type | Description |
|---|---|---|
key | string | Unique across the deployment. Required. Also what a dismissal remembers: give a banner a new key when it must show again to users who dismissed it. |
variant | LayoutBannerVariant | "info", "warning" or "error". Required. Picks the color, the default icon and the ARIA role. |
text | string | ComposedText | The message. A $-prefixed value resolves as an i18n key; a ComposedText is written in the reader's language. Exclusive with component and resolve; one of the three is required. |
actions | BlockLinkAction[] | Link buttons after text, the last one leading. Labels follow the $ convention. |
component | string | Name of a global component rendered as the banner's content. Exclusive with text and resolve. |
props | Record<string, unknown> | Props passed to component. Must be serializable. |
order | number | Stack order, ascending from the top. Defaults to 0; ties keep the registration order. |
icon | string | Leading icon. Defaults to one matching the variant. |
dismissible | boolean | Whether the user can close the banner. Defaults to false. |
visible | LayoutBannerVisibility | Decides per request whether a text or component banner shows. Absent means always. |
resolve | LayoutBannerResolver | Builds { text, actions? } per request, or undefined to hide the banner. Exclusive with text, component and visible. |
Visibility
visible(context) returns a boolean, or a promise of one; resolve(context) returns a LayoutBannerContent ({ text, actions? }), undefined, or a promise of either. They run server-side for every site layout request, all resolvers together, so the request waits for the slowest one rather than for their sum. A resolver that throws or does not answer within two seconds hides its banner for that request, as does a visible returning anything but true or a resolve answering no text; the other banners are unaffected.
LayoutBannerContext
| Field | Type | Description |
|---|---|---|
user | User | undefined | The caller; undefined for an unauthenticated request. |
tenantId | string | The tenant the site layout is built for. |
permissions | Set<string> | The caller's effective permissions in the tenant. |
isOwner | boolean | Whether the caller holds the wildcard permission. |
isTenantAccessDenied | boolean | Whether a tenant access gate currently refuses the caller. |
Unlike product surfaces, a banner reads the permissions a denying access gate would hide: telling a suspended or past-due workspace why is exactly what a banner is for. Check isTenantAccessDenied when the two situations call for different banners.
The resolvers run whenever a client fetches its site layout — on a full page load and after a menu invalidation. When the state a resolver reads changes, call NotifyMenuChanged(tenantId) from @antelopejs/interface-dms/page so the connected clients of that tenant re-fetch it and the banner appears or goes without a reload.
Rendering
Banners render under the header of the dashboard layout, full width, in order, a text banner's actions on its right. Each one is its own live region: error banners carry role="alert", info and warning banners role="status".
A dismissible banner shows a close button. The dismissal lasts for the browser session and is kept in the dms-dismissed-banners cookie, which the server reads too, so a dismissed banner is already absent from the server-rendered page. A banner that is not dismissible ignores any dismissal recorded under its key.
Quick Actions
A quick action is a shortcut the backend contributes to the dashboard's command palette: a labelled entry, grouped under a category, that points at a registered page, a table creation form, a table toolbar button, or…
Introduction
The auth domain answers who is calling: authentication decorators, tokens and sessions, users and external identities.