Issue
I recently came across this dribbble/landing page concept with hollow/filled text.
First off I'm not entirely sure if this concept could be recreated in CSS. A bit of Google did lead me to CSS text masks, but I wasn't able to find any post that can really recreate this effect.
How would I be able to reconstruct the hollow/filled text, depending if the background behind the text has an image or not?
Solution
It can be done in pure-HTML+CSS (without any SVG).
- And it can be done in pure SVG too.
- The SVG-only option will be simpler because SVG has more powerful blending and masking capabilities that aren't (yet) able to be used by HTML elements (CSS boxes).
- And it can be done in pure SVG too.
I decided to do it for HTML+CSS because I felt like a challenge.
- This HTML version is animated and the text is fully selectable.
- This HTML+CSS version can be simplified further by using
mask-image: element(#target)which means we don't need the mask-only text, unfortunately Chrome doesn't seem to supportelement()yet (but Firefox does, though).
A minor wart is the
text-strokeoutline does not exactly line-up with the solid white text (at least in Chrome on Windows 10 with my computer's version ofHelveticaat 96dpi, but at 192dpi (2x, aka Retina)) in the same browser and computer it looks perfect.Here's how it looks at different points in the animation on my machine at 96dpi:

The example implementation below works in the following browsers (at the time of writing):
- Chrome 98
- Edge 98
- Firefox 97
- macOS Safari Technology Preview (Release 137)
- It kinda works in macOS Safari 15.2, but you have to click on it first for some reason.
- It doesn't work in iOS Safari 15.1 (only the outlined text is visible, the solid text doesn't render at all).
- Because it does work in the Safari Preview browser it should work in the next update to iOS Safari and macOS Safari, however.
body {
background-color: #dbdac2;
--solid-white: linear-gradient(white,white);
}
#container,
#container > #div1 {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-size:
153px 302px,
148px 302px,
154px 302px;
background-position:
131px 94px,
309px 28px,
480px 94px;
background-repeat:
no-repeat,
no-repeat,
no-repeat;
animation: moveImages 2s infinite;
animation-direction: alternate;
}
#container {
border: 1px solid white;
position: relative;
width: 711px;
height: 440px;
/* These are the 3 photo images, rendered as separate background-image layers: */
background-image:
url( "https://i.stack.imgur.com/hmwyh.png" ),
url( "https://i.stack.imgur.com/JeHEg.png" ),
url( "https://i.stack.imgur.com/pVgz6.png" );
}
#container p {
margin: 0;
position: static;
padding-top: 192px;
padding-left: 62px;
overflow: hidden;
font-family: Helvetica;
font-size: 99px;
letter-spacing: -2px;
font-weight: 600;
}
#container > #pStroke {
text-stroke: 1px white;
-webkit-text-stroke: 1px white;
color: transparent;
}
#container > #div1 {
/* #div1's background-image layers must match #container's: */
background-image:
var(--solid-white),
var(--solid-white),
var(--solid-white);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@keyframes moveImages {
/* The `@keyframes from {}` rule is optional, btw. */
to {
background-size:
53px 302px,
58px 302px,
154px 302px;
background-position:
431px 94px,
209px 28px,
280px 194px;
}
}
<div id="container">
<div id="div1">
<p id="pWhite">Fashion Give<br />Impression.</p>
</div>
<p id="pStroke">Fashion Give<br />Impression.</p>
</div>
Explanation:
The
div#containerelement has abackground-imageproperty set to 3 different sources images (using meme images in lieu of the fashion photos from your posted example).- These images can be positioned and scaled independently via the
background-sizeandbackground-positionproperties.
- These images can be positioned and scaled independently via the
Another element (
#div1) overlaysdiv#containerthroughabsolutepositioning and has the samebackground-sizeandbackground-positionproperties, but uses a single solid white background image (from alinear-gradient(white,white)) instead of the photos, and then those 3 white background image layers are masked by#div1's inner<p>'s text usingbackground-clip: text;.- This example has to use
background-image: linear-gradient(orbackground-image: url("1x1px-white.png");) instead ofbackground-color: white;because it needs to be repeated in 3 separate layers, whereasbackground-color: white;cannot be used to define a rectangular area within the background, nor can you have multiplebackground-colorlayers (even when semi-transparent). - The example also has to use DOM text to mask the white rectangles - instead of using white rectangles to mask the DOM text - because CSS-in-HTML doesn't support any kind of reversed text masking though, otherwise this would be a lot easier. I believe this is easily possible in SVG, however.
#div1's<p>element is used to position the text correctly by using only its innerpaddinginstead ofposition: absolute;because positioned text cannot be used withbackground-clip: text;, unfortunately.
- This example has to use
Another
<p>element with a copy of the text is used for the stroked text (withtext-stroke: 1px white;)While the text content is duplicated in the HTML source, the more fiddly size and position information of the 3 images (and their respective white masks) does not need to be duplicated in the CSS, fortunately; thanks to how CSS's selectors work (as both
#containerand#div1have theirbackground-sizeandbackground-positionproperties set by a single CSS rule).
Possible alternative approaches:
Instead of using a repeated identically sized
background-imagelayers for the solid white parts, a single (but horribly complicated)clip-pathtracing the 3 boxes (like a single line drawn like an etch-a-sketch) could be used on#pWhitebut this would not be feasibly animatable.I think the best possible approach would be something like this:
- Using 3 separate
<img />elements for the 3 photos (instead ofbackground-image) and usingabsolutepositioning inside a new<div id="images">. - And then the solid white text
<p>(in a sibling element todiv#images) would beabsolute-ly positioned overdiv#imagesbyz-indexand masked withmask-image: element(#images); - But Chrome doesn't seem to support using
element()in HTML+CSS, only Firefox does, as far as I know. - The stroked text would remain as a duplicate
<p>element positioned behind thediv#images. - Another advantage of this approach is that the
<img/>elements can be positioned usingtransform: translateinstead of having to go throughbackground-positionorposition: absolutewhich would yield much better performance and framerates.
- Using 3 separate
I can't think of any approaches that don't require duplicating the text content, though - at least until CSS's
content:property allows elements to copying text from other elements.
Answered By - Dai

0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.