Back to the library

Line draw — draw and erase

Variant: Draw and erase

The loading outline, and the only member of the family with no fade, no dark window and no reset anywhere in it. One dash as long as the whole perimeter is what lets a single monotonically falling offset do both halves of the job: down to zero the outline draws itself around the card, and past zero the same dash slides off the far side so it erases from the corner it started at. The last frame is the first frame — there is no seam to hide, which is why nothing here has to dim. Linear throughout, with the rhythm living in two holds instead: 360ms on the closed outline and a 216ms rest with nothing drawn, because an indicator that never pauses reads as a machine rather than a process. Ease the travel instead and the outline slows into every corner it passes, which is a claim about the shape that nothing in the shape supports.

svgline-drawingloadingloop

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>Line draw — draw and erase</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;
        --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)
        );

        /* Clock — 3.6s: 1.51s of head, a 360ms hold on the closed outline,
           1.51s of tail, and a 216ms rest with nothing drawn. No fade anywhere
           in the file: the erase ends exactly where the draw began, so there is
           no reset to hide. */
        --cycle: 3.6s;
        --t: calc(var(--cycle) / var(--speed));
        --_calm: calc(2.4s / var(--speed));

        --ease-accent: cubic-bezier(0.45, 0, 0.55, 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(280px, 88vw);
        height: auto;
        display: block;
      }

      .field {
        fill: var(--oat);
      }

      .plate {
        fill: var(--paper);
        stroke: var(--oat);
        stroke-width: 1.5;
      }

      /* One dash the length of the whole perimeter is what lets a single
         monotonically falling offset do both halves of the job: from +103 to 0
         the dash advances over the path and the outline draws, and from 0 to
         -103 it slides off the far side and the outline erases from the corner
         it started at. The gap is twice the perimeter and the travel overshoots
         by three units, both so that the dash's round cap is parked off the
         path — at an offset of exactly 100 it still paints, and a stroke that
         is meant to be absent shows as a speck in the top left corner. */
      .trace {
        fill: none;
        stroke: var(--clay);
        stroke-width: 3;
        stroke-linecap: round;
        stroke-dasharray: 100 200;
        animation: trace var(--t) infinite;
      }

      /* Linear throughout. The two holds are where the rhythm lives — ease the
         travel instead and the outline slows into every corner it passes, which
         is a claim about the shape that nothing in the shape supports. */
      @keyframes trace {
        0% {
          stroke-dashoffset: 103;
          animation-timing-function: linear;
        }
        42%,
        52% {
          stroke-dashoffset: 0;
          animation-timing-function: linear;
        }
        94%,
        100% {
          stroke-dashoffset: -103;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* Travel is the thing being removed, so the outline stays closed and
           breathes instead. Between 0.3 and 1 over 2.4s: slow enough not to
           read as a blink, and still obviously running. */
        .trace {
          stroke-dashoffset: 0;
          animation: trace-calm var(--_calm) var(--ease-accent) infinite;
        }

        @keyframes trace-calm {
          0%,
          100% {
            opacity: 0.3;
          }
          50% {
            opacity: 1;
          }
        }
      }
    </style>
  </head>
  <body>
    <svg
      class="card"
      viewBox="0 0 260 140"
      role="img"
      aria-label="An outline drawing itself around a card and then erasing from the same corner it started, on a continuous loop"
    >
      <path
        class="plate"
        d="M 24 10 H 236 A 14 14 0 0 1 250 24 V 116 A 14 14 0 0 1 236 130 H 24 A 14 14 0 0 1 10 116 V 24 A 14 14 0 0 1 24 10 Z"
      />

      <circle class="field" cx="44" cy="42" r="12" />
      <rect class="field" x="66" y="34" width="118" height="9" rx="4.5" />
      <rect class="field" x="66" y="51" width="76" height="9" rx="4.5" />
      <rect class="field" x="32" y="82" width="196" height="9" rx="4.5" />
      <rect class="field" x="32" y="99" width="140" height="9" rx="4.5" />

      <path
        class="trace"
        pathLength="100"
        d="M 24 10 H 236 A 14 14 0 0 1 250 24 V 116 A 14 14 0 0 1 236 130 H 24 A 14 14 0 0 1 10 116 V 24 A 14 14 0 0 1 24 10 Z"
      />
    </svg>
  </body>
</html>

The variant family

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