Advanced CSS Houdini: Custom Layouts and Paint Worklets

Team 5 min read

#css

#houdini

#webdev

#layout

Introduction

CSS Houdini opens a doorway into the browser’s rendering pipeline, exposing low-level hooks that let you customize layout, paint, and other rendering steps. This post focuses on two powerful facets: custom layouts via the Layout API and dynamic rendering via Paint worklets. Together, they enable patterns that were previously difficult or impossible to achieve with pure CSS.

Why Houdini matters for modern UI

  • Fine-grained control: Influence how elements measure, position, and paint without resorting to brittle hacks.
  • Performance opportunities: Move expensive logic off the main thread via dedicated worklets.
  • Progressive enhancement: If a browser doesn’t support Houdini yet, you can provide graceful fallbacks with standard CSS.

Custom Layouts: Layout API

The Layout API lets you implement your own layout algorithm for a container. You can position and size its children according to custom rules, independent of CSS Grid or Flexbox flows. A layout worklet runs on the compositor thread, computing where each child should go.

Key concepts

  • A layout is registered with a name (e.g., masonry, flow).
  • You attach the layout to an element with the layout property (e.g., layout: masonry;).
  • The worklet can read CSS custom properties and adapt its output accordingly.

Code sketch: a simple masonry-like layout worklet

// layout-worklet.js
registerLayout('masonry', class {
  static get inputProperties() { return ['--gap']; }

  layout(children, edges, constraints) {
    const gap = parseInt(this.properties.get('--gap')) || 8;
    const maxInline = constraints.inlineSize;
    let x = 0, y = 0;
    let rowHeight = 0;

    for (const child of children) {
      const w = child.intrinsicInlineSize || child.size.inlineSize;
      const h = child.intrinsicBlockSize || child.size.blockSize;

      // Move to next row if item doesn't fit
      if (x > 0 && x + w > maxInline) {
        x = 0;
        y += rowHeight + gap;
        rowHeight = 0;
      }

      // Position the child
      child.setPosition({ inline: x, block: y });
      x += w + gap;
      if (h > rowHeight) rowHeight = h;
    }

    // Return the final block size to fit all children
    return { inlineSize: maxInline, blockSize: y + rowHeight };
  }
});

Usage (HTML/CSS)

.container {
  /* Enable the custom layout and allow the layout to measure itself */
  layout: masonry;
  /* Optional: control the spacing via CSS variable */
  --gap: 12px;
}
.container > .item {
  width: 140px;
  height: 100px;
  /* item styling... */
}

Usage (HTML)

<div class="container" style="--gap: 12;">
  <div class="item">A</div>
  <div class="item">B</div>
  <div class="item">C</div>
  <div class="item">D</div>
  <!-- more items -->
</div>

Notes

  • The Layout API is experimental in some browsers. Check current browser support and MDN for details.
  • The worklet can read and react to CSS properties on the container or its children, enabling responsive patterns without scripting the layout in the main thread.

Paint Worklets: Rendering with Paint API

Paint worklets let you draw on an element’s background or border via a procedural painter. You define a paint class, register it, and then reference it in CSS with paint(name). This is ideal for dynamic textures, gradients, patterns, or procedural imagery.

Code example: a checkerboard painter

// checkerboard-paint.js
class CheckerboardPainter {
  static get inputProperties() {
    return ['--size', '--fg', '--bg'];
  }

  paint(ctx, geom, properties) {
    const size = parseInt(properties.get('--size')) || 20;
    const fg = properties.get('--fg').toString() || '#333';
    const bg = properties.get('--bg').toString() || '#fff';

    // Clear background
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, geom.width, geom.height);

    // Draw checkerboard
    ctx.fillStyle = fg;
    for (let y = 0; y < geom.height; y += size) {
      for (let x = 0; x < geom.width; x += size) {
        if (((x / size) + (y / size)) % 2 === 0) {
          ctx.fillRect(x, y, size, size);
        }
      }
    }
  }
}
registerPaint('checkerboard', CheckerboardPainter);

Usage (CSS)

@import url('/path/to/checkerboard-paint.js');

.tile {
  width: 240px;
  height: 140px;
  background: paint(checkerboard);
  /* Optional painter inputs */
  --size: 24;
  --fg: #111;
  --bg: #eee;
}

How to load the paint worklet (in HTML)

<script>
  if ('paintWorklet' in CSS) {
    CSS.paintWorklet.addModule('/path/to/checkerboard-paint.js');
  }
</script>

Getting value from inputs and fallbacks

  • You can expose inputs via CSS custom properties on the element, allowing non-JS authors to tweak appearance.
  • If the browser doesn’t support Paint or Houdini worklets, the element falls back to standard CSS backgrounds or colors.

Practical patterns and best practices

  • Progressive enhancement: Provide solid fallbacks (solid colors, gradients) for browsers without Houdini support.
  • Performance discipline: Move heavy, per-pixel drawing off the main thread; use canary-friendly patterns to avoid jank.
  • Debugging tips: Use console logs sparingly in worklets, and visually verify outputs by toggling inputs in real time.
  • Testing: Validate across Chromium-based browsers and keep an eye on caniuse and MDN for status updates.

Getting started: quick steps

  • Set up a small project structure with your page, a layout-worklet.js, and a paint-worklet.js.
  • Register the worklets from your main script:
if ('layoutWorklet' in CSS) {
  CSS.layoutWorklet.addModule('/path/to/layout-worklet.js');
}
if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('/path/to/paint-worklet.js');
}
  • Apply the layout in CSS with layout: your-layout-name; and use paint in CSS where you want procedural visuals.
  • Iterate with simple, isolated examples to validate behavior before integrating into larger components.

Conclusion

CSS Houdini opens doors to patterns that combine precise layout control with rich, on-the-fly painting—without resorting to heavy JavaScript DOM manipulation. Custom layouts unlock new grid and flow patterns tailored to content, while paint worklets empower you to render textures and visuals exactly where you need them. As support evolves, these tools offer a path to more expressive, performant web interfaces that stay within CSS’s declarative spirit.