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
Stroke
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>
);
}