CSS Paint API (Worklets): Custom Paint for Houdini

The CSS Paint API lets CSS ask a paint worklet to draw pixels at render time, so you can create procedural backgrounds, borders, and decorative effects without shipping image assets. It is part of the Houdini family of APIs and is useful when a design needs custom graphics that still respond to CSS sizing, colors, and layout.

Quick answer: The Paint API adds a paint() function to CSS and a paint worklet that draws into a box on demand. Use it for custom visual effects that are hard to build with gradients or images, but do not rely on it for core content or everywhere without fallback support.

Difficulty: Intermediate

You'll understand this better if you know: basic CSS syntax, how background and border properties work, and the difference between a CSS property value and generated pixels.

1. What Is the CSS Paint API?

The CSS Paint API is a browser feature that lets CSS call a paint worklet to generate an image-like result during rendering. Instead of pointing background-image at a file, you point it at paint(), and the browser asks your paint code to draw the visual effect.

The key idea is that the browser still controls layout and painting, while your worklet supplies the drawing instructions for a specific area.

2. Why the CSS Paint API Matters

Before Paint API, custom decorative graphics usually meant images, SVG files, or many layered gradients. Those approaches can be great, but they are not always flexible when the design must react to element size or theme changes.

Paint worklets matter because they let you define visuals in a reusable way that is still controlled by CSS. That gives designers and developers a way to create dynamic decoration without hard-coding assets for every variation.

This is especially useful when you want one effect to scale cleanly across cards, buttons, panels, or shapes while keeping CSS as the source of truth for dimensions and colors.

3. Basic Syntax or Core Idea

The CSS side uses the paint() function. The worklet side registers a paint class that draws into the provided canvas-like context.

CSS usage

Here is the minimal idea in CSS. The custom paint name is passed to paint(), often with optional arguments.

.card {
  background-image: paint(custom-pattern);
}

The browser looks up a paint worklet registered under custom-pattern and asks it to paint the element’s background area.

What the worklet provides

A paint worklet typically defines a class with a paint() method and registers it with a name. The method receives the drawing context, element size, and any custom properties or arguments.

4. Step-by-Step Examples

Example 1: Simple striped background

A common use case for Paint API is drawing a repeating decorative pattern that scales with the element. In practice, this replaces a static image with something procedural.

/* CSS */
.panel {
  background-image: paint(striped-background);
  background-color: white;
}

The CSS asks for a custom paint named striped-background. The visual result depends entirely on how the worklet draws the stripes.

Example 2: Parameterized dots using custom properties

Paint worklets can read CSS custom properties, which makes them useful for theme-aware decorations. Here the element controls the dot color and spacing through CSS variables.

/* CSS */
.badge {
  --dot-color: #1f6feb;
  --dot-size: 6px;
  background-image: paint(dotted-field);
}

This pattern keeps the effect reusable while still letting different components tune the output with CSS alone.

Example 3: Decorative border effect

Paint API is not limited to backgrounds. It can also be used where an image source is accepted, such as a border image source.

/* CSS */
.frame {
  border: 12px solid transparent;
  border-image-source: paint(ornamental-border);
  border-image-slice: 30;
}

This is useful when the border needs to be richer than a flat color or a simple gradient.

Example 4: Responsive layout-based drawing

A paint worklet receives the painted box size, so it can draw differently depending on the element’s dimensions. That makes it good for patterns that should look balanced on small and large cards.

/* CSS */
.tile {
  background-image: paint(responsive-rings);
}

In this pattern, the worklet can compute spacing from the box width and height instead of relying on a fixed-size asset.

5. Practical Use Cases

Paint API is most valuable when the visual effect is decorative, reusable, and parameter-driven.

6. Common Mistakes

Mistake 1: Expecting Paint API to be a general scripting system

Paint worklets are for drawing, not for building application state or running page behavior. They should generate pixels from inputs, then stop.

Problem: Developers sometimes expect paint code to fetch data, update DOM state, or react like a normal page script. That does not fit the worklet model and leads to confusing limitations.

/* Incorrect mental model: trying to use painting for app logic */
.chart {
  background-image: paint(live-dashboard);
}

Fix: Use Paint API only for decoration or visual generation, and keep application logic in normal CSS, HTML, or page code.

.chart {
  background-image: paint(grid-overlay);
  background-color: #fff;
}

The corrected version uses Paint API for the visual layer only, which matches the design of the feature.

Mistake 2: Forgetting a fallback for unsupported browsers

Paint API support is not universal, so relying on paint() alone can leave some users with missing decoration.

Problem: If a browser does not understand the paint function, the background value is ignored and the element may fall back to a plain default or lose the intended styling.

.card {
  background-image: paint(soft-grid);
}

Fix: Provide a regular CSS fallback first, then layer the paint-based effect on top when supported by the browser.

.card {
  background-color: #f8fafc;
  background-image: paint(soft-grid);
}

This works better because users without support still see a usable design.

Mistake 3: Naming the paint effect inconsistently

The name used in CSS must match the name registered by the worklet. A mismatch means the browser cannot find the painter.

Problem: If the CSS calls paint(line-pattern) but the registered name is different, the effect will not render as expected.

/* CSS */
.note {
  background-image: paint(line-pattern);
}

/* Conceptual mismatch: the registered painter has a different name */

Fix: Use one exact name consistently in the CSS and in the paint registration.

/* CSS */
.note {
  background-image: paint(line-pattern);
}

Matching the name exactly is essential because paint() works by lookup, not by fuzzy matching.

7. Best Practices

Prefer decorative, low-risk visuals

Paint API is best when the drawing is nice to have, not when the page depends on it. That makes the feature a good fit for ornamentation, not critical content.

Better approach: Use Paint API for a background texture, but keep readable colors and spacing independent of the effect.

.callout {
  background-color: #ffffff;
  color: #111827;
  background-image: paint(paper-texture);
}

This keeps the element usable even if the paint layer is subtle or unavailable.

Use custom properties for theming

Custom properties make the paint effect easier to reuse across components. They let CSS control the appearance without changing the painter name.

Better approach: Expose color, spacing, or density through variables so one paint effect can serve many designs.

.tag {
  --accent: #7c3aed;
  --gap: 8px;
  background-image: paint(accent-dots);
}

This improves maintainability because style changes stay in CSS rather than in drawing logic.

Keep output predictable and size-aware

Paint worklets should produce consistent results for the same inputs. That makes them easier to test, easier to reason about, and less likely to surprise users at different sizes.

Better approach: Base calculations on the provided box size and simple inputs instead of hidden assumptions.

.hero {
  background-image: paint(diagonal-stripes);
}

That pattern makes the effect scale naturally with the element.

8. Limitations and Edge Cases

Another practical edge case is performance: a very complex paint effect can be expensive if the browser has to repaint it often during resizing or scrolling.

9. Practical Mini Project

Here is a small real-world use case: a reusable card style with a subtle patterned background and a reliable fallback. The idea is to make the component look polished without relying on an image file.

Example code:

.feature-card {
  padding: 1.5rem;
  border-radius: 1rem;
  background-color: #f8fafc;
  background-image: paint(soft-dots);
  color: #0f172a;
}

This example shows the intended pattern: a plain background color guarantees readability, while the paint effect adds character where supported.

10. Key Points

11. Practice Exercise

Expected output: A card or panel that has a plain readable base style and an optional custom painted texture layered on top.

Hint: Start with the fallback background color first, then add the paint() image as enhancement.

Solution:

.panel {
  --accent: #2563eb;
  background-color: #eff6ff;
  background-image: paint(panel-texture);
  border: 1px solid #bfdbfe;
  border-radius: 0.75rem;
}

12. Final Summary

The CSS Paint API gives CSS a way to draw custom visuals through paint worklets, which makes it a strong fit for procedural decoration, scalable patterns, and theme-aware effects. It extends what CSS can do without turning styling into general application logic.

Its main strengths are flexibility and reusability: one painted effect can adapt to different sizes and values while staying controlled by CSS. At the same time, it comes with real limitations, especially browser support and the need for graceful fallbacks.

If you want to go further, the next useful step is to study how paint worklets relate to other Houdini APIs and how CSS custom properties can act as parameters for reusable visual systems.