From our sponsor: Prepare for advanced communication roles and earn a Northwest Master’s degree online.

CSS Paint is an API that allows developers to systematically create and draw graphics where CSS expects an image.

It’s part of CSS Houdin, which is an umbrella for the term seven new low-level APIs that reveal different parts of the CSS engine and allow developers to extend CSS by joining the browser rendering engine style and layout process.

It allows developers to write code that a browser can parse into CSS, creating new CSS features without waiting for their natural implementation in browsers.

Today we’re exploring two special APIs that are part of CSS Houdin’s umbrella:

  1. CSS paint, which was fully implemented in Chrome, Opera, and Edge at the time of this writing, and is available in Firefox and Safari polyfill.
  2. CSS Properties and Values ​​API, which allows us to explicitly define our CSS variables, their initial values, what types of values ​​they support, and whether these variables can be inherited.

CSS Paint allows us to make graphics a PaintWorklet, deleted version CanvasRenderingContext2D. The main differences are:

  • No support for text rendering
  • No direct use / manipulation of pixels

With these two shortcomings in mind, you can draw anything canvas2d, you can draw a standard DOM element using the CSS Paint API. For those of you who have done graphics canvas2d, you should be right at home.

In addition, we as developers have the ability to bypass CSS variables as a contribution from us PaintWorklet and control its presentation with custom predefined attributes.

This allows for high customization even from designers who may not be familiar with Javascript.

You can see more examples here and here. And when this is gone, we get to coding!

The simplest example: two diagonals

A CSS painting is created that, when loaded, draws two diagonal lines over the surface of the DOM element to which it is applied. The surface size of the painting draws adapts to the width and height of the DOM element, and we are able to control the thickness of the diagonal line by moving the CSS variable.

Creating a PaintWorklet

To download a PaintWorklet, we need to create it as a separate Javascript file (diagonal-lines.js).

const PAINTLET_NAME = 'diagonal-lines'

class CSSPaintlet {

  // 👉 Define the names of the input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
    ]
  }

  // 👉 Define names for input CSS arguments supported in paint()
  // ⚠️ This part of the API is still experimental and hidden
  //    behind a flag.
  static get inputArguments () {
    return []
  }

  // 👉 paint() will be executed every time:
  //  - any input property changes
  //  - the DOM element we apply our paintlet to changes its dimensions
  paint(ctx, paintSize, props) {
    // 👉 Obtain the numeric value of our line width that is passed
    //    as a CSS variable
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))

    ctx.lineWidth = lineWidth

    // 🎨 Draw diagonal line #1
    ctx.beginPath()
    ctx.moveTo(0, 0)
    ctx.lineTo(paintSize.width, paintSize.height)
    ctx.stroke()

    // 🎨 Draw diagonal line #2
    ctx.beginPath()
    ctx.moveTo(0, paintSize.height)
    ctx.lineTo(paintSize.width, 0)
    ctx.stroke()
  }
}

// 👉 Register our CSS Paintlet with the correct name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

We define our CSS painting as a standalone class. This class only needs one method to work – paint(), which draws the graphic on the surface to which we point our CSS painting. It is done by changing any CSS variable on which our painting is based, or when our DOM element changes its dimensions.

Another static method inputProperties() is optional. It tells the CSS painting which CSS variables it supports. In our case, it would be --diagonal-lines-line-width. We declare it as an input and use it for use in our assets paint() method. It is important that we throw it in the number by putting it a Number to ensure cross-browser support.

Another optional static method is supported: inputArguments. It reveals arguments to us paint() as follows:

#myImage {
  background-image: paint(myWorklet, 30px, red, 10deg);
}

However, this part of the CSS painting API is still hidden behind the flag and is considered experimental. For usability and compatibility, we won’t cover it in this article, but I encourage you to read it yourself. Instead, we use CSS variables inputProperties() a method to manage all the revenue for our painting.

We register our CSS PaintWorklet service

Afterwards, we need to refer to our CSS spice and register it on our main page. It’s important that we conditionally charge awesome css-paint-polyfill a package that ensures our paintings work in Firefox and Safari.

It should be noted that in our CSS paint shop we can use a new one CSS Properties and Values ​​API, also part of Houdin’s umbrella, to explicitly define the inputs through CSS variables CSS.registerProperty(). Here’s how to manage CSS variables:

  • Their types and syntaxes
  • Whether this CSS variable inherits from any main element
  • What is its initial value if not specified by the user

This API is also not supported in Firefox and Safari, but we can still use it in Chromium browsers. This way, our future-proof demos and browsers that don’t support it will ignore it.

;(async function() {
  // ⚠️ Handle Firefox and Safari by importing a polyfill for CSS Pain    
  if (CSS['paintWorklet'] === undefined) {
    await import('https://unpkg.com/css-paint-polyfill')
  }

  // 👉 Explicitly define our custom CSS variable
  //    This is not supported in Safari and Firefox, so they will
  //    ignore it, but we can optionally use it in browsers that 
  //    support it. 
  //    This way we will future-proof our applications so once Safari
  //    and Firefox support it, they will benefit from these
  //    definitions too.
  //
  //    Make sure that the browser treats it as a number
  //    It does not inherit it's value
  //    It's initial value defaults to 1
  if ('registerProperty' in CSS) {
    CSS.registerProperty({
      name: '--diagonal-lines-line-width',
      syntax: '<number>',
      inherits: false,
      initialValue: 1
    })
  }

  // 👉 Include our separate paintlet file
  CSS.paintWorklet.addModule('path/to/our/external/worklet/diagonal-files.js')
})()

Refer to the target as a CSS background

Once we have included our painting as a JS file, using it is dead simple. We select our DOM target element that we want to format in CSS and distribute our painting paint() CSS command:

#myElement {
   // 👉 Reference our CSS paintlet
   background-image: paint('--diagonal-lines');

   // 👉 Pass in custom CSS variable to be used in our CSS paintlet
   --diagonal-lines-line-width: 10;

   // 👉 Remember - the browser treats this as a regular image
   // referenced in CSS. We can control it's repeat, size, position
   // and any other background related property available
   background-repeat: no-repeat;
   background-size: cover;
   background-position: 50% 50%;

   // Some more styles to make sure we can see our element on the page
   border: 1px solid red;
   width: 200px;
   height: 200px;
   margin: 0 auto;
}

And when this code is out of the way, you get this:

Keep in mind that we can use this CSS painting as a background for any DOM element that has dimensions. Blow up our DOM element full screen, lower it background-size x and y values ​​and set it background-repeat repeat. Here is our updated example:

We use the same CSS painting as in our previous example, but now we’ve expanded it to cover the entire demo page.

So now that we’ve covered our basic example and seen how to organize our code, let’s write some more nice looking demos!

Particle connections

Look at the pen
CSS calculation particles
by Georgi Nikoloff (COM)@gbnikolov) on CodePen.

This paint surface has been inspired awesome presentation by @Helsinki.

Again for those of you who have exercised canvas2d API draws graphics in the past, this is pretty straightforward.

We control how many points we are going to make through the CSS variable “-dots-links-count”. Once we have obtained its numerical value in our painting, we create a suitably sized table and fill it with objects at random. x, y and radius features.

We then loop all the objects in our matrix, draw the ball to its coordinates, find the nearest neighbor (the minimum distance is controlled via the CSS variable “ point-connections-connection-min-distance“) and connect them on the line.

We also control the fill color of the balls and the color of the lines “ point-connections-fill-color“ and --dots-connections-stroke-color CSS variables.

Here is the complete processed code:

const PAINTLET_NAME = 'dots-connections'

class CSSPaintlet {
  // 👉 Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`,
      `--${PAINTLET_NAME}-fill-color`,
      `--${PAINTLET_NAME}-connection-min-dist`,
      `--${PAINTLET_NAME}-count`,
    ]
  }

  // 👉 Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const minDist = Number(props.get(`--${PAINTLET_NAME}-connection-min-dist`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    const fillColor = props.get(`--${PAINTLET_NAME}-fill-color`)
    const numParticles = Number(props.get(`--${PAINTLET_NAME}-count`))
    
    // 👉 Generate particles at random positions
    //    across our DOM element surface
    const particles = new Array(numParticles).fill(null).map(_ => ({
      x: Math.random() * paintSize.width,
      y: Math.random() * paintSize.height,
      radius: 2 + Math.random() * 2,
    }))
    
    // 👉 Assign lineWidth coming from CSS variables and make sure
    //    lineCap and lineWidth are round
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    
    // 👉 Loop over the particles with nested loops - O(n^2)
    for (let i = 0; i < numParticles; i++) {
      const particle = particles[i]
      // 👉 Loop second time 
      for (let n = 0; n < numParticles; n++) {
        if (i === n) {
          continue
        }
        const nextParticle = particles[n]
        // 👉 Calculate distance between the current particle
        //    and the particle from the previous loop iteration
        const dx = nextParticle.x - particle.x
        const dy = nextParticle.y - particle.y
        const dist = Math.sqrt(dx * dx + dy * dy)
        // 👉 If the dist is smaller then the minDist specified via
        //    CSS variable, then we will connect them with a line
        if (dist < minDist) {
          ctx.strokeStyle = strokeColor
          ctx.beginPath()
          ctx.moveTo(nextParticle.x, nextParticle.y)
          ctx.lineTo(particle.x, particle.y)
          // 👉 Draw the connecting line
          ctx.stroke()
        }
      }
      // Finally draw the particle at the right position
      ctx.fillStyle = fillColor
      ctx.beginPath()
      ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
      ctx.closePath()
      ctx.fill()
    }
    
  }
}

// 👉 Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Line loop

Here is our following example. It expects the following CSS variables as input for our painting:

--loop-line-width
--loop-stroke-color
--loop-sides
--loop-scale
--loop-rotation

We loop around the entire circle (PI * 2) and place them parallel to the circumference --loop-sides calculate CSS variables. At each location, re-loop around our entire circle and connect it to all other locations via a ctx.lineTo() command:

const PAINTLET_NAME = 'loop'

class CSSPaintlet {
  // 👉 Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`,
      `--${PAINTLET_NAME}-sides`,
      `--${PAINTLET_NAME}-scale`,
      `--${PAINTLET_NAME}-rotation`,
    ]
  }
  // 👉 Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    const numSides = Number(props.get(`--${PAINTLET_NAME}-sides`))
    const scale = Number(props.get(`--${PAINTLET_NAME}-scale`))
    const rotation = Number(props.get(`--${PAINTLET_NAME}-rotation`))
    
    const angle = Math.PI * 2 / numSides
    const radius = paintSize.width / 2
    ctx.save()
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    ctx.strokeStyle = strokeColor
    ctx.translate(paintSize.width / 2, paintSize.height / 2)
    ctx.rotate(rotation * (Math.PI / 180))
    ctx.scale(scale / 100, scale / 100)
    ctx.moveTo(0, radius)

    // 👉 Loop over the numsides twice in nested loop - O(n^2)
    //    Connect each corner with all other corners
    for (let i = 0; i < numSides; i++) {
      const x = Math.sin(i * angle) * radius
      const y = Math.cos(i * angle) * radius
      for (let n = i; n < numSides; n++) {
        const x2 = Math.sin(n * angle) * radius
        const y2 = Math.cos(n * angle) * radius
        ctx.lineTo(x, y)
        ctx.lineTo(x2, y2);
      }
    }
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
  }   
}

// 👉 Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Noise button

Here is our following example. It is inspired this another awesome CSS Paintlet by Jhey Tompkins. It expects the following CSS variables as input for our painting:

--grid-size
--grid-color
--grid-noise-scale

The painting itself uses pearl noise (code: joeiddon) to adjust the coverage of each individual cell.

const PAINTLET_NAME = 'grid'

class CSSPaintlet {
  // 👉 Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-size`,
      `--${PAINTLET_NAME}-color`,
      `--${PAINTLET_NAME}-noise-scale`
    ]
  }

  // 👉 Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const gridSize = Number(props.get(`--${PAINTLET_NAME}-size`))
    const color = props.get(`--${PAINTLET_NAME}-color`)
    const noiseScale = Number(props.get(`--${PAINTLET_NAME}-noise-scale`))

    ctx.fillStyle = color
    for (let x = 0; x < paintSize.width; x += gridSize) {
      for (let y = 0; y < paintSize.height; y += gridSize) {
        // 👉 Use perlin noise to determine the cell opacity
        ctx.globalAlpha = mapRange(perlin.get(x * noiseScale, y * noiseScale), -1, 1, 0.5, 1)
        ctx.fillRect(x, y, gridSize, gridSize)
      }
    }
  }
}

// 👉 Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Curvy divider

As a final example, something perhaps more useful is done. We systematically draw dividers to separate the text content of a page:

And as usual, here’s the CSS Painting Code:

const PAINTLET_NAME = 'curvy-dividor'

class CSSPaintlet {
  // 👉 Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-points-count`,
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`
    ]
  }
  // 👉 Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const pointsCount = Number(props.get(`--${PAINTLET_NAME}-points-count`))
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    
    const stepX = paintSize.width / pointsCount
    
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    
    ctx.strokeStyle = strokeColor
    
    const offsetUpBound = -paintSize.height / 2
    const offsetDownBound = paintSize.height / 2
    
    // 👉 Draw quadratic bezier curves across the horizontal axies
    //    of our dividers:
    ctx.moveTo(-stepX / 2, paintSize.height / 2)
    for (let i = 0; i < pointsCount; i++) {
      const x = (i + 1) * stepX - stepX / 2
      const y = paintSize.height / 2 + (i % 2 === 0 ? offsetDownBound : offsetUpBound)
      const nextx = (i + 2) * stepX - stepX / 2
      const nexty = paintSize.height / 2 + (i % 2 === 0 ? offsetUpBound : offsetDownBound)
      const ctrlx = (x + nextx) / 2
      const ctrly = (y + nexty) / 2
      ctx.quadraticCurveTo(x, y, ctrlx, ctrly)
    }
    ctx.stroke()
  }
}

// 👉 Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Conclusion

This article covered all the key components and methods of the CSS Paint API. It is quite easy to install and very useful if you want to draw more advanced graphics that CSS does not already support.

We can easily create a library of these CSS paints and continue to use them in our project with minimal requirements.

As a good practice, I encourage you to find a great one canvas2d demos and move them to the new CSS Paint API.

Thumbnail floating effect with SVG filters

LEAVE A REPLY

Please enter your comment!
Please enter your name here