# Snackbar

> Bottom snackbar with an action button; the countdown pauses on hover or focus.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/snackbar
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/snackbar.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/snackbar-demo.tsx"
"use client"

import * as React from "react"

import { Snackbar } from "@/components/snackbar"

export default function SnackbarDemo() {
  const [open, setOpen] = React.useState(false)
  const [archived, setArchived] = React.useState(false)

  return (
    <div className="relative h-72 w-full max-w-md overflow-hidden rounded-2xl border bg-card p-6">
      <div className={`flex items-center justify-between rounded-xl border bg-background p-4 transition-opacity ${archived ? "opacity-40" : ""}`}>
        <div className="text-sm">
          <p className="font-medium">Quarterly report.pdf</p>
          <p className="text-xs text-muted-foreground">{archived ? "Archived" : "Edited 2 hours ago"}</p>
        </div>
        <button
          type="button"
          disabled={archived}
          onClick={() => {
            setArchived(true)
            setOpen(true)
          }}
          className="inline-flex h-8 items-center rounded-lg border px-3 text-xs font-medium hover:bg-accent disabled:pointer-events-none"
        >
          Archive
        </button>
      </div>
      <Snackbar
        open={open}
        onOpenChange={setOpen}
        message="Conversation archived"
        actionLabel="Undo"
        onAction={() => setArchived(false)}
        duration={5000}
      />
    </div>
  )
}
```

## Source

```tsx title="components/snackbar.tsx"
"use client"

import * as React from "react"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

export interface SnackbarProps extends Omit<React.ComponentProps<"div">, "children"> {
  open: boolean
  onOpenChange: (open: boolean) => void
  message: React.ReactNode
  actionLabel?: string
  onAction?: () => void
  /** Auto-dismiss delay in ms, 0 keeps it open */
  duration?: number
}

// Single-line message pinned to the bottom of its positioned container. Pauses the timer while hovered or focused.
export function Snackbar({ open, onOpenChange, message, actionLabel, onAction, duration = 5000, className, ...props }: SnackbarProps) {
  const [paused, setPaused] = React.useState(false)

  React.useEffect(() => {
    if (!open || paused || duration <= 0) return
    const id = setTimeout(() => onOpenChange(false), duration)
    return () => clearTimeout(id)
  }, [open, paused, duration, onOpenChange])

  if (!open) return null

  return (
    <div
      role="status"
      aria-live="polite"
      onPointerEnter={() => setPaused(true)}
      onPointerLeave={() => setPaused(false)}
      onFocus={() => setPaused(true)}
      onBlur={() => setPaused(false)}
      className={cn(
        "absolute bottom-4 left-1/2 z-30 flex min-h-12 w-[calc(100%-2rem)] max-w-md -translate-x-1/2 animate-in items-center gap-3 rounded-lg bg-neutral-900 py-2 pr-2 pl-4 text-sm text-white shadow-xl duration-200 fade-in slide-in-from-bottom-4 dark:bg-neutral-100 dark:text-neutral-900",
        className
      )}
      {...props}
    >
      <span className="flex-1">{message}</span>
      {actionLabel && (
        <button
          type="button"
          onClick={() => {
            onAction?.()
            onOpenChange(false)
          }}
          className="rounded-md px-3 py-1.5 text-sm font-semibold text-sky-300 hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-sky-300 focus-visible:outline-none dark:text-sky-600 dark:hover:bg-black/10"
        >
          {actionLabel}
        </button>
      )}
      <button
        type="button"
        aria-label="Dismiss"
        onClick={() => onOpenChange(false)}
        className="flex size-8 items-center justify-center rounded-md text-white/70 hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-white focus-visible:outline-none dark:text-neutral-500 dark:hover:bg-black/10 dark:hover:text-neutral-900"
      >
        <X className="size-4" />
      </button>
    </div>
  )
}
```
