Outline alerts
Five border-forward alerts that keep the surface transparent and let the frame carry the character: an inset ring, a dashed border, a double hairline, a thick tone border and tone corner accents. 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-outlineDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install lucide-reactCopy the source
components/ui/alert-outline.tsx"use client"
import type * as React from "react"
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"
/* Outline alert family: 5 decorative alert surfaces. The shared idea is being
EDGE-driven - the surface stays transparent or close to it, and all the
character comes from the frame (inner ring, dashed edge, double edge, thick
edge, corner accents). Color comes ONLY from semantic tokens, via alpha
color-mix or the tailwind opacity modifier. Anatomy (Berkay's decision): the
icon marks ONLY the title line - the icon and the title share line 1, and 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",
}
/* Grid: icon column plus content. The icon is a direct child, so the
[&>svg] / [&>i] pair (lucide <svg> and remixicon <i> compatibility) lives
here. */
const base =
"relative grid w-full grid-cols-[calc(var(--spacing)*4)_1fr] items-start gap-x-2 gap-y-0.5 rounded-lg [&>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: only the surface class changes. */
function Frame({
size,
tone,
title,
icon,
children,
className,
surface,
...props
}: Omit<AlertProps, "size" | "tone"> & {
size: StyledSize
tone: StyledTone
surface: string
}) {
return (
<div
data-slot="styled-alert"
data-tone={tone}
role="alert"
className={cn(base, pad[size], iconTone[tone], 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>
)
}
/* Ring: an inner ring instead of a border. The surface is fully transparent, the line thin and crisp. */
const ringTone: Record<StyledTone, string> = {
info: "ring-1 ring-inset ring-info/50",
success: "ring-1 ring-inset ring-success/50",
warning: "ring-1 ring-inset ring-warning/60",
danger: "ring-1 ring-inset ring-danger/50",
}
export function RingAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A borderless surface with a crisp inset ring.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("bg-transparent text-foreground", ringTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Dashed: kesik tone kenar. Gecici/taslak durum hissi. */
const dashedTone: Record<StyledTone, string> = {
info: "border-info/55",
success: "border-success/55",
warning: "border-warning/65",
danger: "border-danger/55",
}
export function DashedAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A dashed tone border on a transparent surface.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("border-2 border-dashed bg-transparent text-foreground", dashedTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Double: CSS border-double - two thin lines with a gap between them. Below 3px it does not render, so the width is a fixed [3px]. */
const doubleTone: Record<StyledTone, string> = {
info: "border-info/70",
success: "border-success/70",
warning: "border-warning/80",
danger: "border-danger/70",
}
export function DoubleAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "Two hairlines with a gap, drawn by a single double border.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("border-[3px] border-double bg-transparent text-foreground", doubleTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Thick: tam opak kalin tone kenar + cok hafif tone yikama. En yuksek sesli
outline varyanti. */
const thickTone: Record<StyledTone, string> = {
info: "border-info bg-[color-mix(in_oklab,var(--color-info)_6%,transparent)]",
success: "border-success bg-[color-mix(in_oklab,var(--color-success)_6%,transparent)]",
warning: "border-warning bg-[color-mix(in_oklab,var(--color-warning)_8%,transparent)]",
danger: "border-danger bg-[color-mix(in_oklab,var(--color-danger)_6%,transparent)]",
}
export function ThickAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A solid, heavy tone border with a faint tone wash.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn("border-2 text-foreground", thickTone[tone])}
className={className}
{...props}
>
{children}
</Frame>
)
}
/* Corner: a neutral thin frame + a tone accent on only two opposite corners
(before = top left, after = bottom right). */
const cornerTone: Record<StyledTone, string> = {
info: "before:border-info after:border-info",
success: "before:border-success after:border-success",
warning: "before:border-warning after:border-warning",
danger: "before:border-danger after:border-danger",
}
export function CornerAlert({
className,
size = "md",
tone = "info",
title = "Heads up",
children = "A neutral hairline frame with tone accents on two corners.",
icon,
...props
}: AlertProps) {
return (
<Frame
size={size}
tone={tone}
title={title}
icon={icon}
surface={cn(
"border border-border bg-transparent text-foreground",
"before:absolute before:left-[-1px] before:top-[-1px] before:size-3.5 before:rounded-tl-lg before:border-l-2 before:border-t-2 before:content-['']",
"after:absolute after:bottom-[-1px] after:right-[-1px] after:size-3.5 after:rounded-br-lg after:border-b-2 after:border-r-2 after:content-['']",
cornerTone[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.
Ring
An inset ring instead of a border on a fully transparent surface.
import { RingAlert } from "@/components/ui/alert-outline"
<RingAlert tone="info" title="Heads up">A transparent surface held by a crisp inset ring.</RingAlert>Dashed
A dashed tone border on a transparent surface.
import { DashedAlert } from "@/components/ui/alert-outline"
<DashedAlert tone="warning" title="Draft">A dashed tone border reads as provisional.</DashedAlert>Double
A CSS double border: two hairlines drawn from one rule.
import { DoubleAlert } from "@/components/ui/alert-outline"
<DoubleAlert tone="success" title="Verified">Two hairlines with a gap between them.</DoubleAlert>Thick
A heavy solid tone border with a faint tone wash.
import { ThickAlert } from "@/components/ui/alert-outline"
<ThickAlert tone="danger" title="Blocked">The loudest outline, with a faint tone wash.</ThickAlert>Corner
A neutral hairline frame with tone accents on two corners.
import { CornerAlert } from "@/components/ui/alert-outline"
<CornerAlert tone="info" title="Note">Tone accents mark two opposite corners.</CornerAlert>ai2 Outline alerts: 5 styled variations on the token system
The ai2 Outline alerts are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around border-forward alerts on a transparent surface. 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 alerts never move.
What is in the ai2 Outline alerts?
5 exports in one file: Ring, Dashed, Double, Thick and Corner. 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 alerts 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 Outline 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.