Detent web components
By Shane Logsdon

Buttons & actions

Magnetic Button

Implemented

Cursor-aware spring translation physics

detent-magnetic-button

Port status
Catalog status
implemented
Origin
upstream
Phase
1
Wave
phase-1-pilot
Group
Buttons & actions
Create project

4.9 KB gzip for the class module · inlines the shared spring

Variants

Invite teammate Export report Cancel

Recorded states

  • focus-visible recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

import '/components/magnetic-button/define.js';

// or register it yourself
import { DetentMagneticButton } from '/components/magnetic-button/magnetic-button.js';

Attributes

  • disabled
  • form
  • glow
  • name
  • size
  • strength
  • type
  • value
  • variant

Properties

  • strength

Slots

  • default

Events

No events are recorded for this component.

Example

Runnable example: site/examples/magnetic-button.ts. The snippet below is generated from the catalogued attributes.

<detent-magnetic-button
  disabled
  form=""
  glow
  name=""
  size="md"
  strength="0.35"
  type="button"
  value=""
  variant="primary"
></detent-magnetic-button>

Source

The pinned upstream source for this row:

  • src/components/ui/MagneticButton.tsx sha256 dbce09819e4e…

Native files

The native module and its artifacts:

  • src/components/magnetic-button/magnetic-button.ts
  • src/components/magnetic-button/styles.css
  • src/components/magnetic-button/define.ts
  • docs/components/magnetic-button.md
  • site/examples/magnetic-button.ts
  • tests/browser/magnetic-button.spec.ts
  • evidence/components/magnetic-button.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
disableddisabledbooleanfalsePresence semantics: present means true, absent means false.
formformstring''Kebab-case attribute; camelCase property.
glowglowbooleantruePresence semantics: present means true, absent means false.
namenamestring''Kebab-case attribute; camelCase property.
sizesize'sm' | 'md' | 'lg''md'Kebab-case attribute; camelCase property.
strengthstrengthnumber0.35Kebab-case attribute; camelCase property.
typetype'button' | 'submit' | 'reset''button'Kebab-case attribute; camelCase property.
valuevaluestring''Kebab-case attribute; camelCase property.
variantvariant'primary' | 'secondary' | 'outline' | 'ghost''primary'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
strengthnumber0.35Property-only: assign from script; not accepted as an attribute.

Events

No custom events are recorded for this component.

Slots

Slots
SlotNotes
defaultConsumer content; rendered through a slot, never re-created.

CSS customisation

  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • Offset is measured from the host box (static) rather than the translated button (moving): settled offset is exactly strength x distance, upstream's was strength x distance / (1 + strength); same visual result at these offsets.
  • Touch input never drags the surface (upstream bound mouse handlers only); touch activation itself is engine-limited in Firefox.
  • Upstream spread React.ButtonHTMLAttributes onto its button; the native element is form-associated (ElementInternals) and maps type/disabled/name/value/form from those attributes. submit uses a transient hidden native submit button as the submitter so name/value are carried, so event.submitter is that proxy, not the host. Disabled (own or via fieldset) suppresses host.click(), form actions and pointer physics. className is replaced by parts.
  • Positional springs use mass 1 (Framer Motion default; upstream states only stiffness 280 / damping 20).
  • whileTap scale 0.96 is a CSS :active scale, suppressed under reduced motion.
  • Variant palette reads the --detent-color-* theme tokens instead of the upstream fixed dark hex values (#fafafa/#0e0e0e/#1f1f1f/#a1a1a1), which rendered light text on a light page. Dark mode keeps the upstream look; --detent-magnetic-primary-bg/--detent-magnetic-primary-text and --detent-magnetic-glow-color still override per instance.
  • glow attribute absence restores the upstream default true (HTML presence semantics); the property glow = false is the opt-out, so glow="false" still means true.

What it does

A real button whose surface is pulled towards the pointer by two positional springs and returns to the origin on leave.

Upstream behaviour
  • Spring physics coordinate tracking via Framer Motion
  • Configurable pull strength and threshold
  • Subtle ambient glow gradient reflection
  • Four restrained surface styles: Primary, Secondary, Outline, Ghost
Differences from upstream
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • Offset is measured from the host box (static) rather than the translated button (moving): settled offset is exactly strength x distance, upstream's was strength x distance / (1 + strength); same visual result at these offsets.
  • Touch input never drags the surface (upstream bound mouse handlers only); touch activation itself is engine-limited in Firefox.
  • Upstream spread React.ButtonHTMLAttributes onto its button; the native element is form-associated (ElementInternals) and maps type/disabled/name/value/form from those attributes. submit uses a transient hidden native submit button as the submitter so name/value are carried, so event.submitter is that proxy, not the host. Disabled (own or via fieldset) suppresses host.click(), form actions and pointer physics. className is replaced by parts.
  • Positional springs use mass 1 (Framer Motion default; upstream states only stiffness 280 / damping 20).
  • whileTap scale 0.96 is a CSS :active scale, suppressed under reduced motion.
  • Variant palette reads the --detent-color-* theme tokens instead of the upstream fixed dark hex values (#fafafa/#0e0e0e/#1f1f1f/#a1a1a1), which rendered light text on a light page. Dark mode keeps the upstream look; --detent-magnetic-primary-bg/--detent-magnetic-primary-text and --detent-magnetic-glow-color still override per instance.
  • glow attribute absence restores the upstream default true (HTML presence semantics); the property glow = false is the opt-out, so glow="false" still means true.
Previous: Folder Cards Next: Meteors

Docs

Components

Project