Back to the library

Edit lease — handoff

Variant: Handoff

Ownership passing directly from one holder to the next, with the lock never opening to let it through. The lock glyph is the only element in the row with no animation at all, and that stillness is the claim. Avatar and name travel together as one identity: the old holder leaves left in 128ms, the new one enters from the right over 224ms, and the entrance waits for the exit so the seat never holds two half-there names. Direction never alternates — the next holder always arrives from the right — so the row reads as a queue moving forward rather than two people trading places. The bar stops where the old tenure left it and refills on the newcomer’s arrival, because a handoff issues a fresh lease, and it is one bar recoloured on the newcomer’s 224ms rather than two layers crossfading — the colour is who holds the lease, and a bar seen empty or thin, even for a frame, is an unlocked file. Use it for explicit "hand over editing" flows and for job queues that pass work worker to worker.

statuspresencetransitionloop

The source

A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Edit lease — handoff</title>
    <style>
      :root {
        /* Neutrals — fixed, never remixed. */
        --ivory: #faf9f5;
        --paper: #ffffff;
        --oat: #e3dacc;
        --slate: #141413;

        /* Knobs — the remix API. A knob exists iff declared here and used below. */
        --hue: 38.8;
        --lift: 0;
        --chroma: 1;
        --round: 1;
        --speed: 1;

        /* Accents — derived from the knobs with the family's offsets baked in,
           so one hue roll moves them together. Defaults reproduce the original hex. */
        --clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
        --clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
        --olive: oklch(
          calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
        );
        --rust: oklch(
          calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
        );

        --_muted: color-mix(in srgb, var(--slate) 50%, transparent);
        --_hair: color-mix(in srgb, var(--slate) 9%, transparent);
        --_ink: color-mix(in srgb, var(--slate) 72%, transparent);

        /* Clock — two tenures of 3.2s, Mara's then yours. Each handoff is
           128ms out, 224ms in, sequenced, at 0% and 50%. */
        --cycle: 6.4s;
        --t: calc(var(--cycle) / var(--speed));

        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        min-height: 100vh;
        display: grid;
        place-items: center;
        background: var(--ivory);
        font-family:
          system-ui,
          -apple-system,
          'Segoe UI',
          Roboto,
          sans-serif;
        color: var(--slate);
      }

      .card {
        width: min(300px, calc(100vw - 32px));
        padding: clamp(14px, 4.5vw, 18px);
        border-radius: calc(14px * var(--round));
        background: var(--paper);
        box-shadow:
          0 0 0 1px var(--_hair),
          0 1px 2px var(--_hair);
      }

      .doc {
        display: flex;
        align-items: center;
        gap: 7px;
        padding-bottom: 12px;
        margin-bottom: 12px;
        border-bottom: 1px solid var(--_hair);
        font-size: 12.5px;
        font-weight: 500;
        letter-spacing: -0.005em;
      }

      .doc svg {
        flex: none;
        width: 14px;
        height: 14px;
        fill: none;
        stroke: var(--_muted);
        stroke-width: 1.4;
        stroke-linejoin: round;
      }

      .hold {
        display: flex;
        align-items: center;
        gap: 8px;
        height: 32px;
      }

      /* The lock is the one thing in the row with no animation at all. That is
         the whole claim: ownership moves, the lock does not open to let it. */
      .lock {
        flex: none;
        width: 16px;
        height: 16px;
        fill: none;
        stroke: var(--_ink);
        stroke-width: 1.6;
        stroke-linecap: round;
      }

      .lock rect {
        fill: var(--_ink);
        stroke: none;
      }

      .seat {
        flex: 1;
        min-width: 0;
        display: grid;
      }

      /* Avatar and name travel as one unit — the holder is a single identity —
         and the next holder always enters from the right, so the row reads as a
         queue moving forward rather than two people swapping places. */
      .holder {
        grid-area: 1 / 1;
        display: flex;
        align-items: center;
        gap: 8px;
        opacity: 0;
        animation: tenure var(--t) var(--ease-out) infinite;
      }

      .holder.you {
        animation-delay: calc(var(--t) / -2);
      }

      .avatar {
        flex: none;
        display: grid;
        place-items: center;
        width: 24px;
        height: 24px;
        border-radius: 50%;
        background: color-mix(in srgb, var(--_c) 18%, var(--paper));
        box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_c) 34%, transparent);
        font-size: 11px;
        font-weight: 600;
        color: color-mix(in srgb, var(--_c) 70%, var(--slate));
      }

      .mara {
        --_c: var(--olive);
      }

      .you {
        --_c: var(--clay);
      }

      .who {
        font-size: 13px;
        font-weight: 500;
        letter-spacing: -0.01em;
        white-space: nowrap;
      }

      .lease {
        position: relative;
        height: 4px;
        margin-top: 8px;
        border-radius: 999px;
        background: var(--oat);
      }

      /* One bar, recoloured — not two stacked layers crossfading, where
         whichever sits on top wins one of the two handoffs. */
      .fill {
        position: absolute;
        inset: 0;
        border-radius: 999px;
        background: var(--clay);
        transform-origin: left center;
        animation:
          lease var(--t) infinite,
          tint var(--t) var(--ease-out) infinite;
      }

      .foot {
        display: flex;
        justify-content: space-between;
        margin-top: 10px;
        font-size: 11.5px;
        color: var(--_muted);
        font-variant-numeric: tabular-nums;
      }

      /* Out in 128ms toward the left, in over 224ms from the right, and the
         entrance waits for the exit. Exit is faster than enter, and the seat is
         never shown with two half-there names in it. */
      @keyframes tenure {
        0%,
        2% {
          opacity: 0;
          transform: translateX(8px);
        }
        5.5%,
        50% {
          opacity: 1;
          transform: translateX(0);
        }
        52%,
        100% {
          opacity: 0;
          transform: translateX(-8px);
        }
      }

      /* A handoff issues a fresh lease: the bar stops where the old holder left
         it, then refills on the new holder's arrival. Both tenures drain at the
         same linear slope, because a lease is a clock no matter who holds it. */
      @keyframes lease {
        0%,
        2% {
          transform: scaleX(0.62);
          animation-timing-function: var(--ease-out);
        }
        5.5% {
          transform: scaleX(1);
          animation-timing-function: linear;
        }
        50%,
        52% {
          transform: scaleX(0.62);
          animation-timing-function: var(--ease-out);
        }
        55.5% {
          transform: scaleX(1);
          animation-timing-function: linear;
        }
        100% {
          transform: scaleX(0.62);
        }
      }

      /* The bar takes the newcomer's colour as it refills, on the same 224ms
         as their arrival — the colour is who holds the lease, so it changes
         exactly when the holder does and never passes through empty. */
      @keyframes tint {
        0%,
        2% {
          background-color: var(--clay);
        }
        5.5%,
        52% {
          background-color: var(--olive);
        }
        55.5%,
        100% {
          background-color: var(--clay);
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* Holders crossfade in place instead of travelling; the bar stays
           because it is the lease. One shared 1.6 keeps every handoff aligned. */
        .holder {
          animation-name: tenure-still;
          animation-duration: calc(var(--t) * 1.6);
        }

        .holder.you {
          animation-delay: calc(var(--t) * -0.8);
        }

        .fill {
          animation-duration: calc(var(--t) * 1.6), calc(var(--t) * 1.6);
        }

        @keyframes tenure-still {
          0%,
          2% {
            opacity: 0;
          }
          5.5%,
          50% {
            opacity: 1;
          }
          52%,
          100% {
            opacity: 0;
          }
        }
      }
    </style>
  </head>
  <body>
    <div
      class="card"
      role="img"
      aria-label="A document lock handed from Mara to you and back. The lock icon never opens: Mara's avatar and name slide out to the left, yours slide in from the right, and the lease bar stops then refills in your colour as a fresh lease is issued"
    >
      <div class="doc">
        <svg viewBox="0 0 14 14" aria-hidden="true">
          <path d="M3 1.5h5.2L11 4.3v8.2H3z M8 1.5v3h3" />
        </svg>
        <span>pricing-q4.md</span>
      </div>
      <div class="hold">
        <svg class="lock" viewBox="0 0 16 16" aria-hidden="true">
          <path d="M5 7.5V5.2a3 3 0 0 1 6 0v2.3" />
          <rect x="3" y="7" width="10" height="7.5" rx="1.8" />
        </svg>
        <span class="seat">
          <span class="holder mara">
            <span class="avatar">MO</span><span class="who">Mara is editing</span>
          </span>
          <span class="holder you">
            <span class="avatar">AK</span><span class="who">You’re editing</span>
          </span>
        </span>
      </div>
      <div class="lease"><i class="fill"></i></div>
      <div class="foot"><span>Lease 30s</span><span>Transferred · no unlock</span></div>
    </div>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.