Chime
StableA soft two-note chime made with Web Audio: arm it inside a click, then play it later, even from a background tab.
Browsers let a page make sound only after someone has interacted with it. armChime() creates or resumes one shared audio context; call it inside the click that starts a timer. playChime() can then sound when the timer ends, minutes later. The chime is synthesised (E5 then B5, sine tones with a quiet overtone and a bell-like fade), so there is no file to load.
Updated View source
Play now or later
import { useEffect, useId, useRef, useState, type CSSProperties } from 'react'import { armChime, playChime } from '@rchursin/product/timer'import { Button } from '@rchursin/product/ui'
/** * Browsers only let a page make sound after a click, so armChime() runs inside the click. After * that, playChime() can sound later on its own: "In 5 seconds" arms now and plays from a timer, * even if you switch tabs meanwhile. Two soft notes, made with Web Audio; no files to load. */type Status = 'idle' | 'ready' | 'waiting' | 'blocked'
const messages: Record<Status, string> = { idle: 'Sound is off until you press a button.', ready: 'Sound is on for this page.', waiting: 'Playing in 5 seconds.', blocked: 'This browser did not allow sound.',}
export default function ChimePlay() { const volumeId = useId()Usage
import { armChime, playChime } from '@rchursin/product/timer'<Button onClick={() => { void armChime(); timer.start() }}>Start</Button>// later, when it endsplayChime()API
armChime()
| Prop | Type | Default | Description |
|---|---|---|---|
returns | Promise<boolean> | – | true when sound can play. false when Web Audio is missing or the browser refused (call it from a click, a key press or a tap). |
playChime(volume?)
| Prop | Type | Default | Description |
|---|---|---|---|
volume | number | 0.22 | Output gain, 0 to 1. The default is deliberately soft. |
returns | boolean | – | true when it played; false, silently, when nothing armed the context. |
Accessibility
- The sound lasts about two seconds and never repeats, within WCAG 1.4.2 (audio that plays on its own for more than three seconds needs a control).
- Sound is an extra, never the only signal: show a banner and announce the end too.
- Let people turn it off; the kit’s focus timer has it off by default.
Guidelines
Do
- Call armChime in the same handler that starts the timer.
- Keep a visible sound on or off control near the timer.
Don’t
- Don’t play sound on page load or on hover.
- Don’t rely on system notifications inside an iframe; browsers block the permission there.
In use
Related
- Timer engineClocks that keep honest time: stopwatch and countdown hooks built on timestamps, one shared frame loop, and duration parsing.
- Focus sessionA Pomodoro session: choose a task, say what you’ll finish, then focus and break in phases, with overtime and an inline Abandon.
- CountdownA countdown with presets or a typed length, +1 min while it runs, a closing square and an honest finished banner.
- BannersIn-page messages for things that happen on their own: a timer that finished, a reminder that came due.
Source
The component’s own files, exactly as they run on this site.
/** * A soft two-note chime, synthesised with Web Audio (no sound files to load). * * Sound only after a click: browsers let a page play sound once the person has interacted with it, and an * AudioContext created before that starts "suspended". So call `armChime()` INSIDE the click handler of Start * (or Resume, Restart, or the sound toggle). That creates or resumes the context while the gesture is live, and the * chime can then play when the timer ends, minutes later, even from a background tab. * * Why there are no system notifications: the Notifications API needs permission asked from a user gesture on a * secure, top-level page. Requests from cross-origin iframes are blocked, and this kit runs inside a sandboxed * artifact iframe, so it would never be granted. The in-page answer is a banner (role="alert"), this chime, and an * honest "Finished 3 min ago" when the person comes back. (Changing document.title from an iframe doesn't change the * browser tab's title either.) */
type AudioContextCtor = typeof AudioContextlet ctx: AudioContext | null = null
function audioContextClass(): AudioContextCtor | undefined { if (typeof window === "undefined") return undefined