company-logo

jQuery fade-in spoiler revealer: The failsafe, progressively enhanced version

Io9_morning_spoilers
***Spoiler Alert***

Don’t read too much further if you haven’t seen “Citizen Kane” and want to be surprised when you do.

I’m a big SF nerd and lover of teen-angst dramas. (That’s why “Buffy the Vampire Slayer” is the Best Show Ever.) I love spoilers, but only when I’ve asked for them. When watching the entire run of “Dawson’s Creek” on DVD years after it aired (don’t ask), I accidentally spoiled Matt Laffey, the buddy who’d turned me on to the series, on how the big romantic triangle ended up in the final episode. I had no clue he had been saving Season 5 for a rainy day.

Since then, I have endeavored to include a ***SPOILERS*** alert in the subject line of any surprise-detroying emails – and to make liberal use of the return key to make sure the contents of such messages are below the fold in email clients with preview panes. This has made me a much more responsible citizen of fandom – though it hasn’t lessened my outrage when entertainment websites post spoilers in their headlines or intro paragraphs without warning me and my fellow geeks. (Sci fi blog io9, by the way, is pictured to illustrate the right way to do things.)

Hats off, then, to Chris Coyier of tutorial site CSS-Tricks for his recent post on using jQuery to create a Fade-in Spoiler Revealer for use on websites. My only reservation about Coyier’s technique was its reliance on JavaScript, and only JavaScript, to hide spoiler-laden content. With RSS and mobile browsing on the rise, lots of people read content in user-agents without JavaScript support. Shouldn’t we try to protect them, too? I commented to this effect on the original article, then realized that I should just write the code myself as proof of concept.

The original version

Just for review, here is Coyier’s original implementation:

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript">
	$(document).ready(function() {
		$("span.spoiler").hide();

		$('<a class="reveal">Reveal
			Spoiler >></a> ').insertBefore('.spoiler')
		;

		$("a.reveal").click(function(){
			$(this).parents("p")
			.children("span.spoiler").fadeIn(2500);
			$(this).parents("p")
			.children("a.reveal").fadeOut(600);
		});
	});
</script>

<p>
In the movie Citizen Kane, Charles Foster Kane's
last word "Rosebud" turns out to
<span class="spoiler">be a sled.</span>
</p>

My first pass

My basic concept was to use old-fashioned methods in my actual markup to hide spoiled content. Web pages typically hide spoiler content by one of the following methods:

  • Rendering it in the same color as the background, thereby forcing users to select it with their cursor to render it visible.
  • Placing it after a warning message and a ridiculous number of line breaks, thereby forcing users to scroll down to read it.

The former isn’t really safe; it renders the content dangerous for user-agents without CSS support. I therefore opted for the line-break route. The HTML looked like this:

<p class="spoiler">
	In the movie "Citizen Kane," Charles Foster Kane's last
	word, "Rosebud," turns out to be ...
	<span class="message">
		(<a href="#answer">Scroll down if you'd like to be spoiled.</a>)
	</span>
	<span class="hidden">
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<br /><br /><br /><br /><br /><br /><br />
		<a name="answer"></a>a sled.
	</span>
</p>

After creating this old-fashioned spoiler-hiding content, my plan was to progressively enhance it with jQuery so JavaScript-capable users could enjoy the slick experience Coyier dreamed up in his original post.

My first pass got the job done, but not very elegantly. I had to save off a reference to the root DOM node of the spoiler content and use three separate statements to achieve my effect. Still, it offered a few advantages over the original demo.

For one thing, Coyier was using chained fadeOut and fadeIn calls to hide the “click here” message and show the spoiler content. This creates timing issues because the effects execute in parallel; to see a smooth replacement of once piece of content with the other, you have to execute the fadeIn over a much longer timeframe (2500ms) than the fadeOut (600ms).

All effects methods in jQuery allow you to assign a callback function. By placing subsequent effects in the callback functions of previous effects, you can cause them to execute consecutively rather than simultaneously. This refinement sped up and beautified the visual effects.

The resulting code looked like this:

<script type="text/javascript">
$(document).ready(function() {
	//save a reference to the root element
	var spoiler = $(".spoiler");
	spoiler
		//hide the spoiler
		.children("span.hidden").hide()
		//hide the whitespace inside it
		.children("br").hide()
	;

	spoiler.children("span.message")
		//replace the scroll message with a click message
		.html('(<a href="#">Click if you'd like to be spoiled.</a>)')
		//add the click handler to show the spoiler
		.click(function() {
			//use a callback so FX execute non-simultaneously
			$(this).fadeOut(600, function() {
				$(this).next().fadeIn(600);
			})
		})
	;
});
</script>

Try No. 2

My second pass cleaned things up a little bit, obviating the need to hold onto variable references for individual DOM nodes. It’s almost a game with jQuery to see how long you can keep an individual method chain going without typing a semicolon and beginning a new statement. Luckily, with methods such as end, it’s not hard to write a complex routine in a single statement.

No changes were required in the markup itself, but the revised JavaScript looked like this:

<script type="text/javascript">
$(document).ready(function() {

	$(".spoiler")
		//hide the spoiler
		.children("span.hidden").hide()
		//hide the whitespace inside it
		.children("br").hide()
		//step back to the root element
		.end().end()
		//find the message node
		.children("span.message")
		//replace the scroll message with a click message
		.html('(<a href="#">Click if you'd like to be spoiled.</a>)')
		//add the click handler to show the spoiler
		.click(function() {
			//use a callback so FX execute non-simultaneously
			$(this).fadeOut(600, function() {
				$(this).next().fadeIn(600);
			})
		})
	;

});
</script>

The final version

For my final pass, I realized that there was no need to make my JavaScript-incapable users scroll to see the spoiler info. By adding an anchor next to the spoiler info, I could turn my “please scroll” message into a “please click” link that would make sense for both script-capable and script-disabled users. I also realized that I wasn’t being very efficient with my DOM traversal. Why step back to the grandparent to find one of its other children when you can just step up to the parent and find a sibling?

Voila: version 3:

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function() {
	$("span.spoiler").hide();
	$('<a class="reveal">Reveal Spoiler >></a> ').insertBefore(’.spoiler’);
	$("a.reveal").click(function(){
		$(this).parents("p")
		.children("span.spoiler").fadeIn(2500);
		$(this).parents("p")
		.children("a.reveal").fadeOut(600);
	});
});
</script>

<p>
	In the movie Citizen Kane, Charles Foster Kane's last word "Rosebud" turns out to
	<span class="spoiler">be a sled.</span>
</p>

I have yet to post a demo like this without insightful comments from more experienced jQuery authors who can further refine my code. In the mean time, a tip of the hat to Chris Coyier for the excellent original post.

You can see my demo code in action over at Pathfinder Labs. Just disable JavaScript and reload to simulate the experience of a scriptless mobile or RSS user.

  1. Chris Coyier Reply

    Hi Brian,

    Nice thoughtful approach, good work =) Kinda ugly seeing all those br tags in there, but if you are trying to be as truly careful with not revealing the spoiler as possible (preparing for both No CSS and No Javascript), they are needed right in the markup itself.

    Quick fix if you don’t mind… My last name is actually “Coyier” and the site is “CSS-Tricks” =)

  2. Brian Dillard Reply

    @Chris: Yeah, progressive enhancement is ugly, but it works! Sorry about the misspellings, now corrected. My eyes aren’t what they could be.

  3. Anonymous Reply

    nice post with actual comments in the JS, something many sites’ examples lack, thanks

  4. Jones Reply

    A little nitpick: rather than slapping an IMO ugly anchor element (ie [spoiler]), just slap an id to a span (or any other relevant tag) with the spoiler itself (eg [spoiler].

  5. Brian Dillard Reply

    @Jones: I know strict semantics heads prefer this solution, but isn’t mobile browser support still lagging for links that point to IDs instead of anchors?

    This page is a few years old, but the caveat on this point within it has led me to avoid this approach to date:

    http://bitesizestandards.com/bites/cleaning-up-code-with-semantic-anchors

Leave a Reply

*

captcha *