Buttons & actions
Book Call Button
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
Recorded states
hoverrecorded state; no setterfocus-visiblerecorded state; no setterexpandedrecorded state; no settercollapsedrecorded state; no setterreduced motionrecorded 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.tsxsha256 52a758274628…
Native files
The native module and its artifacts:
src/components/book-call-button/book-call-button.tssrc/components/book-call-button/styles.csssrc/components/book-call-button/define.tsdocs/components/book-call-button.mdsite/examples/book-call-button.tstests/browser/book-call-button.spec.tsevidence/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
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
href | href | string | undefined | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
onClick | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-click | onClick | () => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
default | Consumer 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.