Design Lounge
Install

Nº 495 of 520

Live cursors on a board

Three named cursors on a rehearsal floor plan. Yours follows the pointer. Click a set piece to claim it.

Preview

Open full size
1280 × 800 · 7.2 KB · 131 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Live cursors on a board" (piece live-cursors-board) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

164 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

A shared floor plan for House Lamp, a fictional theatre company, on Thursday’s call. The sheet is warm paper on a slightly darker ground. Four set pieces sit on the plan: Riser A, Centre table, Window bay, Prompt desk. Three people are on the board. Mira is you. Jo Hale and Ellis Voss drift. Your cursor is a rust arrow with your name. Theirs are pine and navy. Click a set piece and it takes a rust inset ring and the word Mira. The one detail worth copying is that every cursor carries a name, and only the board hides the system pointer.

Reference behaviour

  1. First frame: header 64px, “House Lamp” in serif, “Floor 2 · Thursday call” in small caps. Three presence chips at the right: Mira (you), Jo Hale, Ellis Voss. Centre table is already claimed (aria-pressed="true"), filled #f3d2c6 with a 2px inset rust ring, and the italic word Mira under the title. Mira’s cursor sits on that table. Jo is by the riser. Ellis is on the window bay.
  2. Moving the pointer inside the room moves Mira’s cursor to that point, clamped 8px inside the room edges. The system cursor is none on the board only.
  3. Jo’s cursor loops a three-point path over 16s, alternate, easing cubic-bezier(.2,.7,.2,1). Ellis loops a different path over 18s. Both are CSS animations. They do not follow the pointer.
  4. Click a set piece: it becomes the only pressed piece. The others lose the ring, the wash, and the name. The clicked piece shows “Mira”.
  5. Cursors are pointer-events: none. They never block the click.
  6. Tab reaches the four pieces. Focus-visible is a 2px rust outline, 3px offset.
  7. Reduced motion: Jo and Ellis stay at their first positions. Mira still follows the pointer. Claiming still works.
  8. Leaving the room leaves Mira where the pointer last was.

Structure

1280 × 800
┌ header 64: House Lamp    FLOOR 2 · THURSDAY CALL     (Mira you) (Jo) (Ellis) ┐
│  room, inset 48px from the sides and 28px from top and bottom of the board   │
│  ┌──────────┐                                              ┌────────────┐    │
│  │ 01 Riser │                                              │ 03 Window  │    │
│  └──────────┘              ┌─ claimed ─────────┐           └────────────┘    │
│                            │ 02 Centre table   │                              │
│                            └───────────────────┘     ┌──────────────┐        │
│                                                      │ 04 Prompt    │        │
│  Click a piece to claim it.                          └──────────────┘  plate │
└──────────────────────────────────────────────────────────────────────────────┘
  • header.head: brand in Newsreader 22/600, meta in Public Sans 12/500 uppercase, tracking +0.08em.
  • .who: three .chip pills, height 32px, sheet fill, 1px --line border, radius 999px. An 8px dot, then the name.
  • .board fills the rest. .room is the sheet, 1px ink border, an inner rule at inset: 10px, and four 14px corner ticks.
  • Each set piece is a <button class="block">: small uppercase index, a 20px serif title, and an italic “Mira” that is display: none unless aria-pressed="true".
  • Positions inside the room: Riser 36,48 240×280. Centre table 320,210 340×160. Window bay 760,48 280×190. Prompt desk 700,400 340×150.
  • .plate sits right: 28px; bottom: 24px, 200px wide, a 1px ink rule on top, three uppercase lines.
  • Each .cur is a zero-size point. A 22px filled arrow SVG and a 22px name chip (left: 16px; top: 16px, radius 4px, paper text on the person’s colour).

Tokens

:root {
  --bg: #e4ddd0;          /* page ground */
  --sheet: #f4efe6;       /* plan and chips */
  --ink: #1c1914;         /* titles, borders, plate name */
  --ink-2: #5c564c;       /* indexes, meta, hint */
  --line: #c9c0b2;        /* chip border */
  --mira: #9c3418;        /* you: cursor, claim, focus */
  --jo: #1b5e45;
  --ellis: #243e68;
  --you-soft: #f3d2c6;    /* claimed piece wash */
  --font: "Public Sans", system-ui, sans-serif;
  --serif: "Newsreader", Georgia, serif;
  --ease: cubic-bezier(.2, .7, .2, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
WordmarkNewsreader22px6001−0.02emsentence
Meta, indexes, platePublic Sans11–12px5001.45+0.06em to +0.12emuppercase
Chip namePublic Sans13px60010sentence
Piece titleNewsreader20px6001.10sentence
Claim nameNewsreader13px400 italic1.30sentence
Cursor namePublic Sans12px60022px0sentence
HintPublic Sans12px50010sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
Mira cursorpointermove in the roomtransformprevious point → clamped pointer0—set directly, no loop
Jo cursorloadtransform(150,150) → (250,250) → (170,310)16s--easeinfinite alternate
Ellis cursorloadtransform(880,110) → (960,180) → (820,160)18s--easeinfinite alternate
Claimed piececlickbackground, box-shadowsheet → --you-soft plus 2px inset rust0—instant

Reduced motion: animation: none on Jo and Ellis, so their inline first transforms remain. Mira still tracks the pointer.

States

  • Idle piece: sheet fill, 1px ink border, claim line hidden.
  • Hover piece: background #efe8dc. The system cursor stays hidden.
  • Pressed piece: background --you-soft, box-shadow: inset 0 0 0 2px var(--mira), italic “Mira” visible. Only one piece is pressed.
  • Focus-visible: outline: 2px solid var(--mira); outline-offset: 3px.
  • Your cursor: rust fill #9c3418, chip the same, paper label “Mira”.
  • Jo / Ellis: pine #1b5e45 and navy #243e68. Chips use paper text #f7f3ec.

Accessibility

  • The four set pieces are real buttons. aria-pressed is true on the claimed one only.
  • Presence chips are not buttons. They are labelled by the group aria-label="People on this board".
  • Cursor arrows are aria-hidden. The names are visible text, not the only name of a control.
  • Tab order is the four pieces. Cursors are not in the tab order.
  • Contrast: ink #1c1914 on sheet #f4efe6 is above 12:1. Ink-2 #5c564c on sheet is about 6:1. Paper on rust, pine, and navy is above 4.5:1.
  • Hit targets: each piece is at least 150×140.

Responsive rules

  • ≥ 1280: as drawn.
  • 1024–1279: room padding drops to 24px. Pieces scale down by about 15% but keep the same corners. Cursors stay in percentages of the room if you convert the paths.
  • 768–1023: pieces stack in a 2×2 grid inside the room. Header chips wrap under the wordmark. Cursor paths stay inside the room.
  • < 768: one column of pieces. The board does not hide the system cursor, because a finger has no pointer to follow. Show the three names as chips only, and keep the claim interaction.

Acceptance checklist

Always

  • Exactly three cursors, each with a visible name chip.
  • The local cursor follows the pointer only inside the plan, and is clamped 8px inside the edges.
  • The system cursor is hidden on the plan and visible everywhere else.
  • Cursors do not receive pointer events.
  • Clicking a piece claims it and clears the previous claim.
  • The two remote cursors move on a slow loop. Reduced motion freezes them at the start of that loop.
  • Focus-visible outline is 2px in the local person’s colour.

This demo

  • The people are Mira (you, #9c3418), Jo Hale (#1b5e45), and Ellis Voss (#243e68).
  • Centre table starts claimed.
  • The four pieces are Riser A, Centre table, Window bay, and Prompt desk.
  • Header reads “House Lamp” and “Floor 2 · Thursday call”.

Implementation notes

Remote cursors are CSS so the page does not run a frame loop. Give them a resting transform, then let the animation override it. When reduced motion sets animation: none, the resting transform shows again.

.jo { transform: translate(150px, 150px); animation: jo 16s var(--ease) infinite alternate; }
@keyframes jo {
  0% { transform: translate(150px, 150px); }
  50% { transform: translate(250px, 250px); }
  100% { transform: translate(170px, 310px); }
}
@media (prefers-reduced-motion: reduce) { .jo, .ellis { animation: none; } }

The local cursor is a zero-size element. The arrow and the name hang off it. Clamp to the room, not the page:

room.addEventListener('pointermove', function (e) {
  var r = room.getBoundingClientRect();
  var x = Math.max(8, Math.min(r.width - 28, e.clientX - r.left));
  var y = Math.max(8, Math.min(r.height - 28, e.clientY - r.top));
  you.style.transform = 'translate(' + x + 'px,' + y + 'px)';
});

Claim is exclusive. Set aria-pressed on the clicked button and clear the others. Do not also move that person’s cursor to the piece. The cursor and the claim are separate.

Common mistakes: hiding the cursor on body, so the header cannot be used; running requestAnimationFrame forever for two drifting dots; making the name chip the only accessible name of the button.

Details

Palette
Type
Newsreader · Public Sans
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Noomo

One scroll, one story. Motion carries the page.

All sources