The Problem with Polygon Borders
Creating complex shapes with CSS has become straightforward using clip-path, but adding borders to those shapes remains a persistent challenge. Traditional CSS offers no robust solution for polygon borders, forcing developers to create workarounds that feel hacky and lack flexibility.
The CSS Paint API, part of the CSS Houdini family of APIs, provides an elegant solution by allowing developers to programmatically draw on elements using JavaScript worklets. This capability transforms how we approach web design, enabling custom border effects that would otherwise require images, SVG hacks, or JavaScript canvas rendering.
For modern web applications built with frameworks like Next.js, the Paint API opens new possibilities for creating visually distinctive interfaces without sacrificing performance or maintainability.
Why Traditional CSS Borders Fail
The fundamental issue with creating borders on clipped elements stems from how CSS handles the border property. When you apply clip-path to an element, the standard border property clips along with the element's content, resulting in either no border or distorted border rendering depending on the shape complexity. For rectangular clip-path applications, borders appear normal, but for polygons with angled edges, the standard border property fails to produce the expected visual result.
- Standard border - Clips to the bounding box, not the polygon shape
- border-image - Requires defining source images and slice values that don't translate well to dynamic polygon shapes
- box-shadow - Follows the bounding box, rather than following the polygon shape
For developers working with modern CSS frameworks, these limitations often drive the adoption of more advanced techniques like the Paint API.
CSS border-image provides some flexibility but requires source images that don't adapt well to dynamic shapes. These limitations have led developers to employ various hacks involving pseudo-elements, additional nested divs, or SVG overlays, each introducing their own complexity and maintenance challenges.
The Paint API Solution
The elegant solution combines clip-path with a custom mask created through the Paint API. The approach works in three stages:
- clip-path - Defines the polygon shape and clips the stroke to ensure the border appears only on the intended shape
- Paint API mask - Draws only the border stroke using Canvas API methods
- Combination - The mask's border aligns perfectly with the clipped shape, solving both the rendering problem and the interactive area problem
Without clip-path, the mask would create hover effects on the entire rectangular element, not just the polygon. The Canvas stroke() method draws centered on the path, meaning half the stroke extends inside the polygon and half extends outside. By applying clip-path with the same polygon coordinates, the outer half of the stroke gets clipped away, leaving only the inner half visible.
CSS Setup: Variables for Flexibility
The CSS setup relies on custom properties to make the border system reusable and configurable. A typical implementation defines two key variables: the polygon path coordinates and the border thickness. The --path variable contains comma-separated coordinate pairs defining each vertex of the polygon, while the --border variable specifies the border thickness.
Our /services/web-development/ team often uses these techniques to create visually distinctive landing pages and hero sections that stand out from standard rectangular designs.
1.box {2 --path: 50% 0, 100% 100%, 0 100%;3 --border: 5px;4 5 width: 200px;6 height: 200px;7 background: red;8 clip-path: polygon(var(--path));9 -webkit-mask: paint(polygon-border);10}JavaScript Paint Worklet Implementation
The paint worklet is the JavaScript code that executes to draw the border. It reads the CSS custom properties, converts coordinates to pixel values, draws the polygon shape, and applies the stroke. The inputProperties static getter defines which CSS properties the worklet observes, and the paint method receives the canvas context, element size, and property values.
This approach leverages the same Canvas API that powers advanced data visualizations and interactive graphics, bringing those capabilities to CSS-based designs through the Paint API.
1registerPaint('polygon-border', class {2 static get inputProperties() {3 return ['--path', '--border'];4 }5 6 paint(ctx, size, properties) {7 const path = properties.get('--path').toString().split(',');8 const border = parseFloat(properties.get('--border').value);9 const width = size.width;10 const height = size.height;11 12 // Coordinate conversion function13 const cc = function(x, y) {14 let fx = 0, fy = 0;15 if (x.indexOf('%') > -1) {16 fx = (parseFloat(x) / 100) * width;17 } else if (x.indexOf('px') > -1) {18 fx = parseFloat(x);19 }20 if (y.indexOf('%') > -1) {21 fy = (parseFloat(y) / 100) * height;22 } else if (y.indexOf('px') > -1) {23 fy = parseFloat(y);24 }25 return [fx, fy];26 };27 28 // Draw polygon29 ctx.beginPath();30 let firstPoint = path[0].trim().split(' ');31 let start = cc(firstPoint[0], firstPoint[1]);32 ctx.moveTo(start[0], start[1]);33 34 for (let i = 1; i < path.length; i++) {35 let point = path[i].trim().split(' ');36 let coords = cc(point[0], point[1]);37 ctx.lineTo(coords[0], coords[1]);38 }39 40 ctx.closePath();41 42 // Apply stroke43 ctx.lineWidth = border;44 ctx.strokeStyle = '#000';45 ctx.stroke();46 }47});The Paint API opens up creative possibilities beyond simple borders
Dashed Borders
Use Canvas setLineDash() to create dashed, dotted, or custom patterned borders along polygon paths. Simply pass an array defining dash and gap patterns.
Animated Effects
Animate borders by updating lineDashOffset through CSS custom property animations. Smooth, performant animations without JavaScript loops.
calc() Support
Calculate polygon coordinates dynamically using CSS calc() for responsive shapes. Parse expressions to compute percentage and pixel values at runtime.
Gradient Strokes
Apply gradients or images to borders using Canvas strokeStyle capabilities with createLinearGradient() or createRadialGradient().
Performance Considerations
The Paint API executes efficiently because it runs in a separate worklet thread, preventing main-thread blocking during paint operations. This architecture means paint worklet execution doesn't interfere with JavaScript execution or user interactions.
For Next.js applications, the Paint API integrates naturally with the component-based architecture. Paint worklets can be registered in client-side code, and CSS custom properties can be set through CSS-in-JS solutions or traditional stylesheets. The key is ensuring worklet registration occurs before the paint cycle executes.
When building high-performance websites, our /services/web-development/ specialists ensure that advanced CSS techniques like the Paint API enhance rather than compromise page load times and user experience.
Best Practices
- Register worklets once at the module level to avoid redundant registrations
- Use CSS custom properties instead of inline styles for better performance
- Simplify polygon shapes when possible to reduce paint complexity
- Test with Chrome DevTools Performance panel to monitor paint timing
- Batch property changes to minimize repaint cycles
Performance monitoring should track paint timing and worklet execution duration, particularly for complex shapes with many vertices or animated borders. While modern browsers handle paint operations efficiently, extreme cases may require shape simplification or animation optimization.
Frequently Asked Questions
Sources
- CSS-Tricks: Exploring the CSS Paint API: Polygon Border - Comprehensive technical guide with live CodePen demos
- CSS-Tricks: paint() function - CSS Paint API function reference
- CSS-Tricks: clip-path property - clip-path property reference
- GitHub: CSS-polygon-border by Temani Afif - Open source implementation
- Webolution Designs: CSS Paint API Overview - Modern CSS Paint API overview