CSS Art

Pure CSS using Open Props tokens. No JavaScript, no images, no classes. Each piece is a data-attribute.

Open Props ships --radius-blob-* (organic shapes), --gradient-1--gradient-30, --animation-float, and --ease-spring-*. Combined with ASW's data-attribute vocabulary, these compose into genuine visual objects with a single HTML element.

Floating blob

A single <div> with an organic border-radius from --radius-blob-2, a teal-to-green linear gradient from --gradient-4, and --animation-float — a gentle 2-axis sine wave encoded in 3 lines of CSS.

[data-art="blob-float"] {
  width: 160px;
  height: 160px;
  border-radius: var(--radius-blob-2);
  background: var(--gradient-4);
  animation: var(--animation-float);
}

Spinning prism

--gradient-10 is a conic rainbow sweep — originally designed for color wheels. Applied to an organic shape and spun at 4 seconds per revolution, it becomes something else entirely. The color order is preserved, the edge is alive.

[data-art="prism"] {
  width: 140px;
  height: 140px;
  border-radius: var(--radius-blob-3);
  background: var(--gradient-10);
  animation: spin 4s linear infinite;
}

Layered depth

Three blobs, each a different gradient and opacity, each floating at a different phase offset. The same --animation-float keyframes with staggered animation-delay values create independent rhythms. Nothing is synchronized; depth emerges from desynchronization.

[data-layer="3"] { background: var(--gradient-11); }
[data-layer="2"] { background: var(--gradient-24); }
[data-layer="1"] { background: var(--gradient-4); }

/* stagger: 0s / -1s / -2s */

Pulse ring

Three concentric blobs from --gradient-13 (a deep purple-to-coral radial), pulsing outward with staggered timing. Two are ::before and ::after pseudo-elements — no extra HTML. The core sits above them on a higher stacking context.

[data-art="pulse-ring"]::before,
[data-art="pulse-ring"]::after {
  border-radius: var(--radius-blob-5);
  background: var(--gradient-13);
  animation: pulse 2s var(--ease-out-3) infinite;
}
::after { animation-delay: -1s; }
ASW

Gradient text

--gradient-1 is a purple-to-amber diagonal — normally a background gradient. Applied via background-clip: text, it becomes a fill. The typeset word becomes a window into the gradient space below it.

[data-art="gradient-text"] {
  background: var(--gradient-1);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
--gray-hue: 0 red
--gray-hue: 45 amber
--gray-hue: 150 green
--gray-hue: 220 blue

Hue tinting

ASW's --gray-hue cascades through all surfaces. The same blob, the same gradient formula — only the hue angle changes. The oklch color space keeps perceptual lightness constant across hues, so red and blue land at the same visual weight.

[data-hue="0"]   { --gray-hue: 0;   }
[data-hue="45"]  { --gray-hue: 45;  }
[data-hue="150"] { --gray-hue: 150; }
[data-hue="220"] { --gray-hue: 220; }

background: radial-gradient(
  circle at 30% 30%,
  oklch(65% 0.18 var(--gray-hue)),
  oklch(25% 0.06 var(--gray-hue))
);

Waveform

Twelve bars, each a different height and animation phase. The bounce keyframe is a squish-ease: it overshoots slightly at peak and returns. The stagger is 200ms between each bar — enough to create a wave motion, not enough to look sequential. It reads as a signal, not a list.

[data-art="waveform"] > [data-bar] {
  background: var(--gradient-4);
  animation: var(--animation-bounce);
}
[data-bar="1"]  { height: 45%; animation-delay: -0.0s; }
[data-bar="2"]  { height: 70%; animation-delay: -0.2s; }
/* … 12 bars total */

All pieces use only Open Props tokens and ASW data-attributes. No class, no id, no JavaScript. Source: packs/ · Tokens: design-tokens