---
title: SC Hover Sound
description: "Hover tones for layers: typed pitches and a 21-step list scale."
url: https://secretocho.design/lab/sc-hover-sound/
type: SoftwareSourceCode
status: working
published: 2026-10-05
---

# SC Hover Sound

> Hover tones for layers: typed pitches and a 21-step list scale.

component 26 / 28 · WORKING

- **type**: Code Override
- **platform**: Framer
- **license**: SecretoCho Design

![A still of the SC Hover Sound demo, as its module draws it with reduced motion.](https://secretocho.design/_astro/still.BZ_G5k-w_ZpjcWr.webp)

*fig 00 · live demo, ported from **UseHoverSound.tsx***

Hover-sound code overrides: square-wave enter/leave tones per element type and a 21-step C-major scale for lists, on one shared AudioContext.

UseHoverSound is the set of code overrides behind the old site’s hover sounds. Each override plays a short square-wave tone when the pointer enters a layer and a lower one when it leaves: six element types (card, box, title, image, section, button) with fixed pitches, and twenty-one list overrides (List00–List20) that climb a C-major scale so a list of items plays a melody as you sweep down it. One shared AudioContext, a global cooldown and a single “hovered” id keep nested layers from stacking tones.

- Overrides
- FX
- UI

## Main

### Overview

- **Built With**: Framer code overrides (Override), React hooks, Web Audio API
- **Layout Support**: None — overrides attach pointer handlers to any layer
- **Orientation**: n/a
- **Ideal Use Case**: Navigation, cards, service lists and buttons that should feel tactile

### Tags

- override
- hover
- sound
- web audio
- oscillator
- square wave
- scale
- ui feedback

### Code comment

UseHoverSound.tsx has no doc comment.

### Dependencies

- **Technologies**: Web Audio API (AudioContext, OscillatorNode, GainNode)
- **Notes**: No audio files and no libraries; tones are synthesized.

### Complexity

- **Logic Level**: Low–Medium (shared module state, cooldown, hovered-id guard, delayed leave)
- **Runtime Cost**: Very Low per tone; each mounted override adds two window listeners
- **Component Size**: ~6 KB TSX (6,261 bytes, 216 lines)
- **Notes**: Used on 2,779 layers of the old site (code/_usage.json), across 12 pages and 17 components.

## Guide

### Instructions

1. Add **UseHoverSound.tsx** to the project’s code files.
2. Select a layer, open Code Overrides, pick the file and an override: useHoverSoundCard, useHoverSoundBox, useHoverSoundTitle, useHoverSoundImage, useHoverSoundSection or useHoverSoundButton.
3. For lists apply useHoverSoundList00 … useHoverSoundList20 to the items in order; each step is the next note of the scale.
4. Sound starts after the visitor’s first click (or another user gesture such as a key press); pointer entry never resumes the AudioContext.

### Examples / Presets

Code overrides have no property controls, so there are no presets; the demo exposes the overrides as actions (enable sound, mute) and plays the tones from the table under \[REF\].

## Mechanics

### How it Works

- **Context**: the first override to mount creates one AudioContext (webkitAudioContext fallback) shared by the module; window click and pointerenter listeners are meant to resume it when suspended (only the click does: pointerenter does not bubble).
- **Tone**: playTone makes a square-wave OscillatorNode at the frequency with a GainNode at 0.12 ramping exponentially to 0.001 over 0.15 s, then stops; a 120 ms global cooldown (SOUND\_COOLDOWN) drops tones that come faster.
- **Enter / leave**: each override returns onPointerEnter and onPointerLeave only. A module-wide globalHoveredId makes sure one layer owns the hover; leaving schedules the leave tone 100 ms later. Entering a different layer within those 100 ms cancels it; re-entering the same layer does not (the id still matches, so the enter branch is skipped and the leave tone plays).
- **Types**: card 540 / 300 Hz, box 620 / 400, title 720 / 440, image 800 / 500, section 460 / 260, button 880 / 440 (enter / leave).
- **Lists**: useHoverSoundList(index) picks scale\[index % 21\] from a 21-note C-major scale, C4 261.63 Hz to B6 1975.53 Hz, and plays half that frequency on leave.

### Performance & Tuning

- Each override instance adds two window listeners on mount (removed on unmount); with thousands of layers that is thousands of listeners — apply overrides to the layers that matter.
- The cooldown caps the tone rate at about eight per second; oscillators are created per tone and stop themselves.

### Compatibility & Limits

- Evergreen browsers with Web Audio; every engine starts the context suspended until a user gesture; only the click listener helps.
- Pointer events only: on touch devices pointerenter fires with the tap.
- The resume listeners are click and pointerenter on window; pointerenter does not bubble, so the window listener fires only when the pointer enters the page itself — in practice the first click starts the sound.
- No volume control, mute or reduced-motion / sound preference in the file.

## Quality

### Accessibility

- Unexpected audio can disturb visitors: offer a visible mute and respect a stored preference in a fork.
- Tones are short (150 ms) and quiet (gain 0.12), and never carry information the layer does not also show.

### Security & Privacy

- No network calls, no audio files, no storage, no permissions (Web Audio output needs none; no microphone).
- Module-level state (AudioContext, hovered id, cooldown) is shared by every override on the page.
- Two window listeners (click, pointerenter) per mounted override; a console.warn if resuming the context fails.

## Reference

### Overrides

| override | type | enter → leave (Hz) | for |
| --- | --- | --- | --- |
| useHoverSoundCard | override | 540 → 300 | card layers |
| useHoverSoundBox | override | 620 → 400 | box layers |
| useHoverSoundTitle | override | 720 → 440 | title layers |
| useHoverSoundImage | override | 800 → 500 | image layers |
| useHoverSoundSection | override | 460 → 260 | section layers |
| useHoverSoundButton | override | 880 → 440 | button layers |
| useHoverSoundList00 … useHoverSoundList20 | override | scale[index]: 261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88, 523.25, 587.33, 659.25, 698.46, 783.99, 880.0, 987.77, 1046.5, 1174.66, 1318.51, 1396.91, 1567.98, 1760.0, 1975.53 → enter / 2 | list items, by index |

Code overrides take no property controls; the table lists the exported overrides with their enter → leave tones.

### Notes & Tips

- Keep the list indices in visual order so a sweep plays an ascending scale.
- Use useHoverSoundSection on large areas and the higher button tone on calls to action.
- Nested layers: each layer plays its enter tone as the pointer reaches it (the 120 ms cooldown permitting); the hovered-id guard only stops the outer layer’s leave tone once an inner layer has taken over.

### Troubleshooting / FAQ

No sound at first

The AudioContext is suspended until the visitor’s first click (or another user gesture); pointer entry never resumes it.

Fast sweeps skip notes

Expected: the 120 ms cooldown drops tones that arrive too quickly.

Two tones at once on nested layers

Not expected: entering another override layer cancels the pending leave tone, and the shared 120 ms cooldown stops a second tone from starting; at most the last 30 ms of a 150 ms tone overlaps the next.

A leave tone plays although the pointer came straight back

Expected: re-entering the same layer within 100 ms does not clear the pending leave; only entering another override layer does.

## Source

### Header comment

UseHoverSound.tsx has no doc comment.

[props \[REF\]](#reference)

### Key excerpt

Every hover sound is one square-wave oscillator: a fixed frequency, a 0.12 gain that decays exponentially over 150 ms, and a 120 ms cooldown so a fast sweep does not stutter.

*UseHoverSound.tsx · lines 57–88 · tsx*

```tsx
    const playTone = (freq: number, duration = 0.15) => {
        if (!audioCtx) return

        if (audioCtx.state === "suspended") {
            audioCtx
                .resume()
                .catch((err) =>
                    console.warn("Failed to resume AudioContext:", err)
                )
        }

        const now = performance.now()
        if (now - lastSoundTime < SOUND_COOLDOWN) return
        lastSoundTime = now

        const osc = audioCtx.createOscillator()
        const gain = audioCtx.createGain()

        osc.type = "square"
        osc.frequency.setValueAtTime(freq, audioCtx.currentTime)
        gain.gain.setValueAtTime(0.12, audioCtx.currentTime)
        gain.gain.exponentialRampToValueAtTime(
            0.001,
            audioCtx.currentTime + duration
        )

        osc.connect(gain)
        gain.connect(audioCtx.destination)

        osc.start()
        osc.stop(audioCtx.currentTime + duration)
    }
```

## Credits

### Credits

© SecretoCho Design

- **license**: SecretoCho Design

SecretoCho Design

### Disclaimer

Provided “as is” for visual/UX purposes. Validate performance, accessibility, and licensing within your project.

## Proves

- [Creative coding](https://secretocho.design/about/#capabilities)

## Hiring a design engineer?

I’m open to senior design engineer roles: remote from Belgrade, or relocation with visa sponsorship. Write to me, or read the CV first. Client work stays selective, one project at a time.

[sc@secretocho.design](mailto:sc@secretocho.design)

[read the CV](https://secretocho.design/cv/) · [LinkedIn](https://www.linkedin.com/in/secretocho/)
