trieur.

Rendering cards and zones

The deck draws a stage, some regions and an empty box per card. Everything inside those boxes is yours. That is not a limitation to work around; it is the point. The library does not know what a bookmark, a photograph or a suitor is, and it never has to.

The card

renderCard(item, el) receives the item and an empty <article class="tr-card">. Fill it however you like — innerHTML, DOM calls, a framework’s render function, a <template> clone. It is called once per card, on every render.

new Deck(el, {
  items: photos,
  renderCard: (photo, el) => {
    el.innerHTML = `
      <div class="shot">
        <img src="${photo.thumb}" alt="" loading="lazy" />
        <footer><b>${photo.author}</b><span>${photo.exif.camera}</span></footer>
      </div>`;
  },
});

A few things the deck does to the element for you, so you do not have to:

Size comes from CSS variables rather than options, because it is a style decision:

.my-deck { --tr-card-w: 300px; --tr-card-h: 420px; --tr-radius: 18px; --tr-card-bg: #fff; }

The card is a flex column, so a flex: 1 image and a fixed footer is the shape you usually want. Nothing stops you from making the card a full-bleed image with an absolutely positioned overlay instead.

States you can style

ClassWhen
.tr-cardevery card
.tr-behindthe one showing under the top card
.tr-draggingwhile the finger holds it
.tr-geniein flight towards a zone

Under .tr-multi (the root, while the multi-zone stack is open) the card takes a dashed outline and a breathing ring — both are plain CSS on .tr-card, so you can replace them.

The zone

renderZone(zone, el) receives a placed zone and an empty <div class="tr-zone">. The default is a Finder-style folder tile; here is roughly what it does, so you can start from it:

renderZone: (zone, el) => {
  // the colour drives the region behind the tile too, not just the chip
  if (zone.color) el.style.setProperty('--tr-seg', zone.color);
  el.innerHTML = `
    <span class="tr-glyph">${zone.icon ?? folderSvg}</span>
    <span class="tr-label">${zone.label ?? zone.id}</span>
    ${zone.key ? `<kbd>${zone.key}</kbd>` : ''}`;
}

The zone you get is the placed one, so it carries more than you passed in:

{ id, label, key, color, icon, image,   // yours
  index,      // position, which is what the key is tied to
  empty,      // a free zone: dropping calls onAssign instead
  angle, pos, // where it ended up, in radians and in pixels from the centre
  cell }      // its region, as a polygon — null when `segments: false`

States you can style

ClassWhen
.tr-zoneevery zone
.tr-freea null zone, waiting to be assigned
.tr-nearthe card is heading this way
.tr-armed…and far enough that letting go would file it
.tr-suggestthe model suggests this one
.tr-pickedin the multi-zone stack (data-pick holds its rank)
.tr-catchjust received a card, or just gave one back

The regions get the same three: .tr-seg, plus .tr-near, .tr-armed, .tr-picked.

The layout decides the shape

The zone is a tile because the default layout floats tiles over a Voronoi carving. Choose layout: 'radial' and the same zones become wedges of a pie menu — the tile loses its box (via .tr-layout-radial, a class the deck puts on the root) and the wedge behind it becomes the button. Try the four in the zones demo.

A custom layout is a function, and it may describe its own regions:

// two zones, left and right — the whole of the Tinder demo's layout
layout: (n, { w, h, clearX, clearY, tile }) => [
  { x: -Math.max(clearX, w / 2 - 90), y: 0 },
  { x:  Math.max(clearX, w / 2 - 90), y: 0 },
]

// or hand back the regions as well, and the drop targets become exactly those shapes
layout: (n, box) => ({ points: [...], cells: [[[x, y], …], …] })

Two guarantees are applied to whatever you return, so you cannot accidentally break the deck: nothing ends up under the card (the clearance is an ellipse around it), and nothing ends up off the stage (the whole set is scaled down until the tiles fit).

Everything else is a variable

.tr {
  --tr-accent: #4a54f2;   /* suggestion, armed state, focus ring */
  --tr-multi: #f59e0b;    /* the multi-zone mode */
  --tr-line: …;           /* every hairline */
  --tr-card-bg: #fff;
  --tr-card-w: 260px;
  --tr-card-h: 300px;
  --tr-radius: 14px;
}

Or throw the stylesheet away. The class names above are the contract; the CSS shipped with @trieur/core is one implementation of it.