Detent web components
By Shane Logsdon

Media & effects

Photo Box

Implemented

Frosted paper box with fanning polaroids and tactile zoom inspection

detent-photo-box

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

8.0 KB gzip for the class module

Variants

Hover, tap or press Enter on the frosted cover to slide the drawer out; the polaroids fan and join the tab order. Activate a print to inspect it enlarged, then press Escape (or click anywhere) to put it back — focus returns to the print you inspected. Each user change emits one composed event: detent-open-change with { open } and detent-select with { id }. Prints are plain data; a src is accepted only for relative and http(s) URLs.

Recorded states

  • hover recorded state; no setter
  • open recorded state; no setter
  • closed recorded state; no setter
  • expanded recorded state; no setter
  • collapsed recorded state; no setter
  • reduced motion recorded state; no setter

Usage

Import

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

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

Attributes

  • open
  • subtitle
  • title

Properties

  • onOpenChange
  • onSelect
  • print
  • prints

Slots

No slots are recorded for this component.

Events

  • detent-open-change — { open: boolean }
  • detent-select — { id: string | null }

Example

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

<detent-photo-box
  open
  subtitle="Photo Box · 3 prints"
  title="写真箱"
></detent-photo-box>

Property-only values

const element = document.querySelector('detent-photo-box');
if (element !== null) {
  // prints: PhotoPrint[]
  element.prints = [];
  // onOpenChange: (open: boolean) => void
  element.onOpenChange = () => {};
  // onSelect: (id: string | null) => void
  element.onSelect = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/PhotoBox.tsx sha256 553ff585c289…

Native files

The native module and its artifacts:

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

API

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

Attributes

Attributes
AttributePropertyTypeDefaultNotes
openopenbooleanundefinedPresence semantics: present means true, absent means false.
subtitlesubtitlestring'Photo Box · 3 prints'Kebab-case attribute; camelCase property.
titletitlestring'写真箱'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
onOpenChange(open: boolean) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
onSelect(id: string | null) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
print——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
printsPhotoPrint[]DEFAULT_PRINTSProperty-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.
detent-selectonSelect(id: string | null) => voidComposed, bubbles; fires only for a user action.

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 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.
  • Events are user-driven only; upstream fired both callbacks from useEffect, including on mount and on controlled open writes.
  • Upstream Framer Motion springs (duration 0.55, bounce 0.12..0.24) become CSS transitions over cubic-bezier(0.23,1,0.32,1): 550 ms transform, 280 ms opacity and filter. Owner approved this spring-for-easing substitution on 2026-10-29 (PC-10.4); motion fidelity remains an L6 manual gate.
  • prints[].art (ReactNode) is dropped; the three default ids render local SVG art, an unknown id without src renders a neutral placeholder, and PhotoBoxDemo is not ported.
  • prints[].src is scheme-checked (R-3.1/R-3.2): relative and http(s) accepted, javascript:, data:, vbscript: and file: refused with a development diagnostic and local-art fallback.
  • aria-modal is honest: the rest of the composition is inert while the enlarged view is open, and collapsing returns focus to the inspected print.
  • className is not an attribute; styling uses the documented parts and custom properties.
  • WCAG corrections: cover #a8461a instead of #e8742f (3.0:1 to 5.91:1), subtext at 85% white instead of 75% (4.05:1 to 4.73:1), and an opaque lip caption.

What it does

A frosted paper box: hovering, tapping or pressing Enter on the cover slides the drawer out and fans three polaroids; activating a print inspects it enlarged.

Upstream behaviour
  • Translucent frosted glass cover with realistic backdrop-blur and warm gradient sheen
  • Sliding paper box drawer physics with spring-tuned easing and offset lip reveal
  • Tri-slot polaroid fanning layout that expands on hover, tap, or focus
  • Click-to-zoom polaroid inspection view with high-res framing and title captions
  • Accessible keyboard navigation (Tab through prints, Enter to expand, Esc to dismiss)
  • Responsive auto-scaling stage with ResizeObserver keeping pixel-perfect geometry down to mobile screens
Differences from upstream
  • `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.
  • Events are user-driven only; upstream fired both callbacks from useEffect, including on mount and on controlled open writes.
  • Upstream Framer Motion springs (duration 0.55, bounce 0.12..0.24) become CSS transitions over cubic-bezier(0.23,1,0.32,1): 550 ms transform, 280 ms opacity and filter. Owner approved this spring-for-easing substitution on 2026-10-29 (PC-10.4); motion fidelity remains an L6 manual gate.
  • prints[].art (ReactNode) is dropped; the three default ids render local SVG art, an unknown id without src renders a neutral placeholder, and PhotoBoxDemo is not ported.
  • prints[].src is scheme-checked (R-3.1/R-3.2): relative and http(s) accepted, javascript:, data:, vbscript: and file: refused with a development diagnostic and local-art fallback.
  • aria-modal is honest: the rest of the composition is inert while the enlarged view is open, and collapsing returns focus to the inspected print.
  • className is not an attribute; styling uses the documented parts and custom properties.
  • WCAG corrections: cover #a8461a instead of #e8742f (3.0:1 to 5.91:1), subtext at 85% white instead of 75% (4.05:1 to 4.73:1), and an opaque lip caption.
Previous: Peek Card Next: Pill Navigation

Docs

Components

Project