Inline Alert
Inline alerts in info, success, warning and error that differ by icon and weight, not colour, with actions and dismiss.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/inline-alert.json'use client'
import { useState, type ReactNode } from 'react'
import './inline-alert.css'
export type AlertTone = 'info' | 'success' | 'warning' | 'error'
export interface InlineAlertProps {
tone?: AlertTone
title: string
children?: ReactNode
action?: { label: string; onClick: () => void }
onDismiss?: () => void
fill?: 'dark' | 'light'
className?: string
}
const ICON: Record<AlertTone, ReactNode> = {
info: <><circle cx="9" cy="9" r="7" /><path d="M9 8.2v4.3M9 5.6v.1" /></>,
success: <><circle cx="9" cy="9" r="7" /><path d="m6 9.2 2.1 2.1L12.2 7" /></>,
warning: <><path d="M9 2.5 16 15H2z" /><path d="M9 7.2v3.6M9 12.8v.1" /></>,
error: <><path d="M6.1 2h5.8L16 6.1v5.8L11.9 16H6.1L2 11.9V6.1z" /><path d="m6.8 6.8 4.4 4.4m0-4.4-4.4 4.4" /></>,
}
/** Monochrome inline alert with four tones. */
export function InlineAlert({ tone = 'info', title, children, action, onDismiss, fill = 'dark', className }: InlineAlertProps) {
const [gone, setGone] = useState(false)
return (
<div className={['sd-ial', className].filter(Boolean).join(' ')} data-fill={fill} data-tone={tone} data-gone={gone || undefined}>
<div className="sd-ial__in">
<div className="sd-ial__box" role={tone === 'error' || tone === 'warning' ? 'alert' : 'status'}>
<svg className="sd-ial__ic" viewBox="0 0 18 18" role="img" aria-label={tone}>{ICON[tone]}</svg>
<div>
<div className="sd-ial__t">{title}</div>
{children && <div className="sd-ial__b">{children}</div>}
{action && <button type="button" className="sd-ial__act" onClick={action.onClick}>{action.label}</button>}
</div>
{onDismiss ? <button type="button" className="sd-ial__x" aria-label="Dismiss" onClick={() => { setGone(true); setTimeout(onDismiss, 260) }}><svg viewBox="0 0 12 12" aria-hidden><path d="m3 3 6 6M9 3 3 9" /></svg></button> : <span />}
</div>
</div>
</div>
)
}.sd-ial { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-ial[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-ial button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-ial :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-ial .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-ial { display: grid; grid-template-rows: 1fr; width: min(100%, 330px); transition: grid-template-rows 260ms ease, opacity 200ms; }
.sd-ial[data-gone] { grid-template-rows: 0fr; opacity: 0; }
.sd-ial__in { min-height: 0; overflow: hidden; }
.sd-ial__box { display: grid; grid-template-columns: 20px 1fr auto; gap: 10px; padding: 12px 12px 12px 14px; border-radius: 14px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); animation: sd-ial-in 280ms cubic-bezier(0.2, 1.2, 0.3, 1); }
@keyframes sd-ial-in { from { opacity: 0; transform: translateY(-6px); } }
.sd-ial[data-tone='success'] .sd-ial__box { box-shadow: inset 0 0 0 1px var(--mute); }
.sd-ial[data-tone='warning'] .sd-ial__box { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-ial[data-tone='error'] .sd-ial__box { background: var(--strong); color: var(--on); box-shadow: none; --mute: color-mix(in srgb, var(--on) 65%, transparent); --line: color-mix(in srgb, var(--on) 25%, transparent); }
.sd-ial__ic { width: 18px; height: 18px; margin-top: 1px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.sd-ial__t { font-size: 13.5px; font-weight: 650; }
.sd-ial__b { font-size: 13px; color: var(--mute); margin-top: 2px; line-height: 1.45; }
.sd-ial__act { margin-top: 10px; height: 30px; padding: 0 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
.sd-ial[data-tone='error'] .sd-ial__act { background: var(--on) !important; color: var(--strong) !important; box-shadow: none; }
.sd-ial__x { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; color: var(--mute); margin: -3px -3px 0 0; }
.sd-ial__x:hover { color: inherit; background: color-mix(in srgb, currentColor 12%, transparent) !important; }
.sd-ial__x svg { width: 11px; height: 11px; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
@media (prefers-reduced-motion: reduce) { .sd-ial *, .sd-ial *::before, .sd-ial *::after { animation: none !important; transition: none !important; } }Overview
Four tones told apart by icon shape and border weight so they stay monochrome and still read at a glance: info is quiet, error is inverted.
Each can carry a title, body, a primary action and a dismiss that collapses the alert’s height.
When to use
- Form-level errors, plan limits, sync status, release notes in context.
When not to use
- Transient confirmations: use a toast.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/inline-alert.json$
npx shadcn@latest add https://sundaeui.dev/r/inline-alert.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/inline-alert.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/inline-alert.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy inline-alert.tsx and inline-alert.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { InlineAlert } from '@/components/sundae/inline-alert'
<InlineAlert />Props
| Prop | Type | Default |
|---|---|---|
toneTone. | 'info' | 'success' | 'warning' | 'error' | 'info' |
title*Heading. | string | — |
childrenBody. | ReactNode | — |
actionOptional action. | { label: string; onClick: () => void } | — |
onDismissShows a close button. | () => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Errors and warnings use role="alert"; info and success use role="status".
- Tone is in the icon’s accessible name, not colour alone.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-ial; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Inline Alert free?
- Yes. Inline Alert is free for personal and commercial projects, with full source on this page.
- How do I install Inline Alert?
- Run npx shadcn@latest add https://sundaeui.dev/r/inline-alert.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Inline Alert support keyboard and screen readers?
- Errors and warnings use role="alert"; info and success use role="status". Tone is in the icon’s accessible name, not colour alone.
- Does Inline Alert respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →Toast Stack
Components · RefinedHold to Confirm
Components · RefinedCookie Consent
Components · RefinedSubmit Button
Components · RefinedAnnouncement Bar
Components · RefinedRoadmap Votes
Components · Refined