69

I am using Twitter Bootstrap to create collapsible sections of text. The sections are expanded when a + button is pressed. My html code as follows:

<div class="row-fluid summary">
    <div class="span11">
        <h2>MyHeading</h2>  
    </div>
    <div class="span1">
        <button type="button" class="btn btn-success" data-toggle="collapse" data-target="#intro">+</button>
    </div>
</div>
<div class="row-fluid summary">
    <div id="intro" class="collapse"> 
        Here comes the text...
    </div>
</div>

Is there a way to change the button to display - instead of + after the section is expanded (and change back to + when it is collapsed again)?

Additional information: I hoped there would be a simple twitter-bootstrap/css/html-based solution to my problem. All responses so far make use of JavaScript or PHP. Because of this I want to add some more information about my development environment: I want to use this solution inside a SilverStripe-based (version 3.0.5) website which has some implications for the use of both PHP as well as JavaScript.

Markus M.
  • 1,102
  • 2
  • 8
  • 17

10 Answers10

94

try this. http://jsfiddle.net/fVpkm/

Html:-

<div class="row-fluid summary">
    <div class="span11">
        <h2>MyHeading</h2>  
    </div>
    <div class="span1">
        <button class="btn btn-success" data-toggle="collapse" data-target="#intro">+</button>
    </div>
</div>
<div class="row-fluid summary">
    <div id="intro" class="collapse"> 
        Here comes the text...
    </div>
</div>

JS:-

$('button').click(function(){ //you can give id or class name here for $('button')
    $(this).text(function(i,old){
        return old=='+' ?  '-' : '+';
    });
});

Update With pure Css, pseudo elements

http://jsfiddle.net/r4Bdz/

Supported Browsers

button.btn.collapsed:before
{
    content:'+' ;
    display:block;
    width:15px;
}
button.btn:before
{
    content:'-' ;
    display:block;
    width:15px;
}

Update 2 With pure Javascript

http://jsfiddle.net/WteTy/

function handleClick()
{
    this.value = (this.value == '+' ? '-' : '+');
}
document.getElementById('collapsible').onclick=handleClick;
PSL
  • 120,386
  • 19
  • 245
  • 237
  • This looks like the solution I am looking for, many thanks also for the jsfiddle. I did not yet manage to get it running in my SilverStripe website (see additional information that I added to my original post). – Markus M. Apr 26 '13 at 02:00
  • 2
    Hi, i have updated my answer with pure css solution. Probably this might work out for you. – PSL Apr 26 '13 at 02:18
  • 1
    Did not succeed with the jQuery-solution nor the CSS-based solution but the pure Javascript finally did the trick. I guess jQuery and CSS ran into side effects created by the SilverStripe-Twitter-Bootstrap-theme that I use. Thanks a lot for the effort, really appreciated! Besides now having a solution for my problem I learned a lot from your suggestions. – Markus M. Apr 26 '13 at 03:39
  • Glad i was of some help to your issues.. :) – PSL Apr 26 '13 at 03:40
  • 1
    Figured out how to use the jQuery-based solution, just had to find the right place to include the script. More elegant as it can be applied to a number of sections right away. – Markus M. Apr 29 '13 at 01:53
  • For an alternative approach that allows you to keep the button text, e.g. "View details..." and "Hide details...", together with the button HTML see http://stackoverflow.com/a/28500651/245602 – George Hawkins Feb 13 '15 at 13:29
  • 4
    One note -- this will get out of sync if you click the button repeatedly and quickly. That's because the collapse isn't done executing. Could prevent this by disabling additional clicks until collapse is complete. – Voodoo Jul 29 '16 at 21:02
72

Here's another CSS only solution that works with any HTML layout.

It works with any element you need to switch. Whatever your toggle layout is you just put it inside a couple of elements with the if-collapsed and if-not-collapsed classes inside the toggle element.

The only catch is that you have to make sure you put the desired initial state of the toggle. If it's initially closed, then put a collapsed class on the toggle.

It also requires the :not selector, so it doesn't work on IE8.

HTML example:

<a class="btn btn-primary collapsed" data-toggle="collapse" href="#collapseExample">
  <!--You can put any valid html inside these!-->
  <span class="if-collapsed">Open</span>
  <span class="if-not-collapsed">Close</span>
</a>
<div class="collapse" id="collapseExample">
  <div class="well">
    ...
  </div>
</div>

Less version:

[data-toggle="collapse"] {
    &.collapsed .if-not-collapsed {
         display: none;
    }
    &:not(.collapsed) .if-collapsed {
         display: none;
    }
}

CSS version:

[data-toggle="collapse"].collapsed .if-not-collapsed {
  display: none;
}
[data-toggle="collapse"]:not(.collapsed) .if-collapsed {
  display: none;
}

JS Fiddle

Jens
  • 5,066
  • 5
  • 46
  • 64
  • 1
    This is a great answer. Wish I could upvote more than once – Chris Mar 27 '17 at 14:05
  • 1
    Thanks, I really did a lot of research to come up with this. Being able to use only css and use any html layout you want is a big plus. It's very important for mobile devices where you need the best performance with as little javascript as possible. – Jens Mar 28 '17 at 00:28
  • 1
    I like this answer but it seems that Bootstrap (2.3.1) has a bug with the `.collapse` being added on click to the anchor element. This means that the class can become out of sync with if the actual target has been shown. – harvzor Jun 23 '17 at 13:47
  • 1
    @Harvey I have only tested this with bootstrap 3, it's been a long time since I used bootstrap 2. – Jens Jun 29 '17 at 12:46
  • really a great answer – pdenti Jul 10 '17 at 23:33
  • this code yes its bugged with "collapse in" class if collapse already open, tested with bootstrap 3, but if you dont use "collapse in" this good to use. (when bugged you need to manually clicked it with jquery but dont add "collapse in " class). – Anthony Kal Jul 12 '17 at 07:04
  • 7
    This is a very elegant solution. Note that in order for this to work: If a section is collapsed, ensure that the toggling element has the class `collapsed`. If a section is expanded due to having the class `collapse in`, ensure that the toggling element **does not** have the class `collapsed`. Example: http://jsfiddle.net/ahx7r627/. – evophage Sep 27 '17 at 03:10
  • Fantastic solution. Still works with bootstrap 4.x. – David Gay May 23 '19 at 15:02
6

Add some jquery code, you need jquery to do this :

<script>
        $(".btn[data-toggle='collapse']").click(function() {
            if ($(this).text() == '+') {
                $(this).text('-');
            } else {
                $(this).text('+');
            }
        });
        </script>
AzDesign
  • 1,053
  • 1
  • 6
  • 18
  • Makasih jawabannya. This looks like the correct solution. I did not yet manage to make it run in my SilverStripe instance yet though. – Markus M. Apr 26 '13 at 01:47
  • Wish I could upvote this more. By far the simplest cleanest way to add this function to existing code - thank you! – Hondaman900 Feb 19 '20 at 00:15
5

All the other solutions posted here cause the toggle to get out of sync if it is double clicked. The following solution uses the events provided by the Bootstrap framework, and the toggle always matches the state of the collapsible element:

HTML:

<div class="row-fluid summary">
    <div class="span11">
        <h2>MyHeading</h2>  
    </div>
    <div class="span1">
        <button id="intro-switch" class="btn btn-success" data-toggle="collapse" data-target="#intro">+</button>
    </div>
</div>
<div class="row-fluid summary">
    <div id="intro" class="collapse"> 
        Here comes the text...
    </div>
</div>

JS:

$('#intro').on('show', function() {
  $('#intro-switch').html('-')
})
$('#intro').on('hide', function() {
  $('#intro-switch').html('+')
})

That should work for most cases.

However, I also ran into an additional problem when trying to nest one collapsible element and its toggle switch inside another collapsible element. With the above code, when I click the nested toggle to hide the nested collapsible element, the toggle for the parent element also changes. It may be a bug in Bootstrap. I found a solution that seems to work: I added a "collapsed" class to the toggle switches (Bootstrap adds this when the collapsible element is hidden but they don't start out with it), then added that to the jQuery selector for the hide function:

http://jsfiddle.net/fVpkm/87/

HTML:

<div class="row-fluid summary">
    <div class="span11">
        <h2>MyHeading</h2>  
    </div>
    <div class="span1">
        <button id="intro-switch" class="btn btn-success collapsed" data-toggle="collapse" data-target="#intro">+</button>
    </div>
</div>
<div class="row-fluid summary">
    <div id="intro" class="collapse"> 
        Here comes the text...<br>
        <a id="details-switch" class="collapsed" data-toggle="collapse" href="#details">Show details</a>
        <div id="details" class="collapse">
            More details...
        </div>
    </div>
</div>

JS:

$('#intro').on('show', function() {
    $('#intro-switch').html('-')
})
$('#intro').on('hide', function() {
    $('#intro-switch.collapsed').html('+')
})

$('#details').on('show', function() {
    $('#details-switch').html('Hide details')
})
$('#details').on('hide', function() {
    $('#details-switch.collapsed').html('Show details')
})
linesarefuzzy
  • 1,560
  • 14
  • 17
  • could it be the nested issue is a problem with event bubbling/propagation? should be able to stop that – JoeBrockhaus Mar 31 '15 at 19:49
  • 3
    FYI for anyone trying this, in the latest version of boostrap the event names are 'show.bs.collapse' and 'hide.bs.collapse'. – Avi Dec 15 '15 at 18:38
3

My following JS solution is better than the other approaches here because it ensures that it will always say 'open' when the target is closed, and vice versa.

HTML:

<a href="#collapseExample" class="btn btn-primary" data-toggle="collapse" data-toggle-secondary="Close">
    Open
</a>
<div class="collapse" id="collapseExample">
  <div class="well">
    ...
  </div>
</div>

JS:

$('[data-toggle-secondary]').each(function() {
    var $toggle = $(this);
    var originalText = $toggle.text();
    var secondaryText = $toggle.data('toggle-secondary');
    var $target = $($toggle.attr('href'));

    $target.on('show.bs.collapse hide.bs.collapse', function() {
        if ($toggle.text() == originalText) {
            $toggle.text(secondaryText);
        } else {
            $toggle.text(originalText);
        }
    });
});

Examples:

$('[data-toggle-secondary]').each(function() {
    var $toggle = $(this);
    var originalText = $toggle.text();
    var secondaryText = $toggle.data('toggle-secondary');
    var $target = $($toggle.attr('href'));

    $target.on('show.bs.collapse hide.bs.collapse', function() {
        if ($toggle.text() == originalText) {
            $toggle.text(secondaryText);
        } else {
            $toggle.text(originalText);
        }
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet"/>
<script src="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>

<a href="#collapseExample" class="btn btn-primary" data-toggle="collapse" data-toggle-secondary="Close">
    Open
</a>
<div class="collapse" id="collapseExample">
  <div class="well">
    ...
  </div>
</div>

JS Fiddle

Other benefits of this approach:

  • the code is DRY and reusable
  • each collapse button stays separate
  • you only need to put one change into the HTML: adding the data-toggle-secondary attribute
harvzor
  • 2,540
  • 19
  • 35
1

I guess you could look inside your downloaded code where exactly there is a + sign (but this might not be very easy).

What I'd do? I'd find the class/id of the DOM elements that contain the + sign (suppose it's ".collapsible", and with Javascript (actually jQuery):

<script>
     $(document).ready(function() {
         var content=$(".collapsible").html().replace("+", "-");
         $(".collapsible").html(content));
     });
</script>

edit Alright... Sorry I haven't looked at the bootstrap code... but I guess it works with something like slideToggle, or slideDown and slideUp... Imagine it's a slideToggle for the elements of class .collapsible, which reveal contents of some .info elements. Then:

         $(".collapsible").click(function() { 
             var content=$(".collapsible").html();
             if $(this).next().css("display") === "none") { 
                 $(".collapsible").html(content.replace("+", "-"));
             }
             else $(".collapsible").html(content.replace("-", "+"));
         });

This seems like the opposite thing to do, but since the actual animation runs in parallel, you will check css before animation, and that's why you need to check if it's visible (which will mean it will be hidden once the animation is complete) and then set the corresponding + or -.

Nico
  • 781
  • 1
  • 8
  • 19
  • Thanks for your reply. The `+` is part of my `button` definition (have a look at my code above). If I understand your code correctly, then it will replace all instances of `+` with `-` after the page is fully loaded. What I need though is that the `+` is replaced with `-` after the `+`-button is clicked. – Markus M. Apr 26 '13 at 01:25
  • Hey, check again my answer. I have edited it so you can read it a bit more clearly – Nico Apr 28 '13 at 08:42
  • Thanks for the effort. I already accepted the very first answer which proposes a similar jQuery-based solution. – Markus M. Apr 29 '13 at 01:56
1

I liked the CSS-only solution from PSL, but in my case I needed to include some HTML in the button, and the content CSS property is showing the raw HTML with tags in this case.

In case that could help someone else, I've forked his fiddle to cover my use case: http://jsfiddle.net/brunoalla/99j11h40/2/

HTML:

<div class="row-fluid summary">
    <div class="span11">
        <h2>MyHeading</h2>  
    </div>
    <div class="span1">
        <button class="btn btn-success collapsed" data-toggle="collapse" data-target="#intro">
            <span class="show-ctrl">
                <i class="fa fa-chevron-down"></i> Expand
            </span>
            <span class="hide-ctrl">
                <i class="fa fa-chevron-up"></i> Collapse
            </span>            
        </button>
    </div>
</div>
<div class="row-fluid summary">
    <div id="intro" class="collapse"> 
        Here comes the text...
    </div>
</div>

CSS:

button.btn .show-ctrl{
    display: none;
}
button.btn .hide-ctrl{
    display: block;
}
button.btn.collapsed .show-ctrl{
    display: block;
}
button.btn.collapsed .hide-ctrl{
    display: none;
}
Community
  • 1
  • 1
Bruno A.
  • 1,380
  • 14
  • 13
0

Easier with inline coding

<button type="button" ng-click="showmore = (showmore !=null && showmore) ? false : true;" class="btn float-right" data-toggle="collapse" data-target="#moreoptions">
            <span class="glyphicon" ng-class="showmore ? 'glyphicon-collapse-up': 'glyphicon-collapse-down'"></span>
            {{ showmore !=null && showmore ? "Hide More Options" : "Show More Options" }}
        </button>


<div id="moreoptions" class="collapse">Your Panel</div>
0

Some may take issue with changing the Bootstrap js (and perhaps validly so) but here is a two line approach to achieving this.

In bootstrap.js, look for the Collapse.prototype.show function and modify the this.$trigger call to add the html change as follows:

this.$trigger
  .removeClass('collapsed')
  .attr('aria-expanded', true)
  .html('Collapse')

Likewise in the Collapse.prototype.hide function change it to

this.$trigger
  .addClass('collapsed')
  .attr('aria-expanded', false)
  .html('Expand')

This will toggle the text between "Collapse" when everything is expanded and "Expand" when everything is collapsed.

Two lines. Done.

EDIT: longterm this won't work. bootstrap.js is part of a Nuget package so I don't think it was propogating my change to the server. As mentioned previously, not best practice anyway to edit bootstrap.js, so I implemented PSL's solution which worked great. Nonetheless, my solution will work locally if you need something quick just to try it out.

Scott Decker
  • 3,659
  • 5
  • 21
  • 36
0

You do like this. the function return the old text.

$('button').click(function(){ 
        $(this).text(function(i,old){
            return old=='Read More' ?  'Read Less' : 'Read More';
        });
    });
Mahdi
  • 201
  • 2
  • 11