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.
- It produces pixels for CSS properties such as backgrounds and borders.
- It runs inside a paint worklet, which is separate from the main page script.
- It is designed for decorative, reusable visuals rather than page logic.
- It works best when the output depends on box size, custom properties, or simple parameters.
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
- Decorative card backgrounds that adapt to component size.
- Custom borders and frames that need more detail than a solid line.
- Repeated patterns such as dots, stripes, waves, or grids.
- Theme-based visuals driven by CSS custom properties.
- Procedural placeholders or skeleton-like visual treatments.
- Specialized UI chrome for dashboards, editors, or design systems.
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
- Support is not available in every browser, so fallback styling is necessary.
- Paint API is for visual output only; it does not replace layout, animation, or page scripting.
- Worklets are intentionally constrained, so they do not have the same access as normal page code.
- Results can vary if custom properties are missing or invalid, so defaults matter.
- The effect may not be suitable for text or essential UI because painted content is decorative and can disappear in unsupported environments.
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
- The CSS Paint API lets CSS generate custom visual output through a paint worklet.
- paint() is usually used with backgrounds or borders.
- It is best for decorative, reusable, size-aware effects.
- Fallback styles are important because support is limited compared with basic CSS.
- Use custom properties to make one paint effect flexible across components.
11. Practice Exercise
- Create a reusable panel style that uses a paint-based pattern for decoration.
- Include a solid background color fallback so the panel still looks good without Paint API support.
- Expose at least one CSS custom property to control the accent color or density.
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.