Skip to content
Buse Turgay Sönmez

Design Engineer, Belgrade

Mark Loader

A loader made from a logo without a line of animation code in JavaScript. The mark is nine paths, each with a path length of one, so a single keyframe draws and undraws all of them. Every path carries its quadrant and its depth as CSS variables, and the delay is a calc() of the two: clockwise, outside in.

  • 2026
  • CSS
  • SVG
  • Loader
  • Logo

Controls

Speed
1.00×
Stroke
3.0

Origin

The mark is Align Scale's, the studio I co-founded.

Source

mark-loader.module.css79 lines
/*
  Mark Loader. Pure CSS: no animation library, no frame maths in JavaScript.
  The lines draw themselves, quadrant by quadrant, clockwise.

  Settings (set by the component, or straight in CSS):
    --as-dur      one full turn                 (1.4s)
    --as-stagger  delay between two parts       (0.055s)
    --as-sw       stroke width, in viewBox units
    color         the line colour, currentColor
*/

.mark {
  display: block;
  color: currentColor;
  overflow: visible;

  --as-dur: 1.4s;
  --as-stagger: 0.055s;
}

.path {
  fill: none;
  stroke: currentColor;
  stroke-width: var(--as-sw, 3);
  stroke-linejoin: round;

  /* A round cap does two jobs: it softens the drawing tip, and when a path closes it overlaps the
     start and the end so the seam disappears. The circles need it. */
  stroke-linecap: round;

  /* pathLength="1" on every path, so the same two lines work for every shape. */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;

  animation: as-draw var(--as-dur) cubic-bezier(0.65, 0.05, 0.36, 1) infinite;

  /* Order: quadrants clockwise (--q), and inside a quadrant from the outside in (--l).
     Both variables sit inline on each path in the SVG. */
  animation-delay: calc(var(--as-stagger) * (var(--q) * 2 + var(--l)));

  will-change: stroke-dashoffset;
}

@keyframes as-draw {
  0% {
    stroke-dashoffset: 1;
  }
  50%,
  62% {
    stroke-dashoffset: 0;
  }
  100% {
    stroke-dashoffset: -1;
  }
}

/* With reduced motion the mark does not draw; it breathes in opacity instead. */
@media (prefers-reduced-motion: reduce) {
  .path {
    animation: as-breathe 1.6s ease-in-out infinite alternate;
    animation-delay: 0s;
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  @keyframes as-breathe {
    from {
      opacity: 0.35;
    }
    to {
      opacity: 1;
    }
  }
}

/* The stage's pause button: the drawing stops where it is. */
.paused .path {
  animation-play-state: paused;
}
piece.tsx25 lines
import type { CSSProperties } from "react";
import type { DomPieceProps } from "@/entries/types";
import { cn } from "@/lib/utils";
import styles from "./mark-loader.module.css";
import { PARTS } from "./parts";

/**
 * The Align Scale loader: the mark draws itself quadrant by quadrant, clockwise, outside in. Pure
 * CSS: one keyframe, and each path's delay is calc()'d from its quadrant and depth. No state, no
 * frame maths in JavaScript. From clients/alignscale/align-scale-loader.
 */
export default function MarkLoader({ values, paused }: DomPieceProps) {
  const speed = typeof values.speed === "number" && values.speed > 0 ? values.speed : 1;
  const weight = typeof values.weight === "number" ? values.weight : 3;
  const vars = { "--as-sw": weight, "--as-dur": `${1.4 / speed}s`, "--as-stagger": `${0.055 / speed}s` } as CSSProperties;
  return (
    <div className={cn("flex h-full w-full items-center justify-center text-ink", paused && styles.paused)}>
      <svg className={cn(styles.mark, "h-auto w-[min(46%,300px)]")} viewBox="0 0 163 163" fill="none" aria-hidden="true" style={vars}>
        {PARTS.map((part, i) => (
          <path key={i} className={styles.path} d={part.d} pathLength={1} style={{ "--q": part.q, "--l": part.l } as CSSProperties} />
        ))}
      </svg>
    </div>
  );
}
--ground
oklch(0.985 0.003 85)
--surface
oklch(0.958 0.005 85)
--raised
oklch(1 0 0)
--ink
oklch(0.2 0.006 85)
--muted
oklch(0.43 0.008 85)
--faint
oklch(0.5 0.008 85)
--line
oklch(0.9 0.004 85)
--line-strong
oklch(0.6 0.006 85)
--accent
oklch(0.66 0.22 38)
--accent-ink
oklch(0.52 0.19 38)
--stage
oklch(0.152 0.004 85)
--radius-stage
36px
--radius-stage-sm
28px
--radius-media
24px
--radius-panel
16px
--radius-field
10px
--ease-out-expo
cubic-bezier(0.16, 1, 0.3, 1)

This week · W39One WebGL context, three leaksGeometry · 2026