Detent web components
By Shane Logsdon

Buttons & actions

Book Call Button

Implemented

Expanding accent pill with phone hover state

detent-book-call-button

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

2.8 KB gzip for the class module

Variants

Talk to sales Book a call

Recorded states

  • hover recorded state; no setter
  • focus-visible recorded state; no setter
  • expanded recorded state; no setter
  • collapsed recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

import '/components/book-call-button/define.js';

// or register it yourself
import { DetentBookCallButton } from '/components/book-call-button/book-call-button.js';

Attributes

  • href

Properties

  • onClick

Slots

  • default

Events

  • detent-click — {}

Example

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

<detent-book-call-button
  href
></detent-book-call-button>

Property-only values

const element = document.querySelector('detent-book-call-button');
if (element !== null) {
  // onClick: () => void
  element.onClick = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/BookCallButton.tsx sha256 52a758274628…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
hrefhrefstringundefinedKebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
onClick() => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-clickonClick() => voidComposed, bubbles; fires only for a user action.

Slots

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

CSS customisation

  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • 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.
  • `href` uses a scheme allow-list with a fallback (PC-11.3); `onClick` becomes `detent-click` and is ignored in link mode.
  • `children` becomes the default slot with a 'Book a call' fallback, and the name comes from the visible text instead of a duplicated aria-label (WCAG 2.5.3); `className` maps to parts and custom properties.
  • Framer springs (190/24/0.85, 280/24/0.7, 420/24) become CSS transitions with a monotonic settle (PC-10.4).
  • The focus ring uses --detent-color-accent instead of upstream #82ff22, which measures 1.29:1 on white (WCAG 1.4.11 correction).
  • The pill width is min(306px, 100%) instead of a fixed 306px (PC-9.6); reduced motion also removes the hover lift and tap compression.

What it does

A dark pill whose accent capsule covers 36% of its width at rest and expands to the full pill on hover, keyboard focus or pointer down; the label slides out while a phone glyph fades in. It renders a real <button> by default and a real <a> when href is provided.

Upstream behaviour
  • Three independently-timed motion layers: expanding capsule, label/arrow, and phone icon
  • Capsule grows from "left center" so the expansion feels directional, not scale-from-middle
  • Hover lift (1.01) and tap compression (0.975) on the pill itself, independent of the inner choreography
  • Renders as <a> when href is provided so the same component works on a marketing page or a form action
  • prefers-reduced-motion collapses every spring to an instant state change — no travel, no entrance
  • Decorative SVGs are aria-hidden; the label is conveyed by aria-label on the interactive root
  • Native <button> / <a> elements with focus-visible ring tied to the accent color
Differences from upstream
  • React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • 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.
  • `href` uses a scheme allow-list with a fallback (PC-11.3); `onClick` becomes `detent-click` and is ignored in link mode.
  • `children` becomes the default slot with a 'Book a call' fallback, and the name comes from the visible text instead of a duplicated aria-label (WCAG 2.5.3); `className` maps to parts and custom properties.
  • Framer springs (190/24/0.85, 280/24/0.7, 420/24) become CSS transitions with a monotonic settle (PC-10.4).
  • The focus ring uses --detent-color-accent instead of upstream #82ff22, which measures 1.29:1 on white (WCAG 1.4.11 correction).
  • The pill width is min(306px, 100%) instead of a fixed 306px (PC-9.6); reduced motion also removes the hover lift and tap compression.
Previous: Batch Gesture Tray Next: Branching Submenu

Docs

Components

Project