Skip to content
  1. Feedback
  2. Notice

Notice

A line of context on a tinted well, in the flow of a form, a dialog or a page. Not a card, not a toast.

We will send a verification code to l•••@example.com.
import { Notice } from '@/components/notice'
import { EnvelopeIcon } from '@heroicons/react/16/solid'

export default function Example() {
  return (
    <Notice icon={EnvelopeIcon}>
      We will send a verification code to <strong className="font-medium text-zinc-950 dark:text-white">l•••@example.com</strong>.
    </Notice>
  )
}
<script setup lang="ts">
import Notice from '@/components/notice.vue'
import { EnvelopeIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <Notice :icon="EnvelopeIcon">
    We will send a verification code to
    <strong class="font-medium text-zinc-950 dark:text-white">l•••@example.com</strong>.
  </Notice>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import clsx from 'clsx'
import type React from 'react'

type NoticeColors = {
  background: string
  icon: string
  text: string
  title?: string
  body?: string
  plainBackground?: string
  plainIcon?: string
  barBackground: string
  bar: string
}

const colors = {
  red: {
    background: 'bg-red-500/10',
    icon: 'text-red-600 dark:text-red-400',
    text: 'text-red-700 dark:text-red-400',
    title: 'text-red-700 dark:text-red-400',
    body: 'text-red-700/80 dark:text-red-400/80',
    barBackground: 'bg-red-50/60 ring-red-200/60 dark:bg-red-500/8 dark:ring-red-400/15',
    bar: 'bg-red-500 dark:bg-red-400',
  },
  orange: {
    background: 'bg-orange-500/10',
    icon: 'text-orange-600 dark:text-orange-400',
    text: 'text-orange-700 dark:text-orange-400',
    barBackground: 'bg-orange-50/60 ring-orange-200/60 dark:bg-orange-500/8 dark:ring-orange-400/15',
    bar: 'bg-orange-500 dark:bg-orange-400',
  },
  amber: {
    background: 'bg-amber-500/10',
    icon: 'text-amber-600 dark:text-amber-400',
    text: 'text-amber-700 dark:text-amber-400',
    plainBackground: 'bg-amber-400/15 dark:bg-amber-400/10',
    plainIcon: 'text-amber-500 dark:text-amber-400',
    barBackground: 'bg-amber-50/60 ring-amber-200/60 dark:bg-amber-500/8 dark:ring-amber-400/15',
    bar: 'bg-amber-500 dark:bg-amber-400',
  },
  yellow: {
    background: 'bg-yellow-500/10',
    icon: 'text-yellow-600 dark:text-yellow-400',
    text: 'text-yellow-700 dark:text-yellow-400',
    barBackground: 'bg-yellow-50/60 ring-yellow-200/60 dark:bg-yellow-500/8 dark:ring-yellow-400/15',
    bar: 'bg-yellow-500 dark:bg-yellow-400',
  },
  lime: {
    background: 'bg-lime-500/10',
    icon: 'text-lime-600 dark:text-lime-400',
    text: 'text-lime-700 dark:text-lime-400',
    barBackground: 'bg-lime-50/60 ring-lime-200/60 dark:bg-lime-500/8 dark:ring-lime-400/15',
    bar: 'bg-lime-500 dark:bg-lime-400',
  },
  green: {
    background: 'bg-green-500/5 ring-1 ring-green-600/20 ring-inset dark:bg-green-500/10 dark:ring-green-500/20',
    icon: 'text-green-600 dark:text-green-400',
    text: 'text-zinc-600 dark:text-zinc-400',
    barBackground: 'bg-green-50/60 ring-green-200/60 dark:bg-green-500/8 dark:ring-green-400/15',
    bar: 'bg-green-500 dark:bg-green-400',
  },
  emerald: {
    background: 'bg-emerald-500/10',
    icon: 'text-emerald-600 dark:text-emerald-400',
    text: 'text-emerald-700 dark:text-emerald-400',
    barBackground: 'bg-emerald-50/60 ring-emerald-200/60 dark:bg-emerald-500/8 dark:ring-emerald-400/15',
    bar: 'bg-emerald-500 dark:bg-emerald-400',
  },
  teal: {
    background: 'bg-teal-500/10',
    icon: 'text-teal-600 dark:text-teal-400',
    text: 'text-teal-700 dark:text-teal-400',
    barBackground: 'bg-teal-50/60 ring-teal-200/60 dark:bg-teal-500/8 dark:ring-teal-400/15',
    bar: 'bg-teal-500 dark:bg-teal-400',
  },
  cyan: {
    background: 'bg-cyan-500/10',
    icon: 'text-cyan-600 dark:text-cyan-400',
    text: 'text-cyan-700 dark:text-cyan-400',
    barBackground: 'bg-cyan-50/60 ring-cyan-200/60 dark:bg-cyan-500/8 dark:ring-cyan-400/15',
    bar: 'bg-cyan-500 dark:bg-cyan-400',
  },
  sky: {
    background: 'bg-sky-500/10',
    icon: 'text-sky-600 dark:text-sky-400',
    text: 'text-sky-700 dark:text-sky-400',
    barBackground: 'bg-sky-50/60 ring-sky-200/60 dark:bg-sky-500/8 dark:ring-sky-400/15',
    bar: 'bg-sky-500 dark:bg-sky-400',
  },
  blue: {
    background: 'bg-blue-500/10',
    icon: 'text-blue-600 dark:text-blue-400',
    text: 'text-blue-700 dark:text-blue-400',
    barBackground: 'bg-blue-50/60 ring-blue-200/60 dark:bg-blue-500/8 dark:ring-blue-400/15',
    bar: 'bg-blue-500 dark:bg-blue-400',
  },
  indigo: {
    background: 'bg-indigo-500/10',
    icon: 'text-indigo-600 dark:text-indigo-400',
    text: 'text-indigo-700 dark:text-indigo-400',
    barBackground: 'bg-indigo-50/60 ring-indigo-200/60 dark:bg-indigo-500/8 dark:ring-indigo-400/15',
    bar: 'bg-indigo-500 dark:bg-indigo-400',
  },
  violet: {
    background: 'bg-violet-500/10',
    icon: 'text-violet-600 dark:text-violet-400',
    text: 'text-violet-700 dark:text-violet-400',
    barBackground: 'bg-violet-50/60 ring-violet-200/60 dark:bg-violet-500/8 dark:ring-violet-400/15',
    bar: 'bg-violet-500 dark:bg-violet-400',
  },
  purple: {
    background: 'bg-purple-500/10',
    icon: 'text-purple-600 dark:text-purple-400',
    text: 'text-purple-700 dark:text-purple-400',
    barBackground: 'bg-purple-50/60 ring-purple-200/60 dark:bg-purple-500/8 dark:ring-purple-400/15',
    bar: 'bg-purple-500 dark:bg-purple-400',
  },
  fuchsia: {
    background: 'bg-fuchsia-500/10',
    icon: 'text-fuchsia-600 dark:text-fuchsia-400',
    text: 'text-fuchsia-700 dark:text-fuchsia-400',
    barBackground: 'bg-fuchsia-50/60 ring-fuchsia-200/60 dark:bg-fuchsia-500/8 dark:ring-fuchsia-400/15',
    bar: 'bg-fuchsia-500 dark:bg-fuchsia-400',
  },
  pink: {
    background: 'bg-pink-500/10',
    icon: 'text-pink-600 dark:text-pink-400',
    text: 'text-pink-700 dark:text-pink-400',
    barBackground: 'bg-pink-50/60 ring-pink-200/60 dark:bg-pink-500/8 dark:ring-pink-400/15',
    bar: 'bg-pink-500 dark:bg-pink-400',
  },
  rose: {
    background: 'bg-rose-500/10',
    icon: 'text-rose-600 dark:text-rose-400',
    text: 'text-rose-700 dark:text-rose-400',
    barBackground: 'bg-rose-50/60 ring-rose-200/60 dark:bg-rose-500/8 dark:ring-rose-400/15',
    bar: 'bg-rose-500 dark:bg-rose-400',
  },
  slate: {
    background: 'bg-slate-950/2.5 dark:bg-white/2.5',
    icon: 'text-slate-500 dark:text-slate-400',
    text: 'text-slate-600 dark:text-slate-400',
    barBackground: 'bg-slate-50/60 ring-slate-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-slate-400 dark:bg-slate-500',
  },
  gray: {
    background: 'bg-gray-950/2.5 dark:bg-white/2.5',
    icon: 'text-gray-500 dark:text-gray-400',
    text: 'text-gray-600 dark:text-gray-400',
    barBackground: 'bg-gray-50/60 ring-gray-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-gray-400 dark:bg-gray-500',
  },
  zinc: {
    background: 'bg-zinc-950/2.5 dark:bg-white/2.5',
    icon: 'text-zinc-500 dark:text-zinc-400',
    text: 'text-zinc-600 dark:text-zinc-400',
    barBackground: 'bg-zinc-50/60 ring-zinc-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-zinc-400 dark:bg-zinc-500',
  },
  neutral: {
    background: 'bg-neutral-950/2.5 dark:bg-white/2.5',
    icon: 'text-neutral-500 dark:text-neutral-400',
    text: 'text-neutral-600 dark:text-neutral-400',
    barBackground: 'bg-neutral-50/60 ring-neutral-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-neutral-400 dark:bg-neutral-500',
  },
  stone: {
    background: 'bg-stone-950/2.5 dark:bg-white/2.5',
    icon: 'text-stone-500 dark:text-stone-400',
    text: 'text-stone-600 dark:text-stone-400',
    barBackground: 'bg-stone-50/60 ring-stone-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-stone-400 dark:bg-stone-500',
  },
  mauve: {
    background: 'bg-mauve-950/2.5 dark:bg-white/2.5',
    icon: 'text-mauve-500 dark:text-mauve-400',
    text: 'text-mauve-600 dark:text-mauve-400',
    barBackground: 'bg-mauve-50/60 ring-mauve-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-mauve-400 dark:bg-mauve-500',
  },
  olive: {
    background: 'bg-olive-950/2.5 dark:bg-white/2.5',
    icon: 'text-olive-500 dark:text-olive-400',
    text: 'text-olive-600 dark:text-olive-400',
    barBackground: 'bg-olive-50/60 ring-olive-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-olive-400 dark:bg-olive-500',
  },
  mist: {
    background: 'bg-mist-950/2.5 dark:bg-white/2.5',
    icon: 'text-mist-500 dark:text-mist-400',
    text: 'text-mist-600 dark:text-mist-400',
    barBackground: 'bg-mist-50/60 ring-mist-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-mist-400 dark:bg-mist-500',
  },
  taupe: {
    background: 'bg-taupe-950/2.5 dark:bg-white/2.5',
    icon: 'text-taupe-500 dark:text-taupe-400',
    text: 'text-taupe-600 dark:text-taupe-400',
    barBackground: 'bg-taupe-50/60 ring-taupe-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-taupe-400 dark:bg-taupe-500',
  },
} satisfies Record<string, NoticeColors>

const sizes = {
  md: 'rounded-lg px-3.5 py-3',
  lg: 'rounded-xl px-4 py-3.5',
}

const text = 'text-base/6 sm:text-sm/6'

type NoticeProps = {
  tone?: keyof typeof colors
  variant?: 'filled' | 'bar'
  size?: keyof typeof sizes
  icon?: React.ComponentType<{ className?: string }>
  title?: React.ReactNode
  action?: React.ReactNode
} & React.ComponentPropsWithoutRef<'div'>

export function Notice({
  tone = 'zinc',
  variant = 'filled',
  size = 'md',
  icon: Icon,
  title,
  action,
  className,
  children,
  ...props
}: NoticeProps) {
  let color: NoticeColors = colors[tone]
  let titleColor = color.title ?? 'text-zinc-950 dark:text-white'
  let bodyColor = color.body ?? 'text-zinc-600 dark:text-zinc-400'

  if (variant === 'bar') {
    return (
      <div {...props} className={clsx(className, 'relative rounded-xl py-4 pr-5 pl-6 ring-1', color.barBackground)}>
        <span aria-hidden="true" className={clsx('absolute inset-y-4 left-3 w-0.75 rounded-full', color.bar)} />
        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
          <div className="text-sm/relaxed text-zinc-600 dark:text-zinc-300">
            {title && <p className="font-semibold text-zinc-950 dark:text-white">{title}</p>}
            {children && <div className={clsx(title && 'mt-1')}>{children}</div>}
          </div>
          {action && <div className="flex shrink-0 items-center gap-2 sm:justify-end">{action}</div>}
        </div>
      </div>
    )
  }

  if (action) {
    return (
      <div
        {...props}
        className={clsx(className, 'flex flex-wrap items-center gap-x-4 gap-y-3', sizes[size], color.background)}
      >
        {Icon && <Icon className={clsx('size-4 h-lh shrink-0', color.icon)} />}
        <div className={clsx('min-w-0 flex-1 text-pretty', text, color.text)}>
          {title && <p className={clsx('font-medium', titleColor)}>{title}</p>}
          {children}
        </div>
        <div className="flex shrink-0 items-center gap-3 max-sm:w-full">{action}</div>
      </div>
    )
  }

  if (title) {
    return (
      <div
        {...props}
        className={clsx(
          className,
          Icon ? 'grid grid-cols-[--spacing(4)_1fr] gap-x-2.5 gap-y-1' : 'flex flex-col gap-y-1',
          sizes[size],
          color.background
        )}
      >
        {Icon && <Icon className={clsx('row-start-1 size-4 shrink-0 self-center', color.icon)} />}
        <p className={clsx('font-medium', text, titleColor)}>{title}</p>
        {children && <div className={clsx(Icon && 'col-start-2', 'text-pretty', text, bodyColor)}>{children}</div>}
      </div>
    )
  }

  return (
    <div {...props} className={clsx(className, 'flex items-start gap-2.5', sizes[size], color.plainBackground ?? color.background)}>
      {Icon && <Icon className={clsx('size-4 h-lh shrink-0', color.plainIcon ?? color.icon)} />}
      <div className={clsx('min-w-0 text-pretty', text, color.text)}>{children}</div>
    </div>
  )
}
<script setup lang="ts">
import type { Component } from 'vue'
import { computed, useSlots } from 'vue'

type NoticeColors = {
  background: string
  icon: string
  text: string
  title?: string
  body?: string
  plainBackground?: string
  plainIcon?: string
  barBackground: string
  bar: string
}

const colors = {
  red: {
    background: 'bg-red-500/10',
    icon: 'text-red-600 dark:text-red-400',
    text: 'text-red-700 dark:text-red-400',
    title: 'text-red-700 dark:text-red-400',
    body: 'text-red-700/80 dark:text-red-400/80',
    barBackground: 'bg-red-50/60 ring-red-200/60 dark:bg-red-500/8 dark:ring-red-400/15',
    bar: 'bg-red-500 dark:bg-red-400',
  },
  orange: {
    background: 'bg-orange-500/10',
    icon: 'text-orange-600 dark:text-orange-400',
    text: 'text-orange-700 dark:text-orange-400',
    barBackground: 'bg-orange-50/60 ring-orange-200/60 dark:bg-orange-500/8 dark:ring-orange-400/15',
    bar: 'bg-orange-500 dark:bg-orange-400',
  },
  amber: {
    background: 'bg-amber-500/10',
    icon: 'text-amber-600 dark:text-amber-400',
    text: 'text-amber-700 dark:text-amber-400',
    plainBackground: 'bg-amber-400/15 dark:bg-amber-400/10',
    plainIcon: 'text-amber-500 dark:text-amber-400',
    barBackground: 'bg-amber-50/60 ring-amber-200/60 dark:bg-amber-500/8 dark:ring-amber-400/15',
    bar: 'bg-amber-500 dark:bg-amber-400',
  },
  yellow: {
    background: 'bg-yellow-500/10',
    icon: 'text-yellow-600 dark:text-yellow-400',
    text: 'text-yellow-700 dark:text-yellow-400',
    barBackground: 'bg-yellow-50/60 ring-yellow-200/60 dark:bg-yellow-500/8 dark:ring-yellow-400/15',
    bar: 'bg-yellow-500 dark:bg-yellow-400',
  },
  lime: {
    background: 'bg-lime-500/10',
    icon: 'text-lime-600 dark:text-lime-400',
    text: 'text-lime-700 dark:text-lime-400',
    barBackground: 'bg-lime-50/60 ring-lime-200/60 dark:bg-lime-500/8 dark:ring-lime-400/15',
    bar: 'bg-lime-500 dark:bg-lime-400',
  },
  green: {
    background: 'bg-green-500/5 ring-1 ring-green-600/20 ring-inset dark:bg-green-500/10 dark:ring-green-500/20',
    icon: 'text-green-600 dark:text-green-400',
    text: 'text-zinc-600 dark:text-zinc-400',
    barBackground: 'bg-green-50/60 ring-green-200/60 dark:bg-green-500/8 dark:ring-green-400/15',
    bar: 'bg-green-500 dark:bg-green-400',
  },
  emerald: {
    background: 'bg-emerald-500/10',
    icon: 'text-emerald-600 dark:text-emerald-400',
    text: 'text-emerald-700 dark:text-emerald-400',
    barBackground: 'bg-emerald-50/60 ring-emerald-200/60 dark:bg-emerald-500/8 dark:ring-emerald-400/15',
    bar: 'bg-emerald-500 dark:bg-emerald-400',
  },
  teal: {
    background: 'bg-teal-500/10',
    icon: 'text-teal-600 dark:text-teal-400',
    text: 'text-teal-700 dark:text-teal-400',
    barBackground: 'bg-teal-50/60 ring-teal-200/60 dark:bg-teal-500/8 dark:ring-teal-400/15',
    bar: 'bg-teal-500 dark:bg-teal-400',
  },
  cyan: {
    background: 'bg-cyan-500/10',
    icon: 'text-cyan-600 dark:text-cyan-400',
    text: 'text-cyan-700 dark:text-cyan-400',
    barBackground: 'bg-cyan-50/60 ring-cyan-200/60 dark:bg-cyan-500/8 dark:ring-cyan-400/15',
    bar: 'bg-cyan-500 dark:bg-cyan-400',
  },
  sky: {
    background: 'bg-sky-500/10',
    icon: 'text-sky-600 dark:text-sky-400',
    text: 'text-sky-700 dark:text-sky-400',
    barBackground: 'bg-sky-50/60 ring-sky-200/60 dark:bg-sky-500/8 dark:ring-sky-400/15',
    bar: 'bg-sky-500 dark:bg-sky-400',
  },
  blue: {
    background: 'bg-blue-500/10',
    icon: 'text-blue-600 dark:text-blue-400',
    text: 'text-blue-700 dark:text-blue-400',
    barBackground: 'bg-blue-50/60 ring-blue-200/60 dark:bg-blue-500/8 dark:ring-blue-400/15',
    bar: 'bg-blue-500 dark:bg-blue-400',
  },
  indigo: {
    background: 'bg-indigo-500/10',
    icon: 'text-indigo-600 dark:text-indigo-400',
    text: 'text-indigo-700 dark:text-indigo-400',
    barBackground: 'bg-indigo-50/60 ring-indigo-200/60 dark:bg-indigo-500/8 dark:ring-indigo-400/15',
    bar: 'bg-indigo-500 dark:bg-indigo-400',
  },
  violet: {
    background: 'bg-violet-500/10',
    icon: 'text-violet-600 dark:text-violet-400',
    text: 'text-violet-700 dark:text-violet-400',
    barBackground: 'bg-violet-50/60 ring-violet-200/60 dark:bg-violet-500/8 dark:ring-violet-400/15',
    bar: 'bg-violet-500 dark:bg-violet-400',
  },
  purple: {
    background: 'bg-purple-500/10',
    icon: 'text-purple-600 dark:text-purple-400',
    text: 'text-purple-700 dark:text-purple-400',
    barBackground: 'bg-purple-50/60 ring-purple-200/60 dark:bg-purple-500/8 dark:ring-purple-400/15',
    bar: 'bg-purple-500 dark:bg-purple-400',
  },
  fuchsia: {
    background: 'bg-fuchsia-500/10',
    icon: 'text-fuchsia-600 dark:text-fuchsia-400',
    text: 'text-fuchsia-700 dark:text-fuchsia-400',
    barBackground: 'bg-fuchsia-50/60 ring-fuchsia-200/60 dark:bg-fuchsia-500/8 dark:ring-fuchsia-400/15',
    bar: 'bg-fuchsia-500 dark:bg-fuchsia-400',
  },
  pink: {
    background: 'bg-pink-500/10',
    icon: 'text-pink-600 dark:text-pink-400',
    text: 'text-pink-700 dark:text-pink-400',
    barBackground: 'bg-pink-50/60 ring-pink-200/60 dark:bg-pink-500/8 dark:ring-pink-400/15',
    bar: 'bg-pink-500 dark:bg-pink-400',
  },
  rose: {
    background: 'bg-rose-500/10',
    icon: 'text-rose-600 dark:text-rose-400',
    text: 'text-rose-700 dark:text-rose-400',
    barBackground: 'bg-rose-50/60 ring-rose-200/60 dark:bg-rose-500/8 dark:ring-rose-400/15',
    bar: 'bg-rose-500 dark:bg-rose-400',
  },
  slate: {
    background: 'bg-slate-950/2.5 dark:bg-white/2.5',
    icon: 'text-slate-500 dark:text-slate-400',
    text: 'text-slate-600 dark:text-slate-400',
    barBackground: 'bg-slate-50/60 ring-slate-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-slate-400 dark:bg-slate-500',
  },
  gray: {
    background: 'bg-gray-950/2.5 dark:bg-white/2.5',
    icon: 'text-gray-500 dark:text-gray-400',
    text: 'text-gray-600 dark:text-gray-400',
    barBackground: 'bg-gray-50/60 ring-gray-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-gray-400 dark:bg-gray-500',
  },
  zinc: {
    background: 'bg-zinc-950/2.5 dark:bg-white/2.5',
    icon: 'text-zinc-500 dark:text-zinc-400',
    text: 'text-zinc-600 dark:text-zinc-400',
    barBackground: 'bg-zinc-50/60 ring-zinc-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-zinc-400 dark:bg-zinc-500',
  },
  neutral: {
    background: 'bg-neutral-950/2.5 dark:bg-white/2.5',
    icon: 'text-neutral-500 dark:text-neutral-400',
    text: 'text-neutral-600 dark:text-neutral-400',
    barBackground: 'bg-neutral-50/60 ring-neutral-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-neutral-400 dark:bg-neutral-500',
  },
  stone: {
    background: 'bg-stone-950/2.5 dark:bg-white/2.5',
    icon: 'text-stone-500 dark:text-stone-400',
    text: 'text-stone-600 dark:text-stone-400',
    barBackground: 'bg-stone-50/60 ring-stone-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-stone-400 dark:bg-stone-500',
  },
  mauve: {
    background: 'bg-mauve-950/2.5 dark:bg-white/2.5',
    icon: 'text-mauve-500 dark:text-mauve-400',
    text: 'text-mauve-600 dark:text-mauve-400',
    barBackground: 'bg-mauve-50/60 ring-mauve-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-mauve-400 dark:bg-mauve-500',
  },
  olive: {
    background: 'bg-olive-950/2.5 dark:bg-white/2.5',
    icon: 'text-olive-500 dark:text-olive-400',
    text: 'text-olive-600 dark:text-olive-400',
    barBackground: 'bg-olive-50/60 ring-olive-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-olive-400 dark:bg-olive-500',
  },
  mist: {
    background: 'bg-mist-950/2.5 dark:bg-white/2.5',
    icon: 'text-mist-500 dark:text-mist-400',
    text: 'text-mist-600 dark:text-mist-400',
    barBackground: 'bg-mist-50/60 ring-mist-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-mist-400 dark:bg-mist-500',
  },
  taupe: {
    background: 'bg-taupe-950/2.5 dark:bg-white/2.5',
    icon: 'text-taupe-500 dark:text-taupe-400',
    text: 'text-taupe-600 dark:text-taupe-400',
    barBackground: 'bg-taupe-50/60 ring-taupe-200/60 dark:bg-white/2 dark:ring-white/10',
    bar: 'bg-taupe-400 dark:bg-taupe-500',
  },
} satisfies Record<string, NoticeColors>

const sizes = {
  md: 'rounded-lg px-3.5 py-3',
  lg: 'rounded-xl px-4 py-3.5',
}

const text = 'text-base/6 sm:text-sm/6'

const props = withDefaults(
  defineProps<{
    tone?: keyof typeof colors
    variant?: 'filled' | 'bar'
    size?: keyof typeof sizes
    icon?: Component
    title?: string
  }>(),
  { tone: 'zinc', variant: 'filled', size: 'md' }
)

const slots = useSlots()
const color = computed<NoticeColors>(() => colors[props.tone])
const titleColor = computed(() => color.value.title ?? 'text-zinc-950 dark:text-white')
const bodyColor = computed(() => color.value.body ?? 'text-zinc-600 dark:text-zinc-400')
</script>

<template>
  <div v-if="variant === 'bar'" :class="['relative rounded-xl py-4 pr-5 pl-6 ring-1', color.barBackground]">
    <span aria-hidden="true" :class="['absolute inset-y-4 left-3 w-0.75 rounded-full', color.bar]" />
    <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
      <div class="text-sm/relaxed text-zinc-600 dark:text-zinc-300">
        <p v-if="title" class="font-semibold text-zinc-950 dark:text-white">{{ title }}</p>
        <div v-if="slots.default" :class="title && 'mt-1'">
          <slot />
        </div>
      </div>
      <div v-if="slots.action" class="flex shrink-0 items-center gap-2 sm:justify-end">
        <slot name="action" />
      </div>
    </div>
  </div>

  <div
    v-else-if="slots.action"
    :class="['flex flex-wrap items-center gap-x-4 gap-y-3', sizes[size], color.background]"
  >
    <component :is="icon" v-if="icon" :class="['size-4 h-lh shrink-0', color.icon]" />
    <div :class="['min-w-0 flex-1 text-pretty', text, color.text]">
      <p v-if="title" :class="['font-medium', titleColor]">{{ title }}</p>
      <slot />
    </div>
    <div class="flex shrink-0 items-center gap-3 max-sm:w-full">
      <slot name="action" />
    </div>
  </div>

  <div
    v-else-if="title"
    :class="[icon ? 'grid grid-cols-[--spacing(4)_1fr] gap-x-2.5 gap-y-1' : 'flex flex-col gap-y-1', sizes[size], color.background]"
  >
    <component :is="icon" v-if="icon" :class="['row-start-1 size-4 shrink-0 self-center', color.icon]" />
    <p :class="['font-medium', text, titleColor]">{{ title }}</p>
    <div v-if="slots.default" :class="[icon && 'col-start-2', 'text-pretty', text, bodyColor]">
      <slot />
    </div>
  </div>

  <div v-else :class="['flex items-start gap-2.5', sizes[size], color.plainBackground ?? color.background]">
    <component :is="icon" v-if="icon" :class="['size-4 h-lh shrink-0', color.plainIcon ?? color.icon]" />
    <div :class="['min-w-0 text-pretty', text, color.text]">
      <slot />
    </div>
  </div>
</template>

Component API

PropDefaultDescription
Noticerenders a <div> element
tonezincAny Tailwind color, from `red` to `taupe`. `zinc` for information, `amber` for a warning, `red` for a failure, `green` for something that succeeded.
variantfilled`filled` for a tinted well with an icon, `bar` for a pale block marked by a colored bar on the left.
sizemd`md` inside forms and dialogs, `lg` for the notices of a detail page or a wizard. Not used with `variant="bar"`.
icon-A 16px Heroicon drawn before the text, tinted by the tone. Not shown with `variant="bar"`.
title-First line in medium weight. The body then sits under it, aligned with it.
action-Rendered opposite the text, a button. Wraps under the text on small screens.
PropDefaultDescription
Noticerenders a <div> element
tonezincAny Tailwind color, from `red` to `taupe`. `zinc` for information, `amber` for a warning, `red` for a failure, `green` for something that succeeded.
variantfilled`filled` for a tinted well with an icon, `bar` for a pale block marked by a colored bar on the left.
sizemd`md` inside forms and dialogs, `lg` for the notices of a detail page or a wizard.
icon-A 16px Heroicon drawn before the text, tinted by the tone. Not shown with `variant="bar"`.
title-First line in medium weight. The body then sits under it, aligned with it.
#action-Slot rendered opposite the text, a button. Wraps under the text on small screens.

Examples

Basic example

The default tone reads as information: a grey well and a grey icon. Emphasize a value inline with font-medium:

We will send a verification code to l•••@example.com.
import { Notice } from '@/components/notice'
import { EnvelopeIcon } from '@heroicons/react/16/solid'

export default function Example() {
  return (
    <Notice icon={EnvelopeIcon}>
      We will send a verification code to <strong className="font-medium text-zinc-950 dark:text-white">l•••@example.com</strong>.
    </Notice>
  )
}
<script setup lang="ts">
import Notice from '@/components/notice.vue'
import { EnvelopeIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <Notice :icon="EnvelopeIcon">
    We will send a verification code to
    <strong class="font-medium text-zinc-950 dark:text-white">l•••@example.com</strong>.
  </Notice>
</template>

With a title

Add a title when the notice explains a choice. The icon aligns with the title and the body sits under it:

Secret key

Full access. Keep secret and use server-side only.
<Notice icon={KeyIcon} title="Secret key">
  Full access. Keep secret and use server-side only.
</Notice>
<template>
  <Notice :icon="KeyIcon" title="Secret key">
    Full access. Keep secret and use server-side only.
  </Notice>
</template>

Tones

An untitled amber notice turns fully amber; a titled one keeps its text dark under an amber well. Red is for a failure, green for a step that succeeded:

Your upgrade to Scale is waiting for payment confirmation.

Payouts are stopped

API calls to create a transfer will be refused. Transfers already in flight are not affected.

This customer is blocked and cannot pay.

Chargeback pattern on three payments. Blocked on Aug 13, 2026.

passport.pdf

Uploaded a minute ago.
<Notice tone="amber" icon={ExclamationTriangleIcon}>
  Your upgrade to Scale is waiting for payment confirmation.
</Notice>

<Notice tone="amber" icon={ExclamationTriangleIcon} title="Payouts are stopped">
  API calls to create a transfer will be refused. Transfers already in flight are not affected.
</Notice>

<Notice tone="red" icon={ExclamationTriangleIcon} title="This customer is blocked and cannot pay.">
  Chargeback pattern on three payments. Blocked on Aug 13, 2026.
</Notice>

<Notice tone="green" icon={CheckCircleIcon} title="passport.pdf">
  Uploaded a minute ago.
</Notice>
<template>
  <Notice tone="amber" :icon="ExclamationTriangleIcon">
    Your upgrade to Scale is waiting for payment confirmation.
  </Notice>

  <Notice tone="amber" :icon="ExclamationTriangleIcon" title="Payouts are stopped">
    API calls to create a transfer will be refused. Transfers already in flight are not affected.
  </Notice>

  <Notice tone="red" :icon="ExclamationTriangleIcon" title="This customer is blocked and cannot pay.">
    Chargeback pattern on three payments. Blocked on Aug 13, 2026.
  </Notice>

  <Notice tone="green" :icon="CheckCircleIcon" title="passport.pdf">
    Uploaded a minute ago.
  </Notice>
</template>

Colors

tone accepts every color of the Tailwind palette, in both variants:

Your trial ends in 5 days.
Dark mode is now available in your settings.

Invitation sent

Your teammate will receive an email with a link to join the workspace.
<Notice tone="blue" icon={InformationCircleIcon}>
  Your trial ends in 5 days.
</Notice>

<Notice tone="violet" icon={SparklesIcon}>
  Dark mode is now available in your settings.
</Notice>

<Notice variant="bar" tone="teal" title="Invitation sent">
  Your teammate will receive an email with a link to join the workspace.
</Notice>
<template>
  <Notice tone="blue" :icon="InformationCircleIcon">
    Your trial ends in 5 days.
  </Notice>

  <Notice tone="violet" :icon="SparklesIcon">
    Dark mode is now available in your settings.
  </Notice>

  <Notice variant="bar" tone="teal" title="Invitation sent">
    Your teammate will receive an email with a link to join the workspace.
  </Notice>
</template>

With an action

The action slot puts a button opposite the text. Below sm it wraps to its own line at full width:

Payment analytics are computed on live traffic only. Sandbox payments are not indexed.
<Notice icon={ChartBarIcon} action={<Button outline>Switch to live</Button>}>
  Payment analytics are computed on live traffic only. Sandbox payments are not indexed.
</Notice>
<template>
  <Notice :icon="ChartBarIcon">
    Payment analytics are computed on live traffic only. Sandbox payments are not indexed.
    <template #action>
      <Button outline>Switch to live</Button>
    </template>
  </Notice>
</template>

Large

size="lg" gives the notice the radius and padding of a detail page block, next to cards and tables:

Identity is checked before saving

Your password and second factor are asked once you confirm. The account stays unverified until our team validates it.
<Notice size="lg" icon={ShieldCheckIcon} title="Identity is checked before saving">
  Your password and second factor are asked once you confirm. The account stays
  unverified until our team validates it.
</Notice>
<template>
  <Notice size="lg" :icon="ShieldCheckIcon" title="Identity is checked before saving">
    Your password and second factor are asked once you confirm. The account stays
    unverified until our team validates it.
  </Notice>
</template>

Bar

variant="bar" draws a pale block with a colored bar on the left, for a note that sits in the page longer than a warning. It takes a title, a body and an action, without an icon:

Scheduled maintenance

The app will be unavailable on Sunday from 2:00 to 4:00 AM UTC. Save your work before then.

New export format

Reports can now be exported as CSV or XLSX.
<Notice variant="bar" tone="amber" title="Scheduled maintenance">
  The app will be unavailable on Sunday from 2:00 to 4:00 AM UTC. Save your work before then.
</Notice>

<Notice variant="bar" tone="blue" title="New export format" action={<Button outline>Try it</Button>}>
  Reports can now be exported as CSV or XLSX.
</Notice>
<template>
  <Notice variant="bar" tone="amber" title="Scheduled maintenance">
    The app will be unavailable on Sunday from 2:00 to 4:00 AM UTC. Save your work before then.
  </Notice>

  <Notice variant="bar" tone="blue" title="New export format">
    Reports can now be exported as CSV or XLSX.
    <template #action>
      <Button outline>Try it</Button>
    </template>
  </Notice>
</template>