Tabbed panels using CSS

Tabbed panel interfaces are usually created using JavaScript, but this is unnecessary. This page describes how to style tabbed panels using CSS, as shown in the demonstration below, while keeping the semantic association between the tab labels and panel content.

All human beings are born free and equal in dignity and rights. They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood.

Everyone is entitled to all the rights and freedoms set forth in this Declaration, without distinction of any kind, such as race, colour, sex, language, religion, political or other opinion, national or social origin, property, birth or other status. Furthermore, no distinction shall be made on the basis of the political, jurisdictional or international status of the country or territory to which a person belongs, whether it be independent, trust, non-self-governing or under any other limitation of sovereignty.

Everyone has the right to life, liberty and security of person.

No one shall be held in slavery or servitude; slavery and the slave trade shall be prohibited in all their forms.

No one shall be subjected to torture or to cruel, inhuman or degrading treatment or punishment.

Everyone has the right to recognition everywhere as a person before the law.

Download examples

The archives below contain example files demonstrating the styling methods described on this page.

File Size Description
example.zip 1,548 bytes Zip archive
example.7z 1,465 bytes 7-Zip archive

The HTML

We begin with the following semantic HTML, which represents a panel as a section with a heading and content:

1
2
3
4
5
6
7
8
<section>
  <h1>
    <!-- heading -->
  </h1>
  <div>
    <!-- content -->
  </div>
</section>

We then place the panels in a container and add radio buttons and labels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div class="tabbed">

  <!-- first panel -->

  <input name="tabbed" id="tabbed1" type="radio" checked>
  <section>
    <h1>
      <label for="tabbed1"><!-- heading --></label>
    </h1>
    <div>
    <!-- content -->
    </div>
  </section>

  <!-- second panel -->

  <input name="tabbed" id="tabbed2" type="radio">
  <section>
    <h1>
      <label for="tabbed2"><!-- heading --></label>
    </h1>
    <div>
    <!-- content -->
    </div>
  </section>

  <!-- and so on -->

</div>

The radio buttons are used to indicate the currently selected panel. If there are multiple sets of tabbed panels on a page, each must use a unique name for its radio buttons. The first radio button has its checked attribute set so that the first panel is shown by default.

The CSS styling

We start by hiding the radio buttons:

1
2
3
.tabbed > input{
  display : none;
}

We then hide the content of any panel whose radio button is not checked:

1
2
3
.tabbed > input:not(:checked) + section > div{
  display : none;
}

We then display the tab labels in a row by floating them:

1
2
3
.tabbed > section > h1{
  float : left;
}

At this point the row of tab labels is interrupted by the content of the selected panel. We prevent this by floating the content to the right, with a negative left margin to prevent it affecting the layout and a top margin to position it below the labels:

1
2
3
4
5
.tabbed > section > div{
  float  : right;
  width  : 100%;
  margin : 2.5em 0 0 -100%;
}

Note that the required top margin depends on the styling of the tab labels. The value of 2.5 ems used here is based on a line-height of 1.5 combined with the tab styling described below.

We then style the container so that content following the tabbed panels appears below them:

1
2
3
4
.tabbed{
  float : left;
  width : 100%;
}

Finally, we change the mouse pointer to indicate that tabs can be clicked, and prevent accidental selection of the tab text:

 1
 2
 3
 4
 5
 6
.tabbed > section > h1 > label{
  cursor              : pointer;
     -moz-user-select : none;
      -ms-user-select : none;
  -webkit-user-select : none;
}

Making things look nice

The CSS above provides minimal styling for the panels. The following style rules will produce the appearance shown in the demonstration at the top of this page.

We start by displaying the selected panel content in a box with rounded corners:

1
2
3
4
5
6
7
8
.tabbed > section > div{
  box-sizing    : border-box;
  padding       : 0.5em 0.75em;
  border        : 1px solid #ddd;
  border-radius : 4px;
  box-shadow    : 0 0 0.5em rgba(0,0,0,0.0625);
  background    : #fff;
}

We then display the labels as tabs with rounded corners:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.tabbed > section > h1{
  box-sizing  : border-box;
  margin      : 0;
  padding     : 0.5em 0.5em 0;
  overflow    : hidden;
  font-size   : 1em;
  font-weight : normal;
}

.tabbed > section > h1 > label{
  display                 : block;
  padding                 : 0.25em 0.75em;
  border                  : 1px solid #ddd;
  border-bottom           : none;
  border-top-left-radius  : 4px;
  border-top-right-radius : 4px;
  box-shadow              : 0 0 0.5em rgba(0,0,0,0.0625);
  background              : #fff;
}

The use of padding and the overflow property on the heading ensure that the tab shadow shows on the top, left, and right sides, but not the bottom.

We then indent the first tab so that it is not too close to the edge:

1
2
3
.tabbed > input:first-child + section > h1{
  padding-left : 1em;
}

Finally, we stack the elements so that the selected tab appears above the panel content, which in turn appears above the other unselected tabs:

1
2
3
4
5
6
7
8
9
.tabbed > section > div{
  position : relative;
  z-index  : 1;
}

.tabbed > input:checked + section > h1{
  position : relative;
  z-index  : 2;
}

This causes the selected tab to overlap the panel content, covering up part of the border so that the tab appears joined to the panel content. The panel content also casts a shadow onto the unselected tabs.

Supporting older browsers

The code above works in any modern browser, but not Internet Explorer 8 or older. By hiding the CSS rules with the negation pseudo-class — for example, by replacing each occurrence of .tabbed with .tabbed:not(old) — the semantic HTML ensures that the content will still be accessible in older browsers, with headings in the correct locations.

Where now?

Found this useful? Share it:

Also in HTML and CSS: