It’s a common need in web apps: you click something and the text of the thing you just clicked changes. Perhaps something simple like a “Show” button that swaps to “Hide”, or “Expand Description” to “Collapse Description.” This is a fairly simple thing to do, but there are various considerations to make. Let’s cover a bunch of ways.
jQuery Way (Less Markup / More JavaScript)
You need to store the “swap” text somewhere. I’d say in most cases it is a design/view concern so storing it in the markup is a good idea. We’ll use the example of a button who’s text swaps between “Hide” and “Show”. A data-* attribute is a perfectly good place to store the swap text. So that becomes:
<button data-text-swap="Show">Hide</button>
It’s easy to swap out the text, like:
var button = $("button");
button.text(button.data("text-swap"));
But, if we did that we’d lose the orignal text forever. We need to store the original text first. Another data-* attribute will do.
var button = $("button");
button.data("text-original", button.text());
button.text(button.data("text-swap"));
To do that on a click event, you’d do:
var button = $("button");
button.on("click", function() {
button.data("text-original", button.text());
button.text(button.data("text-swap"));
});
But that only goes one direction. To complete the “swap”, we’ll need to compare the current text value of the button to see if it matches the swap text or not. If it does, change it back to the original. If not, to the swap text. This is what it looks like all done:
$("button").on("click", function() {
var el = $(this);
if (el.text() == el.data("text-swap")) {
el.text(el.data("text-original"));
} else {
el.data("text-original", el.text());
el.text(el.data("text-swap"));
}
});
jQuery Way (More Markup / Less JavaScript)
If we’re willing to set that data-text-original
value in the original markup, we can simplify the JavaScript a bit. We can use a single ternary operator to check if the swap matches the orignal and perform the right action based on the truthiness.
$("button").on("click", function() {
var el = $(this);
el.text() == el.data("text-swap")
? el.text(el.data("text-original"))
: el.text(el.data("text-swap"));
});
Vanilla JavaScript Way
I’m guilty of using too much jQuery around here for things that can be done without it. This is what the first “less markup” version would look like in “raw” JavaScript:
var button = document.querySelectorAll("button")[0];
button.addEventListener('click', function() {
if (button.getAttribute("data-text-swap") == button.innerHTML) {
button.innerHTML = button.getAttribute("data-text-original");
} else {
button.setAttribute("data-text-original", button.innerHTML);
button.innerHTML = button.getAttribute("data-text-swap");
}
}, false);
CSS Way (with jQuery changing class names)
Since this is a view concern and could be considered a “state,” a popular idea is to use JavaScript only to change classes which represent states and let CSS define what the visual change actually is.
We could use the class “on” to represent the swap state. Then that class would apply a pseudo element covering the old word and replacing it with the swap word. I don’t think actual button elements with default browser styling take well to pseudo element so let’s use an anchor here.
a {
position: relative;
}
a.on:after {
content: "Hide";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
}
This is a bit funky, to be fair. I think this is almost worse that putting the swap word in the JavaScript. CSS isn’t really meant for this kind of thing and likely has some accessibility concerns.
This also happens to work because the word “Hide” is smaller than “Show” a little bit. If the swap word was bigger, the original would stick out underneath the white cover. You might be able to get around that by inline-blocking the original, hiding the overflow, and kicking the original out of the box with text-indent. But the fact that the replacement word is absolutely positioned removes it from the flow, which could be an issue, not to mention real world design isn’t always a simple as flat-color-on-flat-color.
CSS-Only Way
But hey as long as we’re getting funky, we could use The Checkbox Hack here to make the text swap entirely CSS. The replacement happens the exact same way, it just happens when an invisible checkbox right before the word is either :checked
or not. This means the word needs to be in a label as well, which is able to toggle that checkbox’s state through the for attribute.
<input id="example-checkbox" type="checkbox">
<label for="example" id="example">Show</label>
#example {
position: relative;
}
#example-checkbox {
display: none;
}
#example-checkbox:checked + #example:after {
content: "Hide";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
}
Demo of All Five Ways
Check out this Pen!
More?
How have you done this kind of thing in the past? We didn’t cover just putting the swap word right in the JavaScript… how do you feel about that?
Nice article Chris, i’m using this right now on a CodePen :-)
What about using
attr()
in css to get data from html instead of hard coding that.—
Not bad Subash, not bad.
Whenever I swap things – even single word – I use js only to switch classes:
and css:
More markup, less js but feels better:)
Yah with
display: none;
I guess it’s not even an accessibility concern really eh?This is how I prefer to do it (usually wrapped in an anchor instead of a span). This way a keyboard user can select it, and the screen reader will always read what it currently says. Change the class, and the screen reader now reads the new text when returning to the link.
The data-* attributes on elements can also be accessed via element.dataset, so you could also write your ‘raw’ JavaScript example like this:
Of course, that won’t work in IE < 11, but we can dream :(
Rather than storing both the original text and the text to swap in their own data attributes and using if/else logic to choose which one to show, you could just use a single data attribute to store whichever text is not currently being shown, and then just swap the values. Less markup required.
Peter, you confirmed what I was just now wondering. I vote for modified Way #1 then :)
Definitely better. Except I guess that it supposes that the event always swaps the text no matter what, without testing the state. But this is so abstract anyway if we’re considering “what ifs” we’d need to get way more complicated.
@Chris, right. I was only addressing the swapping out text task, since your original example didn’t include any particular conditions on swapping it. That logic could be added still, but ideally you would probably want a separate variable to identify the “state”. If you key off of the text of the button, you add complexity if you ever need to use localized text.
I used to do this with vanilla Javascript.
@Chris Well that’s no different than the other ways, and the CSS-only method, while lovely, can’t even do any better than that: on/off, that’s all.
Thank you, Peter! I was thinking the same all the time while reading the article. This should be the default way to toggle something.
Nevertheless an interesting subject. I have often wondering if there is something simpler than doing it by jquery and the answer seems still be no when i’m looking at the css solutions. There is no point of which I would say: yeah, thats much cleaner and simpler than doing it by js. :(
A bit strange in my opinion – we have so much fancy new elements like video and special input fields, but still no simple toggle element.
Here is the vanilla version :
I was thinking same solution Peter Foti suggested. Here is my fiddle.
http://jsfiddle.net/rnavaneethan/Fax7D/1/embedded/result/
It would be overkill for just this rather basic example, but since it’s part of a larger web application, chances are you should already be using one of the many client side MV* frameworks available. Regardless of if you are manually wiring it up with a view yourself in backbone, or using a framework that binds them together for you (such as Ember or Angular), it’s cleaner to store state in a model.
Just wanted to say that the first two javascripts snippets miss a closing parenthesis :
Love your work Chris. One possible shortcut for the JavaScript.
I guess he originally used getElementsByTagName and later changed it for some reason, but that’s hardly the point.
I think we can read that line as “use the method that fits better for you to get that dang button element!”
CSS second way (french touch)
input[type=checkbox].on-off{
appearance:none;
-webkit-appearance:none;
-moz-appearance:none;
}
input[type=checkbox].on-off:after{content:attr(data-unchecked);}
input[type=checkbox].on-off:checked:after{content:attr(data-checked);}
Nice, but what’s the French bit?
Also, if you feel confident to use :checked and appearance, you should also go for ::after (pseudo-element) rather than :after (pseudo-class), as the only reason we still use that syntax is IE8…
This is the best way, no need for js with all css, also no HTML code in the css.
they only thing I will add is display: none for the checkbox because appearance is not supported in all browsers.
also I would not put the input inside of the label, other than that this is great.
<label><input class=”on-off” type=”checkbox” data-checked=”oui” data-unchecked=”non” /></label>
Maybe i’m just weird but normally i set the text initially inside whatever HTML tag i’m using, then when needed use jQuery to replace the content of that text with whatever new text. Like so:
$(“p”).text(“Hide”);
and then set it back to show when necessary
Is there a reason why this isn’t a good idea?
It isn’t because you should keep the content in the DOM rather than your code. To keep things clean and separated, you know.
Your way – the traditional way – is definitely simpler, faster and uses less memory (but if you’re concerned about these things for a task like this you shouldn’t even use jQuery). On the other hand, when the project becomes larger, and you end up changing something, you may regret your decision.
Imagine you have two toggle buttons, but in the other one the words you must show are, for example, “Start” and “Stop”: you have to write the code again for essentially the same task.
Repeat this ad libitum and you’ll get the idea.
@Mike,
Reusability and separation of concerns. Let’s say you have two buttons that toggle text values, and one toggles between Show/Hide and the second between Forward/Backward. Doing it your way, you have to write two sets of JS for each, and associate the View code (i.e., the text displayed) in the code that manages state (on/off).
Using data attributes or something similar, your JS doesn’t need to know what the text is – only the state, and to toggle that on or off.
I’m trying out the last CSS only version in my Dreamweaver. But its not working[chrome,firefox]. When I did the same in codepen, its working find. What did I missed?
CSS
HTML
< input id=”example-checkbox” type=”checkbox”>
< label for=”example” id=”example”>Show < /label >
@Surjith
for attribute corrected
< label for=”example-checkbox” id=”example”>Show < /label >
Thanks Zet.
It worked.
Given in the post is also mistakken.
http://jsfiddle.net/vmJ5h/
I Like the way you used the array to do two things for you :), Smart
Can you explain me the array part of the code: [ el.text(), el.text( el.data(‘swap’) ) ][0]
The whole approach does not have any if-else statements as well as does not use a variable to store the current text value. I’m using array with length=2 for that purpose. The first element of the array is the current text value. Second element actually swaps the text value, using the new value from the data attribute. As we already stored the initial value as first element in the array, we immediately access it via [0]. And we use it to set the new value of the data attribute.
No “if”s, no variables, just 2 lines of code. Clean & simple.
Hi, Thanks for the sharing. I’m using the CSS only way in a new project I’m currently working on.
Btw I love your articles and tutos. :)
Have used jQuery, but mostly just use simplistic Vanilla…
Following can be applied to any number of elements directly.
<button onclick=”elementStateChange(this, [“Show”,”Hide”])”>Show</button>
function elementStateChange(which, display){
if (which.innerText == display[0]){
which.innerText =display[1] ;
}else{
which.innerText =display[0] ;
}
}
Show
Oops should have come out…
<script>
function elementStateChange(which, display){
if (which.innerText == display[0]){
}
</script>
<button onclick=’elementStateChange(this, [“Show”,”Hide”])’>Show</button>
Useful info chris.
@Paul
Even more compactly:
which.innerText = display[which.innerText == display[0] ? 1 : 0];
The old way (works from MS-IE4 / NN4 to current day) is:
<input type=button value=Hide onclick="this.value = this.value == 'Hide' ? 'Show' : 'Hide';">
@Chris, @Peter
State can be seen by what is listed in the button. But I would’t really recommend this as you are binding the view to state.
I’ve got a voice in the back of my head saying this is a bad idea, but another approach is to have two buttons. This HTML:
This CSS:
..and the following jQuery:
Thoughts?
Fiddle for above: http://jsfiddle.net/davidg707/JNB2F/
Regarding the CSS Way (with jQuery changing class names), you could use text-indent and not have to use a background-color and overlay the text on top like that..
http://codepen.io/anon/pen/idmeG
Why not just toggle classes? There is often some HTML involved. Data attr isn’t enough always.
I noticed something cool with CSS Generated Content in Opera (before Chromium). The
content
property actually works on real elements, not just the pseudo before and after elements. Of course, this doesn’t work in any other browser else, and I’m not even sure that the specification calls for this.Just seeing your :after usage made me remember, is all. :)
I’ve just done this with a mixture of both jQuery and Vanilla by creating a function that passes both the target to check state (as per most text swaps are built for) and the text to swap:
and then call it like this:
and
Thoughts?
We need to store data into variables before swap it.
I suggest to use an element without text in HTML:
And this JS to make it live:
I created a pen for it: cdpn.io/yEJdw
nice ways i like css way but Sometimes we are forced to use other ways
How do you get the data attribute to persist on page refresh?