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

Share this article

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

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.

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
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
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
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:

<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M232.5 256C225 251 209.5 262.5 224 281.5C232.736 292.948 238.561 297.756 251 290.5C257 287 256.114 271.924 250 264.5C243.886 257.076 240 261 232.5 256ZM147 92.5C118.738 94.6708 118 17 93 44C68 71 123.543 76.5 108 101.5C90.5 115 70.81 98.3664 64 115C56.7766 132.643 91.1967 136.948 90.5 156C89.4406 184.974 19.1766 161.499 24.5 190C29.9178 219.006 78.6461 172.635 100 193C130.207 221.808 1 248.5 58.5 291.5C94.5576 318.465 114.991 206.551 140.5 211C183.5 218.5 134.5 294 186.5 279.5C207.5 273 174.638 224.658 196 204C223.117 177.777 275.916 253 291.5 218.5C311.375 174.5 228.698 194.565 224 160C219.553 127.282 291.5 123.5 267.5 87.5C238.5 57 247 125.5 196 105.5C147 92.5 229.5 13.5 173.5 2.5C140.5 2.49999 183.532 89.694 147 92.5ZM45 92.5C36.8766 80.3149 14.1234 75.3149 6.00001 87.5C0.584412 95.6234 2.00001 120.357 14.5 115C27.9606 109.231 36.8766 124.685 45 112.5C50.4156 104.377 50.4156 100.623 45 92.5Z" fill="#A91CFF"/>
</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”:

<div class="portrait"></div>

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.

Interaction

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
Three silly poses

Then we need to put them into the portrait:

<div class="portrait">
  <img class="portrait__img" src="/me--standing.png" alt="Me"/>
  <img class="portrait__img" src="/me--noticing.png" alt="Me"/>
  <img class="portrait__img" src="/me--falling.png" alt="Me"/>
</div>

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.

An icon showing angle brackets around a forward slash

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%);
}

Neat.

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:

// Generate a function
genMapper(0, 500, 0, 100)
// Returns a function by going through these steps
const inputRange = 500
const outputRange = 100
const MAP => input => 0 + (((input - 0) / 500) * 100)
// If our input value is 250
(250 / 500) * 100
0.5 * 100
// The result!
50

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 // The amount our icons can move in pixels in either direction
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:

AUDIO.IN.play()

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.OUT.pause()
  AUDIO.IN.currentTime = AUDIO.OUT.currentTime = 0
  AUDIO.IN.play()
})
PORTRAIT.addEventListener('pointerup', () => {
  AUDIO.IN.pause()
  AUDIO.IN.currentTime = AUDIO.OUT.currentTime = 0
  AUDIO.OUT.play()
})

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.

Frequently Asked Questions (FAQs) about CSS Path Function and Squeaky Portraits

What is the CSS Path Function?

The CSS Path function is a feature in CSS that allows you to define a specific path for an object to follow. This function is particularly useful in creating complex shapes and animations. The path function uses the ‘d’ property of SVG paths to define the shape. The syntax for the path function is ‘clip-path: path(‘d property of SVG path’)’.

How can I use the CSS Path Function to create Squeaky Portraits?

Creating Squeaky Portraits using the CSS Path function involves using the function to define the shape of the portrait. You can use the ‘clip-path’ property to clip an image into the shape defined by the path function. This can be done by setting the ‘clip-path’ property to ‘path()’ and then defining the path inside the parentheses.

What is the difference between the CSS Path Function and the CSS Clip-Path Property?

The CSS Path function and the CSS Clip-Path property are often used together, but they serve different purposes. The Path function is used to define a specific path or shape, while the Clip-Path property is used to clip an element into the shape defined by the Path function.

How can I animate a path using CSS?

Animating a path using CSS involves using the ‘animation’ property in conjunction with the Path function. You can define the animation using keyframes, and then apply it to the path using the ‘animation’ property. The animation can include changes in the path’s shape, position, and other properties.

Can I use the CSS Path Function to create shapes other than portraits?

Yes, the CSS Path function can be used to create a wide variety of shapes. The function uses the ‘d’ property of SVG paths, which can define any shape that can be drawn with straight lines and curves. This includes simple shapes like rectangles and circles, as well as more complex shapes like polygons and custom designs.

What are some common issues I might encounter when using the CSS Path Function?

Some common issues when using the CSS Path function include browser compatibility issues, difficulties in defining complex shapes, and challenges in animating the path. However, these issues can often be resolved with careful coding and testing.

How can I ensure that my path is compatible with all browsers?

To ensure browser compatibility, you can use vendor prefixes, which are specific codes that enable certain CSS features in different browsers. You can also use tools like Can I Use, which provide information on the compatibility of various CSS features with different browsers.

Can I use the CSS Path Function with other CSS properties?

Yes, the CSS Path function can be used in conjunction with many other CSS properties. For example, you can use the ‘transform’ property to rotate or scale the path, or the ‘transition’ property to animate changes in the path.

How can I use the CSS Path Function to create responsive designs?

The CSS Path function can be used to create responsive designs by defining paths that adapt to the size of the viewport. This can be done by using percentages instead of fixed values when defining the path, which will cause the path to scale with the size of the viewport.

What resources can I use to learn more about the CSS Path Function?

There are many resources available for learning more about the CSS Path function. These include online tutorials, documentation on developer websites like Mozilla Developer Network and W3Schools, and online communities where you can ask questions and share knowledge with other developers.

Jhey TompkinsJhey Tompkins
View Author

Jhey makes awesome things for awesome people! He’s a web developer with almost 10 years of experience. Working with and for names such as Eurostar, Uber, NearForm, Barclaycard, ATG, O2, and M&C Saatchi. Jhey thrives on bringing ideas to life. He takes pride in crafting innovative solutions with polished user experiences. This whilst possessing a keen eye for design, accessibility, and well-structured code. He’s a CodePen advocate and shares attention-catching demos over on Twitter.

clip-pathCSS pathpathSVG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week