Grow your CSS skills. Land your dream job.

Swapping Out Text, Five Different Ways

Published by Chris Coyier

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?

Comments

  1. Nice article Chris, i’m using this right now on a CodePen :-)

  2. Subash

    What about using attr() in css to get data from html instead of hard coding that.

    <input id="example-checkbox" type="checkbox">
    <label for="example" id="example" data-text="Hide">Show</label>
    

    #example {
      position: relative;
    }
    #example-checkbox {
      display: none;
    }
    #example-checkbox:checked + #example:after {
      content: attr(data-text);
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: white;
    }
    
  3. The data-* attributes on elements can also be accessed via element.dataset, so you could also write your ‘raw’ JavaScript example like this:

    var button = document.querySelectorAll("button")[0];
    button.addEventListener('click', function() {
        if (button.dataset.textSwap === button.textContent) {
            button.textContent = button.dataset.textOriginal;
        } else {
            button.dataset.textOriginal = button.textContent;
            button.textContent = button.dataset.textSwap;
        }
    }, false);
    

    Of course, that won’t work in IE < 11, but we can dream :(

  4. Ravindran

    I was thinking same solution Peter Foti suggested. Here is my fiddle.

    http://jsfiddle.net/rnavaneethan/Fax7D/1/embedded/result/

  5. 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.

  6. Just wanted to say that the first two javascripts snippets miss a closing parenthesis :

    button.text(button.data("text-swap");
    // should be
    button.text(button.data("text-swap"));
  7. Love your work Chris. One possible shortcut for the JavaScript.

    var button = document.querySelectorAll("button")[0];
    // could be simplified with
    var button = document.querySelector("button");
    
    • MaxArt

      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!”

  8. 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);}

    • MaxArt

      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.

  9. <label><input class=”on-off” type=”checkbox” data-checked=”oui” data-unchecked=”non” /></label>

  10. 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?

    • MaxArt

      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.

  11. @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.

  12. 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

    #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;
    }
    

    HTML

    < input id=”example-checkbox” type=”checkbox”>
    < label for=”example” id=”example”>Show < /label >

    • I Like the way you used the array to do two things for you :), Smart

    • Xavi

      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.

  13. 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. :)

  14. 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

  15. Oops should have come out…

    <script>

    function elementStateChange(which, display){

    if (which.innerText == display[0]){

             which.innerText =display[1] ; 
    
    }else{
             which.innerText =display[0] ;
           }
    

    }
    </script>

    <button onclick=’elementStateChange(this, ["Show","Hide"])’>Show</button>

  16. Ramesh Chowdarapally

    Useful info chris.

  17. jsc42

    @Paul

    Even more compactly:

    which.innerText = display[which.innerText == display[0] ? 1 : 0];

  18. jsc42

    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';">

  19. Karl

    @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.

  20. 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:

    Hide
    Show
    

    This CSS:

    #btn-show { display: none; }
    

    ..and the following jQuery:

    $('.btn-show-or-hide').on( 'click', function () {
     $('.btn-show-or-hide').toggle();
    });
    

    Thoughts?

  21. Anthony

    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..

  22. jason

    Why not just toggle classes? There is often some HTML involved. Data attr isn’t enough always.

  23. Permalink to comment#

    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. :)

  24. Jon
    Permalink to comment#

    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:

    changeDisplayText = function(targetId, displayText){
        return displayText = targetId.is(":visible") ? displayText.replace("Hide","Show") : displayText.replace("Show","Hide");
    }
    

    and then call it like this:

    $(this).attr('title', changeDisplayText($(this.hash), $(this).attr('title')));
    

    and

    $(this).html(changeDisplayText($(this.hash), $(this).html()));
    

    Thoughts?

  25. We need to store data into variables before swap it.

    I suggest to use an element without text in HTML:

    button.toggle(data-swap="close", data-text="open")
    

    And this JS to make it live:

    var button = $(".toggle");  
    button.html(button.data("text"));
    button.click(function(){
        var el = $(this);
        var swap = el.data("swap");
        var text = el.data("text");
        el.data("text", swap);
        el.data("swap", text);
        el.html(swap);
    });
    

    I created a pen for it: cdpn.io/yEJdw

  26. Permalink to comment#

    nice ways i like css way but Sometimes we are forced to use other ways

  27. Carol Jenkins
    Permalink to comment#

    How do you get the data attribute to persist on page refresh?

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".