Toast
Display temporary notifications and messages to users with automatic dismissal and customizable placement
Usage
import { ToastProvider, toast } from '@hareui/vue'<script setup lang="ts">import { Icon } from '@iconify/vue'import { Button, toast } from '@hareui/vue'import { h } from 'vue'function show() { const id = toast('You have been invited to join a team', { description: 'Bob sent you an invitation to join HeroUI team', indicator: h(Icon, { icon: 'gravity-ui:persons' }), variant: 'default', action: { label: 'Dismiss', variant: 'tertiary', onClick: () => toast.close(id), }, })}</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <Button size="sm" variant="secondary" @click="show"> Show toast </Button> </div></template>Anatomy
A single ToastProvider renders the whole stack; each toast's layout (indicator, title, description, action, close button) is built in, or fully replaced through the #toast slot (see Custom Toast Rendering).
<ToastProvider />toast('Event created')ui keys: region, toast, content, indicator, title, description, close, action.
Examples
Variants
<script setup lang="ts">import { Icon } from '@iconify/vue'import { Button, toast } from '@hareui/vue'import { h } from 'vue'function showDefault() { const id = toast('You have been invited to join a team', { description: 'Bob sent you an invitation to join HeroUI team', indicator: h(Icon, { icon: 'gravity-ui:persons' }), variant: 'default', action: { label: 'Dismiss', variant: 'tertiary', onClick: () => toast.close(id) }, })}function showAccent() { const id = toast.info('You have 2 credits left', { description: 'Get a paid plan for more credits', action: { label: 'Upgrade', onClick: () => toast.close(id) }, })}function showSuccess() { const id = toast.success('You have upgraded your plan', { description: 'You can continue using HareUI Chat', action: { label: 'Billing', class: 'bg-success text-success-foreground', onClick: () => toast.close(id) }, })}function showWarning() { const id = toast.warning('You have no credits left', { description: 'Upgrade to a paid plan to continue', action: { label: 'Upgrade', class: 'bg-warning text-warning-foreground', onClick: () => toast.close(id) }, })}function showDanger() { const id = toast.danger('Storage is full', { description: 'Remove files to release space. Adding more text to demonstrate longer content display', indicator: h(Icon, { icon: 'gravity-ui:hard-drive' }), action: { label: 'Remove', variant: 'danger', onClick: () => toast.close(id) }, })}</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <div class="flex w-full flex-wrap items-center justify-center gap-4"> <Button size="sm" variant="tertiary" @click="showDefault"> Default toast </Button> <Button size="sm" variant="secondary" @click="showAccent"> Accent toast </Button> <Button class="text-success-soft-foreground" size="sm" variant="tertiary" @click="showSuccess"> Success toast </Button> <Button class="text-warning-soft-foreground" size="sm" variant="tertiary" @click="showWarning"> Warning toast </Button> <Button size="sm" variant="danger-soft" @click="showDanger"> Danger toast </Button> </div> </div></template>Placements
<script setup lang="ts">import { Button, ToastProvider, ToastQueue } from '@hareui/vue'const placements = ['top start', 'top', 'top end', 'bottom start', 'bottom', 'bottom end'] as consttype Placement = typeof placements[number]// A separate queue per placement, so each `ToastProvider` below stacks independently.const placementQueues: Record<Placement, ToastQueue> = Object.fromEntries( placements.map(p => [p, new ToastQueue({ maxVisibleToasts: 3 })]),) as Record<Placement, ToastQueue>function showToast(placement: Placement) { placementQueues[placement].add({ title: 'Event created', description: 'Event has been created', variant: 'default' })}</script><template> <div class="flex h-full flex-col items-center justify-center gap-6"> <ToastProvider v-for="p in placements" :key="p" :placement="p" :queue="placementQueues[p]" /> <div class="flex max-w-xs flex-wrap justify-center gap-2"> <Button v-for="p in placements" :key="p" size="sm" variant="secondary" @click="showToast(p)"> {{ p }} </Button> </div> </div></template>Expanded Stack
<script setup lang="ts">import { Button, ToastProvider, ToastQueue } from '@hareui/vue'const queue = new ToastQueue()function show() { queue.add({ title: 'Simple message', variant: 'default' }) setTimeout(() => queue.add({ title: 'Operation completed', variant: 'success' }), 400) setTimeout(() => queue.add({ title: 'New update available', variant: 'accent' }), 800)}</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <ToastProvider is-expanded aria-label="Expanded notifications" placement="bottom" :queue="queue" /> <Button size="sm" variant="secondary" @click="show"> Show 3 toasts </Button> </div></template>Simple Toasts
<script setup lang="ts">import { Button, toast } from '@hareui/vue'</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <div class="flex w-full flex-wrap items-center justify-center gap-4"> <Button size="sm" variant="secondary" @click="toast('Simple message')"> Default </Button> <Button size="sm" variant="secondary" @click="toast.success('Operation completed')"> Success </Button> <Button size="sm" variant="secondary" @click="toast.info('New update available')"> Info </Button> <Button size="sm" variant="secondary" @click="toast.warning('Please check your settings')"> Warning </Button> <Button size="sm" variant="secondary" @click="toast.danger('Something went wrong')"> Error </Button> </div> </div></template>Custom Indicators
<script setup lang="ts">import { Icon } from '@iconify/vue'import { Button, toast } from '@hareui/vue'import { h } from 'vue'function show() { toast('Custom icon indicator', { indicator: h(Icon, { icon: 'gravity-ui:star' }) })}</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <Button size="sm" variant="secondary" @click="show"> Custom indicator </Button> </div></template>Custom Toast Rendering
<script setup lang="ts">import type { ToastContentValue } from '@hareui/vue'import { Button, CloseButton, ToastProvider, ToastQueue } from '@hareui/vue'const customQueue = new ToastQueue()function show() { customQueue.add({ title: 'Custom layout toast', description: 'This uses a custom render function', variant: 'default' })}</script><template> <div class="flex h-full max-w-xl flex-col items-center justify-center"> <ToastProvider placement="bottom" :queue="customQueue" :ui="{ toast: 'rounded-xl border border-border' }"> <template #toast="{ content, close }"> <div class="flex items-center gap-2"> <svg aria-hidden="true" class="size-4 shrink-0 text-accent-soft-foreground" fill="none" viewBox="0 0 16 16"> <path clip-rule="evenodd" d="M8 13.5a5.5 5.5 0 1 0 0-11a5.5 5.5 0 0 0 0 11M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m1-9.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25 3a.75.75 0 0 0-1.5 0V11a.75.75 0 0 0 1.5 0z" fill="currentColor" fill-rule="evenodd" /> </svg> <div class="flex flex-col pe-6"> <p v-if="(content as ToastContentValue).title" class="text-sm font-medium text-accent-soft-foreground"> {{ (content as ToastContentValue).title }} </p> <p v-if="(content as ToastContentValue).description" class="text-sm text-muted"> {{ (content as ToastContentValue).description }} </p> </div> </div> <CloseButton class="absolute end-2 top-1/2 -translate-y-1/2 border-none bg-transparent opacity-100 [&>svg]:size-4" @click="close" /> </template> </ToastProvider> <Button size="sm" variant="secondary" @click="show"> Custom toast </Button> </div></template>Promise & Loading
Using toast.promise()
Automatically handles loading, success, and error states
Manual Loading State
Manually control loading state with isLoading prop
<script setup lang="ts">import { Button, toast } from '@hareui/vue'function uploadFile(): Promise<{ filename: string, size: number }> { const { promise, resolve } = Promise.withResolvers<{ filename: string, size: number }>() setTimeout(() => resolve({ filename: 'document.pdf', size: 1024 }), 2000) return promise}function createEvent(): Promise<never> { const { promise, reject } = Promise.withResolvers<never>() setTimeout(() => reject(new Error('Network error. Please try again.')), 2000) return promise}function saveData(): Promise<{ count: number }> { const { promise, resolve, reject } = Promise.withResolvers<{ count: number }>() setTimeout(() => { if (Math.random() > 0.5) resolve({ count: 42 }) else reject(new Error('Failed to save data')) }, 2000) return promise}function fetchUser(): Promise<{ name: string, email: string }> { const { promise, resolve } = Promise.withResolvers<{ name: string, email: string }>() setTimeout(() => resolve({ email: '[email protected]', name: 'John Doe' }), 2000) return promise}function uploadWithLoading() { const loadingId = toast('Uploading file...', { description: 'Please wait while we upload your file', isLoading: true, timeout: 0 }) setTimeout(() => { toast.update(loadingId, 'File uploaded', { description: 'Your file has been uploaded successfully', variant: 'success' }) }, 3000)}function paymentProcessing() { const loadingId = toast('Processing payment...', { isLoading: true, timeout: 0 }) setTimeout(() => { toast.update(loadingId, 'Payment processed', { description: 'Your payment has been processed successfully', variant: 'success' }) }, 2500)}function loadingToError() { const loadingId = toast('Saving changes...', { isLoading: true, timeout: 0 }) setTimeout(() => { toast.update(loadingId, 'Failed to save', { description: 'Please try again', variant: 'danger' }) }, 2000)}</script><template> <div class="flex h-full max-w-2xl flex-col items-center justify-center gap-8"> <div class="w-full space-y-3"> <div class="text-center"> <h3 class="text-sm font-medium"> Using toast.promise() </h3> <p class="text-xs text-muted"> Automatically handles loading, success, and error states </p> </div> <div class="flex w-full flex-wrap items-center justify-center gap-4"> <Button size="sm" variant="secondary" @click="toast.promise(uploadFile(), { loading: 'Uploading file...', success: data => `File ${data.filename} uploaded (${data.size}KB)`, error: 'Failed to upload file', })" > Upload file </Button> <Button size="sm" variant="secondary" @click="toast.promise(createEvent(), { loading: 'Creating event...', success: 'Event created', error: err => err.message, })" > Create event (error) </Button> <Button size="sm" variant="secondary" @click="toast.promise(saveData(), { loading: 'Saving changes...', success: data => `Saved ${data.count} items`, error: err => err.message, })" > Save data (random) </Button> <Button size="sm" variant="secondary" @click="toast.promise(fetchUser(), { loading: 'Loading user...', success: data => `Welcome back, ${data.name}!`, error: 'Failed to fetch user', })" > Fetch user </Button> </div> </div> <div class="w-full space-y-3"> <div class="text-center"> <h3 class="text-sm font-medium"> Manual Loading State </h3> <p class="text-xs text-muted"> Manually control loading state with isLoading prop </p> </div> <div class="flex w-full flex-wrap items-center justify-center gap-4"> <Button size="sm" variant="secondary" @click="uploadWithLoading"> Upload with loading </Button> <Button size="sm" variant="secondary" @click="paymentProcessing"> Payment processing </Button> <Button size="sm" variant="secondary" @click="loadingToError"> Loading to error </Button> </div> </div> </div></template>Callbacks
Closed History
No toasts closed yet. Try closing one above!
<script setup lang="ts">import { Button, toast } from '@hareui/vue'import { ref } from 'vue'const closedHistory = ref<Array<{ message: string, time: string }>>([])function addToHistory(message: string) { const time = new Date().toLocaleTimeString() closedHistory.value = [{ message, time }, ...closedHistory.value].slice(0, 5)}</script><template> <div class="flex h-full max-w-2xl flex-col items-center justify-center gap-6"> <div class="flex w-full flex-wrap items-center justify-center gap-4"> <Button size="sm" variant="secondary" @click="toast('File saved', { timeout: 3000, onClose: () => addToHistory('File saved (closed after 3 seconds)') })" > Custom timeout (3s) </Button> <Button size="sm" variant="secondary" @click="toast('Changes saved', { timeout: 10000, onClose: () => addToHistory('Changes saved (closed after 10 seconds)') })" > Custom timeout (10s) </Button> <Button size="sm" variant="secondary" @click="toast.success('Event created', { onClose: () => addToHistory('Event created (closed after default timeout)') })" > With onClose callback </Button> <Button size="sm" variant="secondary" @click="toast('Important notification', { description: 'This toast will stay until dismissed', timeout: 0, onClose: () => addToHistory('Important notification (manually closed)'), })" > Persistent toast </Button> </div> <div class="w-full space-y-2"> <div class="flex items-center justify-between"> <h3 class="text-sm font-medium"> Closed History </h3> <Button v-if="closedHistory.length > 0" class="h-6 text-xs" size="sm" variant="tertiary" @click="closedHistory = []"> Clear </Button> </div> <div class="min-h-[120px] space-y-2 rounded-lg border border-border bg-surface p-4"> <p v-if="closedHistory.length === 0" class="text-sm text-muted"> No toasts closed yet. Try closing one above! </p> <div v-for="(item, index) in closedHistory" :key="`${item.time}-${index}`" class="flex animate-in items-start justify-between gap-3 rounded-md border border-border bg-default px-3 py-2 text-sm duration-200 fade-in slide-in-from-top-2" :style="{ animationDelay: `${index * 50}ms` }" > <div class="flex-1"> <span class="font-medium">{{ item.message }}</span> <span class="ms-2 text-xs text-muted">({{ item.time }})</span> </div> <div class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-success/10 text-success-soft-foreground"> <svg class="size-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" /> </svg> </div> </div> </div> </div> </div></template>Custom Queues
<script setup lang="ts">import { Button, ToastProvider, ToastQueue } from '@hareui/vue'const notificationQueue = new ToastQueue({ maxVisibleToasts: 2 })const errorQueue = new ToastQueue({ maxVisibleToasts: 3 })const successQueue = new ToastQueue({ maxVisibleToasts: 1 })</script><template> <div class="flex h-full max-w-4xl items-center justify-center gap-4"> <ToastProvider placement="bottom" :queue="notificationQueue" /> <div class="flex justify-center gap-2"> <Button size="sm" variant="secondary" @click="notificationQueue.add({ title: 'New notification', description: 'You have a new message', variant: 'default' })" > Add notification (max 2) </Button> </div> <ToastProvider placement="bottom start" :queue="errorQueue" /> <div class="flex justify-center gap-2"> <Button size="sm" variant="danger-soft" @click="errorQueue.add({ title: 'Error occurred', description: 'Failed to save changes', variant: 'danger' })" > Add error (max 3) </Button> </div> <ToastProvider placement="bottom end" :queue="successQueue" /> <div class="flex justify-center gap-2"> <Button class="text-success-soft-foreground" size="sm" variant="secondary" @click="successQueue.add({ title: 'Success!', description: `Operation ${Date.now()}`, variant: 'success' })" > Add success (max 1) </Button> </div> </div></template>Setup
Render the provider once at the root of your app.
<script setup lang="ts">
import { Button, ToastProvider, toast } from '@hareui/vue'
</script>
<template>
<ToastProvider />
<Button @click="toast('Simple message')">
Show toast
</Button>
</template>Customization
Tailwind CSS
<script setup lang="ts">import type { ToastContentValue } from '@hareui/vue'import { Button, ToastProvider, ToastQueue } from '@hareui/vue'const queue = new ToastQueue()function show() { queue.add({ title: 'Saved', description: 'Draft synced', variant: 'default' })}</script><template> <div class="flex flex-col items-center gap-4"> <ToastProvider placement="bottom" :queue="queue" :ui="{ toast: 'rounded-xl border border-border/80 bg-surface shadow-lg ring-1 ring-black/5 dark:ring-white/10' }" > <template #toast="{ content }"> <div class="flex items-center gap-2.5"> <svg aria-hidden="true" class="size-4 shrink-0 text-neutral-600 dark:text-neutral-400" fill="none" viewBox="0 0 16 16"> <path clip-rule="evenodd" d="M8 13.5a5.5 5.5 0 1 0 0-11a5.5 5.5 0 0 0 0 11M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m1-9.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25 3a.75.75 0 0 0-1.5 0V11a.75.75 0 0 0 1.5 0z" fill="currentColor" fill-rule="evenodd" /> </svg> <div class="flex flex-col gap-0.5"> <p v-if="(content as ToastContentValue).title" class="text-sm font-medium text-neutral-900 dark:text-neutral-50"> {{ (content as ToastContentValue).title }} </p> <p v-if="(content as ToastContentValue).description" class="text-sm text-neutral-600 dark:text-neutral-400"> {{ (content as ToastContentValue).description }} </p> </div> </div> </template> </ToastProvider> <Button size="sm" variant="secondary" @click="show"> Show toast </Button> </div></template>Global theme override
app.use(createHareUI({
ui: {
toast: {
slots: { toast: 'rounded-xl shadow-lg', content: 'gap-2' },
},
},
}))Styling Reference
ui key | data-slot | Element |
|---|---|---|
region | toast-region | The positioned landmark region toasts are teleported into |
toast | toast | Each toast's positioned, animated wrapper (Reka has no built-in equivalent; implemented directly) |
content | toast-content | Wrapper for the title and description |
indicator | toast-indicator | Icon/spinner container, rendered unless indicator is null |
title | toast-title | The toast title |
description | toast-description | The toast description |
close | toast-close | The close button |
action | toast-action-button | The optional action button |
Interactive States
[data-frontmost]— applied to the topmost visible toast[data-index]— applied based on toast position in stack[data-placement]— applied based on the region's placement[data-entering]— applied on first paint of a new toast[data-exiting]— applied while a closing toast plays its exit transition[data-expanded]— applied to every toast while the stack is expanded[data-hidden]— applied to toasts beyondmaxVisibleToasts
API Reference
ToastProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "top" | "bottom" | "bottom start" | "bottom end" | "top start" | "top end" | "bottom" | Placement of the toast region. |
gap | number | DEFAULT_GAP | The gap between toasts in pixels. |
isExpanded | boolean | false | Keeps the stack in its expanded layout instead of expanding only on hover or keyboard focus. Forced expansion does not pause toast timers. |
maxVisibleToasts | number | 3, or the queue's own `maxVisibleToasts` | The maximum number of toasts to display at a time (visual only). |
hotkey | string[] | DEFAULT_HOTKEY | Hotkey that focuses the toast region. Modifiers match KeyboardEvent
boolean props, other keys match event.code. Pass [] to disable. |
scaleFactor | number | DEFAULT_SCALE_FACTOR | How much each toast behind the front one scales down. |
width | string | number | DEFAULT_TOAST_WIDTH | The width of the toast region; numbers are pixels. |
queue | ToastQueue<ToastContentValue> | - | Custom toast queue instance; defaults to the shared default queue used by toast(). |
ariaLabel | string | "Notifications" | Accessible label for the region landmark. |
portal | string | boolean | HTMLElement | true | Where the region is teleported. true teleports to body, false renders in place,
a string or element teleports to that target. |
ui | ComponentSlots<{ slots: { region: string[]; toast: string[]; content: string; indicator: string[]; title: string; description: string; close: string[]; action: string; }; variants: { placement: { bottom: { region: string; toast: string; }; 'bottom start': { region: string; toast: string; }; 'bottom end': { region: string; toast: string; }; top: { region: string; toast: string; }; 'top start': { region: string; toast: string; }; 'top end': { region: string; toast: string; }; }; variant: { default: {}; accent: { title: string; }; success: { title: string; indicator: string; }; warning: { title: string; indicator: string; }; danger: { title: string; indicator: string; }; }; }; defaultVariants: { placement: string; variant: string; }; }> | - | Per-slot class overrides, shared by every rendered toast. |
ToastProvider Slots
| Slot | Props | Description |
|---|---|---|
toast | { content: ToastContentValue; close: () => void; } | Replaces a toast's inner content (indicator, title, description, action,
close button). The positioned, animated stack wrapper is still rendered
by the provider; style it with the ui.toast slot override. |
ToastQueue
Manages toast state outside any component, so toasts can be triggered from anywhere — including outside a component instance. Pass a custom instance to ToastProvider's queue prop to run an independent stack; otherwise every ToastProvider shares the module-level default queue used by toast().
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
exitDuration | number | 300 | How long a closing toast stays mounted for its exit animation (0 removes immediately) |
maxVisibleToasts | number | 3 | Maximum number of toasts to display at once (visual only) |
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
add | (content, options?: { timeout?, onClose? }) | string | Adds a toast, returns its key |
update | (key, content, options?: { timeout?, onClose? }) | boolean | Updates a toast in place, preserving its stack position; false if it no longer exists |
close | (key) | void | Closes a toast by its key |
clear | () | void | Closes every toast |
pauseAll | () | void | Pauses every toast's auto-dismiss timer |
resumeAll | () | void | Resumes every toast's auto-dismiss timer |
visibleToasts | — | QueuedToast[] | The currently rendered toasts, newest first (getter) |
toast / useToast()
toast and useToast() are equivalent — useToast() just returns the toast singleton, provided for symmetry with other composables; toast state lives outside any component instance, so toast works from anywhere, including outside <script setup>.
import { toast } from '@hareui/vue'
// Basic toast (auto-dismisses after 4 seconds by default)
toast('Event has been created')
// Variant methods (also auto-dismiss after 4 seconds by default)
toast.success('File saved')
toast.info('New update available')
toast.warning('Please check your settings')
toast.danger('Something went wrong')
// With options. The returned id lets an action close its own toast.
const eventId = toast('Event has been created', {
description: 'Your event has been scheduled for tomorrow',
variant: 'default',
timeout: 5000, // Custom timeout: 5 seconds
onClose: () => console.log('Closed'),
action: { label: 'View', onClick: () => toast.close(eventId) },
indicator: h(CustomIcon),
})
// Update an existing toast in place (keeps its position in the stack).
// Options you omit are inherited, so pass `timeout` to start a countdown
// on a toast that was created persistent.
const id = toast('Saving…', { timeout: 0 })
toast.update(id, 'Saved', { variant: 'success', timeout: 4000 })
// Promise support (automatically shows a loading spinner). The loading toast
// updates in place when the promise settles — same toast, same stack
// position; the auto-dismiss countdown starts at that point.
toast.promise(uploadFile(), {
loading: 'Uploading file...',
success: data => `File ${data.filename} uploaded`,
error: 'Failed to upload file',
})
// Manual loading state (persistent toast - no auto-dismiss)
const loadingId = toast('Creating event...', { isLoading: true, timeout: 0 })
// Later, update in place and start the auto-dismiss countdown
toast.update(loadingId, 'Event created', { variant: 'success', timeout: 4000 })
// Queue methods
toast.close(key)
toast.clear()
toast.pauseAll()
toast.resumeAll()toast Options
| Option | Type | Default | Description |
|---|---|---|---|
description | VNodeChild | - | Optional description text |
variant | 'default' | 'accent' | 'success' | 'warning' | 'danger' | 'default' | Visual variant |
indicator | VNodeChild | null | - | Custom indicator icon (null hides it) |
action | { label, onClick?, variant?, class? } | - | Action button |
isLoading | boolean | false | Shows a spinner in place of the indicator |
timeout | number | 4000 | Auto-dismiss timeout in milliseconds; 0 keeps the toast open |
onClose | () => void | - | Called when the toast is dismissed, as its exit animation starts |
toast.promise Options
| Option | Type | Default | Description |
|---|---|---|---|
loading | VNodeChild | - | Message shown while the promise is pending |
success | VNodeChild | ((data: T) => VNodeChild) | - | Message shown on success |
error | VNodeChild | ((error: Error) => VNodeChild) | - | Message shown on error |
Accessibility
Each toast implements the WAI-ARIA alertdialog pattern, grouped inside a labeled landmark region:
- Landmark region: toasts render in a labeled landmark reachable with
F6;Alt+Tmoves focus straight to it. The hotkey is configurable via thehotkeyprop and can be disabled by passing an empty array; the region label can be overridden withariaLabel - Keyboard:
Tabreaches the controls of visible toasts,Escapecollapses the expanded stack, and dismissing a focused toast moves focus to the nearest remaining toast - Screen readers: each toast uses
role="alertdialog"with its title and description linked; new toasts are announced automatically - Timers: auto-dismiss pauses while the stack is hovered or focused, and while the page is in a background tab





