Pure CSS Collapsibles and Accordions

CSS Collapsibles Can Be Easy and Even Classless

This is how you do a pure CSS collapsible, using the usual checkbox trick, but without all the extra nested <div> containers and one million classes.

See towards the bottom of the page for how to expand this into an accordion.

Overview And Full Code

Three html elements are needed for a collapsible. A checkbox <input>, a <label> and a <div> [note 1]

An overall outer <div> is needed when the collapsible is to follow a non-explicitly closed <p> [note 2]

The html for all our collapsibles is:

<div>  <!-- outer div not always necessary -->

  <input id=collapsible-id class=collapsible type=checkbox checked>
  <label for=collapsible-id>Collapsible Heading</label>
  <div>
    Collapsible content.
  </div>

</div>

And the full CSS:

input.collapsible { display: none; }


input.collapsible + label {
    display: block;
    width: 60%;
    background: #0070ff;
    color: White;
    text-align: center;
    padding: 1em;
    cursor: pointer;
    transition: all 0.5s ease-out;
    /* dynamic */
    border-radius: 4px;
}

input.collapsible:checked + label {
    /* dynamic */
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}


input.collapsible-demo-2 + label::before {
    font: bold 1em sans-serif;
    margin-right: 0.5em;
    /* dynamic */
    content: '+';
}

input.collapsible-demo-2:checked + label::before {
    /* dynamic */
    content: '\2212'; /* full width minus */
}


input.collapsible + label + div {
    overflow: scroll;
    width: 60%;
    background: #0070ff44;
    border: none;
    transition: all 0.5s ease-in-out;
    /* dynamic */
    max-height: 0px;
    padding: 0 1em;
    border-radius: 0;
}

input.collapsible:checked + label + div {
    /* dynamic */
    max-height: 100vh;
    padding-top: 0.5em;
    padding-bottom: 0.5em;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}

In More Detail

Required Elements

Only three html elements are absolutely needed for a collapsible. A checkbox <input>, a <label> and a <div>. [1]

Nothing else is strictly needed; a single overall outer <div> wrapper may be needed only to avoid your input + label getting caught inside an unclosed <p> paragraph or similar [2], or for adding styling such as padding or margins to the overall collapsible structure.

How It Works

You may already be familiar with this, but to recap: the state of the checkbox, tested in CSS with the :checked pseudo-class, determines the state of the collapsible. Since a checkbox is small and ugly for this task, we take advatage of the label, whose whole surface area can be clicked to toggle the checkbox. We can therefore hide the checkbox entirely, and style the label into an exciting UI element as we see fit.

A Classless Collapsible

Using adjacent sibling combinators (+), we require a class only for the first element (the input checkbox), making this very close to the classless ideal.

Minimum Functional CSS

The minimum CSS that makes a working collapsible is the following (in the form of a collapsible). This minimal code also makes it easy to appreciate how the adjacent sibling combinator removes the need for lots of classes.

input.collapsible {
    /* display: none; */
}

input.collapsible + label + div {
    overflow: scroll;
    /* dynamic */
    max-height: 0px;
}

input.collapsible:checked + label + div {
    /* dynamic */
    max-height: 100vh;
}

Yes that's it.

Although this works, the collapsible is hard to comprehend without any visual styling - hence why I disabled hiding the checkbox, so there is at least some feedback.

Designing the Rulesets

The input.collapsible ruleset is easy to understand and hides the input checkbox at all times (the property is shown disabled here as just mentioned).

Setting properties between collapsed and expanded states is subtle. Properties we don't want to change between the two states must be set in the non- :checked ruleset, as this one is the only one that will apply in both cases (it still applies when the checkbox is checked due to the cascade). Properties we do want to change must be set to their collapsed values in the non- :checked ruleset, then overridden to their expanded values in the :checked ruleset.

To this end, I have separated the state-dependent properties under the /* dynamic */ comment in the rulesets.

Ruleset input.collapsible + label + div sets the collapsed height of the content element to zero. Due to the subtlety just described, here is where we also set the content overflow property to handle when the content is too long, as we want the same value in both states.

Finally input.collapsible:checked + label + div is used to override the previous ruleset and restore the height of the content. As stated, the overflow property from the previous ruleset will continue to be applied.

Note that we have used the max-height property to control the size of the collapsible content, and set it using relative length units. Setting the height property directly and using percentages as we might want to, does not work for when we want to animate the height with a transition.

CSS For Styling

Here is the extra CSS for the styling - observe that there are three pairs of rulesets, styling the <label>, the toggling +/- symbol, and the collapsing content <div>, for each of the checked and unchecked states.

input.collapsible + label {
    display: block;
    width: 60%;
    background: #0070ff;
    color: White;
    text-align: center;
    padding: 1em;
    cursor: pointer;
    transition: all 0.5s ease-out;
    /* dynamic */
    border-radius: 4px;
}

input.collapsible:checked + label {
    /* dynamic */
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}


input.collapsible-demo-2 + label::before {
    font: bold 1em sans-serif;
    margin-right: 0.5em;
    /* dynamic */
    content: '+';
}

input.collapsible-demo-2:checked + label::before {
    /* dynamic */
    content: '\2212'; /* full width minus character */
}


input.collapsible + label + div {
    width: 60%;
    background: #0070ff44;
    border: none;
    transition: all 0.5s ease-in-out;
    /* dynamic */
    padding: 0 1em;
    border-radius: 0;
}

input.collapsible:checked + label + div {
    /* dynamic */
    padding-top: 0.5em;
    padding-bottom: 0.5em;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}

To see the functional + styling CSS put together, see the overview at the top of the page.

Collapse Into an Accordion

Making our collapsible into an accordion is very easy. The <input> checkbox is changed to a radio type button, and a name attribute is added; all the sections you want to accorion together should have the same name attribute. No changes to the CSS are required.

As with collapsibles make sure each section has it's own unique id attribute.

The checked attribute should be present only on whichever section you want open upon page load - you can also leave it off all sections, in which case the page will load with all sections collapsed until the user selects one.

Again an outer <div> is optional depending on circumstances. When it is necessary, you can choose between an overall <div> around the whole accordion, or one for each collapsing section.

<input id=accordion-first class=collapsible type=radio name=accordion-example checked>
<label for=accordion-first>Accordion First Heading</label>
<div>
  Accordion content.
</div>

<input id=accordion-second class=collapsible type=radio name=accordion-example>
<label for=accordion-second>Accordion Second Heading</label>
<div>
  Accordion content.
</div>
<div>

    <input id=accordion-first class=collapsible type=radio name=accordion-example checked>
    <label for=accordion-first>Accordion First Heading</label>
    <div>
      Accordion content.
    </div>

    <input id=accordion-second class=collapsible type=radio name=accordion-example>
    <label for=accordion-second>Accordion Second Heading</label>
    <div>
      Accordion content.
    </div>

</div>
<div>
    <input id=accordion-first class=collapsible type=radio name=accordion-example checked>
    <label for=accordion-first>Accordion First Heading</label>
    <div>
      Accordion content.
    </div>
</div>

<div>
    <input id=accordion-second class=collapsible type=radio name=accordion-example>
    <label for=accordion-second>Accordion Second Heading</label>
    <div>
      Accordion content.
    </div>
</div>

Notes

  1. The <div> could be replaced by any generic element that can be rendered as a block level element, however this may require more css. The flip side of this is, using a <div>, are we relying on styling attributes defined elsewhere, and could styling meant for <div> in another part of the page muck up our collapsible?

  2. Input and label elements are allowed inside a <p> element, however a <p> cannot contain a <div>, so an open <p> is closed when a <div> is opened. Thus the input and label become children of the <p>, while the <div> becomes it's sibling, separating them and breaking our css.