Skip to content
On this page Profiles

Feedback

Sound

Opt-in interface sound, synthesized in the browser. Off until the user turns it on, silent before their first gesture, and always a second signal.

Sound lives in its own entry, @codeatlasdev/ui/sound, so the core stays silent and weightless. Components only announce what happened through a no-op soundBus; importing the entry registers the one listener that turns those announcements into short synthesized cues. There are no audio files: every cue is a few oscillators, a noise burst and an envelope, built when it plays. The whole entry (engine, four profiles, the use:sound action and SoundToggle) is under 4 KB gzip.

<script>
  import { Button, Switch } from "@codeatlasdev/ui";
  import { SoundToggle } from "@codeatlasdev/ui/sound";
  let on = $state(false);
</script>

<SoundToggle />                      <!-- off by default; the choice is saved in localStorage -->
<Button sound>Save</Button>          <!-- press and release -->
<Switch sound bind:checked={on} />   <!-- toggle-on / toggle-off -->

Nothing plays until the user presses SoundToggle, and even then the browser’s audio context is only created inside their next pointer or key event. Read the principles before adding sound to a screen.

#988

Hear it

Pick a profile and press the cues. Turn the toggle on first: sound is off by default.

Reference #988 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Button, SegmentedControl } from '@codeatlasdev/ui';
  import { SoundToggle, brutal, glass, sound, system, terminal } from '@codeatlasdev/ui/sound';
  import type { CueName } from '@codeatlasdev/ui';
  import { onDestroy } from 'svelte';

  const PROFILES = { system, glass, brutal, terminal };
  const CUES: CueName[] = ['press', 'toggle-on', 'toggle-off', 'success', 'error', 'warning', 'open', 'close', 'tab'];

  let profile = $state<keyof typeof PROFILES>('system');
  $effect(() => sound.use(PROFILES[profile]));
  onDestroy(() => sound.use(undefined));
</script>

<div class="hear">
  <div class="hear__bar">
    <SoundToggle volume />
    <SegmentedControl
      aria-label="Sound profile"
      bind:value={profile}
      items={[
        { value: 'system', label: 'System' },
        { value: 'glass', label: 'Glass' },
        { value: 'brutal', label: 'Brutalism' },
        { value: 'terminal', label: 'Terminal' },
      ]}
    />
  </div>
  <div class="hear__board" role="group" aria-label="Cue board">
    {#each CUES as cue (cue)}
      <Button variant="outlined" size="sm" sound={cue}>{cue}</Button>
    {/each}
  </div>
</div>

<style>
  .hear { display: grid; gap: var(--ca-space-4); }
  .hear__bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ca-space-3); }
  .hear__board { display: flex; flex-wrap: wrap; gap: var(--ca-space-2); }
</style>

Profiles

A profile is a handful of numbers (waveform, root pitch, cue length, filter, noise share, gain), not a set of recordings. The profile follows the active design style, so a brutalist theme clicks and a glass theme chimes with no extra setup.

Profile Design style Character
system default, material, swiss, … neutral, short, low-gain sine blips
glass glassmorphism, liquid soft sine chimes with a high shimmer and a short tail
brutal brutalism dry square-wave and noise clicks, hard stops
terminal terminal square-wave beeps and keyboard-like ticks

designStyle("brutalism") writes --ca-sound-profile: brutal on :root, and the engine reads it per cue. Without it the engine falls back to the surface material (<html data-style>), then to system. Pin one yourself with sound.use(profile) (and sound.use(undefined) to follow the style again).

Tune a profile from CSS, read once per cue:

Token Effect
--ca-sound-profile which profile plays (system, glass, brutal, terminal)
--ca-sound-gain multiplies the profile level, default 1
--ca-sound-pitch multiplies the root pitch, default 1
--ca-sound-brightness multiplies the filter cutoff, default 1

On components

Every component takes the same prop. sound is true for the component’s own cue, or a cue name to play that cue instead. Without the sound entry the prop does nothing.

#989

Components

Each control with its sound prop on, behind one toggle.

Reference #989 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Button, Dialog, Slider, Switch, Tabs, TabsList, TabsTrigger, toast } from '@codeatlasdev/ui';
  import { SoundToggle } from '@codeatlasdev/ui/sound';

  let on = $state(false);
  let level = $state(40);
  let tab = $state('a');
  let open = $state(false);
</script>

<div class="comp">
  <SoundToggle />
  <Button sound>Save</Button>
  <Switch sound bind:checked={on} label="Notifications" />
  <Tabs sound bind:value={tab}>
    <TabsList>
      <TabsTrigger value="a">Overview</TabsTrigger>
      <TabsTrigger value="b">Activity</TabsTrigger>
    </TabsList>
  </Tabs>
  <Slider sound bind:value={level} />
  <Button variant="outlined" onclick={() => toast.success('Saved', { sound: true })}>Toast</Button>
  <Button variant="outlined" onclick={() => (open = true)}>Dialog</Button>
  <Dialog sound bind:open title="Sound on open and close">Every cue also has a visible change.</Dialog>
</div>

<style>
  .comp { display: grid; justify-items: start; gap: var(--ca-space-4); }
</style>

The sound prop

PropTypeDefaultDescription
Buttonboolean | CueNamefalse`press` on pointer down, `release` on pointer up. A disabled or loading button stays silent.
Switchboolean | CueNamefalse`toggle-on` or `toggle-off` when the user changes it.
Toast / toast.*(msg, { sound })boolean | CueNamefalse`success`, `error` or `warning` by variant, `notify` otherwise, when the toast appears.
Dialogboolean | CueNamefalse`open` and `close` as `open` changes, including from outside through `bind:open`.
Tabsboolean | CueNamefalse`tab` when the selected tab changes.
Sliderboolean | CueNamefalse`tick` per step the user crosses. Off under reduced motion.

For any other element use the action: <div use:sound={{ on: "pointerenter", cue: "hover" }}>. hover only plays in profiles that opt in (glass) and never under reduced motion.

API

PropTypeDefaultDescription
SoundToggle{ label?, size?, volume?, class? }–A speaker button with `aria-pressed`, an accessible name and the persisted choice. `volume` adds a slider while sound is on. It uses the `ca-rail-btn` look, so it sits next to `ThemeToggle` in the AppShell `railBottom`.
sound.enabled / sound.volumeboolean / number–Persisted under `localStorage` key `ca-sound` as `{ enabled, volume }`. Off and `0.25` by default.
sound.enable(on?) / sound.setVolume(v)(boolean) => void / (number) => void–Turn sound on or off, set the master volume from 0 to 1.
sound.play(cue)(CueName) => boolean–Play a cue now. Returns whether it played (false when off, locked, hidden, throttled or suppressed).
sound.profile / sound.use(profile)SoundProfile / (SoundProfile | undefined) => void–The profile in force, and a way to pin one.
soundBus.emit(cue)(CueName) => void–Exported from the root package. A no-op until the sound entry is imported. Use it to give your own components a cue.
use:soundAction<HTMLElement, { on?, cue? }>–The `sound` export is also the action: `use:sound={{ on: 'click', cue: 'press' }}` for any element.

Cues are press, release, tap, toggle-on, toggle-off, select, check, success, error, warning, notify, open, close, forward, back, tab, tick and hover.