Back to the library

Schematic — detail callout

Variant: Detail callout

A magnifier built in strict dependency order: a ring is ruled around a groove too small to read, a leader rules out from it, and the detail bubble grows from the exact point the leader touched — scale 0.94 to 1, because an enlargement of something that already exists must never appear from nothing, and from that origin, because the magnification belongs to the thing that asked for it. Inside, the profile is redrawn rather than zoomed: the chamfers exist only at 2.5:1, since at true scale they are too small to read and an honest drawing omits what it cannot show. The material tint waits for the profile to finish before it arrives — the line says what shape the metal is, the tint says which side the metal is on, and the second claim depends on the first being complete. Use it wherever a UI needs to say "look closer at this exact thing": feature tours, anatomy diagrams, zoom-on-spec moments.

svgline-drawingdiagramentranceloop

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>Schematic — detail callout</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)
        );

        --_muted: color-mix(in srgb, var(--slate) 62%, transparent);

        /* Clock — 6.8s: ring, leader, bubble, then the magnified profile, in
           strict dependency order; the finished callout holds for over three
           seconds because the payoff is a drawing you read, not a move. */
        --cycle: 6.8s;
        --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);
      }

      .sheet {
        width: min(320px, 90vw);
        height: auto;
        display: block;
      }

      .part {
        fill: var(--paper);
        stroke: var(--slate);
        stroke-width: 2;
      }

      .bore {
        fill: var(--ivory);
        stroke: var(--slate);
        stroke-width: 2;
      }

      .groove {
        fill: none;
        stroke: var(--slate);
        stroke-width: 2;
      }

      /* The part's top edge is broken across the groove mouth — the parent
         view and the detail view must describe the same open feature. */
      .mouth {
        stroke: var(--paper);
        stroke-width: 3;
      }

      .centreline {
        stroke: var(--slate);
        stroke-width: 1;
        stroke-dasharray: 9 3 2 3;
        opacity: 0.3;
      }

      .anno {
        animation: anno-clear var(--t) var(--ease-out) infinite;
      }

      /* The marker ring is ruled around the feature at a constant rate — it is
         the compass stroke that nominates what deserves magnifying. */
      .marker {
        fill: none;
        stroke: var(--clay);
        stroke-width: 1.5;
        stroke-dasharray: 100 200;
        stroke-dashoffset: 100;
        animation: marker-draw var(--t) linear infinite;
      }

      .tag {
        fill: var(--clay-d);
        font-family: inherit;
        font-size: 13px;
        font-weight: 600;
        opacity: 0;
        animation: tag-in var(--t) var(--ease-out) infinite;
      }

      .leader {
        stroke: var(--clay);
        stroke-width: 1.25;
        stroke-dasharray: 100 200;
        stroke-dashoffset: 100;
        animation: leader-draw var(--t) linear infinite;
      }

      /* The bubble grows out of the point the leader touched, not its own
         centre: the magnification is anchored to the thing that asked for it.
         Scale from 0.94 — a detail view is an enlargement of something that
         already exists, so it must never appear from nothing. */
      .bubble {
        opacity: 0;
        transform: scale(0.94);
        transform-box: view-box;
        transform-origin: 198.2px 65.7px;
        animation: bubble-in var(--t) var(--ease-out) infinite;
      }

      .lens {
        fill: var(--paper);
        stroke: var(--clay-d);
        stroke-width: 1.5;
      }

      /* At 2.5:1 the profile is redrawn, not zoomed: the chamfers exist only
         here, because at true scale they are two units nobody could read and
         an honest drawing omits what it cannot show. */
      .profile {
        fill: none;
        stroke: var(--slate);
        stroke-width: 2;
        stroke-dasharray: 100 200;
        stroke-dashoffset: 100;
        animation: profile-draw var(--t) linear infinite;
      }

      .material {
        fill: var(--oat);
        opacity: 0;
        animation: material-in var(--t) var(--ease-out) infinite;
      }

      .label {
        fill: var(--_muted);
        font-family: inherit;
        font-size: 12.5px;
        font-weight: 600;
        letter-spacing: 0.12em;
        opacity: 0;
        animation: label-in var(--t) var(--ease-out) infinite;
      }

      @keyframes marker-draw {
        0%,
        6% {
          stroke-dashoffset: 100;
        }
        12%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      @keyframes tag-in {
        0%,
        12% {
          opacity: 0;
        }
        14.5%,
        100% {
          opacity: 1;
        }
      }

      @keyframes leader-draw {
        0%,
        13.5% {
          stroke-dashoffset: 100;
        }
        17.5%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      @keyframes bubble-in {
        0%,
        17.5% {
          opacity: 0;
          transform: scale(0.94);
        }
        23%,
        100% {
          opacity: 1;
          transform: scale(1);
        }
      }

      @keyframes profile-draw {
        0%,
        23% {
          stroke-dashoffset: 100;
        }
        31%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      /* The material tint follows the finished profile rather than riding it:
         the line says what shape the metal is, the tint says which side the
         metal is on, and the second claim waits for the first to be complete. */
      @keyframes material-in {
        0%,
        31% {
          opacity: 0;
        }
        35%,
        100% {
          opacity: 0.45;
        }
      }

      @keyframes label-in {
        0%,
        35% {
          opacity: 0;
          transform: translateY(2px);
        }
        40%,
        100% {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes anno-clear {
        0%,
        84% {
          opacity: 1;
        }
        91%,
        100% {
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* The draws carry the meaning and stay, at half rate; the bubble's
           6% of scale travel is below the threshold worth removing. */
        .anno,
        .marker,
        .tag,
        .leader,
        .bubble,
        .profile,
        .material,
        .label {
          animation-duration: calc(var(--t) * 2);
        }
      }
    </style>
  </head>
  <body>
    <svg
      class="sheet"
      viewBox="0 0 300 162"
      role="img"
      aria-label="A detail callout on an engineering drawing: a ring is drawn around a small groove on a plate's edge, a leader line rules out to a magnified bubble, and the groove's profile is redrawn at 2.5 to 1 with its chamfers visible"
    >
      <defs>
        <clipPath id="lens-clip">
          <circle cx="240" cy="52" r="43" />
        </clipPath>
      </defs>

      <rect class="part" x="30" y="82" width="150" height="64" rx="8" />
      <circle class="bore" cx="105" cy="114" r="16" />
      <line class="centreline" x1="79" y1="114" x2="131" y2="114" />
      <line class="centreline" x1="105" y1="88" x2="105" y2="140" />
      <line class="mouth" x1="137.2" y1="82" x2="146.8" y2="82" />
      <path class="groove" d="M136 82 V86 H148 V82" />

      <g class="anno">
        <circle class="marker" cx="142" cy="84" r="14" pathLength="100" />
        <text class="tag" x="154" y="62">A</text>
        <line class="leader" pathLength="100" x1="155.3" y1="79.7" x2="198.2" y2="65.7" />

        <g class="bubble">
          <circle class="lens" cx="240" cy="52" r="44" />
          <g clip-path="url(#lens-clip)">
            <path class="material" d="M196 50 H220 L225 55 V60 H255 V55 L260 50 H284 V94 H196 Z" />
            <path
              class="profile"
              pathLength="100"
              d="M196 50 H220 L225 55 V60 H255 V55 L260 50 H284"
            />
          </g>
        </g>

        <text class="label" x="240" y="112" text-anchor="middle">DETAIL A</text>
        <text class="label" x="240" y="127" text-anchor="middle">SCALE 2.5:1</text>
      </g>
    </svg>
  </body>
</html>

The variant family

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