4 Visual Effects

15 One-sided shadows

The problem

One of the most common questions I see being asked about box-shadow on Q&A websites is how a shadow could be applied on one (or, more rarely, two) sides only. A quick search on stackoverflow.com reveals close to a thousand results for this. This makes sense, as showing a shadow only on one side creates a subtler, but equally realistic effect. Often, frustrated developers will even write to the CSS Working Group mailing list requesting new properties like box-shadow-bottom to be able to do this. However, such effects are already possible with clever use of the good ol’ box-shadow property we’ve learned and love.

Shadow on one side

Most people use box-shadow with three lengths and a color, like so:

box-shadow: 2px 3px 4px rgba(0,0,0,.5);
image

FIGURE 4.1 Example mental model of a box-shadow being painted

The following series of steps is a good (albeit not completely technically accurate) way to visualize how this shadow is drawn (Figure 4.1):

Unless otherwise noted, referring to an element’s dimensions here means the dimensions of its border box, not its CSS width and height.

1. A rgba(0,0,0,.5) rectangle is drawn with the same dimensions and position as our element.

2. It’s moved 2px to the right and 3px to the bottom.

3. It’s blurred by 4px with a Gaussian blur algorithm (or similar). This essentially means that the color transition on the edges of the shadow between the shadow color and complete transparency will be approximately as long as double the blur radius (8px, in our example).

4. The blurred rectangle is then clipped where it intersects with our original element, so that it appears to be “behind” it. This is a little different from the way most authors visualize shadows (a blurred rectangle underneath the element). However, for some use cases, it’s important to realize that no shadow will be painted underneath the element. For example, if we set a semi-transparent background on the element, we will not see a shadow underneath. This is different than text-shadow, which is not clipped underneath the text.

The use of 4px blur radius means that the dimensions of our shadow are approximately 4px larger than our element’s dimensions, so part of the shadow will show through from every side of the element. We could change the offsets to hide any shadow from the top and left, by increasing them to at least 4px. However, then this results in a way too conspicuous shadow, which doesn’t look nice (Figure 4.2). Also, even if this wasn’t a problem, we wanted a shadow on only one side, not two, remember?

To be precise, we will see a 1px shadow on the top (4px - 3px), 2px on the left (4px - 2px), 6px on the right (4px + 2px), and 7px on the bottom (4px + 3px). In practice, it will look smaller because the color transition on the edges is not linear, like a gradient would be.

The solution lies in the lesser known fourth length parameter, specified after the blur radius, which is called the spread radius. The spread radius increases or (if negative) decreases the size of the shadow by the amount you specify. For example, a spread radius of -5px will reduce the width and height of the shadow by 10px (5px on each side).

It logically follows that if we apply a negative spread radius whose absolute value is equal to the blur radius, then the shadow has the exact same dimensions as the element it’s applied on. Unless we move it with offsets (the first two lengths), we will not see any of it. Therefore, if we apply a positive vertical offset, we will start seeing a shadow on the bottom of our element, but not on any of the other sides, which is the effect we were trying to achieve:

image

FIGURE 4.2 Trying to hide the shadow from the top and left sides by using offsets equal to the blur radius

box-shadow: 0 5px 4px -4px black;

You can see the result in Figure 4.3.

image PLAY! play.csssecrets.io/shadow-one-side

image

FIGURE 4.3 box-shadow on the bottom side only

Shadow on two adjacent sides

Another frequently asked question concerns applying a shadow on two sides. If the two sides are adjacent (e.g., right and bottom), then this is easier: you can either settle for an effect like the one in Figure 4.2 or apply a variation of the trick discussed in the previous section, with the following differences:

image

FIGURE 4.4 box-shadow on two adjacent sides only

  • We don’t want to shrink the shadow to account for blurring in both sides, but only one of them. Therefore, instead of the spread radius having the opposite value of the blur radius, it will be half of that.

  • We need both offsets, as we want to move the shadow both horizontally and vertically. Their value needs to be greater or equal to half the blur radius, as we want to hide the remaining shadow from the other two sides.

For example, here is how we can apply a image black, 6px shadow to the right and bottom sides:

box-shadow: 3px 3px 6px -3px black;

You can see the result in Figure 4.4.

image PLAY! play.csssecrets.io/shadow-2-sides

Shadow on two opposite sides

It starts getting trickier when we want a shadow on two opposite sides, such as the left and right. Because the spread radius is applied on all sides equally (i.e., there is no way to specify that we want to enlarge the shadow horizontally and shrink it vertically), the only way to do this is to use two shadows, one on each side. Then we basically apply the trick discussed in the Shadow on one side” section on page 130 twice:

There are discussions in the CSS WG about allowing for separate horizontal/vertical spread radius values in the future, which would simplify this.

box-shadow: 5px 0 5px -5px black,
           -5px 0 5px -5px black;

You can see the result in Figure 4.5.

image

FIGURE 4.5 box-shadow on two opposite sides

image PLAY! play.csssecrets.io/shadow-opposite-sides

RELATED
SPECS

16 Irregular drop shadows

The problem

box-shadow works great when we want to cast a drop shadow on a rectangle or any shape that can be created with border-radius (refer to the Flexible ellipses” secret on page 76 for a few examples on that). However, it becomes less useful when we have pseudo-elements or other semi-transparent decorations, because box-shadow shamelessly ignores transparency. Some examples include:

  • Semi-transparent images, background images, or border-images (e.g., a vintage gold picture frame)

  • Dotted, dashed, or semi-transparent borders with no background (or when background-clip is not border-box)

  • Speech bubbles, with their pointer created via a pseudo-element

  • Cutout corners like the ones we saw in the Cutout corners” secret on page 96

  • Most folded corner effects, including the one later in this chapter

  • Shapes created via clip-path, like the diamond images in the Diamond images” secret on page 90

image

FIGURE 4.6 Elements with CSS styling that renders box-shadow useless; the value of the box-shadow applied is 2px 2px 10px rgba(0,0,0,.5)

The results of the futile attempt to apply box-shadow to some of them is shown in Figure 4.6. Is there a solution for such cases, or do we have to give up shadows altogether?

The solution

image

The Filter Effects specification (w3.org/TR/filter-effects) offers a solution to this problem, through a new filter property, borrowed from SVG. However, although CSS filters are basically SVG filters, they do not require any SVG knowledge. Instead, they are specified through a number of convenient functions, such as blur(), grayscale(), or—wait for it—drop-shadow()! You may even daisy-chain multiple filters if you want to, by white-space separating them, like this:

filter: blur() grayscale() drop-shadow();

The drop-shadow() filter accepts the same parameters as basic box-shadows, meaning no spread radius, no inset keyword, and no multiple, comma-separated shadows. For example, instead of:

box-shadow: 2px 2px 10px rgba(0,0,0,.5);

we would write:

filter: drop-shadow(2px 2px 10px rgba(0,0,0,.5));

You can see the result of this drop-shadow() filter when applied on the same elements as Figure 4.6 in Figure 4.7.

image These might use different blur algorithms, so you might need to adjust your blur value!

image

FIGURE 4.7 A drop-shadow() filter, applied to the elements from Figure 4.6

The best thing about CSS filters is that they degrade gracefully: when they are not supported, nothing breaks, there is just no effect applied. You can get slightly better browser support by using an SVG filter alongside, if you absolutely need this effect to work in as many browsers as possible. You can find the corresponding SVG filters for every filter function in the Filter Effects specification (w3.org/TR/filter-effects/). You can include both the SVG filter and the simplified CSS one alongside and let the cascade take care of which one wins:

filter: url(drop-shadow.svg#drop-shadow);
filter: drop-shadow(2px 2px 10px rgba(0,0,0,.5));

Unfortunately, if the SVG filter is a separate file, it’s not as customizable as a nice, human-friendly function that’s right in your CSS code, and if it’s inline, it clutters the code. The parameters are fixed inside the file, and it’s not practical to have multiple files if we want a slightly different shadow. We could use data URIs (which would also save the extra HTTP request), but they would still contribute to a large filesize. Because this is a fallback, it makes sense to use one or two variations, even for slightly different drop-shadow() filters.

Another consideration to keep in mind is that every non-transparent area will cast a shadow indiscriminately, including text (when your background is transparent), as you have already seen in Figure 4.7. You might think you can cancel this by using text-shadow: none;, but text-shadow is completely separate and will not cancel the effects of a drop-shadow() filter on text. In addition, if you’re using text-shadow to cast an actual shadow on the text, this shadow will also be shadowed by a drop-shadow() filter, essentially creating a shadow of a shadow! Take a look at the following example CSS code (and excuse the cheesiness of the result—it’s trying to demonstrate the issue in all its weirdness):

image

FIGURE 4.8 text-shadows also cast a shadow through the drop-shadow() filter

color: deeppink;
border: 2px solid;
text-shadow: .1em .2em yellow;
filter: drop-shadow(.05em .05em .1em gray);

You can see a sample rendering in Figure 4.8, showing both the text-shadow and the drop-shadow() it casts.

image PLAY! play.csssecrets.io/drop-shadow

RELATED
SPECS

17 Color tinting

The problem

Adding a color tint to a grayscale image (or an image that has been converted to grayscale) is a popular and elegant way to give visual unity to a group of photos with very disparate styles. Often, the effect is applied statically and removed on :hover and/or some other interaction.

Traditionally, we use an image editing application to create two versions of the image, and write some simple CSS code to take care of swapping them. This approach works, but it adds bloat and extra HTTP requests, and is a maintenance nightmare. Imagine deciding to change the color of the effect: you would have to go through all the images and create new monochrome versions!

image

FIGURE 4.9 The CSSConf 2014 website used this effect for speaker photos, but showed the full color picture on hover and focus

Other approaches involve overlaying a semi-transparent color on top of the image or applying opacity to the image and overlaying it on a solid color. However, this is not a real tint: in addition to not converting all the colors in the image to tints of the target color, it also reduces contrast significantly.

There are also scripts that turn images into a <canvas> element and apply the tint through JavaScript. This does produce proper tinting, but is fairly slow and restrictive.

Wouldn’t it be so much easier to be able to apply a color tint to images straight from our CSS?

Filter-based solution

image

Because there is no single filter function specifically designed for this effect, we need to get a bit crafty and combine multiple filters.

The first filter we will apply is sepia(), which gives the image a de-saturated orange-yellow tint, with most pixels having a hue of around 35–40. If this is the color we wanted, then we’re done. However, in most cases it won’t be. If our color is more saturated, we can use the saturate() filter to increase the saturation of every pixel. Let’s assume we want to give the image a tint of image hsl(335, 100%, 50%). We need to increase saturation quite a bit, so we will use a parameter of 4. The exact value depends on your case, and we generally have to eyeball it. As Figure 4.11 demonstrates, this combined filter gives our image a warm golden tint.

As nice as our image now looks, we didn’t want to colorize it with this orangish yellow, but with a deep, bright pink. Therefore, we also need to apply a hue-rotate() filter, to offset the hue of every pixel by the degrees we specify. To make the hue 335 from around 40, we’d need to add around 295 (335 - 40) to it:

image

FIGURE 4.10 Top: Original image Bottom: Image after sepia() filter

filter: sepia() saturate(4) hue-rotate(295deg);

At this point, we’ve colorized our image and you can check out how it looks in Figure 4.12. If it’s an effect that gets toggled on :hover or other states, we could even apply CSS transitions to it:

image

FIGURE 4.11 Our image after adding a saturate() filter

img {
    transition: .5s filter;
    filter: sepia() saturate(4) hue-rotate(295deg);
}

img:hover,
img:focus {
    filter: none;
}
image

FIGURE 4.12 Our image after adding a hue-rotate() filter as well

image PLAY! play.csssecrets.io/color-tint-filter

Blending mode solution

image

The filter solution works, but you might have noticed that the result is not exactly the same as what can be obtained with an image editor. Even though we were trying to colorize with a very bright color, the result still looks rather washed out. If we try to increase the parameter in the saturate() filter, we start getting a different, overly stylized effect. Thankfully, there is a better way to approach this: blending modes!

If you’ve ever used an image editor such as Adobe Photoshop, you are probably already familiar with blending modes. When two elements overlap, blending modes control how the colors of the topmost element blend with the colors of whatever is underneath it. When it comes to colorizing images, the blending mode you need is luminosity. The luminosity blending mode maintains the HSL lightness of the topmost element, while adopting the hue and saturation of its backdrop. If the backdrop is our color and the element with the blending mode applied to it is our image, isn’t this essentially what color tinting is supposed to do?

image

FIGURE 4.13 Comparison of the filter method (top) and the blending mode method (bottom)

To apply a blending mode to an element, there are two properties available to us: mix-blend-mode for applying blending modes to entire elements and background-blend-mode for applying blending modes to each background layer separately. This means that to use this method on an image we have two options, neither of them ideal:

  • Wrapping our image in a container with a background color of the color we want

  • Using a <div> instead of an image, with its background-image set to the image we want to colorize and a second background layer underneath with our color

Depending on the specific use case, we can choose either of the two. For example, if we wanted to apply the effect to an <img> element, we would need to wrap it in another element. However, if we already have another element, such as an <a>, we can use that:

HTML

<a href="#something">
    <img src="tiger.jpg" alt="Rawrrr!" />
</a>

Then, you only need two declarations to apply the effect:

a {
    background: hsl(335, 100%, 50%);
}

img {
    mix-blend-mode: luminosity;
}

Just like CSS filters, blending modes degrade gracefully: if they are not supported, no effect is applied but the image is still perfectly visible.

An important consideration is that while filters are animatable, blending modes are not. We already saw how you can animate the picture slowly fading into monochrome with a simple CSS transition on the filter property, but you cannot do the same with blending modes. However, do not fret, as this does not mean animations are out of the question, it just means we need to think outside the box.

As already explained, mix-blend-mode blends the whole element with whatever is underneath it. Therefore, if we apply the luminosity blending mode through this property, the image is always going to be blended with something. However, using the background-blend-mode property blends each background image layer with the ones underneath it, unaware of anything outside the element. What happens then when we only have one background image and a transparent background color? You guessed it: no blending takes place!

We can take advantage of that observation and use the background-blend-mode property for our effect. The HTML will have to be a little different:

HTML

<div class="tinted-image"
     style="background-image:url(tiger.jpg)">
</div>

Then we only need to apply CSS to that one <div>, as this technique does not require any extra elements:

.tinted-image {
    width: 640px; height: 440px;
    background-size: cover;
    background-color: hsl(335, 100%, 50%);
    background-blend-mode: luminosity;
    transition: .5s background-color;
}

.tinted-image:hover {
    background-color: transparent;
}

However, as mentioned previously, neither of the two techniques are ideal. The main issues at play here are:

  • The dimensions of the image need to be hardcoded in the CSS code.

  • Semantically, this is not an image and will not be read as such by screen readers.

Like most things in life, there is no perfect way to do this, but in this section we’ve seen three different ways to apply this effect, each with its own pros and cons. The one you choose depends on the specific needs of your project.

image PLAY! play.csssecrets.io/color-tint

image

Hat tip to Dudley Storey (demosthenes.info) for coming up with the animating trick for blending modes (demosthenes.info/blog/888/Create-Monochromatic-Color-Tinted-Images-With-CSS-blend).

RELATED
SPECS

18 Frosted glass effect

The problem

We are using the term “backdrop” here to mean the part of the page that is underneath an element, which shows through its semi-transparent background.

One of the first use cases of semi-transparent colors was using them as backgrounds, over photographic or otherwise busy backdrops, to decrease contrast and make the text possible to read. The result is quite impressive, but can still be hard to read, especially with very low opacity colors and/or busy backdrops. For example, take a look at Figure 4.14, where the main element has a semi-transparent white background. The markup looks like this:

HTML

<main>
    <blockquote>
        "The only way to get rid of a temptation[…]"
        <footer>
            <cite>
                Oscar Wilde,
                The Picture of Dorian Gray
            </cite>
        </footer>
    </blockquote>
</main>

And the CSS looks like this (with all irrelevant bits omitted for brevity):

body {
    background: url("tiger.jpg") 0 / cover fixed;
}

main {
    background: hsla(0,0%,100%,.3);
}

As you can observe, the text is really hard to read, due to the image behind it being busy and the background color only being 25% opaque. We could of course improve readability by increasing the alpha parameter of the background color, but then the effect will not be as interesting (see Figure 4.15).

image

FIGURE 4.14 Our semi-transparent white background makes the text hard to read

image

FIGURE 4.15 Increasing the alpha value of our background color does fix the readability issue, but also makes our design less interesting

In traditional print design, this issue is often addressed by blurring the part of the photo that is underneath our text container. Blurred backgrounds are not as busy, and thus, text on them is easier to read. Because blurring is computationally expensive, in the past its toll on resources was prohibitive for using this technique in websites and UI design. However, with GPUs improving and hardware acceleration becoming more commonplace for more and more things, these days it’s used quite frequently. In the past few years, we have seen this technique in newer versions of both Microsoft Windows, as well as Apple iOS and Mac OS X (Figure 4.16).

image

FIGURE 4.16 Translucent UIs with a blurred backdrop have been becoming increasingly common in the past few years, as the toll of blurring on resources has stopped being prohibitively expensive (Apple iOS 8.1 is shown on the left and Apple OS X Yosemite is shown on the right)

We also got the ability to blur elements in CSS, via the blur() filter, which is essentially a hardware-accelerated version of the corresponding SVG blur filter primitive that we always had for SVG elements. However, if we directly apply a blur() filter to our example, the entire element is blurred, which makes it even less readable (Figure 4.17). Is there any way to just apply it to the element’s backdrop (i.e., the part of the background that is behind our element)?

image

FIGURE 4.17 Applying a blur() filter to the element itself makes things worse

The solution

Provided that our element has a background-attachment of fixed, this is possible, albeit a bit tricky. Because we cannot apply the blurring to our element itself, we will apply it to a pseudo-element that is positioned behind the element and whose background seamlessly matches the one on <body>.

It’s also possible even with non-fixed backgrounds, just messier.

First, we add the pseudo-element and position it absolutely, with all offsets being 0, so that it covers the entire <main> element:

main {
    position: relative;
    /* [Rest of styling] */

}

main::before {
    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    background: rgba(255,0,0,.5); /* for debugging */
}

We also applied a semi-transparent imagered background, so we can see what we’re doing, otherwise debugging becomes difficult when we’re dealing with a transparent (and therefore, invisible) element. As you can see in Figure 4.18, our pseudo-element is currently above our content, thus obscuring it. We can fix this by adding z-index: -1; (Figure 4.20).

image Be careful when using a negative z-index to move a child underneath its parent: if said parent is nested within other elements with backgrounds, the child will go below those as well.

Now it’s time to replace that semi-transparent red background, with one that actually matches our backdrop, either by copying over the <body> background, or by splitting it into its own rule. Can we blur now? Let’s try it:

Why not just use background: inherit on main::before? Because then it will inherit from main, not body, so the pseudo-element will get a semi-transparent white background as well.

body, main::before {
    background: url("tiger.jpg") 0 / cover fixed;
}

main {
    position: relative;
    background: hsla(0,0%,100%,.3);
}

main::before {
    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    filter: blur(20px);
}
image

FIGURE 4.18 The pseudo-element is currently obscuring the text

image

FIGURE 4.19 We fixed the faded blurring at the edges, but now there is some blurring outside our element too

As you can see in Figure 4.21, we’re pretty much there. The blurring effect looks perfect toward the middle, but is less blurred closer to the edges. This happens because blurring reduces the area that is covered by a solid color by the blur radius. Applying a imagered background to our pseudo-element helps clarify what’s going on (Figure 4.22).

image

FIGURE 4.20 Moving the pseudo-element behind its parent, with z-index: -1;

To circumvent this issue, we will make the pseudo-element at least 20px (as much as our blur radius) larger than the dimensions of its container, by applying a margin of -20px or less to be on the safe side, as different browsers might use different blurring algorithms. As Figure 4.19 demonstrates, this fixes the issue with the faded blurring at the edges, but now there is also some blurring outside our container, which makes it look like a smudge instead of frosted glass. Thankfully, this is also easy to fix: we will just apply overflow: hidden; to main, in order to clip that extraneous blurring. The final code looks as follows, and its result can be seen in Figure 4.23:

body, main::before {
    background: url("tiger.jpg") 0 / cover fixed;
}

main {
    position: relative;
    background: hsla(0,0%,100%,.3);
    overflow: hidden;
}

main::before {

    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    filter: blur(20px);
    margin: -30px;
}
image

FIGURE 4.21 Blurring our pseudo-element almost works, but its less blurry on the edges, diminishing the frosted glass illusion

image

FIGURE 4.22 Adding a imagered background helps make sense of what’s happening

image

FIGURE 4.23 Our final result

Note how much more readable our page has now become, and how much more elegant it looks. It’s debatable whether the fallback for this effect constitutes graceful degradation. If filters are not supported, we will get the result we saw in the beginning (Figure 4.14). We can make our fallback a bit more readable by increasing the opacity of the background color.

image PLAY! play.csssecrets.io/frosted-glass

RELATED
SPECS

19 Folded corner effect

The problem

Styling one corner (usually the top-right or bottom-right one) of an element in a way that makes it look folded, with various degrees of realism, has been a very popular decoration for years now.

These days, there are several helpful pure CSS solutions, the first of which was published as easly as 2010 by the pseudo-element master, Nicolas Gallagher (nicolasgallagher.com/pure-css-folded-corner-effect). Their main premise is usually adding two triangles on the top-left corner: one for the page flip and a white one, to obscure the corner of the main element. These triangles are usually created with the old border trick.

image

FIGURE 4.24 Several earlier redesigns of css-tricks.com featured folded corners, on the top-right corner of every article box

Impressive as these solutions were for their time, today they are very limiting and fall short in several cases:

  • When the background behind our element is not a solid color, but a pattern, a texture, a photo, a gradient, or any other kind of background image

  • When we want a different angle than 45° and/or a rotated fold

Is there a way to create a more flexible folded corner effect with CSS that doesn’t fail on these cases?

The 45° solution

We will start from an element with a beveled top-right corner, which is created with the gradient-based solution in the Cutout corners” secret on page 96. To create a top-right bevel corner of size 1em with this technique, the code looks like this and the sample rendering can be seen in Figure 4.25:

image

FIGURE 4.25 Our starting point: an element with a top-right cutout corner, done via a gradient

background: #58a; /* Fallback */
background:
    linear-gradient(-135deg, transparent 2em, #58a 0);

At this point, we’re already halfway done: all we need to do is to add a darker triangle for the page flip. We will do that by adding another gradient to create the triangle, which we will resize to our needs with background-size and position on the top-right corner.

To create the triangle, all we need is an angled linear gradient with two stops that meet in the middle:

background:
    linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        no-repeat 100% 0 / 2em 2em;

You can see the result of having only this background in Figure 4.26. The last step would be to combine them, and we’ll be done, right? Let’s try that, making sure that the page flip triangle is above our cutout corner gradient:

image

FIGURE 4.26 Our second gradient for the folded triangle, isolated; the text is shown here as faint gray instead of white, so you can see where it is

background: #58a; /* Fallback */
background:
    linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        no-repeat 100% 0 / 2em 2em,
    linear-gradient(-135deg, transparent 2em, #58a 0);

As you can see in Figure 4.27, the result is not exactly what we expected. Why don’t the sizes match? They’re both 2em!

The reason is that (as we’ve discussed in the Cutout corners” secret on page 96) the 2em corner size in our second gradient is in the color stop, and thus is measured along the gradient line, which is diagonal. On the other hand, the 2em length in background-size is the width and height of the background tile, which is measured horizontally and vertically.

image

FIGURE 4.27 Combining the two gradients doesn’t produce exactly the expected result

To make the two align, we need to do one of the following, depending on which of the two sizes we want to keep:

  • To keep the diagonal 2em size, we can multiply the background-size with image.

  • To keep the horizontal and vertical 2em size, we can divide the color stop position of our cutout corner gradient by image.

Because the background-size is repeated twice, and most other CSS measurements are not measured diagonally, going with the latter is usually preferable. The color stop position will become image, which we will round up to 1.5em:

background: #58a; /* Fallback */
background:
    linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        no-repeat 100% 0 / 2em 2em,
    linear-gradient(-135deg,
        transparent 1.5em, #58a 0);

As you can see in Figure 4.28, this finally gives us a nice, flexible, minimalistic rounded corner.

image

FIGURE 4.28 After changing the color stop position of the blue gradient, our folded corner finally works

image Make sure to have at least as much padding as the corner size, otherwise the text will overlap the corner (because it’s just a background), spoiling the folded corner illusion.

image PLAY! play.csssecrets.io/folded-corner

Solution for other angles

Folded corners in real life are rarely exactly 45°. If we want something a tad more realistic, we can use a slightly different angle, for example -150deg for a 30° one. If we just change the angle of the beveled corner, however, the triangle representing the flipped part of the page will not adjust, resulting in breakage that looks like Figure 4.29. However, adjusting its dimensions is not straightforward. The size of that triangle is not defined by an angle, but by its width and height. How can we find what width and height we need? Well, it’s time for some—gasp—trigonometry!

image

FIGURE 4.29 Changing the angle of our cutout corner causes this breakage

The code currently looks like this:

background: #58a; /* Fallback */
background:
    linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        no-repeat 100% 0/2em 2em,
    linear-gradient(-150deg,
        transparent 1.5em, #58a 0);

A 30-60-90 right triangle is a right triangle whose other two angles are 30° and 60°.

As you can see in Figure 4.30, we basically need to calculate the length of the hypotenuse from two 30-60-90 right triangles when we know the length of one of their legs. As the trigonometric circle shown in Figure 4.31 reminds us, if we know the angles and the length of one of a right triangle’s sides, we can calculate the length of its other two sides by using sines, cosines, and the Pythagorean theorem. We know from math (or a calculator) that cos image and image. We also know from the trigonometric circle that in our case, sin image and image.

Therefore:

image
image

FIGURE 4.30 Our cutout corner, enlarged (the gray marked angles are 30°)

image

FIGURE 4.31 Sines and cosines help us calculate the legs of right triangles based on their angle and hypotenuse

At this point, we can also calculate z, via the Pythagorean theorem:

image

We can now resize the triangle to match:

background: #58a; /* Fallback */
background:
    linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        no-repeat 100% 0 / 3em 1.73em,
    linear-gradient(-150deg,
        transparent 1.5em, #58a 0);

At this point, our corner looks like Figure 4.32. As you can see, the triangle now does match our cutout corner, but the result looks even less realistic! Although we might not be able to easily figure out why, our eyes have seen many folded corners before and instantly know that this grossly deviates from the pattern they are used to. You can help your conscious mind understand why it looks so fake by trying to fold an actual sheet of paper in approximately this angle. There is literally no way to fold it and make it look even vaguely like Figure 4.32.

image

FIGURE 4.32 Although we did achieve the result we wanted, it turns out that it looks even less realistic than before

As you can see in an actual, real-life folded corner, such as the one in Figure 4.33, the triangle we need to create is slightly rotated and has the same dimensions as the triangle we “cut” from our element’s corner. Because we cannot rotate backgrounds, it’s time to move the effect to a pseudo-element:

.note {
    position: relative;
    background: #58a; /* Fallback */
    background:
        linear-gradient(-150deg,
            transparent 1.5em, #58a 0);

}
.note::before {
    content: '';
    position: absolute;
    top: 0; right: 0;
    background: linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        100% 0 no-repeat;
    width: 3em;
    height: 1.73em;
}
image

FIGURE 4.33 An analog version of the folded corner effect (fancy sheet of paper courtesy of Leonie and Phoebe Verou)

At this point, we’ve just replicated the same effect as in Figure 4.32 with pseudo-elements. Our next step would be to change the orientation of the existing triangle by swapping its width and height to make it mirror the cutout corner instead of complementing it. Then, we will rotate it by 30° ((90° – 30°) – 30°) counterclockwise, so that its hypotenuse becomes parallel to our cutout corner:

.note::before {
    content: '';
    position: absolute;
    top: 0; right: 0;
    background: linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.4) 0)
        100% 0 no-repeat;
    width: 1.73em;
    height: 3em;
    transform: rotate(-30deg);
}

You can see how our note looks after these changes in Figure 4.34. As you can see, we’re basically there and we just need to move the triangle so that the hypotenuses of our two triangles (the dark one and the cutout one) coincide. As things currently stand, we need to move the triangle both horizontally and vertically, so it’s more difficult to figure out what to do. We can make things easier for ourselves by setting transform-origin to bottom right, so that the bottom-right corner of the triangle becomes the center of rotation, and thus, stays fixed in the same place:

image

FIGURE 4.34 We’re starting to get there, but we need to move the triangle

.note::before {
    /* [Rest of styling] */
    transform: rotate(-30deg);
    transform-origin: bottom right;
}

As you can see in Figure 4.35, we now only need to move our triangle vertically toward the top. To find the exact amount, we can use some geometry again. As you can see in Figure 4.36, the vertical offset our triangle needs is image, which we can round up to 1.3em:

image

FIGURE 4.35 Adding transform-origin: bottom right; makes things easier: now we only need to move our triangle vertically

.note::before {
    /* [Rest of styling] */
    transform: translateY(-1.3em) rotate(-30deg);
    transform-origin: bottom right;
}

The sample rendering in Figure 4.37 confirms that this finally gives us the effect we were going for. Phew, that was intense! In addition, now that our triangle is generated via pseudo-elements, we can make it even more realistic, by adding rounded corners, (actual) gradients, and box-shadows! The final code looks as follows:

image

FIGURE 4.36 Figuring out how much to move our triangle isn’t as difficult as it first looks

image Make sure to put the translateY() transform before the rotation, otherwise our triangle will move along its 30° angle, as every transformation also transforms the entire coordinate system of the element, not just the element per se!

.note {
    position: relative;
    background: #58a; /* Fallback */
    background:
        linear-gradient(-150deg,
            transparent 1.5em, #58a 0);
    border-radius: .5em;
}
.note::before {
    content: '';
    position: absolute;
    top: 0; right: 0;
    background: linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.2) 0, rgba(0,0,0,.4))
        100% 0 no-repeat;
    width: 1.73em;
    height: 3em;
    transform: translateY(-1.3em) rotate(-30deg);
    transform-origin: bottom right;
    border-bottom-left-radius: inherit;
    box-shadow: -.2em .2em .3em -.1em rgba(0,0,0,.15);
}
image

FIGURE 4.37 Our triangles are finally aligned and touching

And you can admire the fruits of our labor in Figure 4.38.

image

FIGURE 4.38 With a few more effects, our folded corner comes to life

image PLAY! play.csssecrets.io/folded-corner-realistic

The effect looks nice, but how DRY is it? Let’s think about some common edits and variations one might want to make:

  • It only takes one edit to change the element dimensions and other metrics (padding, etc.).

  • It only takes two edits (one without the fallback) to change the background color.

  • It takes four edits and several nontrivial calculations to change the folded corner size.

  • It takes five edits and several even less trivial calculations to change the folded corner angle.

The last two are really bad. It might be time for a preprocessor mixin:

SCSS

@mixin folded-corner($background, $size,
                     $angle: 30deg) {
position: relative;
background: $background; /* Fallback */
background:
    linear-gradient($angle - 180deg,
        transparent $size, $background 0);
border-radius: .5em;

$x: $size / sin($angle);
$y: $size / cos($angle);

&::before {
    content: '';
    position: absolute;
    top: 0; right: 0;
    background: linear-gradient(to left bottom,
        transparent 50%, rgba(0,0,0,.2) 0,
        rgba(0,0,0,.4)) 100% 0 no-repeat;
    width: $y; height: $x;
    transform: translateY($y - $x)
               rotate(2*$angle - 90deg);
    transform-origin: bottom right;
    border-bottom-left-radius: inherit;
    box-shadow: -.2em .2em .3em -.1em rgba(0,0,0,.2);
}
}

/* used as... */
.note {
    @include folded-corner(#58a, 2em, 40deg);
}

image At the time of writing, SCSS does not support trigonometric functions natively. To enable support, you could use the Compass framework (compass-style.org), among other libraries. You could even write them yourself, using the Taylor expansions of the functions! LESS, on the other hand, includes them out of the box.

image PLAY! play.csssecrets.io/folded-corner-mixin

RELATED
SPECS

Get CSS Secrets now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.