Alert
An inline callout for neutral, success, warning or danger messages, with optional icon, title and description.
Preview
Ines shared “Pebble notes”
You can view and comment. Request edit access
Backup complete
1,204 files · 3.2 GB copied to Vault 2 at 09:41.
Storage nearly full
7.4 / 8 GB
Payment declined
The card ending 0619 was declined on Nov 1. Update it by Nov 8 to keep Priya’s team workspace active.
Install
Add the editable source with the shadcn CLI. Your project also needs the shared theme tokens this component uses. Setup and requirements. The Pebble installer is not published yet.
npx shadcn@latest add https://pebbleui.com/r/alert.jsonProps
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "neutral" | "success" | "warning" | "danger" | "neutral" | Colour and default live role. |
| role | AriaRole | "status" or "alert" | status for neutral and success, alert for warning and danger. Use "note" for static content present at page load. |
Build notes
A plain server component: no context, no hooks. The variant sets a role (`status` for neutral/success, `alert` for warning/danger) and a CSS variable `--alert-title` that `AlertTitle` reads with `text-(--alert-title)`, so the title color follows the variant without React context. Layout is a two-column grid: column one is `0` wide, or `1.75rem` when the first child is an `svg` (`has-[>svg]:`), and every non-svg child is placed in column two, so the icon is optional with no extra wrapper. Status colours come from the `--success/--warning/--danger` tokens, with the `*-strong` tokens for text (see the Pebble paragraph for the current surface). **Pebble (2026-11-03).** The alert is now a raised Pebble card on `bg-card` with `shadow-raised-sm` and the inset highlight, instead of a tinted panel; status shows through a tinted hairline ring (`ring-<status>/30`) and the icon, which becomes a 28 px recessed tile (status tint at 12%, inset ring, `-strong` glyph colour) so it reads at a glance without flooding the card. The title takes the `-strong` token through an `--alert-title` variable set per variant; neutral leaves it on the foreground. Two parts were added: `AlertActions` (a wrapping button row placed in the text column) and `AlertClose` (a 28 px ghost icon button in the corner; the root reserves `pr-11` with `has-[>[data-slot=alert-close]]` so the title never runs under it). The alert fades and rises 4 px in on mount with `starting:` (no keyframes), `--motion-base` and `ease-enter`; reduced motion drops both. Roles and the icon-column grid are unchanged. <!-- PEBBLE-LICENSE -->
View source
registry/feedback/alert/alert.tsx
/* PEBBLE-LICENSE
Built by Pebble. Component: alert.
First-party license: MIT
The following first-party terms apply only to original Pebble material.
MIT License
Copyright (c) 2026 Praxys Solutions LLC (Pebble)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END PEBBLE-LICENSE */
import * as React from "react";
import { cn } from "@/lib/utils";
// Pebble: every alert is a raised card (hairline ring, small layered shadow,
// top highlight). Status lives in the icon tile and a tinted ring, not in a
// flooded background, so a stack of alerts stays calm and readable.
const variants = {
neutral:
"ring-hairline [--alert-title:initial] [&>svg]:bg-muted [&>svg]:text-foreground [&>svg]:ring-hairline",
// --alert-title is read by AlertTitle, so the title colour needs no
// context and Alert stays a server component.
success:
"ring-success/30 [--alert-title:var(--color-success-strong)] [&>svg]:bg-success/12 [&>svg]:text-success-strong [&>svg]:ring-success/25",
warning:
"ring-warning/35 [--alert-title:var(--color-warning-strong)] [&>svg]:bg-warning/15 [&>svg]:text-warning-strong [&>svg]:ring-warning/30",
danger:
"ring-danger/30 [--alert-title:var(--color-danger-strong)] [&>svg]:bg-danger/10 [&>svg]:text-danger-strong [&>svg]:ring-danger/25",
} as const;
// Warnings and errors interrupt; everything else waits its turn.
const roles = {
neutral: "status",
success: "status",
warning: "alert",
danger: "alert",
} as const;
type Variant = keyof typeof variants;
export type AlertProps = React.ComponentProps<"div"> & {
variant?: Variant;
};
export function Alert({ variant = "neutral", className, ...props }: AlertProps) {
return (
<div
role={roles[variant]}
data-slot="alert"
data-variant={variant}
className={cn(
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-xl bg-card px-4 py-3.5 text-sm text-card-foreground",
"shadow-raised-sm ring-1 inset-shadow-highlight",
// Icon: a direct svg child becomes a small recessed tile. Negative
// margin keeps the 28 px tile from stretching the title row, and
// centres it on the first line of the title.
"has-[>svg]:grid-cols-[1.75rem_1fr] has-[>svg]:gap-x-3 [&>svg]:-my-1 [&>svg]:size-7 [&>svg]:rounded-md [&>svg]:p-1.5 [&>svg]:ring-1 [&>svg]:ring-inset",
"[:where(&>:not(svg))]:col-start-2",
// Room for a dismiss button in the top corner.
"has-[>[data-slot=alert-close]]:pr-11",
// Enter: a short fade and rise when the alert is inserted.
"transition-[opacity,translate] duration-(--motion-base) ease-enter motion-reduce:transition-none starting:translate-y-1 starting:opacity-0 motion-reduce:starting:translate-y-0",
variants[variant],
className,
)}
{...props}
/>
);
}
export function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn("col-start-2 font-medium tracking-[-0.01em] text-(--alert-title)", className)}
{...props}
/>
);
}
export function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"col-start-2 text-[13px] leading-relaxed text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:decoration-border-control [&_a]:underline-offset-4 [&_a]:hover:decoration-foreground [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
/** A row of actions under the description. */
export function AlertActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-actions"
className={cn("col-start-2 mt-2.5 flex flex-wrap items-center gap-2", className)}
{...props}
/>
);
}
/** A dismiss button pinned to the top-right corner. Wire `onClick` yourself. */
export function AlertClose({
className,
children,
"aria-label": ariaLabel = "Dismiss",
type = "button",
...props
}: React.ComponentProps<"button">) {
return (
<button
data-slot="alert-close"
type={type}
aria-label={ariaLabel}
className={cn(
"absolute top-2.5 right-2.5 inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground",
"transition-[background-color,color,transform] duration-(--motion-fast) ease-enter hover:bg-muted hover:text-foreground active:scale-[0.94]",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100",
className,
)}
{...props}
>
{children ?? (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
aria-hidden="true"
className="size-4"
>
<path d="m4.5 4.5 7 7m0-7-7 7" />
</svg>
)}
</button>
);
}
