Physical-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
Preset

Installation

pnpm dlx shadcn@latest add https://kagadmodyaa.vercel.app/r/tactile-toggle.json

Needs 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/effects
import { 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

  • Aluminium
  • Graphite
  • Porcelain
  • Walnut
  • Signal

Props

NameTypeDefaultDescription
preset'Aluminium' | 'Graphite' | 'Porcelain' | 'Walnut' | 'Signal'none (the defaults below)A named theme. Any prop you pass as well overrides the preset’s value.
bodystring'#c3c8d0'The machined body: the slide track, flip plate, button skirt and dial base. A hex colour.
knobstring'#e9ebef'The part you touch: the slide knob, lever ball, button cap and dial. A hex colour.
trimstring'#aeb4bd'Metal trim: bezels, the flip guard, the dial pointer. A hex colour.
accentstring'#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.
brushednumber0.6How strong the brushed, grain or wood texture is. Range 0 to 1.
depthnumber1How far the pieces stand off the surface. Range 0 to 1.5.
glownumber1Strength of the LED glow and the light it spills. Range 0 to 1.5.
stiffnessnumber1Higher snaps faster. Range 0.4 to 2.
bouncenumber0.50 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.
labelsbooleantrueEngraved 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.
soundbooleanfalseA short synthesised click on each change, only after a user gesture.
hapticsbooleanfalseA tiny vibration on each change, where the device supports it.
classNamestring—Extra class on the root element. The effect fills its parent: give the parent a height.
styleCSSProperties—Inline style on the root element.

Files

What the command adds to your project, and where.

  • components/effects/tactile-toggle/tactile-toggle.tsx

    The component.

  • components/effects/tactile-toggle/toggle.tsx

    The component.

  • components/effects/tactile-toggle/flip.tsx

    The component.

  • components/effects/tactile-toggle/push.tsx

    The component.

  • components/effects/tactile-toggle/dial.tsx

    The component.

  • components/effects/tactile-toggle/showcase.tsx

    The component.

  • components/effects/tactile-toggle/core.tsx

    The component.

  • components/effects/tactile-toggle/hooks.ts

    The component.

  • components/effects/tactile-toggle/look.ts

    The component.

  • components/effects/tactile-toggle/feel.ts

    The component.

  • components/effects/tactile-toggle/styles.ts

    The component.

  • components/effects/tactile-toggle/stage-styles.ts

    The component.

  • components/effects/tactile-toggle/textures.ts

    The component.

  • lib/effect-runtime.ts

    Shared 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.

See all effects