Back to the library

Schematic — exploded view

Variant: Exploded

A three-part assembly opened along its centreline — and the axis fades in before anything moves, because an exploded view is not parts drifting apart, it is one object opened along one line, and the rail has to exist before anything can ride it. The rail is earned: a dashed hidden-line bore runs through all three parts, which is why the assembly has an axis and the parts have names. Cap and base separate on the same ease-in-out beat 110ms apart, splitting a fixed gap unevenly so the lightest part does most of the travelling. The leaders rule out only after the parts have stopped, since a position is not worth naming until it is final, and each ends in a dot on the face it names — an arrow points at an edge, a dot claims the part; all three leave together, because a staggered exit would be a second performance. Closing runs a third faster than opening, 432ms against 648ms on an ease-out scoped to the departure — disassembly is an explanation, reassembly is just putting the thing back. Use it in docs and product pages where what-is-inside is the actual content.

svgdiagramtransitionstaggerloop

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 — exploded view</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 — 7.2s: 648ms to open, the exploded state held for 3.7s (it is
           the one worth reading), then 432ms to close on an ease-out — a third
           faster than the open, because parts seat, they do not hover home. */
        --cycle: 7.2s;
        --t: calc(var(--cycle) / var(--speed));

        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
        --ease-in-out: cubic-bezier(0.77, 0, 0.175, 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(310px, 90vw);
        height: auto;
        display: block;
      }

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

      /* The bore that runs through all three parts, drawn as hidden lines. It
         is why the assembly has an axis at all — and why the parts have names. */
      .hidden {
        stroke: var(--slate);
        stroke-width: 1.25;
        stroke-dasharray: 4 3;
        opacity: 0.4;
      }

      /* The axis arrives before anything moves, because it is the rail the
         parts travel on: an exploded view is not parts drifting apart, it is
         one assembly opened along one line. */
      .axis {
        stroke: var(--slate);
        stroke-width: 1;
        stroke-dasharray: 9 3 2 3;
        opacity: 0;
        animation: axis-hold var(--t) var(--ease-out) infinite;
      }

      /* Both travelling parts ride the same curve on the same beat — the base
         simply travels less. Splitting a fixed gap unevenly (26 up, 18 down)
         keeps the cap, the lightest part, doing most of the moving. */
      .cap {
        animation: cap-travel var(--t) var(--ease-in-out) infinite;
      }

      .base {
        animation: base-travel var(--t) var(--ease-in-out) infinite;
      }

      /* Name tags terminate in a dot on the face they name, not an arrowhead —
         an arrow points at an edge; a dot claims the whole part. */
      .dot {
        fill: var(--clay);
        opacity: 0;
        animation: dot-1 var(--t) var(--ease-out) infinite;
      }

      .dot.d2 {
        animation-name: dot-2;
      }

      .dot.d3 {
        animation-name: dot-3;
      }

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

      .lead.l2 {
        animation-name: lead-2;
      }

      .lead.l3 {
        animation-name: lead-3;
      }

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

      .tag.t2 {
        animation-name: tag-2;
      }

      .tag.t3 {
        animation-name: tag-3;
      }

      /* Open on ease-in-out over 10-19% — the parts are on screen the whole
         time — and hold apart for half the cycle. The close runs 70-76% with
         an ease-out scoped onto the departure keyframe: 432ms against the
         open's 648ms, because reassembly is not an explanation, it is just
         putting the thing back. */
      @keyframes cap-travel {
        0%,
        10% {
          transform: translateY(0);
        }
        19%,
        70% {
          transform: translateY(-26px);
          animation-timing-function: var(--ease-out);
        }
        76%,
        100% {
          transform: translateY(0);
        }
      }

      @keyframes base-travel {
        0%,
        11.5% {
          transform: translateY(0);
        }
        20.5%,
        71.5% {
          transform: translateY(18px);
          animation-timing-function: var(--ease-out);
        }
        77.5%,
        100% {
          transform: translateY(0);
        }
      }

      @keyframes axis-hold {
        0%,
        5% {
          opacity: 0;
        }
        9%,
        78% {
          opacity: 0.3;
        }
        86%,
        100% {
          opacity: 0;
        }
      }

      /* Leaders rule left to right at a constant rate, 86ms apart, only after
         the parts have stopped: labels name positions, and a position is not
         worth naming until it is final. All three leave together — a staggered
         exit would be a second performance. */
      @keyframes lead-1 {
        0%,
        22% {
          stroke-dashoffset: 100;
          opacity: 1;
        }
        27%,
        60% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        64%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      @keyframes lead-2 {
        0%,
        23.2% {
          stroke-dashoffset: 100;
          opacity: 1;
        }
        28.2%,
        60% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        64%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      @keyframes lead-3 {
        0%,
        24.4% {
          stroke-dashoffset: 100;
          opacity: 1;
        }
        29.4%,
        60% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        64%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      @keyframes dot-1 {
        0%,
        22% {
          opacity: 0;
        }
        23.5%,
        60% {
          opacity: 1;
        }
        64%,
        100% {
          opacity: 0;
        }
      }

      @keyframes dot-2 {
        0%,
        23.2% {
          opacity: 0;
        }
        24.7%,
        60% {
          opacity: 1;
        }
        64%,
        100% {
          opacity: 0;
        }
      }

      @keyframes dot-3 {
        0%,
        24.4% {
          opacity: 0;
        }
        25.9%,
        60% {
          opacity: 1;
        }
        64%,
        100% {
          opacity: 0;
        }
      }

      @keyframes tag-1 {
        0%,
        26% {
          opacity: 0;
          transform: translateY(2px);
        }
        30.5%,
        60% {
          opacity: 1;
          transform: translateY(0);
        }
        64%,
        100% {
          opacity: 0;
          transform: translateY(0);
        }
      }

      @keyframes tag-2 {
        0%,
        27.2% {
          opacity: 0;
          transform: translateY(2px);
        }
        31.7%,
        60% {
          opacity: 1;
          transform: translateY(0);
        }
        64%,
        100% {
          opacity: 0;
          transform: translateY(0);
        }
      }

      @keyframes tag-3 {
        0%,
        28.4% {
          opacity: 0;
          transform: translateY(2px);
        }
        32.9%,
        60% {
          opacity: 1;
          transform: translateY(0);
        }
        64%,
        100% {
          opacity: 0;
          transform: translateY(0);
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* The ±26px/18px travel is exactly the motion this setting asks to
           lose. The parts hold their exploded positions, the axis holds, and
           what keeps looping is the naming: leaders and tags fading through
           their beats over a stack that never moves. */
        .cap {
          animation: none;
          transform: translateY(-26px);
        }

        .base {
          animation: none;
          transform: translateY(18px);
        }

        .axis {
          animation: none;
          opacity: 0.3;
        }

        .lead,
        .dot,
        .tag {
          animation-duration: calc(var(--t) * 1.5);
        }
      }
    </style>
  </head>
  <body>
    <svg
      class="sheet"
      viewBox="0 0 292 160"
      role="img"
      aria-label="An exploded view of a three-part assembly: cap, housing, and base separate along a dash-dot centreline, leader lines rule out to name each part, then the stack closes back together"
    >
      <line class="axis" x1="130" y1="10" x2="130" y2="150" />

      <g class="cap">
        <rect class="part" x="98" y="42" width="64" height="18" rx="4" />
        <line class="hidden" x1="123" y1="42" x2="123" y2="60" />
        <line class="hidden" x1="137" y1="42" x2="137" y2="60" />
        <!-- Authored 26px low: leader, dot, and tag ride the cap's group, so
             their coordinates are pre-travel and land right once exploded. -->
        <circle class="dot" cx="154" cy="51" r="2" />
        <line class="lead" pathLength="100" x1="154" y1="51" x2="216" y2="51" />
        <text class="tag" x="222" y="55">CAP</text>
      </g>

      <rect class="part" x="82" y="60" width="96" height="34" rx="3" />
      <line class="hidden" x1="123" y1="60" x2="123" y2="94" />
      <line class="hidden" x1="137" y1="60" x2="137" y2="94" />
      <circle class="dot d2" cx="170" cy="77" r="2" />
      <line class="lead l2" pathLength="100" x1="170" y1="77" x2="216" y2="77" />
      <text class="tag t2" x="222" y="81">HOUSING</text>

      <g class="base">
        <rect class="part" x="66" y="94" width="128" height="20" rx="3" />
        <line class="hidden" x1="123" y1="94" x2="123" y2="114" />
        <line class="hidden" x1="137" y1="94" x2="137" y2="114" />
        <circle class="dot d3" cx="186" cy="104" r="2" />
        <line class="lead l3" pathLength="100" x1="186" y1="104" x2="216" y2="104" />
        <text class="tag t3" x="222" y="108">BASE</text>
      </g>
    </svg>
  </body>
</html>

The variant family

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