Detent web components
By Shane Logsdon

Media & effects

Package Box

Implemented

Interactive 3D package box with flap folding and rising document motion

detent-package-box

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

9.9 KB gzip for the class module

Variants

Activate the button under a box (or click the cardboard, or press Escape once it is open) to cut the tape, fold the flaps and raise the packing slip. The box in this section reserves the full open height, so nothing on the page moves while it is closed. Both boxes keep the document inert and aria-hidden until they open, and each user toggle emits one composed detent-open-change with { open }; property writes emit none.

Recorded states

  • open recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • reduced motion recorded state; no setter
  • active recorded state; no setter

Usage

Import

import '/components/package-box/define.js';

// or register it yourself
import { DetentPackageBox } from '/components/package-box/package-box.js';

Attributes

  • close-label
  • default-open
  • hint
  • inner
  • max-width
  • minor
  • open
  • open-label
  • outer
  • paper-class-name
  • reserve-space
  • tape
  • transform

Properties

  • angle
  • colors
  • cut
  • data
  • onOpenChange
  • outerStyle
  • slip

Slots

  • default
  • flap
  • label
  • symbols
  • tape-half

Events

  • detent-open-change — { open: boolean }

Example

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

<detent-package-box
  close-label="Close"
  default-open
  hint
  inner
  max-width="460"
  minor
  open
  open-label="Click to open"
  outer
  paper-class-name=""
  reserve-space
  tape
  transform
></detent-package-box>

Property-only values

const element = document.querySelector('detent-package-box');
if (element !== null) {
  // onOpenChange: (open: boolean) => void
  element.onOpenChange = () => {};
  // slip: PackageSlip
  element.slip = undefined;
  // colors: PackageBoxColors
  element.colors = undefined;
}

Source

The pinned upstream source for this row:

  • src/components/ui/PackageBox.tsx sha256 12f25d921e82…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
close-labelcloseLabelstring'Close'Kebab-case attribute; camelCase property.
default-opendefaultOpenbooleanfalsePresence semantics: present means true, absent means false.
hinthintbooleantruePresence semantics: present means true, absent means false.
inner———Recorded in the catalog as an attribute.
max-widthmaxWidthnumber460Kebab-case attribute; camelCase property.
minor———Recorded in the catalog as an attribute.
openopenbooleanundefinedPresence semantics: present means true, absent means false.
open-labelopenLabelstring'Click to open'Kebab-case attribute; camelCase property.
outer———Recorded in the catalog as an attribute.
paper-class-namepaperClassNamestring''Kebab-case attribute; camelCase property.
reserve-spacereserveSpacebooleanfalsePresence semantics: present means true, absent means false.
tapetapebooleantruePresence semantics: present means true, absent means false.
transform———Recorded in the catalog as an attribute.

Properties

Properties
PropertyTypeDefaultNotes
angle——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
colorsPackageBoxColorsundefinedProperty-only: assign from script; not accepted as an attribute.
cut——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
data——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
onOpenChange(open: boolean) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
outerStyle——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
slipPackageSlipDEFAULT_SLIPProperty-only: assign from script; not accepted as an attribute.

Events

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

Slots

Slots
SlotNotes
defaultConsumer content; rendered through a slot, never re-created.
flapConsumer content; rendered through a slot, never re-created.
labelConsumer content; rendered through a slot, never re-created.
symbolsConsumer content; rendered through a slot, never re-created.
tape-halfConsumer 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.
  • children, label, symbols, flap and tapeHalf React nodes become the default, label, symbols, flap and tape-half slots; label={false} and symbols={false} are expressed by slotting an empty element.
  • className is not an attribute; styling uses the documented parts and custom properties.
  • onOpenChange becomes the composed detent-open-change event plus an optional property hook; programmatic writes emit nothing.
  • slip, colors and outerStyle stay property-only plain data; angle, cut and data are read-only snapshots; inner, outer, transform and minor are presentation attributes with the pinned source defaults.
  • PackageBoxDemo is not ported (a page-level demo wrapper); the docs example and preview replace it.
  • clamp:false flap slices are clamped to the pinned end angles (-238deg and -212deg) instead of extrapolating.
  • WCAG corrections: muted slip ink is opaque #5c5850 (3.4:1 to 6.78:1) and the stamp is #a8431c at full opacity (4.34:1 to 5.77:1).
  • hint=false keeps a visible focus target: the hidden controller row becomes visible while focused.

What it does

A 3D cardboard parcel: activating the controller (or clicking the box, or pressing Escape) cuts the tape, folds the flaps and raises a packing slip out of the box.

Upstream behaviour
  • Pure CSS 3D cardboard box geometry with authentic grain texture and lighting gradients
  • Choreographed master timeline: seam tape cut, sequential flap folding, and smooth paper rise
  • Responsive stage scaling that fluidly adapts to any container width via ResizeObserver
  • Customizable packing slip with itemization, totals, shipping info, and tactile rubber stamp
  • Configurable color tokens for cardboard, tape, paper, typography, and stamp accent
  • Full accessibility support with aria-controls, Esc dismiss, and prefers-reduced-motion adaptation
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.
  • children, label, symbols, flap and tapeHalf React nodes become the default, label, symbols, flap and tape-half slots; label={false} and symbols={false} are expressed by slotting an empty element.
  • className is not an attribute; styling uses the documented parts and custom properties.
  • onOpenChange becomes the composed detent-open-change event plus an optional property hook; programmatic writes emit nothing.
  • slip, colors and outerStyle stay property-only plain data; angle, cut and data are read-only snapshots; inner, outer, transform and minor are presentation attributes with the pinned source defaults.
  • PackageBoxDemo is not ported (a page-level demo wrapper); the docs example and preview replace it.
  • clamp:false flap slices are clamped to the pinned end angles (-238deg and -212deg) instead of extrapolating.
  • WCAG corrections: muted slip ink is opaque #5c5850 (3.4:1 to 6.78:1) and the stamp is #a8431c at full opacity (4.34:1 to 5.77:1).
  • hint=false keeps a visible focus target: the hidden controller row becomes visible while focused.
Previous: OTP Input Next: Particle Delete

Docs

Components

Project