Banners
Show announcement banners on marketing and dashboard surfaces, gated per surface by a config flag.
Announcement banners surface promos and notices across the app. They are gated by config.banner (per-surface booleans), with content defined per-app in apps/web-nuxt/app/config/banner.ts.
Feature flag
config.banner holds two independent booleans. A generated project ships them false (banners off); the demo build sets both true.
// packages/config/src/index.ts
banner: { marketing: false, dashboard: false },- When a surface flag is off, that banner never renders.
- A surface also self-hides when its slot has no
text, so emptying the copy removes it without touching the flag.
Configuring content
Define one slot per surface in bannerConfig (a BannerSlots). Icons are Phosphor components imported directly from @phosphor-icons/vue, text is a banner.* i18n key, and link accepts an internal path or external URL:
// apps/web-nuxt/app/config/banner.ts
import { PhGift, PhSparkle } from "@phosphor-icons/vue";
export const bannerConfig: BannerSlots<Component> = {
marketing: {
text: "banner.marketing", // i18n key
icon: PhGift,
variant: "primary", // BannerVariant, default "primary"
link: "/pricing",
},
dashboard: {
text: "banner.dashboard",
icon: PhSparkle,
variant: "primary",
link: "/settings/developers",
// dashboard-only, all optional:
// dismissible: true, roles: ["admin"], plans: ["pro"],
},
};Marketing vs dashboard
The two surfaces differ in lifecycle and capabilities:
| Aspect | Marketing | Dashboard |
|---|---|---|
| Audience | Public visitors | Authenticated users |
| Rendering | Static, prerendered | Resolved server-side per user |
| Dismissable | No (always shown) | Yes via dismissible (default true) |
| Dismiss persistence | - | dashboard_banner_dismissed cookie, keyed by content |
| Targeting | None | By roles and/or plans |
| Slot type | BannerSlot | DashboardBannerSlot |
Slot fields
text, icon, variant, and link apply to both slots; the rest extend DashboardBannerSlot only:
| Key | Type | Default | Description |
|---|---|---|---|
text | string | - | A banner.* i18n key; absence hides the surface |
icon | Component | - | Optional Phosphor icon component |
variant | BannerVariant | "primary" | primary / secondary / destructive / accent / muted, mapped to shadcn bg + fg classes |
link | string | - | Wraps the banner text. Internal paths route through localePath(); external URLs open in a new tab (rel="noopener noreferrer") |
dismissible | boolean | true | Dashboard only - show the ✕ and persist dismissal |
roles | UserRole[] | - | Dashboard only - restrict to matching roles |
plans | string[] | - | Dashboard only - restrict to matching plan ids |
The dashboard layout resolves visibility server-side with useCookie plus the matchesBannerTargeting and bannerContentKey helpers from @repo/utils/helpers.
There is no separate maintenance-mode system. To announce downtime or critical notices, use a dashboard banner with a destructive variant and roles/plans targeting.
Frequently asked questions
How do I hide a banner?
Set its surface to false in config.banner, or remove the slot's text - a slot with no text self-hides.
Why can't visitors dismiss the marketing banner? Marketing pages are prerendered with no per-user state, so the marketing banner is intentionally static and non-dismissable. Use a dashboard banner when you need dismissal.
How is a dismissed dashboard banner remembered?
Dismissal is stored in the dashboard_banner_dismissed cookie, keyed by a hash of the banner's text, variant, and link (bannerContentKey). Editing any of those re-shows the banner to users who dismissed the old one.
Can I target a banner to specific users?
Yes, on the dashboard surface only - set roles and/or plans, evaluated by matchesBannerTargeting (omitted or empty means "match everyone").