# Match Sound Weight to the Action

**SHOULD** · **ID:** `interactions-sound-weight-matches-action` · **Category:** interactions
**Source:** Custom
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/interactions-sound-weight-matches-action

> SHOULD: Make a cue's sound proportional to the action's consequence: a trivial action gets a short, high, quiet tick; a heavy or irreversible one gets a lower, longer, slightly louder tone. Do not reuse one blip for everything — identical sound erases hierarchy. Stay subtle and keep the visual channel primary.

Scale a sound's pitch, length, and heft to the consequence — a light tick for the trivial, a low weighty tone for the grave

Cherry-picked from the weight-matching rules in Raphael Salaja's sounds-on-the-web skill, and it presumes interactions-sound-only-for-significant-events has already decided WHICH events get a sound at all — this rule governs, among those, how MUCH sound. The audio channel is a hierarchy channel or it is noise: if starring an item and deleting an account make the same 880Hz blip, the ear learns nothing and tunes the whole product out. Weight is carried by three knobs, and they stack — lower pitch reads heavier, longer duration reads weightier, and a slightly higher level reads more significant, so a grave, irreversible action wants a low tone that lingers (and can glide downward), while a trivial toggle wants a short, high, quiet tick. This is the audio sibling of two motion rules the corpus already holds: animations-lottie-distance-duration derives duration from how far an element travels, and animations-emil-frequency withholds motion from high-frequency actions — same instinct, that feedback must be proportional to what it is reporting. Stay inside the other sound constraints while you do it: keep the loudest tone subtle and user-controlled (interactions-sound-is-user-owned) and never let the sound carry meaning the screen does not (interactions-sound-not-sole-channel).

## Rule snippet

```tsx
star()   => playTone({ frequency: 1046, duration: 0.09, volume: 0.22 });
delete() => playTone({ frequency: 220, endFrequency: 150, duration: 0.34, volume: 0.34 });
```

## Bad — do not do this

`interactions-sound-weight-matches-action-bad`

```tsx
import { useState } from 'react';
import { playBlip } from '@/components/demo-kit/playBlip';

/**
 * The same 880Hz blip fires for a trivial star and for deleting an account. The audio
 * channel carries no hierarchy — the ear cannot tell the reversible from the grave.
 */
export function SoundWeightMatchesActionBad() {
  const [soundEnabled, setSoundEnabled] = useState(false);
  const [last, setLast] = useState<string>('');

  const act = (label: string) => {
    if (soundEnabled) playBlip(); // identical sound regardless of consequence
    setLast(label);
  };

  return (
    <div className="w-full max-w-sm">
      <div className="rounded-lg border border-border bg-card p-4">
        <label className="mb-4 flex items-center gap-2 text-xs text-muted-foreground">
          <input
            type="checkbox"
            checked={soundEnabled}
            onChange={(e) => setSoundEnabled(e.target.checked)}
            className="size-4 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          />
          Enable sound for this demo (off by default)
        </label>

        <div className="flex gap-2">
          <button
            type="button"
            onClick={() => act('Starred')}
            className="flex-1 rounded-md bg-muted px-3 py-2 text-sm text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            ☆ Star
          </button>
          <button
            type="button"
            onClick={() => act('Account deleted')}
            className="flex-1 rounded-md bg-destructive/15 px-3 py-2 text-sm text-destructive ring-1 ring-destructive focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            Delete account
          </button>
        </div>
        <p role="status" aria-live="polite" className="mt-3 min-h-4 text-xs text-muted-foreground">
          {last}
        </p>
      </div>
      <p className="mt-4 text-xs text-destructive">
        Same blip for both: the sound of starring is the sound of an irreversible delete.
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-sound-weight-matches-action-good`

```tsx
import { useState } from 'react';
import { playTone } from '@/components/demo-kit/playBlip';

/**
 * Sound scaled to consequence. A light action gets a short, high, quiet tick; a heavy,
 * irreversible one gets a low, longer, weightier tone that glides down. The ear now
 * ranks the two before the label is even read.
 */
export function SoundWeightMatchesActionGood() {
  const [soundEnabled, setSoundEnabled] = useState(false);
  const [last, setLast] = useState<string>('');

  const starred = () => {
    if (soundEnabled) playTone({ frequency: 1046, duration: 0.09, volume: 0.22 });
    setLast('Starred');
  };
  const deleted = () => {
    // Low, longer, a touch louder, with a downward glide — audibly heavier.
    if (soundEnabled) playTone({ frequency: 220, endFrequency: 150, duration: 0.34, type: 'triangle', volume: 0.34 });
    setLast('Account deleted');
  };

  return (
    <div className="w-full max-w-sm">
      <div className="rounded-lg border border-border bg-card p-4">
        <label className="mb-4 flex items-center gap-2 text-xs text-muted-foreground">
          <input
            type="checkbox"
            checked={soundEnabled}
            onChange={(e) => setSoundEnabled(e.target.checked)}
            className="size-4 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          />
          Enable sound for this demo (off by default)
        </label>

        <div className="flex gap-2">
          <button
            type="button"
            onClick={starred}
            className="flex-1 rounded-md bg-muted px-3 py-2 text-sm text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            ☆ Star
          </button>
          <button
            type="button"
            onClick={deleted}
            className="flex-1 rounded-md bg-destructive/15 px-3 py-2 text-sm text-destructive ring-1 ring-destructive focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
          >
            Delete account
          </button>
        </div>
        <p role="status" aria-live="polite" className="mt-3 min-h-4 text-xs text-muted-foreground">
          {last}
        </p>
      </div>
      <p className="mt-4 text-xs text-success">
        Light tick for the star, low weighty tone for the delete — the sound encodes the stakes.
      </p>
    </div>
  );
}
```

## References

- [Raphael Salaja: sounds-on-the-web SKILL.md](https://github.com/raphaelsalaja/skill/blob/main/skills/sounds-on-the-web/SKILL.md)
- [MDN: OscillatorNode](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode)
