How can I implement a spoiler quote with just CSS?

Here's something very similar to what I use in SOUP:

.spoiler, .spoiler > * { transition: color 0.5s, opacity 0.5s }
.spoiler:not(:hover) { color: transparent }
.spoiler:not(:hover) > * { opacity: 0 }
/* fix weird transitions on Chrome: */
blockquote, blockquote > *:not(a) { color: black }

.spoiler, .spoiler > * { transition: color 0.5s, opacity 0.5s }
.spoiler:not(:hover) { color: transparent }
.spoiler:not(:hover) > * { opacity: 0 }
/* fix weird transitions on Chrome: */
blockquote, blockquote > *:not(a) { color: black }

/* some basic bg styles for demonstration purposes */
blockquote { background: #fed; margin: 1em 0; padding: 8px; border-left: 2px solid #cba }
code { background: #ccc; padding: 2px }
img { vertical-align: middle }
<blockquote class="spoiler">
  Soopah sekkrit text with <code>code</code> and <a href="#">links</a> and <img src="//sstatic.net/stackexchange/img/logos/so/so-logo-med.png" width="100" /> images!
  <p>You can also have paragraphs in here.</p>
  <ul><li>And lists too!</li></ul>
  <blockquote class="spoiler">Even nested spoilers work!</blockquote>
</blockquote>

This is somewhat simpler than your own solution, and works for arbitrary content including images and even nested spoilers! (See demo snippet above.)

Alas, this method seems to suffer from weird transition effects on Chrome if any of the child elements of the spoiler have color: inherit. (Basically, what's happening is that these elements will have both their text color set to transparent and their opacity set to 0. Because opacities combine multiplicatively, the combined transition will thus appear slower — halfway through the fade-in, when the element itself is at 50% opacity, the text in it is at 50% × 50% = 25% opacity.) I've added an extra CSS rule to the example above to fix this, but it does make things a bit complicated.


What I actually do in SOUP is slightly different. I wrap the contents of each spoiler in an extra inner <div>, which lets me simplify the CSS further to just:

.spoiler > div { opacity: 0; transition: opacity 0.5s }
.spoiler:hover > div { opacity: 1 }

.spoiler > div { opacity: 0; transition: opacity 0.5s }
.spoiler:hover > div { opacity: 1 }

/* some basic bg styles for demonstration purposes */
blockquote { background: #fed; margin: 1em 0; padding: 8px; border-left: 2px solid #cba }
code { background: #ccc; padding: 2px }
img { vertical-align: middle }
<blockquote class="spoiler"><div>
  Soopah sekkrit text with <code>code</code> and <a href="#">links</a> and <img src="//sstatic.net/stackexchange/img/logos/so/so-logo-med.png" width="100" /> images!
  <p>You can also have paragraphs in here.</p>
  <ul><li>And lists too!</li></ul>
  <blockquote class="spoiler"><div>Even nested spoilers work!</div></blockquote>
<div></blockquote>

The main advantages of this method are simplicity and robustness: I don't have to use :not() selectors, improving compatibility with older browsers, and the transition styles can't conflict with other transitions possibly defined on the elements inside the spoiler. This method also doesn't suffer from the color transition weirdness on Chrome described above, since it only uses opacity transitions.

Overall, this is the method I recommend. The disadvantage, of course, is that you need to include the extra <div>s in your HTML.


Ps. Please consider also providing some way to make the spoilers permanently visible, especially for touch screen users who may find it very hard to "hover" the cursor over an element. A simple solution is to use a JavaScript click event handler to toggle the spoiler class, e.g. like this (using jQuery):

$('.spoiler').on( 'click', function (e) {
  $(this).toggleClass('spoiler');
  e.stopPropagation();
} );

$('.spoiler').on( 'click', function (e) {
  $(this).toggleClass('spoiler');
  e.stopPropagation();
} );
.spoiler > div { opacity: 0; transition: opacity 0.5s }
.spoiler:hover > div { opacity: 1 }

/* some basic bg styles for demonstration purposes */
blockquote { background: #fed; margin: 1em 0; padding: 8px; border-left: 2px solid #cba }
code { background: #ccc; padding: 2px }
img { vertical-align: middle }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<blockquote class="spoiler"><div>
  Soopah sekkrit text with <code>code</code> and <a href="#">links</a> and <img src="//sstatic.net/stackexchange/img/logos/so/so-logo-med.png" width="100" /> images!
  <p>You can also have paragraphs in here.</p>
  <ul><li>And lists too!</li></ul>
  <blockquote class="spoiler"><div>Even <a href="//example.com">nested</a> spoilers work!</div></blockquote>
<div></blockquote>

or, if you'd prefer to use delegated event handling (so that you don't have to keep adding new click handlers every time you load new content that includes spoilers via Ajax):

$(document).on( 'click', '.spoiler, .spoiler-off', function (e) {
  $(this).toggleClass('spoiler').toggleClass('spoiler-off');
  e.stopPropagation();
} );

$(document).on( 'click', '.spoiler, .spoiler-off', function (e) {
  $(this).toggleClass('spoiler').toggleClass('spoiler-off');
  e.stopPropagation();
} );
.spoiler > div { opacity: 0; transition: opacity 0.5s }
.spoiler:hover > div { opacity: 1 }

/* some basic bg styles for demonstration purposes */
blockquote { background: #fed; margin: 1em 0; padding: 8px; border-left: 2px solid #cba }
code { background: #ccc; padding: 2px }
img { vertical-align: middle }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<blockquote class="spoiler"><div>
  Soopah sekkrit text with <code>code</code> and <a href="#">links</a> and <img src="//sstatic.net/stackexchange/img/logos/so/so-logo-med.png" width="100" /> images!
  <p>You can also have paragraphs in here.</p>
  <ul><li>And lists too!</li></ul>
  <blockquote class="spoiler"><div>Even <a href="//example.com">nested</a> spoilers work!</div></blockquote>
<div></blockquote>

(These should work with either of the CSS variants shown above.)


Yes, this is possible with CSS. Essentially, you want to make all of the contents be invisible. In CSS, this means transparent.

First use the hover pseudo-class inside the not pseudo-class:

.spoiler:not(:hover)

But we also need to select all the child elements of the hovered spoiler, to set their colors and backgrounds:

.spoiler:not(:hover) *

And we set both the color and background (only for the child elements) to transparent to make them invisible to the user. All together:

.spoiler:not(:hover), .spoiler:not(:hover) * { color: transparent }
.spoiler:not(:hover) * { background: transparent }

code { padding: 2px; background: #bbb }
a { color: #00f }
Hover: <blockquote class="spoiler">Some stuff <a>and a colored link</a> <code>and some code!</code></blockquote>

We can also add a transition to make it smoother:

.spoiler { transition: color 0.5s } /* we have to put this outside the :hover to make it work fading both in and out */
.spoiler:not(:hover), .spoiler:not(:hover) * { color: transparent }
.spoiler * { transition: color 0.5s, background 0.5s }
.spoiler:not(:hover) * { background: transparent }

code { padding: 2px; background: #bbb; color: #000 } /* add color to prevent double transition */
a { color: #00f }
Hover: <blockquote class="spoiler">Some stuff <a>and a colored link</a> <code>and some code!</code></blockquote>

To make it obvious to the user that the blockquote is hoverable, you can add some text with the ::after pseudo-element to be shown when the blockquote isn't hovered:

.spoiler { transition: color 0.5s; position: relative } /* relative position for positioning the pseudo-element */
.spoiler:not(:hover), .spoiler:not(:hover) * { color: transparent }
.spoiler * { transition: color 0.5s, background 0.5s }
.spoiler:not(:hover) * { background: transparent }
.spoiler::after {
    content: 'hover to view spoiler';
    position: absolute;
    top: 0; left: 0;
    color: transparent;
}
.spoiler:not(:hover)::after {
    color: #666;
    transition: color 0.3s 0.3s; /* delayed transition to keep the text from overlapping */
}

code { padding: 2px; background: #bbb; color: #000 }
a { color: #00f }
<blockquote class="spoiler">
    Some stuff <a>and a colored link</a> <code>and some code!</code>
    <blockquote class="spoiler">Nesting bonus!</blockquote>
</blockquote>

For stuff like images, svgs (tho inline SVG can be very granularly controlled), canvases, and all that fancy stuff, instead of color you'd have to use opacity. We can make it work with these by adding this:

.spoiler img { transition: opacity 0.5s, background 0.5s }
.spoiler:not(:hover) img { opacity: 0 }

Tags:

Html

Css

Hover