Tactile Toggle
NewFreePhysical-feeling switches you want to touch: a slide toggle, an aircraft flip switch under its guard, a latching push button and a detented rotary dial, in machined metal, ceramic and wood with spring motion, glowing LEDs and optional click sounds.
- toggle
- switch
- button
- dial
- skeuomorphic
- spring
- led
- hardware
- css
- accessible
Installation
pnpm dlx shadcn@latest add https://kagadmodyaa.vercel.app/r/tactile-toggle.jsonNeeds a React + Tailwind project set up with shadcn. Copies the effect into your project, along with lib/effect-runtime.ts, the small WebGL layer it runs on. No other dependencies.
Or from npm
pnpm add @kagadmodyaa/effectsimport { TactileToggle } from '@kagadmodyaa/effects/tactile-toggle'One package with every free effect, one import path each. No Tailwind or shadcn needed, and no stylesheet to import: the few layout rules an effect needs come with it. The npm package and the command above ship the same source.
Usage
Use it like any other component. Choose a theme with preset; any prop you pass as well overrides the preset.
import { TactileToggle, TactileFlip, TactilePush, TactileDial } from '@/components/effects/tactile-toggle/tactile-toggle'
export function Panel() {
const [armed, setArmed] = useState(false)
return (
<>
{/* Slide toggle: uncontrolled, or controlled with checked + onCheckedChange */}
<TactileToggle preset="Graphite" label="Monitor" defaultChecked onCheckedChange={v => setMonitor(v)} />
{/* Flip switch under its guard */}
<TactileFlip preset="Signal" label="Arm" checked={armed} onCheckedChange={setArmed} />
{/* Latching push button with travel and an LED ring */}
<TactilePush preset="Porcelain" label="Power" sound haptics />
{/* Rotary dial with 3 to 5 detents: drag, tap or arrow keys */}
<TactileDial preset="Walnut" label="Mode" steps={4} stepLabels={['A', 'B', 'C', 'D']} defaultValue={1} />
</>
)
}
// Every piece takes the same look props (preset, body, knob, trim, accent, finish, glow, brushed, depth, stiffness,
// bounce, size, sound, haptics, labels, tone), plus ordinary button or div props (ref, aria-*, id, onClick).
// The stage above is <TactileToggleShowcase />, also exported, if you want the same panel.<TactileToggle preset="Graphite" body={"#c3c8d0"} />Presets
AluminiumGraphitePorcelainWalnutSignal
Props
| Name | Type | Default | Description |
|---|---|---|---|
| preset | 'Aluminium' | 'Graphite' | 'Porcelain' | 'Walnut' | 'Signal' | none (the defaults below) | A named theme. Any prop you pass as well overrides the preset’s value. |
| body | string | '#c3c8d0' | The machined body: the slide track, flip plate, button skirt and dial base. A hex colour. |
| knob | string | '#e9ebef' | The part you touch: the slide knob, lever ball, button cap and dial. A hex colour. |
| trim | string | '#aeb4bd' | Metal trim: bezels, the flip guard, the dial pointer. A hex colour. |
| accent | string | '#2ff08c' | Colour of the LEDs. A hex colour. |
| finish | 'brushed' | 'matte' | 'gloss' | 'wood' | 'brushed' | Surface of the body: brushed metal, fine matte grain, gloss ceramic or a wood inlay. |
| brushed | number | 0.6 | How strong the brushed, grain or wood texture is. Range 0 to 1. |
| depth | number | 1 | How far the pieces stand off the surface. Range 0 to 1.5. |
| glow | number | 1 | Strength of the LED glow and the light it spills. Range 0 to 1.5. |
| stiffness | number | 1 | Higher snaps faster. Range 0.4 to 2. |
| bounce | number | 0.5 | 0 settles without overshoot; high values wobble. Range 0 to 1. |
| size | 'sm' | 'md' | 'lg' | 'md' | sm 12px, md 16px or lg 20px base unit; the whole piece scales with it. |
| labels | boolean | true | Engraved captions under each piece (and the dial's detent labels). |
| tone | 'auto' | 'dark' | 'light' | 'auto' | Which surface the pieces sit on. auto follows the page theme. |
| sound | boolean | false | A short synthesised click on each change, only after a user gesture. |
| haptics | boolean | false | A tiny vibration on each change, where the device supports it. |
| className | string | — | Extra class on the root element. The effect fills its parent: give the parent a height. |
| style | CSSProperties | — | Inline style on the root element. |
Files
What the command adds to your project, and where.
components/effects/tactile-toggle/tactile-toggle.tsxThe component.
components/effects/tactile-toggle/toggle.tsxThe component.
components/effects/tactile-toggle/flip.tsxThe component.
components/effects/tactile-toggle/push.tsxThe component.
components/effects/tactile-toggle/dial.tsxThe component.
components/effects/tactile-toggle/showcase.tsxThe component.
components/effects/tactile-toggle/core.tsxThe component.
components/effects/tactile-toggle/hooks.tsThe component.
components/effects/tactile-toggle/look.tsThe component.
components/effects/tactile-toggle/feel.tsThe component.
components/effects/tactile-toggle/styles.tsThe component.
components/effects/tactile-toggle/stage-styles.tsThe component.
components/effects/tactile-toggle/textures.tsThe component.
lib/effect-runtime.tsShared by every shader effect: resizing, pausing, reduced motion, context loss and the no-WebGL fallback. Installed once.
Behaviour
- Each piece is a real control. The slide, flip and push are a <button role="switch" aria-checked>; the dial is a role="slider" with aria-valuenow and aria-valuetext. Space and Enter flip a switch; Arrow keys, Home and End step the dial. A visible focus ring, disabled, controlled (checked / value) and uncontrolled (defaultChecked / defaultValue) all work, and refs are forwarded.
- Motion is a small damped spring written straight to CSS custom properties each frame, so nothing re-renders while it moves. Stiffness and bounce are props. The slide knob can be dragged and settles into the nearer end; the dial resists between detents and clicks over each one.
- Everything is DOM, CSS and inline SVG: layered gradients, inset and outset shadows, specular highlights and a brushed, grain or wood texture from a tiny SVG filter. It stays sharp at any size and needs no canvas, WebGL or image files.
- Under prefers-reduced-motion every state change is instant: no spring, no easing, no overshoot.
- Sound is off by default. With sound on, a click is synthesised with WebAudio (no files) and only ever plays from a click, key press or drag, never before the first gesture. With haptics on, navigator.vibrate(8) buzzes where supported.
- Tone is 'auto' by default: it follows a dark or light class on <html> (or the OS setting) and lightens or deepens shadows and engraving to suit. Pieces are sized in em, so size or a parent font-size scales the whole piece.