No.188 · Data· New
Block Handles
Document blocks with a quiet gutter: + inserts a block below, and the ⋮⋮ handle drags it, with a blue line showing where it will land. Keyboard users pick it up and walk it with the arrows.
The prompt
Written for the variant you picked above: Light
Build a reorderable list of document blocks in a calm document style (white page, ink #37352f, Inter 16px/1.5). Blocks are a heading (20px, 600), plain paragraphs and to-dos with native checkboxes (checked ones go faint and struck through). Each row is a grid: a 52px gutter then the content. The gutter holds two small ghost buttons, a + (15px stroke icon) and a 6-dot ⋮⋮ handle (10×16px), both faint grey and invisible until the row is hovered or focused; they get an 8% ink background on hover. On touch screens the gutter stays at 60% opacity. Dragging the handle lifts the row: it follows the pointer vertically with a white background, a hairline-ring shadow and 92% opacity, while every other gutter hides. A 3px rounded blue line (#2383e2 at 70%) appears in the gap where the block will land, starting after the gutter. On release, the order changes and every block slides from its old position to its new one in 220ms (FLIP). The + inserts an empty line below with a borderless input ('Write something, Enter to keep it'); Enter keeps it, Escape or leaving it empty removes it. Selected variant — Light (light): Light theme: page #ffffff, ink #37352f, muted #787774, faint #a5a29a, hover rgb(55 53 47 / 0.08), drop line and focus #2383e2, picked row rgb(35 131 226 / 0.1), lift shadow 0 0 0 1px rgb(15 15 15 / 0.05), 0 4px 12px rgb(15 15 15 / 0.1). This selection takes precedence over references to other themes or options in the general brief. Implement this selected appearance; do not add a variant picker or alternative appearances unless requested. Implementation requirements: Use React and TypeScript. Keep the component self-contained, with uniquely scoped CSS and no animation libraries. Include required imports, styles and font setup in the result. Interaction: Pointer: press the handle and move at least 4px to start a drag (pointer capture, touch-action none); the drop gap is the count of other blocks whose midpoint is above the pointer. Keyboard: focus a handle and press Space or Enter to pick the block up (aria-pressed, a pale blue row), ↑/↓ move it one place at a time, Space drops it and Escape returns it to where it started. onChange receives the new array. Motion: Reordering uses FLIP via element.animate: rows slide 220ms with cubic-bezier(.2,.8,.2,1). The gutter fades in over 120ms. Reduced motion skips the slide and fades. Accessibility: An ordered list labelled 'Blocks'. Each handle is a button named 'Move “…”, position 3 of 6', described by hidden instructions, with aria-pressed while picked up. An assertive live region announces pick-up, each new position, the drop and cancellations. Focus is kept on the handle while it moves through the DOM. Responsive behavior: The gutter narrows to 46px in containers under 24rem; text wraps anywhere, so long words never push the row wider than the column. Touch: The gutter is always faintly visible on touch, the handle can be dragged with a finger, and the + is a normal tap. Preserve keyboard access, visible focus and reduced-motion behavior. Keep fictional demo data and simulated service calls in a separate usage example; expose callbacks for real actions and recover from rejected requests. Keep usage-example links inside the demo: use local buttons or prevent navigation. Demonstration links must not navigate to Vitrine routes or open other pages; preserve real URL support in the reusable component itself. Verify the result at narrow and wide viewport sizes, with keyboard input and reduced motion enabled.
Notes
- Interaction
- Pointer: press the handle and move at least 4px to start a drag (pointer capture, touch-action none); the drop gap is the count of other blocks whose midpoint is above the pointer. Keyboard: focus a handle and press Space or Enter to pick the block up (aria-pressed, a pale blue row), ↑/↓ move it one place at a time, Space drops it and Escape returns it to where it started. onChange receives the new array.
- Motion
- Reordering uses FLIP via element.animate: rows slide 220ms with cubic-bezier(.2,.8,.2,1). The gutter fades in over 120ms. Reduced motion skips the slide and fades.
- Accessibility
- An ordered list labelled 'Blocks'. Each handle is a button named 'Move “…”, position 3 of 6', described by hidden instructions, with aria-pressed while picked up. An assertive live region announces pick-up, each new position, the drop and cancellations. Focus is kept on the handle while it moves through the DOM.
- Responsive
- The gutter narrows to 46px in containers under 24rem; text wraps anywhere, so long words never push the row wider than the column.
- On touch
- The gutter is always faintly visible on touch, the handle can be dragged with a finger, and the + is a normal tap.