Banner alerts
Five edge-to-edge alert banners that drop the radius and read as page strips rather than cards: a top notice, an inline strip, a sticky blurred strip, a compact status line and a saturated bar. Each is sized, driven by a tone prop (info, success, warning, danger) and fully token-based.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/alert-bannerDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install lucide-reactCopy the source
components/ui/alert-banner.tsx"use client"
import type * as React from "react"
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"
/* Banner alert family: 5 decorative alert surfaces. The shared idea is an
EDGE-TO-EDGE strip - no radius (rounded-none), horizontal lines cross the full
width, and the surface reads as a page strip rather than a card. The variants
differ by where the strip sits on the page (top, in the flow, sticky,
compressed, solid bar). Colour comes ONLY from semantic tokens; alpha via
color-mix or the tailwind opacity modifier. Anatomy (Berkay's decision): the
icon marks the TITLE ROW ONLY - icon and title share line 1, the description
starts BELOW the icon and takes the full width. No motion, this family is
entirely static. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
export type StyledTone = "info" | "success" | "warning" | "danger"
const pad: Record<StyledSize, string> = {
sm: "p-3 text-sm",
md: "p-4 text-sm",
lg: "p-4 text-base",
xl: "p-5 text-base",
}
/* Compact uses its own narrower scale - the strip stays close to a single line. */
const padCompact: Record<StyledSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-5 py-2.5 text-base",
xl: "px-6 py-3 text-base",
}
/* Grid: icon column plus content. The icon is a direct child, so the
[&>svg] / [&>i] pair (lucide <svg> and remixicon <i> compatibility) lives
here. Because this is a strip, rounded-none is baked into the base. */
const base =
"relative grid w-full grid-cols-[calc(var(--spacing)*4)_1fr] items-start gap-x-2 gap-y-0.5 rounded-none [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5 [&>i]:block [&>i]:size-4 [&>i]:shrink-0 [&>i]:translate-y-0.5 [&>i]:text-base [&>i]:leading-none"
/* Tone -> varsayilan lucide ikon. */
const toneIcon: Record<StyledTone, React.ReactNode> = {
info: <Info />,
success: <CircleCheck />,
warning: <TriangleAlert />,
danger: <CircleAlert />,
}
/* Icon colour comes from the root; the [&>i] pair is required for remixicon
compatibility. */
const iconTone: Record<StyledTone, string> = {
info: "[&>svg]:text-info [&>i]:text-info",
success: "[&>svg]:text-success [&>i]:text-success",
warning: "[&>svg]:text-warning-soft-foreground [&>i]:text-warning-soft-foreground",
danger: "[&>svg]:text-danger [&>i]:text-danger",
}
type AlertProps = Omit<React.ComponentProps<"div">, "title"> & {
size?: StyledSize
tone?: StyledTone
title?: React.ReactNode
icon?: React.ReactNode
}
/* Shared shell: the surface and the size map are supplied from outside. */
function Frame({
size,
tone,
title,
icon,
children,
className,
surface,
scale = pad,
...props
}: Omit<AlertProps, "size" | "tone"> & {
size: StyledSize
tone: StyledTone
surface: string
scale?: Record<StyledSize, string>
}) {
return (
<div
data-slot="styled-alert"
data-tone={tone}
role="alert"
className={cn(base, scale[size], surface, className)}
{...props}
>
{icon ?? toneIcon[tone]}
<div
data-slot="styled-alert-title"
className="col-start-2 min-h-4 font-medium tracking-tight"
>
{title}
</div>
<div
data-slot="styled-alert-description"
className="col-span-2 col-start-1 text-sm opacity-90 [&_p]:leading-relaxed"
>
{children}
</div>
</div>
)
}
/* A soft tone ground plus a tone underline. Top, Sticky and Compact share
this. */
const softTone: Record<StyledTone, string> = {
info: "bg-info-soft text-info-soft-foreground border-info/40",
success: "bg-success-soft text-success-soft-foreground border-success/40",
warning: "bg-warning-soft text-warning-soft-foreground border-warning/50",
danger: "bg-danger-soft text-danger-soft-foreground border-danger/40",
}
/* Top: an announcement strip pinned to the very top of the page - underline
only. */
export function TopAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A page-top notice strip with a single bottom rule.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("border-b", softTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Inline: a neutral strip dropped into the middle of the content flow - a line
above and below, colour only in the icon. */
export function InlineAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A neutral strip that sits inside the content flow.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("border-y border-border bg-surface-2 text-foreground", iconTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Sticky: a strip that sticks to the top on scroll. Semi-transparent plus a
backdrop blur so the content underneath stays readable; falls back to an
opaque soft ground where blur is unsupported. */
const stickyTone: Record<StyledTone, string> = {
info: "bg-info-soft/85 text-info-soft-foreground border-info/40 supports-[backdrop-filter]:bg-info-soft/60",
success:
"bg-success-soft/85 text-success-soft-foreground border-success/40 supports-[backdrop-filter]:bg-success-soft/60",
warning:
"bg-warning-soft/85 text-warning-soft-foreground border-warning/50 supports-[backdrop-filter]:bg-warning-soft/60",
danger:
"bg-danger-soft/85 text-danger-soft-foreground border-danger/40 supports-[backdrop-filter]:bg-danger-soft/60",
}
export function StickyAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A strip that sticks to the top of its scroll container.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn(
"sticky top-0 z-10 border-b supports-[backdrop-filter]:backdrop-blur-md",
stickyTone[tone]
)}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Compact: the narrowest strip - for persistent displays such as system
status. */
export function CompactAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "The tightest strip, sized for persistent status.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
scale={padCompact}
surface={cn("border-y", softTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Bar: a solid tone bar. The loudest member of the strip family; text and icon
come from tone -foreground. */
const barTone: Record<StyledTone, string> = {
info: "bg-info text-info-foreground",
success: "bg-success text-success-foreground",
warning: "bg-warning text-warning-foreground",
danger: "bg-danger text-danger-foreground",
}
export function BarAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A saturated edge-to-edge bar in the solid tone.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={barTone[tone]}
className={className}
{...props}
>
{children}
</Frame>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Top
A page-top notice strip with one bottom rule.
import { TopAlert } from "@/components/ui/alert-banner"
<TopAlert tone="info" title="Heads up">A page-top notice strip with a single bottom rule.</TopAlert>Inline
A neutral strip with rules above and below; color stays in the icon.
import { InlineAlert } from "@/components/ui/alert-banner"
<InlineAlert tone="success" title="Synced">A neutral strip that sits inside the content flow.</InlineAlert>Sticky
A translucent, blurred strip that sticks to the top of its scroll container.
import { StickyAlert } from "@/components/ui/alert-banner"
<StickyAlert tone="warning" title="Offline">A translucent strip that sticks to the top on scroll.</StickyAlert>Compact
The tightest strip, on its own compact scale.
import { CompactAlert } from "@/components/ui/alert-banner"
<CompactAlert tone="info" title="Beta">The tightest strip, sized for persistent status.</CompactAlert>Bar
A saturated solid tone bar, the loudest banner.
import { BarAlert } from "@/components/ui/alert-banner"
<BarAlert tone="danger" title="Incident">A saturated edge-to-edge bar in the solid tone.</BarAlert>ai2 Banner alerts: 5 styled variations on the token system
The ai2 Banner alerts are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around edge-to-edge alert strips with no radius. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: there is no motion in this family; every variation is static. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, nothing changes, because the banners never move.
What is in the ai2 Banner alerts?
5 exports in one file: Top, Inline, Sticky, Compact and Bar. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: there is no motion in this family; every variation is static.
- Reduced-motion aware: Under prefers-reduced-motion, nothing changes, because the banners never move.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Banner alerts sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.