[data-reveal]{opacity:1!important;transform:none!important}
Layout Banners
Interfaces

Layout Banners

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.

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

FieldTypeDescription
keystringUnique 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.
variantLayoutBannerVariant"info", "warning" or "error". Required. Picks the color, the default icon and the ARIA role.
textstring | ComposedTextThe 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.
actionsBlockLinkAction[]Link buttons after text, the last one leading. Labels follow the $ convention.
componentstringName of a global component rendered as the banner's content. Exclusive with text and resolve.
propsRecord<string, unknown>Props passed to component. Must be serializable.
ordernumberStack order, ascending from the top. Defaults to 0; ties keep the registration order.
iconstringLeading icon. Defaults to one matching the variant.
dismissiblebooleanWhether the user can close the banner. Defaults to false.
visibleLayoutBannerVisibilityDecides per request whether a text or component banner shows. Absent means always.
resolveLayoutBannerResolverBuilds { 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

FieldTypeDescription
userUser | undefinedThe caller; undefined for an unauthenticated request.
tenantIdstringThe tenant the site layout is built for.
permissionsSet<string>The caller's effective permissions in the tenant.
isOwnerbooleanWhether the caller holds the wildcard permission.
isTenantAccessDeniedbooleanWhether 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.