Toast
A transient notification with a status icon that auto-dismisses after a timeout or when closed.
A transient notification with a status icon that auto-dismisses after a timeout or when closed. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-toast"use client"
import * as React from "react"
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastIcon,
ToastTitle,
} from "@/registry/bases/newt/ui/toast"
export default function ToastDemo() {
const [key, setKey] = React.useState(0)
return (
<div className="flex flex-col items-start gap-4">
<Toast key={key} variant="success" duration={5000}>
<ToastIcon />
<ToastContent>
<ToastTitle>Success title</ToastTitle>
<ToastDescription>Description text.</ToastDescription>
</ToastContent>
<ToastClose />
</Toast>
<button
type="button"
className="rounded-sm bg-newt-brand px-3 py-1.5 text-sm font-medium text-white transition-colors duration-fast ease-newt hover:bg-newt-brand-hover"
onClick={() => setKey((k) => k + 1)}
>
Show toast again
</button>
</div>
)
}<script setup lang="ts">
import { ref } from "vue"
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastIcon,
ToastTitle,
} from "~~/registry/bases/newt/ui/toast"
const key = ref(0)
</script>
<template>
<div class="flex flex-col items-start gap-4">
<Toast :key="key" variant="success" :duration="5000">
<ToastIcon />
<ToastContent>
<ToastTitle>Success title</ToastTitle>
<ToastDescription>Description text.</ToastDescription>
</ToastContent>
<ToastClose />
</Toast>
<button
type="button"
class="rounded-sm bg-newt-brand px-3 py-1.5 text-sm font-medium text-white transition-colors duration-fast ease-newt hover:bg-newt-brand-hover"
@click="key++"
>
Show toast again
</button>
</div>
</template>Installation
pnpm dlx newtui@latest add toastnpx newtui@latest add toastyarn dlx newtui@latest add toastbunx newtui@latest add toastInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityCopy the source into your project
"use client"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
const toastVariants = cva(
"flex max-w-[360px] items-start gap-3 rounded-md border border-newt-border bg-newt-bg-elevated px-3.5 py-3 text-newt-text-primary shadow-elevation-high",
{
variants: {
variant: {
success: "",
error: "",
},
},
defaultVariants: {
variant: "success",
},
}
)
const toastIconVariants = cva(
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs text-white",
{
variants: {
variant: {
success: "bg-newt-online",
error: "bg-newt-dnd",
},
},
defaultVariants: {
variant: "success",
},
}
)
export interface ToastProps
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof toastVariants> {
/** Auto-dismiss after this many ms. Defaults to 5000. Pass 0 to disable. */
duration?: number
/** Controlled open state. */
open?: boolean
/** Called when the toast closes, either by timeout or the close button. */
onOpenChange?: (open: boolean) => void
}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
(
{
className,
variant,
duration = 5000,
open,
onOpenChange,
children,
...props
},
ref
) => {
const [internalOpen, setInternalOpen] = React.useState(true)
const isControlled = open !== undefined
const isOpen = isControlled ? open : internalOpen
const close = React.useCallback(() => {
if (!isControlled) setInternalOpen(false)
onOpenChange?.(false)
}, [isControlled, onOpenChange])
React.useEffect(() => {
if (!isOpen || duration <= 0) return
const id = window.setTimeout(close, duration)
return () => window.clearTimeout(id)
}, [isOpen, duration, close])
if (!isOpen) return null
return (
<ToastContext.Provider value={{ variant: variant ?? "success", close }}>
<div
ref={ref}
role="status"
aria-live="polite"
data-variant={variant ?? "success"}
className={cn(toastVariants({ variant }), className)}
{...props}
>
{children}
</div>
</ToastContext.Provider>
)
}
)
Toast.displayName = "Toast"
type ToastContextValue = {
variant: NonNullable<VariantProps<typeof toastVariants>["variant"]>
close: () => void
}
const ToastContext = React.createContext<ToastContextValue>({
variant: "success",
close: () => {},
})
const ToastIcon = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
const { variant } = React.useContext(ToastContext)
return (
<div
ref={ref}
aria-hidden="true"
className={cn(toastIconVariants({ variant }), className)}
{...props}
>
{children ?? (variant === "success" ? "✓" : "!")}
</div>
)
})
ToastIcon.displayName = "ToastIcon"
const ToastContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("min-w-0 flex-1", className)} {...props} />
))
ToastContent.displayName = "ToastContent"
const ToastTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = "ToastTitle"
const ToastDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"mt-0.5 text-[13px] text-[var(--newt-text-secondary)]",
className
)}
{...props}
/>
))
ToastDescription.displayName = "ToastDescription"
const ToastClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, onClick, children, ...props }, ref) => {
const { close } = React.useContext(ToastContext)
return (
<button
ref={ref}
type="button"
aria-label="Dismiss"
className={cn(
"shrink-0 cursor-pointer border-0 bg-transparent p-0 text-base leading-none text-newt-text-muted transition-colors duration-fast ease-newt hover:text-newt-text-primary",
className
)}
onClick={(e) => {
onClick?.(e)
if (!e.defaultPrevented) close()
}}
{...props}
>
{children ?? "×"}
</button>
)
})
ToastClose.displayName = "ToastClose"
export {
Toast,
ToastIcon,
ToastContent,
ToastTitle,
ToastDescription,
ToastClose,
toastVariants,
toastIconVariants,
}<script setup lang="ts">
import {
computed,
onBeforeUnmount,
provide,
ref,
watch,
type HTMLAttributes,
} from "vue"
import { cn } from "@/lib/utils"
import { TOAST_INJECTION_KEY, toastVariants, type ToastVariants } from "."
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
variant?: ToastVariants["variant"]
/** Auto-dismiss after this many ms. 0 disables. */
duration?: number
open?: boolean
}>(),
{ variant: "success", duration: 5000, open: undefined }
)
const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
const internalOpen = ref(true)
const isControlled = computed(() => props.open !== undefined)
const isOpen = computed(() =>
isControlled.value ? props.open : internalOpen.value
)
function close() {
if (!isControlled.value) internalOpen.value = false
emit("update:open", false)
}
let timer: ReturnType<typeof setTimeout> | undefined
function arm() {
if (timer) clearTimeout(timer)
timer = undefined
if (isOpen.value && props.duration > 0)
timer = setTimeout(close, props.duration)
}
watch([isOpen, () => props.duration], arm, { immediate: true })
onBeforeUnmount(() => timer && clearTimeout(timer))
provide(TOAST_INJECTION_KEY, {
variant: computed(() => props.variant ?? "success"),
close,
})
</script>
<template>
<div
v-if="isOpen"
role="status"
aria-live="polite"
:data-variant="props.variant"
:class="cn(toastVariants({ variant: props.variant }), props.class)"
>
<slot />
</div>
</template><script setup lang="ts">
import { computed, inject, type HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { TOAST_INJECTION_KEY, toastIconVariants } from "."
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const ctx = inject(TOAST_INJECTION_KEY, null)
const variant = computed(() => ctx?.variant.value ?? "success")
</script>
<template>
<div
aria-hidden="true"
:class="cn(toastIconVariants({ variant }), props.class)"
>
<slot>{{ variant === "success" ? "✓" : "!" }}</slot>
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div :class="cn('min-w-0 flex-1', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div :class="cn('text-sm font-semibold', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
:class="
cn('mt-0.5 text-[13px] text-[var(--newt-text-secondary)]', props.class)
"
>
<slot />
</div>
</template><script setup lang="ts">
import { inject, type HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { TOAST_INJECTION_KEY } from "."
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
const ctx = inject(TOAST_INJECTION_KEY, null)
</script>
<template>
<button
type="button"
aria-label="Dismiss"
:class="
cn(
'shrink-0 cursor-pointer border-0 bg-transparent p-0 text-base leading-none text-newt-text-muted transition-colors duration-fast ease-newt hover:text-newt-text-primary',
props.class
)
"
@click="ctx?.close()"
>
<slot>×</slot>
</button>
</template>import { cva, type VariantProps } from "class-variance-authority"
import type { ComputedRef, InjectionKey } from "vue"
export { default as Toast } from "./Toast.vue"
export { default as ToastIcon } from "./ToastIcon.vue"
export { default as ToastContent } from "./ToastContent.vue"
export { default as ToastTitle } from "./ToastTitle.vue"
export { default as ToastDescription } from "./ToastDescription.vue"
export { default as ToastClose } from "./ToastClose.vue"
export const toastVariants = cva(
"flex max-w-[360px] items-start gap-3 rounded-md border border-newt-border bg-newt-bg-elevated px-3.5 py-3 text-newt-text-primary shadow-elevation-high",
{
variants: {
variant: {
success: "",
error: "",
},
},
defaultVariants: {
variant: "success",
},
}
)
export const toastIconVariants = cva(
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs text-white",
{
variants: {
variant: {
success: "bg-newt-online",
error: "bg-newt-dnd",
},
},
defaultVariants: {
variant: "success",
},
}
)
export type ToastVariants = VariantProps<typeof toastVariants>
export interface ToastContext {
variant: ComputedRef<NonNullable<ToastVariants["variant"]>>
close: () => void
}
export const TOAST_INJECTION_KEY: InjectionKey<ToastContext> =
Symbol("newt-toast")Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { Toast } from "@/components/ui/toast"
export function Example() {
return <Toast />
}<script setup lang="ts">
import { Toast, ToastIcon, ToastContent, ToastTitle, ToastDescription, ToastClose } from "@/components/ui/toast"
</script>
<template>
<Toast />
</template>API reference
Toast
duration?number | undefined
Auto-dismiss after this many ms. Defaults to 5000. Pass 0 to disable.
number | undefinedopen?boolean | undefined
Controlled open state.
boolean | undefinedonOpenChange?(open: boolean) => void | undefined
Called when the toast closes, either by timeout or the close button.
(open: boolean) => void | undefinedThe registry exports Toast, ToastIcon, ToastContent, ToastTitle, ToastDescription, ToastClose. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Use role="status" for routine updates and role="alert" only for urgent failures. Do not move focus into a toast, and give its close button an accessible name.
Tokens
Toast reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-toast.