Squeaky Portraits: Having Fun with the CSS path() Function – SitePoint

With the Chrome 88 release, we got support for clip-path: path(). That means it now has support in “most” major browsers!
With path(), we’re able to use path definitions for a clip-path. (You catch up on what clip-path is here). These path definition strings are the same as those we can use with the SVG path element. What’s cool about this is that it provides a way to create shapes that before may have meant using SVG. We can even create paths that break without requiring any tricks.
With the increased support came an opportunity to try something fun with it! Let’s make “Squeaky Portraits”! It’s a fun take on using clip-path: path() to clip the viewable area of an element into these “Nickelodeon-esque” splats.

“Squeaky Portraits 👇😅”
Wasn’t done playing with CSS clip-path: path() 😂
Aiming for a splat but settled for a squeaky sound effect 😆
Powered by scoped variables! 💪
(Better with sound 👍)
👉 https://t.co/Nuqyivpm5Y via @CodePen pic.twitter.com/TCCouglKpd
— Jhey 🐻🛠 (@jh3yy) February 12, 2021

Creating a Path
First up, we need our own SVG style path definition string. And in this case, more than one. The neat thing with clip-path is that we can transition them with CSS. As long as the clip-path function and number of nodes are consistent, we can transition.
To make some paths, we can hop in any vector graphic editor. In this case, I’m using Figma. And instead of creating the paths from scratch, we can use a desired “splat” as a foundation. This one looks good!
Splat Example Found Online
The trick here is to create more splats based on the foundation splat. And we need to do this without introducing or removing any nodes. These are the three splats I came up with. But you could make any shapes you like as long as you stick to that rule!
Three Different Splats Built From One Splat
You may notice that the third splat has two blobs that separate off from the main shape. This is fine, because SVG path definitions allow us to do this. We can start a line, close it, and move to another point to start another.
But didn’t I say they needed a consistent number of points? They do. And that’s what we have here! Those two blobs appear for each splat. But the trick is that we can move them behind the rest of the path when they aren’t needed.
Figma showing two blobs behind main path
Once we have our splats, we can export them and grab the path definition strings:

See the Pen 1. SVG Splats by SitePoint (@SitePoint)on CodePen.

Applying Splats
To apply the splats, we’re going to create variables for each path:
.portrait {
–splat: “M161 188.375C170 193.5 177.919 193.854 186 188.375C197.919 180.294…”;
–splattier: “M161 188.375C170 193.5 177.919 193.854 186 188.375C197.919…”;
–splatted: “M232.5 256C225 251 209.5 262.5 224 281.5C232.736 292.948…”;

These are the paths we’ve lifted straight out of the exported SVG.
We’re going with the names “splat”, “splattier”, and “splatted”. Naming things is hard. Ha! But take, for example, the “splatted” SVG:

We’re lifting out the d attribute from the path elements and creating CSS variables for them. Next, we need an element to apply these to. Let’s create an element with the class “portrait”:

Next, apply some styling to it:
.portrait {
–splat: “M161 188.375C170 193.5 177.919 193.854 186 188.375C197.919 180.294…”;
–splattier: “M161 188.375C170 193.5 177.919 193.854 186 188.375C197.919…”;
–splatted: “M232.5 256C225 251 209.5 262.5 224 281.5C232.736 292.948…”;
–none: “”;
height: 300px;
width: 300px;
background: #daa3f5;
clip-path: path(var(–clip, var(–none)));
transition: clip-path 0.2s;

And we’re good to go! Here’s a demo where you can switch between the different clip states:

See the Pen 2. Applying Clip Splat by SitePoint (@SitePoint)on CodePen.

Note how the shape transitions between the three splat shapes. But, also note how we’ve given our element an explicit height and width. This size matches the dimensions of our SVG exports. This is important. This is the one drawback of using clip-path: path(). It’s not responsive. The path definition is relative to the dimensions of your element. This is the same problem faced by CSS motion paths.
This is fine if we’re mindful of the sizes of things we’re clipping. We could also create different path variables for different viewport sizes. But if you have images that resize in a fluid way, other solutions using SVG are going to be more robust.
For our demo, we want the splat to be interactive. We can do this with CSS alone. We can use a scoped CSS variable — –clip — to control the current clip. And then we can update that variable on both :hover and :active. The –active state is triggered when we press our pointer down:
.portrait {
clip-path: path(var(–clip, var(–splat)));
.portrait:hover {
–clip: var(–splattier);
.portrait:active {
–clip: var(–splatted);

Throw that together and we get something like this. Try hovering over the splat and pressing it:

See the Pen 3. Interactive Splat by SitePoint (@SitePoint)on CodePen.

Adding Some Character
Now that we can transition the splat, it needs a little something extra. What if we transform it in those states too?
.portrait {
transition: clip-path 0.2s, transform 0.2s;
transform: scale(var(–scale, 1)) rotate(var(–rotate, 0deg));
.portrait:hover {
–scale: 1.15;
–rotate: 30deg;
.portrait:active {
–scale: 0.85;
–rotate: -10deg;

Using scoped CSS variables to apply a transform, we can add something. Here we update the scale and rotation of our splat. We can experiment with different values and play with different effects here. Translating the element a little could look good?

See the Pen 4. Adding Some Character by SitePoint (@SitePoint)on CodePen.

Adding a Portrait
Now for the fun stuff! I wouldn’t recommend using these pictures of me. But you can if you want, ha! I had this idea that I’d take three silly pictures of myself and have them respond to the user. I got some help and ended up with these three pictures:
Three silly poses
Then we need to put them into the portrait:


That won’t look great. They need some styles:
.portrait {
position: relative;
.portrait__img {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;

Almost there:

See the Pen 5. Getting Portraits in Place by SitePoint (@SitePoint)on CodePen.

How can we show and hide them on :hover and :active. It’s a little verbose, but we can use nth-of-type with display: none:
.portrait__img {
display: none;
.portrait__img:nth-of-type(1) {
display: block;
.portrait:hover .portrait__img:nth-of-type(1),
.portrait:hover .portrait__img:nth-of-type(3) {
display: none;
.portrait:hover .portrait__img:nth-of-type(2) {
display: block;
.portrait:active .portrait__img:nth-of-type(1),
.portrait:active .portrait__img:nth-of-type(2) {
display: none;
.portrait:active .portrait__img:nth-of-type(3) {
display: block;

Why not refactor those styles and group them up? The cascade will kick in and we won’t get the effect we want.

See the Pen 6. Show/Hide Portraits by SitePoint (@SitePoint)on CodePen.

Parallax Icons
We’re getting there, but it looks a little bland. We could create a rudimentary parallax effect if we pulled in an icon. Let’s go with this one.

The trick here is to use an image as a background for our element but size it so that it tiles with background-repeat:
.portrait {
background-image: url(“/code-icon.svg”);
background-color: hsl(10, 80%, 70%);


See the Pen 7. Icons BG by SitePoint (@SitePoint)on CodePen.

But we want parallax! To get that parallax effect, we can update the background-position in response to pointer movement. And we can map the pointer position against some limit that we define.
Let’s start by creating a utility that generates a mapping function for us. The returned function will give us the result of a value in one range mapped onto another:
const genMapper = (inputLower, inputUpper, outputLower, outputUpper) => {
const inputRange = inputUpper – inputLower
const outputRange = outputUpper – outputLower
const MAP = input => outputLower + (((input – inputLower) / inputRange) * outputRange || 0)
return MAP

Take a moment to understand what’s happening here. For example, if our input range was 0 to 500 and our output range was 0 to 100, what would the result of calling the returned function be with 250? It would be 50:

genMapper(0, 500, 0, 100)

const inputRange = 500
const outputRange = 100
const MAP => input => 0 + (((input – 0) / 500) * 100)

(250 / 500) * 100
0.5 * 100


Once we have our utility function to generate the mapping functions, we need a limit to use with it. And we need to generate a mapper for both the horizontal and vertical axes:
const LIMIT = 25
const getX = genMapper(0, window.innerWidth, -LIMIT, LIMIT)
const getY = genMapper(0, window.innerHeight, -LIMIT, LIMIT)

The final part is tying that up to an event listener. We destructure the x and y value from the event and set CSS variables on the portrait element. The value comes from passing x and y into the respective mapping functions:
const PORTRAIT = document.querySelector(‘.portrait’)
document.addEventListener(‘pointermove’, ({ x, y }) => {
PORTRAIT.style.setProperty(‘–x’, getX(x))
PORTRAIT.style.setProperty(‘–y’, getY(y))

And now we have parallax icons!

See the Pen 8. Parallax Icons by SitePoint (@SitePoint)on CodePen.

The Squeak
Last touch. It’s in the title. We need some squeaks. I usually find audio bytes on sites like freesound.org. You can get them in all sorts of places, though, and even record them yourself if you want.
It’s not a bad idea to create an object where you can reference your Audio:
const AUDIO = {
IN: new Audio(‘/squeak-in.mp3’),
OUT: new Audio(‘/squeak-out.mp3’),

Then, to play an audio clip, all we need do is this:

We need to integrate this with our portrait. We can use the pointerdown and pointerup events here — the idea being that we play one squeak when we press and another on release.
If a user clicks the portrait a lot in quick succession, this could cause undesirable effects. The trick is to play the desired sound and at the same time, stop the other. To “stop” a piece of Audio, we can pause it and set the currentTime to 0:
PORTRAIT.addEventListener(‘pointerdown’, () => {
AUDIO.IN.currentTime = AUDIO.OUT.currentTime = 0
PORTRAIT.addEventListener(‘pointerup’, () => {
AUDIO.IN.currentTime = AUDIO.OUT.currentTime = 0

And that gives us a “Squeaky Portrait”!

See the Pen 9. A Squeaky Portrait! by SitePoint (@SitePoint)on CodePen.

That’s It!
That’s how you make “Squeaky Portraits”. But the actionable thing here is having fun while trying out new things.

See the Pen Squeaky Portraits 😅 (clip-path: path()) by SitePoint (@SitePoint)on CodePen.

I could have morphed a couple of shapes and left it there. But why stop there? Why not come up with an idea and have some fun with it? It’s a great way to try things out and explore techniques.
In summary, we:
created the clips
morphed them with transitions
made interactive images
added Audio
created parallax with a mapping utility
What could you do with clip-path: path()? What would your “Squeaky Portrait” look like? It could do something completely different. I’d love to see what you make!
As always, thanks for reading. Wanna see more? Come find me on Twitter or check out the the live stream!
P.S. If you want to grab all the code, it’s here in this CodePen collection.

Coded at

Share your love

Leave a Reply