Bevel
Browse the docs

Get started

Sound and haptics

Opt-in clicks, detents and thunks, synthesised in the browser. Off unless you turn them on.

Real switches make a noise, and the noise tells you the switch went. Bevel controls can do the same: a tick for each detent on a knob, a snap for a toggle, a thunk when a breaker or a plug lands. It's off by default, and stays off until your app turns it on.

Off: the controls are silent.

OnWeb search

Not listening

Turn it on

Install the feedback item (components that make sounds pull it in for you), then wrap the part of your app that should click in BevelFeedback. Drive it from a setting, so people who don't want sound never hear it.

Terminal
npx shadcn@latest add @bevel/feedback
app/providers.tsx
"use client"

import { BevelFeedback } from "@/lib/feedback"

export function Providers({ children, settings }) {
  return (
    <BevelFeedback sound={settings.sound} haptics={settings.haptics}>
      {children}
    </BevelFeedback>
  )
}

Your own controls

useBevelFeedback() returns click, detent, thunk and toggle. Call them from event handlers. Outside the provider, or with it off, they do nothing, so a component can call them unconditionally.

A button that clicks
const { click } = useBevelFeedback()

<button onClick={() => { click(); save() }}>Save</button>

How it works

  • Sounds are synthesised with the Web Audio API: short bursts of filtered noise, with a falling tone under the heavy ones. There are no audio files to load.
  • The audio context is created on the first sound, which always comes from a click or a key press, so browsers let it start.
  • Haptics use navigator.vibrate, which Android browsers support. Elsewhere they do nothing.
  • Only actions make a sound. Nothing clicks when a value changes on its own.
Sound is separate from reduced motion: someone who wants less movement may still want to hear a switch go. Give people a setting for each, and leave both off until they choose.