Detent web components
By Shane Logsdon

Buttons & actions

PressButton

Implemented

Compresses slightly on press, then settles

detent-press-button

Port status
Catalog status
implemented
Origin
upstream
Phase
2
Wave
phase-2-controls
Group
Buttons & actions
Save changes

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

Variants

Cancel Learn more Skip Small Large + Full width Strong press Disabled

Recorded states

  • focus-visible recorded state; no setter
  • pressed recorded state; no setter
  • disabled recorded state; no setter
  • dark theme recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

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

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

Attributes

  • disabled
  • full-width
  • press-strength
  • size
  • variant

Properties

  • variant
  • size
  • pressStrength
  • fullWidth
  • disabled

Slots

  • default

Events

No events are recorded for this component.

Example

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

<detent-press-button
  disabled
  full-width
  press-strength="0.04"
  size="md"
  variant="primary"
></detent-press-button>

Source

The pinned upstream source for this row:

  • src/components/ui/PressButton.tsx sha256 6f985aaeb748…

Native files

The native module and its artifacts:

  • src/components/press-button/press-button.ts
  • src/components/press-button/styles.css
  • src/components/press-button/define.ts
  • docs/components/press-button.md
  • site/examples/press-button.ts
  • tests/browser/press-button.spec.ts
  • evidence/components/press-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.
full-widthfullWidthbooleanfalsePresence semantics: present means true, absent means false.
press-strengthpressStrengthnumber0.04Kebab-case attribute; camelCase property.
sizesize'sm' | 'md' | 'lg' | 'icon''md'Kebab-case attribute; camelCase property.
variantvariant'primary' | 'secondary' | 'outline' | 'ghost''primary'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
variant'primary' | 'secondary' | 'outline' | 'ghost''primary'Property-only: assign from script; not accepted as an attribute.
size'sm' | 'md' | 'lg' | 'icon''md'Property-only: assign from script; not accepted as an attribute.
pressStrengthnumber0.04Property-only: assign from script; not accepted as an attribute.
fullWidthbooleanfalseProperty-only: assign from script; not accepted as an attribute.
disabledbooleanfalseProperty-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.
  • The layered scaleX/scaleY and whileTap compose into one press amount on the shared spring with the upstream springSnappy constants.
  • `press-strength` clamps to 0.01-0.06 and reflects its normalised form; the size scale applies to ghost too, because upstream skipped size classes for ghost and left no minimum height (WCAG 2.5.8).
  • Disabled mutes surface and text without fading text (upstream opacity-30 measured 1.9:1); the secondary/outline boundary is a color-mix of the text token because upstream --border measures 1.4:1 on white.
  • Hover mixes the surface instead of hover:opacity-90; reduced motion removes the squash and keeps a brightness cue.
  • aria-label and aria-labelledby are forwarded to the internal control so `size="icon"` has a name.

What it does

A button that compresses slightly on press — a layered scaleX/scaleY squash composed with the tap scale — and settles back with the snappy spring, so the interaction feels physical without moving the layout.

Upstream behaviour
  • Layered scaleX + scaleY squash for an organic compression on press
  • Tunable pressStrength (0-1) to control how much the button compresses
  • Snappy spring with a tiny overshoot so the button "settles" instead of snapping
  • 4 visual variants: Primary, Secondary, Outline, Ghost — light/dark theme aware
  • 4 size dimensions: Small (sm), Medium (md), Large (lg), and square Icon
  • Full keyboard accessibility with focus-ring and disabled state
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.
  • The layered scaleX/scaleY and whileTap compose into one press amount on the shared spring with the upstream springSnappy constants.
  • `press-strength` clamps to 0.01-0.06 and reflects its normalised form; the size scale applies to ghost too, because upstream skipped size classes for ghost and left no minimum height (WCAG 2.5.8).
  • Disabled mutes surface and text without fading text (upstream opacity-30 measured 1.9:1); the secondary/outline boundary is a color-mix of the text token because upstream --border measures 1.4:1 on white.
  • Hover mixes the surface instead of hover:opacity-90; reduced motion removes the squash and keeps a brightness cue.
  • aria-label and aria-labelledby are forwarded to the internal control so `size="icon"` has a name.
Previous: Pill Navigation Next: Pricing

Docs

Components

Project