Detent web components
By Shane Logsdon

Media & effects

Hanging Amulet

Implemented

Pointer-driven hanging amulet with layered spring physics

detent-hanging-amulet

Port status
Catalog status
implemented
Origin
upstream
Phase
4
Wave
phase-4-pointer
Group
Media & effects

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

Variants

Move the pointer across an amulet: the cord, the metallic cap and the disc each swing by a different amount from the same normalized pointer offset (cord ±9°, cap ±7°, disc ±5°) on top of the swing wrapper's max-rotation. The springs are the pinned stiffness-x/stiffness-y/damping values, so leaving an amulet eases it back to its rest pose and the loop stops. Each amulet carries one role="img" name; the painted SVG is decorative.

With prefers-reduced-motion: reduce no pointer listener is wired at all and every amulet rests in its static pose. Without a PointerEvent constructor the component degrades to the same rest pose.

Recorded states

  • reduced motion recorded state; no setter
  • completed recorded state; no setter

Usage

Import

import '/components/hanging-amulet/define.js';

// or register it yourself
import { DetentHangingAmulet } from '/components/hanging-amulet/hanging-amulet.js';

Attributes

  • caption
  • damping
  • max-rotation
  • stiffness-x
  • stiffness-y

Properties

No properties are recorded for this component.

Slots

No slots are recorded for this component.

Events

No events are recorded for this component.

Example

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

<detent-hanging-amulet
  caption
  damping="9"
  max-rotation="13"
  stiffness-x="55"
  stiffness-y="45"
></detent-hanging-amulet>

Source

The pinned upstream source for this row:

  • src/components/ui/EvilEye.tsx sha256 63d4592bc72a…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
captioncaptionstringundefinedKebab-case attribute; camelCase property.
dampingdampingnumber9Kebab-case attribute; camelCase property.
max-rotationmaxRotationnumber13Kebab-case attribute; camelCase property.
stiffness-xstiffnessXnumber55Kebab-case attribute; camelCase property.
stiffness-ystiffnessYnumber45Kebab-case attribute; camelCase property.

Properties

No property-only values are recorded for this component.

Events

No custom events are recorded for this component.

Slots

No slots are recorded for this component.

CSS customisation

  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The upstream duplicate accessible name (aria-label plus an sr-only span) collapses to the single host aria-label.
  • The hanging line uses --detent-hanging-amulet-ceiling (#6b7280) instead of neutral-300/500 (1.4:1 on white), an accessibility correction.
  • Gradient ids are host-local (detent-hanging-amulet-<n>-...) so two instances cannot collide.

What it does

A hanging amulet painted as one SVG whose cord, cap and disc swing with layered spring physics as the pointer moves across the element.

Upstream behaviour
  • Pointer position drives a normalized MotionValue pipeline (no React renders per frame)
  • Layered secondary motion — braided cord, metallic cap, and amulet disc each swing independently
  • Painted SVG with radial gradients for the navy disc, sky-blue ring, and the signature pupil glint
  • Configurable spring stiffness, damping, and max rotation for tuning the physical feel
  • Geometry-aware normalization that adapts to the actual container size
  • Respects prefers-reduced-motion with a clean static rest pose
  • Graceful degradation when pointer events are unavailable
Differences from upstream
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • The upstream duplicate accessible name (aria-label plus an sr-only span) collapses to the single host aria-label.
  • The hanging line uses --detent-hanging-amulet-ceiling (#6b7280) instead of neutral-300/500 (1.4:1 on white), an accessibility correction.
  • Gradient ids are host-local (detent-hanging-amulet-<n>-...) so two instances cannot collide.
Previous: Status Island Next: Expandable Data Row

Docs

Components

Project