Alert
alertuiStatic, in-flow notice block with a leading icon, title, and description. Use for inline page-level messages — info banners, success confirmations, warning callouts. Two tones: `default` and `destructive`.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/alert.json$npx shadcn@latest add https://uipkge.dev/r/lit/alert.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/alert.json$bunx shadcn@latest add https://uipkge.dev/r/lit/alert.jsonManifest:
https://uipkge.dev/r/lit/alert.jsonInstalls to:src/uip/components/alert/Examples
Live demo coming soon — source below is exact.
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''destructive' | default | optional |
icon | AlertIcon | — | optional |
title | string | — | optional |
text | string | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-alert': UipAlert
'uip-alert-title': UipAlertTitle
'uip-alert-description': UipAlertDescription
}
}npm dependencies
Includes
Files installed (2)
src/uip/components/alert/alert.ts7.9 kB
import { LitElement, css, html, nothing } from 'lit' import { html as staticHtml, unsafeStatic } from 'lit/static-html.js' import { AlertCircle, CheckCircle, Info, TriangleAlert } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' import { alertVariants, type AlertVariants } from './alert.variants' type Variant = NonNullable<AlertVariants['variant']> export type AlertIcon = 'info' | 'warning' | 'error' | 'success' const builtInIcons = { error: { node: AlertCircle, name: 'circle-alert' }, success: { node: CheckCircle, name: 'circle-check-big' }, warning: { node: TriangleAlert, name: 'triangle-alert' }, info: { node: Info, name: 'info' }, } as const /** * <uip-alert> — the registry Alert as a web component, with <uip-alert-title> * and <uip-alert-description> for the composition API. * * <uip-alert variant="destructive"> * <svg><!-- slotted icon, like React's <AlertCircle /> child --></svg> * <uip-alert-title>Error</uip-alert-title> * <uip-alert-description>Something went wrong.</uip-alert-description> * </uip-alert> * * <uip-alert icon="info" title="Heads up" text="…"></uip-alert> * * Class strings are React's verbatim. Two shadow-DOM adaptations (a slotted * <svg> is not a child of the inner root, so `[&>svg]` can't reach it): * - the slot carries the icon rules as `[&::slotted(svg)]:…`; * - React's `[&>svg~*]:pl-7` (pad everything after the icon) becomes * `data-icon` on the inner root, mirrored onto slotted * <uip-alert-title>/<uip-alert-description> so they pad themselves; the * `[&>svg+div]:translate-y-[-3px]` nudge becomes `data-nudge` on the * description when no title precedes it. * * `icon` renders a built-in Lucide glyph (info | warning | error | success); * slot your own <svg> instead for custom icons. `title` / `text` are the * shorthand for a title + description; combine them with slotted children * freely (props render first). * * Parts: `base` on each element (style React's `className` overrides with * `class="[&::part(base)]:…"` on the host). */ export class UipAlert extends LitElement { // React's root is a block <div>. static styles = [tailwind, css`:host { display: block; }`] static properties = { variant: { reflect: true }, icon: { reflect: true }, title: {}, text: {}, hasSlottedIcon: { state: true }, hasSlottedTitle: { state: true }, } variant: Variant = 'default' icon?: AlertIcon override title: string = '' text?: string private hasSlottedIcon = false private hasSlottedTitle = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'alert') } private get hasIcon() { return !!this.icon || this.hasSlottedIcon } private onSlotChange(e: Event) { const assigned = (e.target as HTMLSlotElement).assignedElements() this.hasSlottedIcon = assigned.some((el) => el.localName === 'svg') this.hasSlottedTitle = assigned.some( (el) => el.localName === 'uip-alert-title' || el.getAttribute('data-slot') === 'alert-title', ) // Mirror the icon state onto the slotted parts (see class comment). const hasTitle = !!this.title || this.hasSlottedTitle for (const el of assigned) { if (el.localName !== 'uip-alert-title' && el.localName !== 'uip-alert-description') continue if (this.hasIcon) el.setAttribute('data-icon', '') else el.removeAttribute('data-icon') if (el.localName === 'uip-alert-description') { if (this.hasIcon && !hasTitle) el.setAttribute('data-nudge', '') else el.removeAttribute('data-nudge') } } } render() { const hasIcon = this.hasIcon const builtIn = this.icon ? builtInIcons[this.icon] : undefined // A slotted <svg> also matches the variants' `[&>svg]` tint when it is a // real shadow child — it isn't, so repeat the colour on the slot. A slotted // svg's own page classes (e.g. text-info) still apply from the light DOM. const slottedIconColor = this.variant === 'destructive' ? '[&::slotted(svg)]:text-destructive' : '[&::slotted(svg)]:text-foreground' return html`<div part="base" role="alert" data-uipkge="" data-slot="alert" ?data-icon=${hasIcon} class=${alertVariants({ variant: this.variant })}> ${builtIn ? icon(builtIn.node, builtIn.name) : nothing} ${this.title ? html`<p data-uipkge="" data-slot="alert-title" class=${cn('mb-1 text-sm leading-none font-medium tracking-tight', hasIcon && 'pl-7')}> ${this.title} </p>` : nothing} ${this.text ? html`<div data-uipkge="" data-slot="alert-description" class=${cn( 'text-muted-foreground text-sm leading-relaxed [&_p]:leading-relaxed', hasIcon && 'pl-7', hasIcon && !this.title && !this.hasSlottedTitle && 'translate-y-[-3px]', )} > ${this.text} </div>` : nothing} <slot class=${cn( '[&::slotted(svg)]:absolute [&::slotted(svg)]:left-4 [&::slotted(svg)]:top-4 [&::slotted(svg)]:size-4', slottedIconColor, )} @slotchange=${this.onSlotChange} ></slot> </div>` } } const titleClasses = 'mb-1 text-sm leading-none font-medium tracking-tight' const descriptionClasses = 'text-muted-foreground text-sm leading-relaxed [&_p]:leading-relaxed' /** <uip-alert-title> — React's AlertTitle (`as` = h1-h6 | div, default h5). */ export class UipAlertTitle extends LitElement { static styles = [tailwind, css`:host { display: block; }`] static properties = { as: { reflect: true }, dataIcon: { type: Boolean, attribute: 'data-icon', reflect: true }, } as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' = 'h5' /** Set by <uip-alert> when an icon precedes the title (React's `[&>svg~*]:pl-7`). */ dataIcon = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'alert-title') } render() { const tag = unsafeStatic(this.as) return staticHtml`<${tag} part="base" data-uipkge="" data-slot="alert-title" class=${cn(titleClasses, this.dataIcon && 'pl-7')}><slot></slot></${tag}>` } } /** <uip-alert-description> — React's AlertDescription. */ export class UipAlertDescription extends LitElement { static styles = [tailwind, css`:host { display: block; }`] static properties = { dataIcon: { type: Boolean, attribute: 'data-icon', reflect: true }, dataNudge: { type: Boolean, attribute: 'data-nudge', reflect: true }, } /** Set by <uip-alert> when an icon precedes the description (React's `[&>svg~*]:pl-7`). */ dataIcon = false /** Set by <uip-alert> when the icon is directly adjacent (React's `[&>svg+div]:translate-y-[-3px]`). */ dataNudge = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'alert-description') } render() { return html`<div part="base" data-uipkge="" data-slot="alert-description" class=${cn(descriptionClasses, this.dataIcon && 'pl-7', this.dataNudge && 'translate-y-[-3px]')} > <slot></slot> </div>` } } customElements.get('uip-alert') || customElements.define('uip-alert', UipAlert) customElements.get('uip-alert-title') || customElements.define('uip-alert-title', UipAlertTitle) customElements.get('uip-alert-description') || customElements.define('uip-alert-description', UipAlertDescription) declare global { interface HTMLElementTagNameMap { 'uip-alert': UipAlert 'uip-alert-title': UipAlertTitle 'uip-alert-description': UipAlertDescription } }src/uip/components/alert/alert.variants.ts1.1 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so `Alert.vue` can `import { alertVariants } from * './alert.variants'` without creating a circular dependency through the * index. See card.variants.ts for the same pattern + the SSR symptom that * motivated the split. * * Layout: composition children (svg + AlertTitle + AlertDescription) are * direct descendants of the root so `[&>svg]` absolute positioning works. */ export const alertVariants = cva( 'relative w-full rounded-lg border p-4 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:size-4 [&>svg]:text-foreground [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px]', { variants: { variant: { default: 'bg-background text-foreground border-border', destructive: 'border-destructive/20 text-destructive bg-destructive/5 [&>svg]:text-destructive', }, }, defaultVariants: { variant: 'default', }, }, ) export type AlertVariants = VariantProps<typeof alertVariants>
Raw manifest:https://uipkge.dev/r/lit/alert.json