Show Image Under Text (with Acceptable Fallback)

Published by Chris Coyier

Updated April 2014 with more modern information.

WebKit supports the cool background-clip CSS3 property, which you can use to do some pretty neat stuff. The first time we touched on it was the iPhone Slide-to-unlock idea where we set a gradient to animate through the background of the text. Then we touched on it again for the transparent borders idea.

Let's take a look at using it to have an image be visibile only through the letters of the text. One more cool thing that we no longer need to drool over print designers ability to do.

View Demo

The Basic Idea

h1 {
   color: white;  /* Fallback: assume this color ON TOP of image */
   background: url(images/fire.jpg) no-repeat;
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;
}

That's all there is to it. Set a background image on the element, then clip it out, and set the text fill color to transparent.

The Problem

As I'm sure everyone is painfully aware, this isn't going to work in all browsers. The current support is WebKit only. So what happens for a fallback? As you can see in the code above, you declare a color value as well. This value gets overridden by -webkit-text-fill-color in browsers that support it, so we're clear there. Then in browsers that don't support -webkit-background-clip: text we will see the background image in full, so we will see text on top of that background image. So if you read no further, set a color value that will be nice and visible on that background image.

So we get this going on:

Not the absolute end of the world, at least we prepared the text to be somewhat readable. But this is a long way from what we envision and what WebKit users will experience. Basically: that fallback sucks, let's do better.

A Better Fallback

The ultimate tool for better fallbacks is Modernizr. Simply include the (fairly small) JavaScript file on your page, and it adds classnames to the html tag of your page indicating what the current browser is capable of. It also provides an API for testing features in JavaScript, but we won't need that today.

Unfortunately, Modernizr doesn't have a test for background-clip right out of the box. I asked one of the creators, Paul Irish, who hooked me up with this quick way to add this test. The whole bit:

<script src="modernizr-1.6.min.js"></script>
<script>
  Modernizr.addTest('backgroundclip',function() {

    var div = document.createElement('div');

    if ('backgroundClip' in div.style)
      return true;

    'Webkit Moz O ms Khtml'.replace(/([A-Za-z]*)/g,function(val) { 
      if (val+'BackgroundClip' in div.style) return true;
    });

  });
</script>

Now we'll know if the current browser supports background-clip, or does not. If it does, the html tag will have a backgroundclip class, if it does not, it will have a no-backgroundclip class.

Now we only apply the background image if we are certain the browser supports background-clip.

.backgroundclip h1 {
  background: url(images/west.jpg) -100px -40px no-repeat;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

h1 {
  color: orangered;
}

Badda bing, now the fallback is a straight up solid color instead of a messy-looking image knockout.

View Demo

But... there is no perfect system for this.

There is an issue with Android (up to and including 4.2) where it doesn't actually support -webkit-background-clip - even though any test will return that it does. The property, and any value it might have including text. Even Modernizr's testAllProps() can't catch it. But -webkit-text-fill-color does work, so essentially you get an image with no text. Pretty bad.

If you absolutely need to use this, you might just need to UA test for Android:

var NastyBrowserSniffing = {

  ua: navigator.userAgent.toLowerCase(),

  init: function() {
    var isAndroid = NastyBrowserSniffing.ua.indexOf("android") > -1;
    if (isAndroid) {
      $("html").addClass("android");
    }
  }

};

NastyBrowserSniffing.init();

Then revert the effect if it's ndroid:

html.android .gradient-text {
  color: white;
  background: none;
  -webkit-text-fill-color: white;
  -webkit-background-clip: border-box;
}
.gradient-text {
  background: -webkit-linear-gradient(gray, black);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

Related

Stephen Morley covers how to do it for IE with filters.