Moving Backgrounds

We often think of background images as texture or something that provides contrast for legible content — in other words, not really content. If it was content, you’d probably reach for an <img> anyway, accessibility and whatnot.

But there are times when the position or scale of a background image might sit somewhere between the poles of content and decoration. Context is king, right? If we change the background image’s position, it may convey a bit more context or experience.

How so? Let’s look at a few examples I’ve seen floating around.

As we get started, I’ll caution that there’s a fine line in these demos between images used for decoration and images used as content. The difference has accessibility implications where backgrounds are not announced to screen readers. If your image is really an image, then maybe consider an <img> tag with proper alt text. And while we’re talking accessibility, it’s a good idea to consider a user’s motion preference’s as well.

Show me more!

Chris Coyier has this neat little demo from several years back.

The demo is super practical in lots of ways because it’s a neat approach for displaying ads in content. You have the sales pitch and an enticing image to supplement it.

The big limitation for most ads, I’d wager, is the limited real estate. I don’t know if you’ve ever had to drop an ad onto a page, but I have and typically ask the advertiser for an image that meets exact pixel dimensions, so the asset fits the space.

But Chris’s demo alleviates the space issue. Hover the image and watch it both move and scale. The user actually gets more context for the product than they would have when the image was in its original position. That’s a win-win, right? The advertiser gets to create an eye-catching image without compromising context. Meanwhile, the user gets a little extra value from the newly revealed portions of the image.

If you peek at the demo’s markup, you’ll notice it’s pretty much what you’d expect. Here’s an abridged version:

<div class="ad-container">
  <a href="#" target="_blank" rel="noopener">
    <!-- Background image container  -->
    <div class="ad-image"></div>
  </a> 
  <div class="ad-content">
    <!-- Content -->
  </div>
</div>

We could probably quibble over the semantics a bit, but that’s not the point. We have a container with a linked-up <div> for the background image and another <div> to hold the content.

As far as styling goes, the important pieces are here:

.container {
  background-image: url("/path/to/some/image.png");
  background-repeat: no-repeat;
  background-position: 0 0;
  height: 400px;
  width: 350px;
}

Not bad, right? We give the container some dimensions and set a background image on it that doesn’t repeat and is positioned by its bottom-left edge.

The real trick is with JavaScript. We will use that to get the mouse position and the container’s offset, then convert that value to an appropriate scale to set the background-position. First, let’s listen for mouse movements on the .container element:

let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
    // Our function
  }
);

From here, we can use the container’s offsetX and offsetY properties. But we won’t use these values directly, as the value for the X coordinate is smaller than what we need, and the Y coordinate is larger. We will have to play around a bit to find a constant that we can use as a multiplier.

It’s a bit touch-and-feel, but I’ve found that 1.32 and 0.455 work perfectly for the X and Y coordinates, respectively. We multiply the offsets by those values, append a px unit on the result, then apply it to the background-position values.

let container = document.querySelector(".container");
container.addEventListener("mousemove", function(e) {
    container.style.backgroundPositionX = -e.offsetX * 1.32 + "px";
    container.style.backgroundPositionY = -e.offsetY * 0.455 + "px";
  }
);

Lastly, we can also reset the background positions back to the original if the user leaves the image container.

container.addEventListener("mouseleave", function() {
    container.style.backgroundPosition = "0px 0px";
  }
);

Since we’re on CSS-Tricks, I’ll offer that we could have done a much cheaper version of this with a little hover transition in vanilla CSS:

Paint a bigger picture

No doubt you’ve been to some online clothing store or whatever and encountered the ol’ zoom-on-hover feature.

This pattern has been around for what feels like forever (Dylan Winn-Brown shared his approach back in 2016), but that’s just a testament (I hope) to its usefulness. The user gets more context as they zoom in and get a better idea of a sweater’s stitching or what have you.

There’s two pieces to this: the container and the magnifier. The container is the only thing we need in the markup, as we’ll inject the magnifier element during the user’s interaction. So, behold our HTML!

<div class="container"></div>

​​In the CSS, we will create width and height variables to store the dimensions of the of the magnifier glass itself.  Then we’ll give that .container​ some shape and a background-image​:

​​:root {
​​  --magnifer-width: 85;
​​  --magnifer-height: 85;
​​}

.container {
  width: 500px;
  height: 400px;
  background-size: cover;
  background-image: url("/path/to/image.png");
  background-repeat: no-repeat;
  position: relative;
}

There are some things we already know about the magnifier before we even see it, and we can define those styles up-front, specifically the previously defined variables for the .maginifier‘s width and height:

.magnifier {
  position: absolute;
  width: calc(var(--magnifer-width) * 1px);
​​  height: calc(var(--magnifer-height) * 1px);
​​  border: 3px solid #000;
​​  cursor: none;
​​  background-image: url("/path/to/image.png");
​​  background-repeat: no-repeat;
}

It’s an absolutely-positioned little square that uses the same background image file as the .container. Do note that the calc function is solely used here to convert the unit-less value in the variable to pixels. Feel free to arrange that however you see fit as far as eliminating repetition in your code.

Now, let’s turn to the JavaScript that pulls this all together. First we need to access the CSS variable defined earlier. We will use this in multiple places later on. Then we need get the mouse position within the container because that’s the value we’ll use for the the magnifier’s background position.

​​// Get the css variables
​​let root = window.getComputedStyle(document.documentElement);
​​let magnifier_width = root.getPropertyValue("--magnifer-width");
​​let magnifier_height = root.getPropertyValue("--magnifer-height");

let container = document.querySelector(".container");
let rect = container.getBoundingClientRect();
let x = (e.pageX - rect.left);
let y = (e.pageY - rect.top);

// Take page scrolling into account
x = x - window.pageXOffset;
y = y - window.pageYOffset;

What we need is basically a mousemove event listener on the .container. Then, we will use the event.pageX or event.pageY property to get the X or Y coordinate of the mouse. But to get the exact relative position of the mouse on an element, we need to subtract the position of the parent element from the mouse position we get from the JavaScript above. A “simple” way to do this is to use getBoundingClientRect(), which returns the size of an element and its position relative to the viewport.

Notice how I’m taking scrolling into account. If there is overflow, subtracting the window pageX and pageY offsets will ensure the effect runs as expected.

We will first create the magnifier div. Next, we will create a mousemove function and add it to the image container. In this function, we will give the magnifier a class attribute. We will also calculate the mouse position and give the magnifier the left and top values we calculated earlier.

Let’s go ahead and build the magnifier when we hear a mousemove event on the .container:

// create the magnifier
let magnifier = document.createElement("div");
container.append(magnifier);

Now we need to make sure it has a class name we can scope to the CSS:

// run the function on `mousemove`
container.addEventListener("mousemove", (e) => {
  magnifier.setAttribute("class", "magnifier");
}

The example video I showed earlier positions the magnifier outside of the container. We’re gonna keep this simple and overlay it on top of the container instead as the mouse moves. We will use if statements to set the magnifier’s position only if the X and Y values are greater or equal to zero, and less than the container’s width or height. That should keep it in bounds. Just be sure to subtract the width and height of the magnifier from the X and Y values.

// Run the function on mouse move.
container.addEventListener("mousemove", (e) => {
  magnifier.setAttribute("class", "magnifier");

  // Get mouse position
  let rect = container.getBoundingClientRect();
  let x = (e.pageX - rect.left);
  let y = (e.pageY - rect.top);
  
  // Take page scrolling into account
  x = x - window.pageXOffset;
  y = y - window.pageYOffset;

  // Prevent magnifier from exiting the container
  // Then set top and left values of magnifier
  if (x >= 0 && x <= container.clientWidth - magnifier_width) {
    magnifier.style.left = x + "px";
  }
  if (y >= 0 && y <= container.clientHeight - magnifier_height) {
    magnifier.style.top = y + "px";
  }
});

Last, but certainly not least… we need to play with the magnifier’s background image a bit. The whole point is that the user gets a BIGGER view of the background image based on where the hover is taking place. So, let’s define a magnifier we can use to scale things up. Then we’ll define variables for the background image’s width and height so we have something to base that scale on, and set all of those values on the .magnifier styles:

// Magnifier image configurations
let magnify = 2;
let imgWidth = 500;
let imgHeight = 400;

magnifier.style.backgroundSize = imgWidth * magnify + "px " + imgHeight * magnify + "px";

​​Let’s take the X and Y coordinates of the magnifier’s image and apply them to the .magnifier​ element’s background-position​. As before with the magnifier position, we need to subtract the width and height of the magnifier from the X and Y values using the CSS variables.

// the x and y positions of the magnifier image
let magnify_x = x * magnify + 15;
let magnify_y = y * magnify + 15;

// set backgroundPosition for magnifier if it is within image
if (
  x <= container.clientWidth - magnifier_width &&
  y <= container.clientHeight - magnifier_height
) {
  magnifier.style.backgroundPosition = -magnify_x + "px " + -magnify_y + "px";
}

Tada!

Make it cinematic

Have you seen the Ken Burns effect? It’s classic and timeless thing where an image is bigger than the container it’s in, then sorta slides and scales slow as a slug. Just about every documentary film in the world seems to use it for image stills. If you have an Apple TV, then you’ve certainly seen it on the screen saver.

There are plenty of examples over at CodePen if you wanna get a better idea.

You’ll see that there are a number of ways to approach this. Some use JavaScript. Others are 100% CSS. I’m sure the JavaScript approaches are good for some uses cases, but if the goal is simply to subtly scale the image, CSS is perfectly suitable.

We could spice things up a bit using multiple backgrounds rather than one. Or, better yet, if we expand the rules to use elements instead of background images, we can apply the same animation to all of the backgrounds and use a dash of animation-delay to stagger the effect.

Lots of ways to do this, of course! It can certainly be optimized with Sass and/or CSS variables. Heck, maybe you can pull it off with a single <div> If so, share it in the comments!

Bonus: Make it immersive

I don’t know if anything is cooler than Sarah Drasner’s “Happy Halloween” pen… and that’s from 2016! It is a great example that layers backgrounds and moves them at varying speeds to create an almost cinematic experience. Good gosh is that cool!

GSAP is the main driver there, but I imagine we could make a boiled-down version that simply translates each background layer from left to right at different speeds. Not as cool, of course, but certainly the baseline experience. Gotta make sure the start and end of each background image is consistent so it repeats seamlessly when the animation repeats.


That’s it for now! Pretty neat that we can use backgrounds for much more than texture and contrast. I’m absolutely positive there are tons of other clever interactions we can apply to backgrounds. Temani Afif did exactly that with a bunch of neat hover effects for links. What do you have in mind? Share it with me in the comments!


Moving Backgrounds originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

In Praise of Shadows

Our dear friend Robin has a new essay called In Praise of Shadows. Now, before you hop over there looking for nuggets on CSS box shadows, text shadows, and shadow filters… this is not that. It’s an essay on photography and what Robin has learned about handing shadows with a camera.

So, why share this? Because it’s cool as heck that he made an article directed page dedicated to one essay. And you’ll learn a lot about CSS if you crack open DevTools on it:

  • Centering techniques. Notice how CSS Grid is used on the <body> simply to center the pamphlet. Then Robin reaches for it again on each .frame of the essay to do the same thing with the content.
  • “Faux” background images. Robin could have made a lot of work for himself by creating a CSS class for each .frame to get the background images. Instead, he uses object-fit: cover on inlined HTML <img>s to maintain the aspect ratio while filling the .frame container. (He’s actually written about this before.) That sure saves a lot of CSS’ing, but it also allows him to use alt text if needed. I sorta wonder if a <figure>/<figcaption> structure could’ve worked here instead but I doubt it would provide much additional benefit for what’s going on.
  • Stacking contexts. Another perk of those faux background images? They use absolute positioning which creates a stacking context, allowing Robin to set a z-index: 0 on the images. That way, the text stacks directly on top with z-index: 1. Again, CSS Grid is handling all the centering so things are nicely aligned.
  • Scroll snapping. I always love it when I see CSS scroll snapping in the wild. Robin made a nice choice to use it here, as it really lends to the whole page-turning experience while scrolling through frames. Horizontal scrolling can be maddening, but also extremely elegant when executed well as it is here in how it enhances the narrow-column design. If you want a nice explanation of how it all works, Robin has also written about horizontal scroll snapping.

If nothing else, Robin is an excellent writer and it’s worth reading anything he publishes, CSS and beyond.

To Shared LinkPermalink on CSS-Tricks


In Praise of Shadows originally published on CSS-Tricks. You should get the newsletter.

Repetition Image Hover Effects

The other day, I encountered a very interesting animation. It’s kind of a repetition effect on an image where the same gets scaled, layer by layer. This is Eva Habermann’s website where this element has that exact effect:

While this is a scroll based animation, there was also a hover effect somewhere, I just can’t recall where! If you’ve seen it, please let me know so that I can add it here.

There are some interesting parameters that we can play with in order to create different touches, all with a unique look and feel, so this is what I came up with, hope you enjoy it!

The way that we can define the parameters is as follows:

<div 
	class="image image--style-3" 
	data-repetition data-repetition-elems="6" 
	data-repetition-origin="150% 100%" 
	data-repetition-animate="scaleY" 
	data-repetition-stagger="-0.12" 
	data-repetition-initial-scale="1.3" 
	data-repetition-duration="0.5" 
	data-repetition-ease="power1.inOut" 
	style="background-image:url(img/11.jpg);">
</div>

We make the following structure out of this:

<div class="image image--style-3" style="background-image: none; transform-origin: 150% 100%; transform: translate(0px, 0px);">
    <div class="image__wrap">
        <div class="image__element" style="background-image: url(11.jpg); transform-origin: 150% 100%; transform: translate(0px, 0px);"></div>
    </div>
    <div class="image__element" style="background-image: url(11.jpg); transform: translate(0px, 0px);"></div>
    <div class="image__element" style="background-image: url(11.jpg); transform: translate(0px, 0px);"></div>
    <div class="image__element" style="background-image: url(11.jpg); transform: translate(0px, 0px);"></div>
    <div class="image__element" style="background-image: url(11.jpg); transform: translate(0px, 0px);"></div>
    <div class="image__element" style="background-image: url(11.jpg); transform: translate(0px, 0px);"></div>
</div>

Then we animate the “image_element” divs according to the parameters set.

Here’s a short explanation of the parameters:

data-repetition 					
// this is so that we know we have to apply the effect

data-repetition-elems="4" 			
// number of inner elements/images

data-repetition-animate="scale" 	
// property to animate: scale, scaleX, scaleY 

data-repetition-origin="50% 50%" 	
// transform origin

data-repetition-stagger="-0.1" 		
// GSAP animation stagger value between each inner image

data-repetition-initial-scale="2" 	
// this is the initial scale that is applied to the first inner child

data-repetition-duration="0.8" 		
// animation duration

data-repetition-ease="power2.inOut" 
// animation ease

There’s lots of things to experiment with here, so I hope you can use this to have some fun with it!

Here’s our first example:

The main idea is to have repeated layers of the same image and do something to them, like scale them up and down, like in this case. There’s a little twist added here, which is that the first and last layer also have a zoom effect on the image. Changing the transform origin, can also add a nice touch to it:

I really hope you enjoy this and find it useful!

Thanks for checking by and if you want to support our work, please share and give us a follow @codrops!

The post Repetition Image Hover Effects appeared first on Codrops.

The Search For a Fixed Background Effect With Inline Images

I was working on a client project a few days ago and wanted to create a certain effect on an <img>. See, background images can do the effect I was looking for somewhat easily with background-attachment: fixed;. With that in place, a background image stays in place—even when the page scrolls. It isn’t used all that often, so the effect can look unusual and striking, especially when used sparingly.

It took me some time to figure out how to achieve the same effect only with an inline image, rather than a CSS background image. This is a video of the effect in action:

The exact code for the above demo is available in this Git repo. Just note that it’s a Next.js project. We’ll get to a CodePen example with raw HTML in a bit.

Why use <img> instead of background-image?

The are a number of reasons I wanted this for my project:

  • It’s easier to lazy load (e.g. <img loading="lazy"… >.
  • It provides better SEO (not to mention accessibility), thanks to alt text.
  • It’s possible to use srcset/sizes to improve the loading performance.
  • It’s possible to use the <picture> tag to pick the best image size and format for the user’s browser.
  • It allows users to download save the image (without resorting to DevTools).

Overall, it’s better to use the image tag where you can, particularly if the image could be considered content and not decoration. So, I wound up landing on a technique that uses CSS clip-path. We’ll get to that in a moment, right after we first look at the background-image method for a nice side-by-side comparison of both approaches.

1. Using CSS background-image

This is the “original” way to pull off a fixed scrolling effect. Here’s the CSS:

.hero-section {
  background-image: url("nice_bg_image.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center; 
  background-attachment: fixed;
}

But as we just saw, this approach isn’t ideal for some situations because it relies on the CSS background-image property to call and load the image. That means the image is technically not considered content—and thus unrecognized by screen readers. If we’re working with an image that is part of the content, then we really ought to make it accessible so it is consumed like content rather than decoration.

Otherwise, this technique works well, but only if the image spans the whole width of the viewport and/or is centered. If you have an image on the right or left side of the page like the example, you’ll run into a whole number of positioning issues because background-position is relative to the center of the viewport.

Fixing it requires a few media queries to make sure it is positioned properly on all devices.

2. Using the clip-path trick on an inline image

Someone on StackOverflow shared this clip-path trick and it gets the job done well. You also get to keep using the<img> tag, which, as we covered above, might be advantageous in some circumstances, especially where an image is part of the content rather than pure decoration.

Here’s the trick:

.image-container {
  position: relative;
  height: 200px;
  clip-path: inset(0);
}

.image {
  object-fit: cover;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

Check it out in action:

Now, before we rush out and plaster this snippet everywhere, it has its own set of downsides. For example, the code feels a bit lengthy to me for such a simple effect. But, even more important is the fact that working with clip-path comes with some implications as well. For one, I can’t just slap a border-radius: 10px; in there like I did in the earlier example to round the image’s corners. That won’t work—it requires making rounded corners from the clipping path itself.

Another example: I don’t know how to position the image within the clip-path. Again, this might be a matter of knowing clip-path really well and drawing it where you need to, or cropping the image itself ahead of time as needed.

Is there something better?

Personally, I gave up on using the fixed scrolling effect on inline images and am back to using a CSS background image—which I know is kind of limiting.

Have you ever tried pulling this off, particularly with an inline image, and managed it well? I’d love to hear!


The Search For a Fixed Background Effect With Inline Images originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Maybe there kinda is background-opacity?

I was reading Jake’s “Cross-fading any two DOM elements is currently impossible” which is a wonderfully nerdy deep dive into how there is no real way to literally cross-fade elements. Yeah, you can animate both of their opacities, but even that isn’t quite the same. Turns out there is a Chrome/WebKit-only CSS function call called -webkit-cross-fade() that does the trick. MDN says it’s specced, but the implemented version differs, so it’s a bit of a mess… but it does exist:

.el {
  background: -webkit-cross-fade(url(img1.svg), url(img2.svg), 50%);
}

I didn’t even know that was a thing.

The first thing I thought of was: if one of the images was just a blank transparent image, wouldn’t that apply partial transparency to the other one? And thus it’s kind of a proxy for background-opacity (which doesn’t exist but feels like it should).

I gave it a test to see if it would work:

It seems to work! This is the meat of it:

.el {
  background-image: -webkit-cross-fade(
    url(image.jpg),
    url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7),
    50%
  );

That’s a 1px transparent GIF Base64 encoded.

It doesn’t work in Firefox but it does in everything else. Plus, you can test for support right in CSS and do something different if this isn’t just an enhancement.

@supports (background: -webkit-cross-fade(url(), url(), 50%)) {

  /* Only apply the idea if supported, do the Firefox fallback outside of this */

}

That’s baked into the demo above.

More Control Over CSS Borders With background-image

You can make a typical CSS border dashed or dotted. For example:

.box {
   border: 1px dashed black;
   border: 3px dotted red;
}

You don’t have all that much control over how big or long the dashes or gaps are. And you certainly can’t give the dashes slants, fading, or animation! You can do those things with some trickery though.

Amit Sheen build this really neat Dashed Border Generator:

The trick is using four multiple backgrounds. The background property takes comma-separated values, so by setting four backgrounds (one along the top, right, bottom, and left) and sizing them to look like a border, it unlocks all this control.

So like:

.box {
  background-image: repeating-linear-gradient(0deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(90deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(180deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(270deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px);
  background-size: 3px 100%, 100% 3px, 3px 100% , 100% 3px;
  background-position: 0 0, 0 0, 100% 0, 0 100%;
  background-repeat: no-repeat;
}

I like gumdrops.


The post More Control Over CSS Borders With background-image appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

How to Repeat Text as a Background Image in CSS Using element()

There’s a design trend I’ve seen popping up all over the place. Maybe you’ve seen it too. It’s this sort of thing where text is repeated over and over. A good example is the price comparison website, GoCompare, who used it in a major multi-channel advertising campaign.

Nike has used it as well, like in this advertisement:

Diggin' that orange! (Source)

I couldn’t help but wonder how I would implement this sort of design for the web. I mean, we could obviously just repeat the text in markup. We could also export the design as an image using something like Photoshop, but putting text in images is bad for both SEO and accessibility. Then there's the fact that, even if we did use actual text, it’s not like we’d want a screen reader speak it out.

Versatility
Versatility
Versatility
Versatility

OK, stop already!

These considerations make it seem unrealistic to do something like this on the web. Then I found myself pining for the long-existing, yet badly supported, element() feature in CSS. It enables the use of any HTML element as a background image, whether it be a single button element, or an entire <div> full of content.

According to the spec:

The element() function only reproduces the appearance of the referenced element, not the actual content and its structure. Authors should only use this for decorative purposes.

For our purposes, we’d be referencing a text element to get that repeating effect.

Let’s define an ID we can apply to the text element we want to repeat. Let’s call it #thingy. Note that when we use #thingy, we’ve got to prefix the element() value with -moz-. While element() has been supported in Firefox since 2010, it sadly hasn’t landed in any other browser since.

.element {
  background-image: -moz-element(#thingy);
}

Here’s a somewhat loose recreation of the Nike advertisement we saw earlier. Again, Firefox is required to see the demo as intended.

See how that works conceptually? I placed an element (#versatility) on the page, hid it by giving it zero height, set it as the background-image on the body, then used the background-repeat property to duplicate it vertically down the page.

The element() background is live. That means the background-image appearance on the thing using it will change if the referenced HTML element changes. It’s the same sort of deal when working with custom properties: change the variable and it updates everywhere it’s used.

There are, of course, other use cases for this property. Check out how Preethi used it to make in-page scrolling navigation for an article. You could also use a HTML canvas element as a background if you want to get fancy. One way I’ve used it is to show screenshots of pages in a table of contents. Vincent De Oliveira, has documented some wildly creative examples. Here's an image-reflection effect, if you’re into retro web design:


Pretty neat, right? Again, I wish I could say this is a production-ready approach to get that neat design effect, but things are what they are at the moment. Actually, that’s a good reminder to make your voice heard for features you’d like to see implemented in browsers. There are open tickets in WebKit and Chromium where you can do that. Hopefully we’ll eventually get this feature in Safari-world and Chrome-world browsers.

The post How to Repeat Text as a Background Image in CSS Using element() appeared first on CSS-Tricks.

Background Scale Hover Effect with CSS Clip-path

Today I’d like to share a simple hover effect with you. It’s a recreation of the hover effect seen in the menu on the DDD Hotel website by Garden Eight. The idea is to scale down the background image and “fitting” it to a clip shape which contains the same background image. The shape is visible because the opacity of the background is a bit lower:

When I saw the effect on the DDD Hotel website, I wanted to try to do it using the clip-path property and explore different shapes.

It’s very straightforward: one layer has the background image and a second layer has the additional clip path with a basic shape.

For the last demo, where I wanted to show two circles, I simply stacked two clip-path layers. But for more complex paths, one could also use an SVG instead.

Other interesting things that could be done here is to animate the clip-path (scale it/move it) or change the shape for each link. What do you think?

Background Scale Hover Effect with CSS Clip-path was written by Mary Lou and published on Codrops.

Creating a Maintainable Icon System with Sass

One of my favorite ways of adding icons to a site is by including them as data URL background images to pseudo-elements (e.g. ::after) in my CSS. This technique offers several advantages:

  • They don't require any additional HTTP requests other than the CSS file.
  • Using the background-size property, you can set your pseudo-element to any size you need without worrying that they will overflow the boundaries (or get chopped off).
  • They are ignored by screen readers (at least in my tests using VoiceOver on the Mac) so which is good for decorative-only icons.

But there are some drawbacks to this technique as well:

  • When used as a background-image data URL, you lose the ability to change the SVG's colors using the "fill" or "stroke" CSS properties (same as if you used the filename reference, e.g. url( 'some-icon-file.svg' )). We can use filter() as an alternative, but that might not always be a feasible solution.
  • SVG markup can look big and ugly when used as data URLs, making them difficult to maintain when you need to use the icons in multiple locations and/or have to change them.

We're going to address both of these drawbacks in this article.

The situation

Let's build a site that uses a robust iconography system, and let's say that it has several different button icons which all indicate different actions:

  • A "download" icon for downloadable content
  • An "external link" icon for buttons that take us to another website
  • A "right caret" icon for taking us to the next step in a process

Right off the bat, that gives us three icons. And while that may not seem like much, already I'm getting nervous about how maintainable this is going to be when we scale it out to more icons like social media networks and the like. For the sake of this article, we're going to stop at these three, but you can imagine how in a sophisticated icon system this could get very unwieldy, very quickly.

It's time to go to the code. First, we'll set up a basic button, and then by using a BEM naming convention, we'll assign the proper icon to its corresponding button. (At this point, it's fair to warn you that we'll be writing everything out in Sass, or more specifically, SCSS. And for the sake of argument, assume I'm running Autoprefixer to deal with things like the appearance property.)

.button {
  appearance: none;
  background: #d95a2b;
  border: 0;
  border-radius: 100em;
  color: #fff;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: background-color 200ms ease-in-out;

  &:hover,
  &:focus,
  &:active {
    background: #8c3c2a;
  }
}

This gives us a simple, attractive, orange button that turns to a darker orange on the hover, focused, and active states. It even gives us a little room for the icons we want to add, so let's add them in now using pseudo-elements:

.button {

  /* everything from before, plus... */

  &::after {
    background: center / 24px 24px no-repeat; // Shorthand for: background-position, background-size, background-repeat
    border-radius: 100em;
    bottom: 0;
    content: '';
    position: absolute;
    right: 0;
    top: 0;
    width: 48px;
  }

  &--download {

    &::after {
      background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>' );
      }
    }

  &--external {

    &::after {
      background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>' );
    }
  }

  &--caret-right {

    &::after {
      background-image: url( 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>' );
    }
  }
}

Let's pause here. While we're keeping our SCSS as tidy as possible by declaring the properties common to all buttons, and then only specifying the background SVGs on a per-class basis, it's already starting to look a bit unwieldy. That's that second downside to SVGs I mentioned before: having to use big, ugly markup in our CSS code.

Also, note how we're defining our fill and stroke colors inside the SVGs. At some point, browsers decided that the octothorpe ("#") that we all know and love in our hex colors was a security risk, and declared that they would no longer support data URLs that contained them. This leaves us with three options:

  1. Convert our data URLs from markup (like we have here) to base-64 encoded strings, but that makes them even less maintainable than before by completely obfuscating them; or
  2. Use rgba() or hsla() notation, not always intuitive as many developers have been using hex for years; or
  3. Convert our octothorpes to their URL-encoded equivalents, "%23".

We're going to go with option number three, and work around that browser limitation. (I will mention here, however, that this technique will work with rgb(), hsla(), or any other valid color format, even CSS named colors. But please don't use CSS named colors in production code.)

Moving to maps

At this point, we only have three buttons fully declared. But I don't like them just dumped in the code like this. If we needed to use those same icons elsewhere, we'd have to copy and paste the SVG markup, or else we could assign them to variables (either Sass or CSS custom properties), and reuse them that way. But I'm going to go for what's behind door number three, and switch to using one of Sass' greatest features: maps.

If you're not familiar with Sass maps, they are, in essence, the Sass version of an associative array. Instead of a numerically-indexed array of items, we can assign a name (a key, if you will) so that we can retrieve them by something logical and easily remembered. So let's build a Sass map of our three icons:

$icons: (
  'download':    '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>',
  'external':    '<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>',
  'caret-right': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>',
);

There are two things to note here: We didn't include the data:image/svg+xml;utf-8, string in any of those icons, only the SVG markup itself. That string is going to be the same every single time we need to use these icons, so why repeat ourselves and run the risk of making a mistake? Let's instead define it as its own string and prepend it to the icon markup when needed:

$data-svg-prefix: 'data:image/svg+xml;utf-8,';

The other thing to note is that we aren't actually making our SVG any prettier; there's no way to do that. What we are doing is pulling all that ugliness out of the code we're working on a day-to-day basis so we don't have to look at all that visual clutter as much. Heck, we could even put it in its own partial that we only have to touch when we need to add more icons. Out of sight, out of mind!

So now, let's use our map. Going back to our button code, we can now replace those icon literals with pulling them from the icon map instead:

&--download {

  &::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'download' ) );
  }
}

&--external {

  &::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'external' ) );
  }
}

&--next {

  &::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'caret-right' ) );
  }
}

Already, that's looking much better. We've started abstracting out our icons in a way that keeps our code readable and maintainable. And if that were the only challenge, we'd be done. But in the real-world project that inspired this article, we had another wrinkle: different colors.

Our buttons are a solid color which turn to a darker version of that color on their hover state. But what if we want "ghost" buttons instead, that turn into solid colors on hover? In this case, white icons would be invisible for buttons that appear on white backgrounds (and probably look wrong on non-white backgrounds). What we're going to need are two variations of each icon: the white one for the hover state, and one that matches button's border and text color for the non-hover state.

Let's update our button's base CSS to turn it in from a solid button to a ghost button that turns solid on hover. And we'll need to adjust the pseudo-elements for our icons, too, so we can swap them out on hover as well.

.button {
  appearance: none;
  background: none;
  border: 3px solid #d95a2b;
  border-radius: 100em;
  color: #d95a2b;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: bold;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: 200ms ease-in-out;
  transition-property: background-color, color;

  &:hover,
  &:focus,
  &:active {
    background: #d95a2b;
    color: #fff;
  }
}

Now we need to create our different-colored icons. One possible solution is to add the color variations directly to our map... somehow. We can either add new different-colored icons as additional items in our one-dimensional map, or make our map two-dimensional.

One-Dimensional Map:

$icons: (
  'download-white':  '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>',
  'download-orange': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>',
);

Two-Dimensional Map:

$icons: (
  'download': (
    'white':  '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>',
    'orange': '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>',
  ),
);

Either way, this is problematic. By just adding one additional color, we're going to double our maintenance efforts. Need to change the existing download icon with a different one? We need to manually create each color variation to add it to the map. Need a third color? Now you've just tripled your maintenance costs. I'm not even going to get into the code to retrieve values from a multi-dimensional Sass map because that's not going to serve our ultimate goal here. Instead, we're just going to move on.

Enter string replacement

Aside from maps, the utility of Sass in this article comes from how we can use it to make CSS behave more like a programming language. Sass has built-in functions (like map-get(), which we've already seen), and it allows us to write our own.

Sass also has a bunch of string functions built-in, but inexplicably, a string replacement function isn't one of them. That's too bad, as its usefulness is obvious. But all is not lost.

Hugo Giradel gave us a Sass version of str-replace() here on CSS-Tricks in 2014. We can use that here to create one version of our icons in our Sass map, using a placeholder for our color values. Let's add that function to our own code:

@function str-replace( $string, $search, $replace: '' ) {

  $index: str-index( $string, $search );

  @if $index {
    @return str-slice( $string, 1, $index - 1 ) + $replace + str-replace( str-slice( $string, $index + str-length( $search ) ), $search, $replace);
  }

  @return $string;
}

Next, we'll update our original Sass icon map (the one with only the white versions of our icons) to replace the white with a placeholder (%%COLOR%%) that we can swap out with whatever color we call for, on demand.

$icons: (
  'download':    '<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%%COLOR%%" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%%COLOR%%" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g></svg>',
  'external':    '<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%%COLOR%%" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></g></svg>',
  'caret-right': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%%COLOR%%" stroke-width="3"/></svg>',
);

But if we were going to try and fetch these icons using just our new str-replace() function and Sass' built-in map-get() function, we'd end with something big and ugly. I'd rather tie these two together with one more function that makes calling the icon we want in the color we want as simple as one function with two parameters (and because I'm particularly lazy, we'll even make the color default to white, so we can omit that parameter if that's the color icon we want).

Because we're getting an icon, it's a "getter" function, and so we'll call it get-icon():

@function get-icon( $icon, $color: #fff ) {

  $icon:        map-get( $icons, $icon );
  $placeholder: '%%COLOR%%';

  $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

  @return str-replace( $data-uri, '#', '%23' );
}

Remember where we said that browsers won't render data URLs that have octothorpes in them? Yeah, we're str-replace()ing that too so we don't have to remember to pass along "%23" in our color hex codes.

Side note: I have a Sass function for abstracting colors too, but since that's outside the scope of this article, I'll refer you to my get-color() gist to peruse at your leisure.

The result

Now that we have our get-icon() function, let's put it to use. Going back to our button code, we can replace our map-get() function with our new icon getter:

&--download {

  &::before {
    background-image: get-icon( 'download', #d95a2b );
  }

  &::after {
    background-image: get-icon( 'download', #fff ); // The ", #fff" isn't strictly necessary, because white is already our default
  }
}

&--external {

  &::before {
    background-image: get-icon( 'external', #d95a2b );
  }

  &::after {
    background-image: get-icon( 'external' );
  }
}

&--next {

  &::before {
    background-image: get-icon( 'arrow-right', #d95a2b );
  }

  &::after {
    background-image: get-icon( 'arrow-right' );
  }
}

So much easier, isn't it? Now we can call any icon we've defined, with any color we need. All with simple, clean, logical code.

  • We only ever have to declare an SVG in one place.
  • We have a function that gets that icon in whatever color we give it.
  • Everything is abstracted out to a logical function that does exactly what it looks like it will do: get X icon in Y color.

Making it fool-proof

The one thing we're lacking is error-checking. I'm a huge believer in failing silently... or at the very least, failing in a way that is invisible to the user yet clearly tells the developer what is wrong and how to fix it. (For that reason, I should be using unit tests way more than I do, but that's a topic for another day.)

One way we have already reduced our function's propensity for errors is by setting a default color (in this case, white). So if the developer using get-icon() forgets to add a color, no worries; the icon will be white, and if that's not what the developer wanted, it's obvious and easily fixed.

But wait, what if that second parameter isn't a color? As if, the developer entered a color incorrectly, so that it was no longer being recognized as a color by the Sass processor?

Fortunately we can check for what type of value the $color variable is:

@function get-icon( $icon, $color: #fff ) {

  @if 'color' != type-of( $color ) {

    @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
    @return null;
  }

  $icon:        map-get( $icons, $icon );
  $placeholder: '%%COLOR%%';
  $data-uri:    str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

  @return str-replace( $data-uri, '#', '%23' );
}

Now if we tried to enter a nonsensical color value:

&--download {

  &::before {
    background-image: get-icon( 'download', ce-nest-pas-un-couleur );
  }
}

...we get output explaining our error:

Line 25 CSS: The requested color - "ce-nest-pas-un-couleur" - was not recognized as a Sass color value.

...and the processing stops.

But what if the developer doesn't declare the icon? Or, more likely, declares an icon that doesn't exist in the Sass map? Serving a default icon doesn't really make sense in this scenario, which is why the icon is a mandatory parameter in the first place. But just to make sure we are calling an icon, and it is valid, we're going to add another check:

@function get-icon( $icon, $color: #fff ) {

  @if 'color' != type-of( $color ) {

    @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
    @return null;
  }

  @if map-has-key( $icons, $icon ) {

    $icon:        map-get( $icons, $icon );
    $placeholder: '%%COLOR%%';
    $data-uri:    str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

    @return str-replace( $data-uri, '#', '%23' );
  }

  @warn 'The requested icon - "' + $icon + '" - is not defined in the $icons map.';
  @return null;
}

We've wrapped the meat of the function inside an @if statement that checks if the map has the key provided. If so (which is the situation we're hoping for), the processed data URL is returned. The function stops right then and there — at the @return — which is why we don't need an @else statement.

But if our icon isn't found, then null is returned, along with a @warning in the console output identifying the problem request, plus the partial filename and line number. Now we know exactly what's wrong, and when and what needs fixing.

So if we were to accidentally enter:

&--download {

  &::before {
    background-image: get-icon( 'ce-nest-pas-une-icône', #d95a2b );
  }
}

...we would see the output in our console, where our Sass process was watching and running:

Line 32 CSS: The requested icon - "ce-nest-pas-une-icône" - is not defined in the $icons map.

As for the button itself, the area where the icon would be will be blank. Not as good as having our desired icon there, but soooo much better than a broken image graphic or some such.

Conclusion

After all of that, let's take a look at our final, processed CSS:

.button {
  -webkit-appearance: none;
      -moz-appearance: none;
          appearance: none;
  background: none;
  border: 3px solid #d95a2b;
  border-radius: 100em;
  color: #d95a2b;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: 200ms ease-in-out;
  transition-property: background-color, color;
}
.button:hover, .button:active, .button:focus {
  background: #d95a2b;
  color: #fff;
}
.button::before, .button::after {
  background: center / 24px 24px no-repeat;
  border-radius: 100em;
  bottom: 0;
  content: '';
  position: absolute;
  right: 0;
  top: 0;
  width: 48px;
}
.button::after {
  opacity: 0;
  transition: opacity 200ms ease-in-out;
}
.button:hover::after, .button:focus::after, .button:active::after {
  opacity: 1;
}
.button--download::before {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23d95a2b" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>');
}
.button--download::after {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="30.544" height="25.294" viewBox="0 0 30.544 25.294"><g transform="translate(-991.366 -1287.5)"><path d="M1454.5,1298.922l6.881,6.881-6.881,6.881" transform="translate(2312.404 -157.556) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><path d="M8853.866,5633.57v9.724h27.544v-9.724" transform="translate(-7861 -4332)" fill="none" stroke="%23fff" stroke-linejoin="round" stroke-width="3"/><line y2="14" transform="translate(1006.5 1287.5)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>');
}
.button--external::before {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23d95a2b" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23d95a2b" stroke-width="3"/></g></svg>');
}
.button--external::after {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="31.408" height="33.919" viewBox="0 0 31.408 33.919"><g transform="translate(-1008.919 -965.628)"><g transform="translate(1046.174 2398.574) rotate(-135)"><path d="M0,0,7.879,7.879,0,15.759" transform="translate(1025.259 990.17) rotate(90)" fill="none" stroke="%23fff" stroke-width="3"/><line y2="16.032" transform="translate(1017.516 980.5)" fill="none" stroke="%23fff" stroke-width="3"/></g><path d="M10683.643,5322.808v10.24h-20.386v-21.215h7.446" transform="translate(-9652.838 -4335)" fill="none" stroke="%23fff" stroke-width="3"/></g></svg>');
}
.button--next::before {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23d95a2b" stroke-width="3"/></svg>');
}
.button--next::after {
  background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.129 34.016"><path d="M1454.5,1298.922l15.947,15.947-15.947,15.947" transform="translate(-1453.439 -1297.861)" fill="none" stroke="%23fff" stroke-width="3"/></svg>');
}

Yikes, still ugly, but it's ugliness that becomes the browser's problem, not ours.

I've put all this together in CodePen for you to fork and experiment. The long goal for this mini-project is to create a PostCSS plugin to do all of this. This would increase the availability of this technique to everyone regardless of whether they were using a CSS preprocessor or not, or which preprocessor they're using.

"If I have seen further it is by standing on the shoulders of Giants."
– Isaac Newton, 1675

Of course we can't talk about Sass and string replacement and (especially) SVGs without gratefully acknowledging the contributions of the others who've inspired this technique.

The post Creating a Maintainable Icon System with Sass appeared first on CSS-Tricks.

Smooth Scrolling Image Effects

Picking up on our last tutorial on how to add smooth scrolling plus image animations to a page, we’d like to explore some more ideas for animations. We’ve made a small set of effects that show how you can apply some interesting transforms to elements like images and text while scrolling the page smoothly.

Inspirations for some of the effects come from Jesper Landberg’s smooth scroll with skew effect demo, Manuel Rovira’s Dribbble shot Lusaxweb Home and Jo Mor’s website.

The animations are powered by TweenMax.

Attention: Note that the demos are experimental and that we use modern CSS properties that might not be supported in older browsers.

For the demos, we’ve created different (grid) layouts with images that have decorative elements and captions.

We’ve used background images that are wrapped in a division with its overflow set to hidden, so that we can animate the scale or translate of the inner images in some examples. There are many possibilities to explore, for example, rotating the images:

SmoothScrollingEffects_01

…or adding a blend mode to one of the moving elements:

SmoothScrollingEffects_02

As you can also see in Jesper Landberg’s smooth scroll with skew effect demo, you can use the acceleration to control the transform amount. So when you scroll faster, the elements distort more.

Here’s a little GIF to show a detail of one of the animations:

smoothscrolleffect.2019-07-23 11_04_22

Note that when using the scale transform, the animations in Firefox don’t perform so smoothly.

We hope you enjoy this little set and find it inspirational.

References and Credits

Smooth Scrolling Image Effects was written by Mary Lou and published on Codrops.

Managing Multiple Backgrounds with Custom Properties

One cool thing about CSS custom properties is that they can be a part of a value. Let's say you're using multiple backgrounds to pull off a a design. Each background will have its own color, image, repeat, position, etc. It can be verbose!

You have four images:

body {
  
  background-position:
    top 10px left 10px,
    top 10px right 10px,
    bottom 10px right 10px,
    bottom 10px left 10px;
  
  background-repeat: no-repeat;
  
  background-image:
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);
  
}

You want to add a fifth in a media query:

@media (min-width: 1500px) {
  body {
    /* REPEAT all existing backgrounds, then add a fifth. */
  }
}

That's going to be super verbose! You'll have to repeat each of those four images again, then add the fifth. Lots of duplication there.

One possibility is to create a variable for the base set, then add the fifth much more cleanly:

body {
  --baseBackgrounds: 
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg),
    url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);

  background-position:
    top 10px left 10px,
    top 10px right 10px,
    bottom 10px right 10px,
    bottom 10px left 10px;
  
  background-repeat: no-repeat;
  
  background-image: var(--baseBackgrounds);
}
@media (min-width: 1500px) {
  body {
    background-image: 
      var(--baseBackgrounds),
      url(added-fifth-background.svg);
  }
}

But, it's really up to you. It might make more sense and be easier manage if you made each background image into a variable, and then pieced them together as needed.

body {
  --bg1: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg);
  --bg2: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg);
  --bg3: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg);
  --bg4: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);
  --bg5: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);
  
  background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4);
}
@media (min-width: 1500px) {
  body {
    background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4), var(--bg5);
  }
}

Here's a basic version of that, including a supports query:

See the Pen
Multiple BGs with Custom Properties
by Chris Coyier (@chriscoyier)
on CodePen.

Dynamically changing just the part of a value is a huge strength of CSS custom properties!

Note, too, that with backgrounds, it might be best to include the entire shorthand as the variable. That way, it's much easier to piece everything together at once, rather than needing something like...

--bg_1_url: url();
--bg_1_size: 100px;
--bg_1_repeat: no-repeat;
/* etc. */

It's easier to put all of the properties into shorthand and use as needed:

body {  
  --bg_1: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg) top 10px left 10px / 86px no-repeat;
  --bg_2: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg) top 10px right 10px / 86px no-repeat;
  --bg_3: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg) bottom 10px right 10px / 86px no-repeat;
  --bg_4: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg) bottom 10px left 10px  / 86px no-repeat;
    
  background:
    var(--bg_1), var(--bg_2),var(--bg_3),var(--bg_4);
}

Like this.

The post Managing Multiple Backgrounds with Custom Properties appeared first on CSS-Tricks.

How to Add Smooth Scrolling with Inner Image Animations to a Web Page

Today we want to show you how to add smooth scrolling in HTML with some additional subtle animations on images. With “smooth scrolling” we don’t mean smoothly scrolling to an element, but rather a smoothly animated kind of scrolling behavior. There are many beautiful examples of such smooth scrolling behavior on some recent websites, like Elena Iv-skaya, or Ada Sokół and the stunning site of Rafal Bojar, and many many others. The latter also has a very nice image animation that is synced with the scrolling. This kind of “inner” image animation adds another interesting layer to the whole scroll movement.

Why is this kind of smooth scrolling something you’d like to add to a web page? If you have ever animated something on scroll, you might have experienced that browsers have difficulties in displaying the incoming content jank-free; especially images may show tiny abrupt jumps on scroll. It just feel easy on the eye. To avoid that, we can use the trick of animating the content itself by translating it up or down instead of using the “native” scroll.

Smooth Scrolling

Jesper Landberg created some really great Codepen demos showcasing how smooth scrolling can be applied to different scenarios. The Smooth scroll with skew effect demo shows how to add a skew effect to images while (smooth) scrolling. Here you can also see how smooth scrolling with translating the content works: a content wrapper is set to position fixed with the overflow set to hidden so that its child can be moved. The body will get the height of the content set to it, so that we preserve the scroll bar. When we scroll, the fixed wrapper will stay in place while we animate the inner content. This trick makes a simple yet effective smooth scrolling behavior possible.

In our example we’ll use the following structure:

<body class="loading">
	<main>
		<div data-scroll>
			<!-- ... --->
		</div>
	</main>
</body>

The main element will serve as fixed, or “sticky”, container while the [data-scroll] div will get translated.

Inner Image Animation

For the inner image animation we need an image and a parent container that has its overflow set to “hidden”. The idea is to move the image up or down while we scroll. We will work with a background image on a div so that we can control the overflow size better. Mainly, we need to make sure that the image div is bigger than its parent. This is our markup:

<div class="item">
	<div class="item__img-wrap"><div class="item__img"></div></div>
	<!-- ... --->
</div>

Let’s set the styles for these elements. We will use a padding instead of a height so that we can set the right aspect ratio for the inner div which will have the image as background. For this, we use an aspect ratio variable so that we simply need to set the image width and height and leave the calculation to our stylesheet. Read more about this and other brilliant techniques in Apect Ratio Boxes on CSS-Tricks.

We set an image variable for the background image in the item__img-wrap class so that we don’t have to write too many rules. This does not need to be done like that, of course, especially if you’d like support for older browsers that don’t know what variables are. Set it to the item__img directly as background-image instead, if that’s the case.

.item__img-wrap {
	--aspect-ratio: 1/1.5;
	overflow: hidden;
	width: 500px;
	max-width: 100%;
	padding-bottom: calc(100% / (var(--aspect-ratio))); 
	will-change: transform;
}

.item:first-child .item__img-wrap {
	--aspect-ratio: 8/10;
	--image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/1.jpg);
}

.item:nth-child(2) .item__img-wrap {
	width: 1000px;
	--aspect-ratio: 120/76;
	--image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/2.jpg);
}

...

The div with the background image is the one we want to move up or down on scroll, so we need to make sure that it’s taller than its parent. For that, we define an “overflow” variable that we’ll use in a calculation for the height and the top. We set this variable because we want to be able to easily change it in some modified classes. This allows us to set a different overflow to each image which changes the visual effect subtly.

.item__img {
	--overflow: 40px;
	height: calc(100% + (2 * var(--overflow)));
	top: calc( -1 * var(--overflow));
	width: 100%;
	position: absolute;
	background-image: var(--image);
	background-size: cover;
	background-position: 50% 0%;
	will-change: transform;
}

.item__img--t1 {
	--overflow: 60px;
}

.item__img--t2 {
	--overflow: 80px;
}

.item__img--t3 {
	--overflow: 120px;
}

Now, let’s do the JavaScript part. Let’s start with some helper methods and variables.

const MathUtils = {
    // map number x from range [a, b] to [c, d]
    map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
    // linear interpolation
    lerp: (a, b, n) => (1 - n) * a + n * b
};

const body = document.body;

We will need to get the window’s size, specifically it’s height, for later calculations.

let winsize;
const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
calcWinsize();

We will also need to recalculate this value on resize.

window.addEventListener('resize', calcWinsize);

Also, we need to keep track of how much we scroll the page.

let docScroll;
const getPageYScroll = () => docScroll = window.pageYOffset || document.documentElement.scrollTop;
window.addEventListener('scroll', getPageYScroll);

Now that we have these helper functions ready, let’s get to the main functionality.
Let’s create a class for the smooth scrolling functionality.

class SmoothScroll {
    constructor() {
        this.DOM = {main: document.querySelector('main')};
        this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');
        this.items = [];
        [...this.DOM.main.querySelectorAll('.content > .item')].forEach(item => this.items.push(new Item(item)));
        
        ...
    }
}

new SmoothScroll();

So far we have a reference to the main element (the container that needs to become “sticky”) and the scrollable element (the one we will be translating to simulate the scroll).

Also, we create an array of our item’s instances. We will get to that in a moment.

Now, we want to update the translateY value as we scroll but we might as well want to update other properties like the scale or rotation. Let’s create an object that stores this configuration. For now let’s just set up the translationY.

constructor() {
    ...

    this.renderedStyles = {
        translationY: {
            previous: 0, 
            current: 0, 
            ease: 0.1,
            setValue: () => docScroll
        }
    };
}

We will be using interpolation to achieve the smooth scrolling effect. The “previous” and “current” values are the values to interpolate. The current translationY will be a value between these two values at a specific increment. The “ease” is the amount to interpolate. The following formula calculates our current translation value:

previous = MathUtils.lerp(previous, current, ease)

The setValue function sets the current value, which in this case will be the current scroll position.
Let’s go ahead and execute this initially on page load to set up the right translationY value.

constructor() {
    ...

    this.update();
}

update() {
    for (const key in this.renderedStyles ) {
        this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
    }   
    this.layout();
}

layout() {
    this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;
}

We set both interpolation values to be the same, in this case the scroll value, so that the translation gets set immediately without the being animated. We just want the animation happening when we scroll the page. After that, we call the layout function which will apply the transformation to our element. Note that the value will be negative since the element moves upwards.

As for the layout changes, we need to:

  • set the position of the main element to fixed and the overflow to hidden so it sticks to the screen and doesn’t scroll.
  • set the height of the body in order to keep the scrollbar on the page. It will be the same as the scrollable element’s height.
constructor() {
    ...

    this.setSize();
    this.style();
}

setSize() {
    body.style.height = this.DOM.scrollable.scrollHeight + 'px';
}

style() {
    this.DOM.main.style.position = 'fixed';
    this.DOM.main.style.width = this.DOM.main.style.height = '100%';
    this.DOM.main.style.top = this.DOM.main.style.left = 0;
    this.DOM.main.style.overflow = 'hidden';
}

We also need to reset the body’s height on resize:

constructor() {
    ...

    this.initEvents();
}

initEvents() {
    window.addEventListener('resize', () => this.setSize());
}

Now we start our loop function that updates the values as we scroll.

constructor() {
    ...

    requestAnimationFrame(() => this.render());
}

render() {
    for (const key in this.renderedStyles ) {
        this.renderedStyles[key].current = this.renderedStyles[key].setValue();
        this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
    }
    this.layout();
    
    // for every item
    for (const item of this.items) {
        // if the item is inside the viewport call it's render function
        // this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
        if ( item.isVisible ) {
            item.render();
        }
    }
    
    // loop..
    requestAnimationFrame(() => this.render());
}

The only new thing here is the call to the item’s render function which is called for every item that is inside the viewport. This will update the translation of the item’s inner image as we will see ahead.

Since we rely on the scrollable element’s height, we need to preload the images so they get rendered and we get to calculate the right value for the height. We are using the imagesLoaded to achieve this:

const preloadImages = () => {
    return new Promise((resolve, reject) => {
        imagesLoaded(document.querySelectorAll('.item__img'), {background: true}, resolve);
    });
};

After the images are loaded we remove our page loader, get the scroll position (this might not be zero if we scrolled the page before the last refresh) and initialize our SmoothScroll instance.

preloadImages().then(() => {
    document.body.classList.remove('loading');
    // Get the scroll position
    getPageYScroll();
    // Initialize the Smooth Scrolling
    new SmoothScroll(document.querySelector('main'));
});

So now that the SmoothScroll is covered let’s create an Item class to represent each of the page items (the images).

class Item {
    constructor(el) {
        this.DOM = {el: el};
        this.DOM.image = this.DOM.el.querySelector('.item__img');
        
        this.renderedStyles = {
            innerTranslationY: {
                previous: 0, 
                current: 0, 
                ease: 0.1,
                maxValue: parseInt(getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 10),
                setValue: () => {
                    const maxValue = this.renderedStyles.innerTranslationY.maxValue;
                    const minValue = -1 * maxValue;
                    return Math.max(Math.min(MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, minValue, maxValue), maxValue), minValue)
                }
            }
        };
    }
    ...
}

The logic here is identical to the SmoothScroll class. We create a renderedStyles object that contains the properties we want to update. In this case we will be translating the item’s inner image (this.DOM.image) on the y-axis. The only extra here is that we are defining a maximum value for the translation (maxValue). This value we’ve previously set in our CSS variable –overflow. Also, we assume the minimum value for the translation will be -1*maxVal.

The setValue function works as follows:

  • When the item’s top value (relative to the viewport) equals the window’s height (item just came into the viewport) the translation is set to the minimum value.
  • When the item’s top value (relative to the viewport) equals “-item’s height” (item just exited the viewport) the translation is set to the maximum value.

So basically we are mapping the item’s top value (relative to the viewport) from the range [window’s height, -item’s height] to [minVal, maxVal].

Next thing to do is setting the initial values on load. We also calculate the item’s height and top since we’ll need those to apply the function described before.

constructor(el) {
    ...
    
    this.update();
}

update() {
    this.getSize();
    for (const key in this.renderedStyles ) {
        this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
    }
    this.layout();
}

layout() {
    this.DOM.image.style.transform = `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)`;
}

getSize() {
    const rect = this.DOM.el.getBoundingClientRect();
    this.props = {
        height: rect.height,
        top: docScroll + rect.top 
    }
}

We need the same for when the window gets resized:

initEvents() {
    window.addEventListener('resize', () => this.resize());
}
resize() {
    this.update();
}

Now we need to define the render function called inside the SmoothScroll render loop function (requestAnimationFrame):

render() {
    for (const key in this.renderedStyles ) {
        this.renderedStyles[key].current = this.renderedStyles[key].setValue();
        this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
    }
    this.layout();
}

This, as mentioned before, is only executed for items that are inside of the viewport. We can achieve this by using the IntersectionObserver API:

constructor(el) {
    ...

    this.observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);
    });
    this.observer.observe(this.DOM.el);

    ...
}

And that’s it!

We hope you enjoyed this tutorial and find it useful!

How to Add Smooth Scrolling with Inner Image Animations to a Web Page was written by Mary Lou and published on Codrops.