Here’s an idea! Let’s make an HTML <video>
play inside the shape of some letters. Like “Knockout Text” except instead of an image behind, it’s video. A live demo will explain more clearly:
If you’re viewing in Safari, you might need to reference this comment to get things working right.
A key objective here is to develop this responsively. The idea is to not only scale the video in size to fit the parent container (as video generally does), but scale the text as well, maintaining the size relationship between the type and underlying video.
We’re going to get there by using the CSS clip-path
property to clip the video against an SVG path defined within a element.
First, we’ll cover the core concept. Then we’ll add some eye candy by adding a few more features and demonstrate them with a couple additional demos.
Setting Up the Video in HTML
Let’s start with the video markup. Inside the <video>
tag we specify the URLs for the WebM and MP4 versions of the video, to support a broad range of browsers:
<video>
<source src="/video/mt-baker.webm" type="video/webm">
<source src="/video/mt-baker.mp4" type="video/mp4">
</video>
Then we add some attributes to customize the video playing behavior:
<video loop autoplay muted playsinline>
<source src="/video/mt-baker.webm" type="video/webm">
<source src="/video/mt-baker.mp4" type="video/mp4">
</video>
- The
loop
attribute makes the video plays forever - The
autoplay
andmuted
attributes are used to allow the automatic video playback at page load on all devices and ensure it play without audio - The
playsinline
attribute tries to disable the full-screen expansion of the video when it is played on some mobile devices
Then a container is wrapped around the <video>
element. This will come in handy when we’ll spice up the basic demo with some visual effects.
Here’s the complete markup:
<div class="video-container">
<video loop autoplay muted playsinline>
<source src="/video/mt-baker.webm" type="video/webm">
<source src="/video/mt-baker.mp4" type="video/mp4">
</video>
</div>
The Basic CSS to Position and Scale the Video
Here, the main task to fulfill is to make the video responsive. It must scale up and down, spanning the width of its parent container and keeping its intrinsic aspect ratio:
.video-container video {
display: block;
width: 100%;
}
After formatting the video as a block-level element with display: block
, assigning a 100% value to its width makes it as large as its container. And that’s all we need, because the initial auto
value of the height
property and the video intrinsic aspect ratio make it uniformly scale, without distortion.
In the original demo, I repeated the video several times in the container to test this responsive behavior. Here’s the CSS I’m using to test the responsiveness of each video at different widths:
.video-container {
margin: 40px auto;
outline: 1px solid #dadada;
&:nth-of-type(1) { width: 25%; } /* first video instance, and so on */
&:nth-of-type(2) { width: 50%; }
&:nth-of-type(3) { width: 75%; }
&:nth-of-type(4) { width: 100%; }
}
The outline
set on the containers will help us visualize the location and size of the videos when they are clipped. Speaking of which…
Clipping the Video in CSS
Thanks to the clip-path
CSS property, we can define a region of an element to be displayed instead of the entire thing — and do it with a single line:
video {
clip-path: url(#clip-00);
}
With the url()
function, we specify the id of the SVG clipPath
element where the clipping text is defined.
Note that although the visible geometry of the video is altered by the clipping, the video element is still a rectangular box for the browser, so the page layout is the same as if the clipping was not used. This is part of the beauty of clipping and, if you’re new to the concept, then definitely check out Sarah’s explanation of it.
Now the trick is actually creating that clip path!
clipPath
element
Creating the SVG I am going to create the text design, do the editing, then export the work in Inkscape. Similar steps could be put together with other vector editors. Feel free to report your experiences in the comments.
The first step is to keep note of the video aspect ratio, which is its width:height
ratio.
Next, create a document with the same size as the video or just with the same aspect ratio. So, for example, if you’re video is 1000px
wide and 500px
tall, then you could use that as the document size or anything with a 2:1 ratio. This will act as a placeholder for the video, so make sure the document borders are visible. Also, remove the default layer created by the program, as this will simplify the code generated by SVG, avoiding the creation of unnecessary group elements.
Select the text tool, pick a nice bold and thick font, enter the text and stretch it to cover the document rectangle as you please.
Now, if we take a look at the code generated by Inkscape we see something like this:
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:1147.68029785px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="23.404421"
y="939.04187"
id="text3336"
sodipodi:linespacing="125%"
transform="scale(0.8988993,1.1124717)"
>
<tspan
sodipodi:role="line"
id="tspan3338"
x="23.404421"
y="939.04187"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-family:Impact;-inkscape-font-specification:'Impact Condensed'"
>
TEXT
</tspan>
</text>
That’s a bit unwieldy, so let’s convert the SVG element into a path:
<g
transform="scale(0.8988993,1.1124717)"
style="font-style:normal;font-weight:normal;font-size:1147.68029785px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3336"
>
<path
d="m 545.68862,31.769213 0,181.566607 -140.09769,0 0,725.70605 -235.92452,0 0,-725.70605 -139.5373,0 0,-181.566607 515.55951,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-family:Impact;-inkscape-font-specification:'Impact Condensed'"
id="path3348" />
<path
d="m 599.48616,31.769213 393.39432,0 0,181.566607 -157.46981,0 0,172.03997 147.38277,0 0,172.60036 -147.38277,0 0,199.49911 173.16073,0 0,181.56661 -409.08524,0 0,-907.272657 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-family:Impact;-inkscape-font-specification:'Impact Condensed'"
id="path3350" />
<path
d="M 1554.9524,31.769213 1472.575,433.009 l 124.4067,506.03287 -218.5524,0 q -39.2273,-135.61457 -71.73,-330.07016 -8.9662,85.73978 -24.6572,182.127 l -22.4156,147.94316 -229.7602,0 85.1794,-506.03287 -85.1794,-401.239787 228.079,0 q 6.1643,37.546181 24.6572,124.967137 14.5702,66.68651 24.0968,122.16519 l 50.4352,-247.132327 197.8179,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-family:Impact;-inkscape-font-specification:'Impact Condensed'"
id="path3352" />
<path
d="m 2105.8165,31.769213 0,181.566607 -140.0976,0 0,725.70605 -235.9246,0 0,-725.70605 -139.5373,0 0,-181.566607 515.5595,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-family:Impact;-inkscape-font-specification:'Impact Condensed'"
id="path3354" />
</g>
Now, we have a group containing a path element for each letter of the text. Un-grouping it leaves this set of paths at the root level. This step, besides removing the unneeded group, modifies the coordinates of the path elements to account for the transform that was applied to the removed parent group. So, after this step, there are no transform attributes in the SVG:
<path
inkscape:connector-curvature="0"
id="path3348"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:medium;line-height:125%;font-family:Impact;-inkscape-font-specification:'Impact Condensed';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 490.51912,35.34235 0,201.98771 -125.93372,0 0,807.32744 -212.07238,0 0,-807.32744 -125.429984,0 0,-201.98771 463.436084,0 z" />
<path
inkscape:connector-curvature="0"
id="path3350"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:medium;line-height:125%;font-family:Impact;-inkscape-font-specification:'Impact Condensed';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 538.87769,35.34235 353.62188,0 0,201.98771 -141.5495,0 0,191.3896 132.48227,0 0,192.01302 -132.48227,0 0,221.93711 155.65406,0 0,201.98771 -367.72644,0 0,-1009.31515 z" />
<path
inkscape:connector-curvature="0"
id="path3352"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:medium;line-height:125%;font-family:Impact;-inkscape-font-specification:'Impact Condensed';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1397.7456,35.34235 -74.049,446.36791 111.8291,562.94724 -196.4566,0 q -35.2614,-150.86737 -64.478,-367.19371 -8.0597,95.38308 -22.1644,202.61114 l -20.1493,164.58257 -206.5313,0 76.5677,-562.94724 -76.5677,-446.36791 205.02,0 q 5.5411,41.769064 22.1644,139.0224 13.0971,74.18686 21.6606,135.90532 l 45.3362,-274.92772 177.8183,0 z" />
<path
inkscape:connector-curvature="0"
id="path3354"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;font-size:medium;line-height:125%;font-family:Impact;-inkscape-font-specification:'Impact Condensed';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1892.917,35.34235 0,201.98771 -125.9337,0 0,807.32744 -212.0724,0 0,-807.32744 -125.43,0 0,-201.98771 463.4361,0 z" />
Those ugly style attributes are still there, but it doesn’t matter because we are not going to use them.
We have finished with Inkscape, so we can save the SVG document and open it in our favorite code editor.
Saving SVG files from an illustration application is an art form in and of itself. Here are some common tips and common settings for Adobe Illustrator and a comparison of SVG exports from Illustrator, Sketch and Figma.
In the HTML page where the video is played, insert an inline SVG element. This will host the clip path:
<svg class="clipping-paths">
<defs>
</defs>
</svg>
We don’t want this container element to affect the page layout, so:
.clipping-paths {
width: 0;
height: 0;
position: absolute;
}
Add a new clipPath
element to the container SVG, with a proper id to identify it in the clip-path
property, as seen before:
<svg class="clipping-paths">
<defs>
<clipPath id="clip-00"></clipPath>
<clipPath id="clip-01"></clipPath>
...
</defs>
</svg>
As showed in the above snippet, multiple clipping paths can be added, if needed.
Now, we must assign the objectBoundingBox
value to the clipPath
‘s attribute clipPathUnits
:
<clipPath id="clip-00" clipPathUnits="objectBoundingBox"></clipPath>
This is one of the essential things to do to have a responsive cutout. We tell the browser which coordinate system to use for the numerical values listed in the d
attributes of the paths that will be appended to the clipPath
element.
The default value for the clipPathUnits
attribute is userSpaceOnUse
. According to the spec, this means that:
…the contents of the
clipPath
element represent values in the current user coordinate system in place at the time when the (clipPath)element is referenced (i.e., the user coordinate system for the element referencing theclipPath
element via the clip-path property).
Instead, by using the other available value for clipPathUnits
, objectBoundingBox
, the coordinate values and the lengths specified in the clipPath
content are considered as fractional values in the [0,1] range, relative to the bounding box of the clipped element. For instance, if a graphical primitive in the clipPath
referred to the coordinates (0,0), (1,0), (1,1), (0,1) and (0.5,0.5), these points would represent respectively, the top left, top right, bottom right, bottom left corners, and the center of the video, no matter the actual size of the video on screen. It is this behavior that allows the text cutout to scale in the same way as the underlying video.
After having removed any transforms from the SVG file in Inkscape, all the numerical values of the path elements are now in the document range. To map them into the [0,1] interval required for objectBoundingBox
, it is sufficient to divide such numerical values by the document size. The x coordinates are divided by the document width and the y coordinates by the document height.
To accomplish this division, a scaling with the SVG transform
attribute can be employed:
<clipPath id="clip-00" clipPathUnits="objectBoundingBox" transform="scale(0.0005208333333333333, 0.000925925925925926)">
The horizontal scaling must be the reciprocal of the document width, while the vertical scaling must be the reciprocal of the document height. So, in our case, given that the document has the same exact size as the video, that is, 1920 X 1080
, we get the two factors 1 / 1920 = 0.0005208333333333333
and 1 / 1080 = 0.000925925925925926
.
The last step in this workflow is to copy the elements from the file generated by Inkscape inside the
clipPath
element, getting something like this:
<clipPath id="clip-00" clipPathUnits="objectBoundingBox" transform="scale(0.0005208333333333333, 0.000925925925925926)">
<path d="m 341.4087,58.040727 0,951.927273 -112.60986,0 -66.8335,-432.74767 0,432.74767 -107.42188,0 0,-951.927273 107.42188,0 72.02149,428.631863 0,-428.631863 107.42187,0 z" />
...
<path d="m 1642.6782,58.040727 214.2334,0 0,190.503053 -85.7544,0 0,180.50751 80.2613,0 0,181.09549 -80.2613,0 0,209.31816 94.2994,0 0,190.50306 -222.7784,0 0,-951.927273 z" />
</clipPath>
To reiterate, we have copied just the d
attribute of the paths, discarding all the other stuff generated by Inkscape.
Putting it All Together
And there we have it, gluing together the above snippets with some more code(see the pen source for details), we get the first basic demo:
Other Examples
We can reuse the above workflow to create some more cutouts, like in the second demo here:
The last example is worthy of attention because it is the negative of the text used in the first demo; that is, the internal and external regions delimited by the paths were swapped. So, the white text we see is a hole through which the background color of the page body is visible. This negative cutout was derived from the original with a difference path operation in Inkscape.
Adding Background Colors
Building on top of the basic demo, we can add a color to the text cutout by assigning a background-color
to the video container, because this wrapper element is visible everywhere the video is clipped out:
.video-container {
// ...
&:nth-of-type(1) { background: #c6c7c5; }
&:nth-of-type(2) { background: #dcf3ff; }
&:nth-of-type(3) { background: #a2d2df; }
&:nth-of-type(4) { background: #406e8d; }
&:nth-of-type(5) { background: linear-gradient(180deg, #bdc3c2, #2d5d89); }
}
The result can be seen in the third demo:
Adding Tint
How about tinting the video? We can do that with a pseudo element and CSS Blend Modes:
.video-container {
/* ... */
position: relative;
}
.video-container::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
clip-path: url(#clip-00);
}
.video-container:nth-of-type(1) {
background: #406e8d;
}
.video-container:nth-of-type(1)::after {
background-color: #3f51b5;
mix-blend-mode: screen;
}
The ::after
pseudo element of the video wrapper is absolutely positioned on top of the video, covering it entirely, and then it is clipped with the same path used for the video. In this way, it is possible to use different colors for the background and the tint.
Finally, after having assigned the desired tint color to the pseudo element, with the mix-blend-mode
property we can pick one of the available blending modes to obtain the wanted effect.
Browser Support
At the time of writing, almost all the latest versions of desktop and mobile browsers support the CSS clip-path
property. The prefixed version, -webkit-clip-path
, has to be used on WebKit-based user agents such as Safari and Samsung Internet.
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
126 | 54 | No | 123 | TP |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
123 | 124 | 123 | 17.5 |
Unfortunately, an exception is given by the Microsoft browsers. There is definitely no support on Internet Explorer, and on Edge the feature is under development. Please vote it up on the Microsoft Developer Feedback page!
Of course, for the last demo, a browser supporting CSS Blend Modes is required. In this case, the support is wider, being this feature is even implemented by Edge.
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
30 | 20 | No | 13 | 6.1 |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
123 | 124 | 4.4 | 7.0-7.1 |
References and Credits
Eric Meyer writes about the scaling of the SVG clipping path for an HTML element, using the objectBoundingBox
value for the clipPathUnits
attribute, and the scaling transform. That was a huge inspiration for this post.
Thanks to Coverr for the video that was used in these demos.
This is amazing! Really cool, Giulio!
So… none of the CodePens work on iOS 11.3 in the latest version of Chrome or Safari. :/
Thanks for your feedback!
It seems that WebKit is having some hard times with clipping html elements against a path defined with a SVG clipPath element. I’m putting together a couple of pens to show the issues. Stay tuned!
Last 2 dont show the video in Chrome 64bit linux
WebKit(as tested on iOS Safari 11.2.6) has some trouble clipping a
<video>
even with the simplest<clipPath>
, nothing is rendered.Also, https://lab.iamvdo.me/css-svg-masks/ features several interesting tests(not with videos though).
@jgarcianewemage
Could you tell me what happens on the third pen(background colors)?
Which Chrome version?
Also, have you tried with these tests?
Update: the basic pen works on iOS Safari if the video is wrapped in a DIV and the
clip-path
is applied to this container: https://codepen.io/mgiulio/pen/bMLWWw?editors=0100Though, the text cutout is not transformed perfecly, there is a little upward shifting, as it is revealed by the uneven purple padding. The same issue happens when using an image instead of a video.