← Back to gallery
CSS

Typed Halftone Background Drift

A procedural halftone background that registers dot offset and stop values with @property so the radial-gradient pattern drifts smoothly instead of jumping between gradient strings.

@propertyhalftoneradial-gradienttyped-custom-propertybackground-positionprocedural-bgreduced-motion

Background / @property / halftone

Typed Halftone Background Drift

Three procedural halftone backgrounds register typed custom properties so dot size, phase, and stop values interpolate instead of jumping.

@property · phase drift

Dot Phase

Typed offset variables move the dot lattice smoothly.

  • @property
  • radial-gradient
  • phase

Dot radius · registered length

Density Wave

Register the radius value so density changes interpolate.

  • dot radius
  • density
  • no string swap

Layered dots · independent phase

Duotone Scan

Layered radial gradients use independent typed offsets.

  • two layers
  • offset
  • duotone

Halftone inspector

Dot Phase

Dot Phase
  • @property
  • radial-gradient
  • phase

Dot offset is a registered length value, so the halftone pattern glides rather than snapping between gradient strings.

Helped you ship something? 🐟 Send my cat a churu

/* Dot Phase: Typed offset variables move the dot lattice smoothly. */
.motion-halftone-drift {
  --motion-duration: 5.4s;
  --motion-intensity: 1.00;
  position: relative;
  display: grid;
  place-items: center;
  inline-size: min(100%, 18rem);
  min-block-size: 9rem;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, #67e8f9 35%, transparent);
  border-radius: 1rem;
  background: #08111f;
  isolation: isolate;
}

.motion-halftone-drift__layer {
  position: absolute;
  inset: 22%;
  border-radius: 999px;
  background: linear-gradient(135deg, #67e8f9, #a78bfa);
  transform-origin: center;
  animation: halftone-drift-motion var(--motion-duration) ease-in-out infinite;
  will-change: transform, opacity, filter;
}

.motion-halftone-drift__layer:nth-child(2) {
  animation-delay: calc(var(--motion-duration) * -0.33);
  opacity: .68;
}
.motion-halftone-drift__layer:nth-child(3) {
  animation-delay: calc(var(--motion-duration) * -0.66);
  opacity: .42;
}

@keyframes halftone-drift-motion {
  0%, 100% { transform: translate3d(-8%, -3%, 0) scale(.84); opacity: .45; }
  50% { transform: translate3d(calc(12% * var(--motion-intensity)), calc(8% * var(--motion-intensity)), 0) scale(1.08); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .motion-halftone-drift__layer { animation: none; opacity: .72; }
}

How to make this

A typed halftone background registers dot offset variables with @property, then drifts radial-gradient dot layers without swapping whole background strings.

html
<div class="halftone-panel">  <strong>Halftone</strong></div> <style>6@property --dot-x {  syntax: '<length>';  inherits: false;  initial-value: 0px;}@property --dot-y {  syntax: '<length>';  inherits: false;  initial-value: 0px;}.halftone-panel {  --dot-x: 0px;  --dot-y: 0px;  width: 18rem;  aspect-ratio: 16 / 9;  display: grid;  place-items: center;  border-radius: 1rem;  color: white;  background:26    radial-gradient(circle at calc(10px + var(--dot-x)) calc(10px + var(--dot-y)), rgba(103,232,249,.86) 0 3px, transparent 4px),    radial-gradient(circle at calc(18px - var(--dot-x)) calc(18px - var(--dot-y)), rgba(244,114,182,.58) 0 2px, transparent 3px),    #0f172a;29  background-size: 24px 24px, 18px 18px, auto;  animation: halftone-drift 5s linear infinite;}@keyframes halftone-drift {33  to { --dot-x: 12px; --dot-y: 8px; }}35@media (prefers-reduced-motion: reduce) {  .halftone-panel { animation: none; }}</style>

Annotated snippet

  1. Line 6Registering length variables gives the browser numeric values to interpolate instead of treating the background as one string.

    Unregistered custom properties tend to step. Registered length values can interpolate smoothly.

    PitfallWhy use @property for halftone drift?

    It gives custom properties a type, so the browser can interpolate offsets smoothly instead of snapping between string values.

  2. Line 26The dots are procedural paint. No image asset is needed and density can change per breakpoint.
    PitfallHow dense should the dots be?

    Dense enough to read as halftone, but not so dense that text loses contrast. Increase the solid overlay before increasing motion.

  3. Line 29Dot grid size is separate from dot offset. That keeps drift from resizing the pattern.

    Changing background-size makes dot spacing shimmer. A fixed grid with offset drift keeps the texture stable.

    PitfallShould background-size animate?

    Usually no. Animate offset or a typed scale variable; changing background-size can shimmer and distract from foreground content.

  4. Line 33The keyframe changes typed controls, not the entire background declaration.
    PitfallWhat counts as a different showcase example?

    Different expression methods such as offset drift, density shimmer, or scale pulse. Color-only domain changes are not enough.

  5. Line 35Reduced motion stops the procedural drift while retaining the halftone texture.
    PitfallHow should reduced motion handle procedural backgrounds?

    Pin the background at a balanced frame and keep foreground contrast. Do not replace the drift with a new animated fallback.

Other pitfalls

What should I verify before shipping this pattern?
Check that the preview card and showcase communicate the same start and end state, every inspector control visibly changes the animation, compare demos stay fixed-height and centered, and reduced motion preserves the information without running a substitute loop.
Advanced

Register a typed --dot-hue angle and let the same Houdini chain cycle palette

Same 4.8s cycle, same radial-gradient dot layers, same @property --dot-x/--dot-y offset drift on both — the dot positions interpolate identically. BEFORE keeps the palette frozen at the gradient's authored colors. AFTER registers --dot-hue as a typed angle and animates it through filter: hue-rotate(); because the property is typed, the browser tweens 0deg → 360deg smoothly while the offset drift continues on its own animation. Two Houdini-interpolated streams running in parallel on the same panel.

View explanation and full code25 lines

The base recipe registers --dot-x and --dot-y as typed lengths so the dot offset can interpolate smoothly through the Houdini @property mechanism. Production halftone effects ALSO drift their palette over time — but a naive color swap re-paints the entire gradient string each frame and stops interpolating. Register one more typed property — --dot-hue as an angle — and feed it into a filter: hue-rotate() on the same panel. The hue cycle runs on its own keyframe alongside the existing offset drift; both keep the typed-interpolation guarantee the base teaches. Same @property pattern, one more registered value extending the chain to color.

Append these rules inside the <style> block from the base snippet above.

css
/* Advanced: typed hue cycle — extends the base recipe.   Same @property mechanism the base uses for --dot-x/--dot-y,   extended with a registered angle for --dot-hue. The hue is fed   into filter: hue-rotate() and the animation list grows by one   keyframe — the browser interpolates the angle smoothly because the   property is typed, exactly like the offset interpolation the base   relies on. No string-swap fallback. */@property --dot-hue {  syntax: '<angle>';  inherits: false;  initial-value: 0deg;}.halftone-panel {  --dot-hue: 0deg;  filter: hue-rotate(var(--dot-hue));  animation:    halftone-drift 5s linear infinite,    halftone-hue 8s linear infinite;}@keyframes halftone-hue {  to { --dot-hue: 360deg; }}@media (prefers-reduced-motion: reduce) {  .halftone-panel { animation: none; filter: hue-rotate(0deg); }}

Notes

Overview

Typed halftone drift turns a procedural dot field into an animatable surface. The key is that the dots are not a bitmap and the animation is not swapping entire gradient declarations. radial-gradient() layers define the halftone, while @property exposes numeric controls for offset, radius, density, or angle.

When to use it

Use it for editorial headers, poster-like cards, small branded backgrounds, and data-adjacent surfaces where the dot structure is part of the visual language. It is strongest when the viewer should notice the printing or screen pattern itself. Avoid it behind long text, dense controls, or small numeric labels unless a stable overlay preserves contrast.

How it works

The background is made from repeated radial gradients whose positions and sizes are driven by custom properties. @property registers values with syntax such as <length>, <percentage>, or <angle>, allowing keyframes to move numeric controls instead of a whole gradient string.

Production gotchas

A new showcase example should change the expression method: offset drift, density shimmer, scale pulse, and duotone phase are distinct. Merely recoloring the same dot drift for another domain is not enough. Also watch paint cost at large sizes; procedural backgrounds can still repaint if the animated properties affect the gradient surface every frame.

Accessibility

The halftone is decorative. Keep text outside the moving dot contrast or behind a stable surface, and test the highest-density frame as well as the rest frame. Reduced motion should stop both dot drift and density shimmer while leaving a static halftone that still fits the design.

References

Implementation depth

Typed halftone drift treats dot spacing and offsets as animatable numeric values. Registering custom properties lets the browser interpolate the procedural background controls instead of swapping a whole radial-gradient string at keyframe boundaries.

The important design rule is expression method first. Dot scale, offset drift, and density shimmer are different halftone behaviors; changing only the color or domain label is not a new pattern. Keep text over the dots inside a contrast-tested surface.

Typed variables also make inspector controls honest. If a slider claims to control phase, radius, or density, it should update a registered value that visibly interpolates, not toggle a class that jumps between two hardcoded backgrounds.

Large halftone fields can still repaint, so keep them scoped. Use the effect for compact surfaces, cap density at small sizes, and verify that the reduced-motion frame does not create a noisy optical texture behind content.