GenerateSaaS

Banners

Add marketing and dashboard announcement banners in your Next.js SaaS, toggled by config.banner with role and plan targeting and dismissal.

Announcement bars gated by config.banner per surface; content lives per-app in apps/web-next/config/banner.ts. Each surface is independent - ship a marketing announcement without touching the dashboard.

The 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 },
BehaviorEffect
marketing: falseMarketing bar not rendered
dashboard: falseDashboard bar not rendered
Flag on, slot text emptySurface still self-hides (no text = nothing to show)
Content editsLive in apps/web-next/config/banner.ts, never in @repo/config

Marketing vs dashboard

AspectMarketing bannerDashboard banner
Flagconfig.banner.marketingconfig.banner.dashboard
Slot typeBannerSlotDashboardBannerSlot
RenderingStatic, prerendered with the pageRendered in the authenticated shell
DismissableNo - always shownYes (dismissible, default true)
TargetingEveryoneBy roles and/or plans
Use forLaunch notices, promos, global newsPlan-specific notices, upgrade nudges, role alerts

Both surfaces render the same "use client" <AnnouncementBanner> component. The marketing layout renders it with serializable props only, so the page stays prerenderable - but the component itself still ships as client JS.

Content fields

FieldTypeNotes
textstringi18n key, rendered as plain text (no HTML). Copy lives in @repo/i18n translations
iconPhosphor IconImported directly from @phosphor-icons/react/ssr, rendered weight="fill"
variantBannerVariant"primary" (default) | "secondary" | "destructive" | "accent" | "muted" - shadcn bg/fg
linkstringOptional. Internal path (next-intl Link) or external URL (opens in a new tab)

Dashboard-only fields

FieldTypeDefaultNotes
dismissiblebooleantrueRenders the ✕ button
rolesUserRole[]omit = all rolesResolved from the session (free)
plansstring[]omit = all plansLazily resolves the viewer's plan id only when set and the role check passed

Targeting and dismissal are resolved server-side in the dashboard layout (via matchesBannerTargeting / bannerContentKey from @repo/utils/helpers), so the banner never flashes. Dismissal persists in the dashboard_banner_dismissed cookie (1-year lifetime) keyed to the content - editing the copy mints a new key and re-shows it.

apps/web-next/config/banner.ts
export const bannerConfig: BannerSlots<Icon> = {
  marketing: {
    text: "banner.marketing", // i18n key
    icon: GiftIcon,
    variant: "primary",
    link: "/pricing"
  },
  dashboard: {
    text: "banner.dashboard",
    icon: SparkleIcon,
    variant: "primary",
    link: "/settings/developers"
    // dismissible?: boolean  (default true)
    // roles?: UserRole[]     (omit = everyone)
    // plans?: string[]       (omit = every plan)
  }
};

There is no maintenance mode. To warn users about upcoming downtime or incidents, enable the dashboard banner and target the relevant roles/plans.

Frequently asked questions

Can I show a banner only to paid users? Yes - use the dashboard banner and set plans to the paid plan IDs. Users on other plans won't see it.

Why won't my marketing banner go away when I click it? The marketing banner is intentionally non-dismissable and prerendered. If you need a closeable bar, use the dashboard banner, which persists dismissal in a cookie.

Where do I change the banner text and link? Edit the slot's link, icon, and variant in apps/web-next/config/banner.ts; the text is an i18n key whose copy lives in @repo/i18n translations. The config.banner flag only toggles each surface on or off.

Do I need both banners enabled? No. The marketing and dashboard booleans are independent - enable either, both, or neither.

On this page