Interaction Choreography

Radius & Shadow Tuner

Drag handles to adjust a box's corner radius, shadow and padding with live readouts and the resulting CSS — ideal for demonstrating design tokens in docs.

An interactive specimen for docs sites and design-system pages. The centre box defaults to 240×160: drag the round top-right handle inward along the diagonal to change the corner radius, the handle below the box downward to deepen the shadow, and the right-hand handle sideways to change the padding. Each handle carries a readout label that follows it, with digits that roll into place. The code panel underneath mirrors border-radius, box-shadow and padding and copies them in one click. Values snap to multiples of step and are clamped to max; both controlled and uncontrolled modes are supported, and initial values can come straight from your design tokens.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add radius-shadow-tuner.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

1
500
Active edited usageDefault props
<RadiusShadowTuner/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/radius-shadow-tuner.en.json

Or, once registries are configured in components.json, install it as @webberui/radius-shadow-tuner.

Usage

import { RadiusShadowTuner } from "@/components/ui/radius-shadow-tuner";

<RadiusShadowTuner
  defaultValues={{ radius: 24, shadow: 32, padding: 16 }}
  step={4}
  onChange={(v) => console.log(v.radius, v.shadow, v.padding)}
>
  <p className="text-sm font-semibold">Order shipped</p>
</RadiusShadowTuner>

// Radius only, capped at 40px
<RadiusShadowTuner properties={["radius"]} max={{ radius: 40 }} showCode={false} />

Override the CSS variables on an ancestor to restyle it:

.my-docs {
  --wb-radius-drag-accent: #3b82f6; /* handle and readout colour */
  --wb-radius-drag-roll: 160ms; /* per-digit roll duration */
}

Props

PropTypeDefaultDescription
valuesRadiusShadowValues—Controlled values; when omitted the component manages them internally (uncontrolled)
defaultValuesPartial<RadiusShadowValues>{ radius: 16, shadow: 24, padding: 16 }Initial values in uncontrolled mode — pass your design tokens straight in; missing fields use the built-in defaults
onChange(values: RadiusShadowValues) => void—Called whenever any value changes, with the full set of three values
propertiesRadiusShadowProperty[]["radius", "shadow", "padding"]Which handles to show (also decides which CSS the code panel lists)
maxPartial<RadiusShadowValues>{ radius: 64, shadow: 48, padding: 48 }Per-property maximum (px); missing fields use the defaults
stepnumber1Step for dragging and arrow keys; values snap to multiples of it (typically 1 or 4)
showCodebooleantrueShow the CSS code panel and copy button underneath
stiffnessnumber500Stiffness of the spring the handle bounces back with on release
dampingnumber30Damping of the spring the handle bounces back with on release
childrenReact.ReactNode—Content rendered inside the box; a hint is shown when omitted
classNamestring—Forwarded to the outermost container

RadiusShadowValues

FieldTypeDefaultDescription
radiusnumber16Corner radius (px)
shadownumber24Shadow blur radius (px); the vertical offset is derived as 3/8 of the blur
paddingnumber16Padding (px)

RadiusShadowProperty is "radius" \| "shadow" \| "padding".

How it works

  • The radius handle sits at (width − r, r) inside the box; while dragging, only the pointer movement projected onto the inward diagonal counts, so dragging at a slight angle never jitters.
  • Dragging uses pointer events plus setPointerCapture, so the drag continues even when the pointer leaves the handle; there is no drag inertia — it stops the moment you let go.
  • The box's border-radius, box-shadow and padding are driven directly by motion values, so dragging does not wait for the whole component tree to re-render; React state only updates when the snapped value actually changes.
  • The shadow's y offset is 3/8 of the blur (a 32px blur gives a 12px offset), in a fixed rgba(0, 0, 0, 0.18).
  • When the right-hand padding handle is enabled, a dashed guide inside the box marks the edge of the content area.
  • Each readout digit is a 0–9 column scrolled to the target with translateY; the duration comes from --wb-radius-drag-roll.
  • The copy button calls navigator.clipboard.writeText inside the click handler and shows "Copied" for 1.5 seconds on success.

Accessibility

  • Every handle is a role="slider" with an aria-label (Radius / Shadow / Padding), aria-valuemin, aria-valuemax, aria-valuenow and an aria-valuetext that includes the unit
  • Once a handle is focused, the arrow keys move it ±step, Shift+arrow or PageUp / PageDown move it ±10, and Home / End jump to 0 and the maximum; keyboard focus shows a focus-visible ring
  • The rolling readout digits are hidden from assistive technology to avoid double announcements; the copy result is announced through a role="status" live region
  • When the user has reduce motion enabled at the system level, readout digits swap without rolling, and handles neither grow on press nor bounce back on release