Standard tab control


I would like to be able to provide a tabbed interface on a website, without having to use JavaScript. Clicking on a tab should display the corresponding tab panel, and the HTML markup alone should be fully accessible (keyboard, screen reader).

An example of a tab UI is Gmail:


Hmm not sure how something like this would work without Javascript without making many many assumptions about what should happen. There are a many things that need to be either explicitly declared in your markup or implied.

  1. When does a tab get transitioned to? After a ‘click’, maybe when enter key is pressed?
  2. If transition is triggered by a key press, which variation? ‘keyDown’, ‘keyUp’?
  3. How should the content load once transitioning to a tab? (Lazily vs eager)
  4. Should the tab content already be in the DOM waiting to be shown? Or injected when its requested?
  5. Should the tab content be cached upon first view? If so, when do we query for up-to-date content?
  6. Should it have a loading spinner?
  7. If there is a loading spinner, when should it show/hide? Right before content is shown?
  8. How would the loading spinner get displayed? Fade in? Fade out? Css transition? Css Animations?

I’ve only scratched the surface here…


1 and 2. Keyboard interaction with tab controls is standardized in ARIA, so that is taken care of.
3, 4 and 5. The tab control is merely responsible for displaying the correct tab panel. Whether or not tab panels are prepopulated or lazyloaded is up to the web app.
6, 7 and 8. No loading indicator is needed since the tab panel is displayed immediately (it’s the same mechanism as with the <details>/<summary> elements).

I hope this makes it clear that my proposal is only for a basic tabbed UI functionality.


Should the <details/summary> style be reused for this? If so, what should the element names be?


please look at Panels and panelsets

Also, ARIA standardising keyboard control through adding javascript isn’t necessarily a good answer. There are cases where different platforms do, and probably should do, something other than what the ARIA models propose, generally based on Windows if there are differences.


I quite like the panel and panelset idea. It’s got a good balance of high level semantics without overly restricting developers. The mediaquery control of preferred-display is particularly nice.


@chaals @chaoaretasty I like the panel/panel sets proposal.

The first panel element within a panelset element should be expanded by default. Any subsequent claims of expansion trump the original claim.


I’m not sure what “trump the original claim” means. Does it mean that expanding another panel automatically collapses the previously expanded panel? This would be needed for built-in interaction functionality in a a tabbed interface, which is what I want.

Also, having to set the expandable attribute on each panel isn’t optimal (it would be code repetition, since a tab panel is expandable by design); it would be nice if the attribute could be set on the panel set element.


Reading the “requirements” of this particular discussion

I would like to be able to

  • provide a tabbed interface on a website
  • without having to use JavaScript
  • clicking on a tab should display the corresponding tab panel
  • the HTML markup alone should be fully accessible

I mocked up a quick proof-of-concept with HTML, CSS, and no JS.

In making it, I didn’t feel as though I really needed additional elements, however that meant relying heavily on ARIA attributes. What I felt was lacking were certain CSS selectors.

It’d be great if there were a way to use attribute references in selectors in some way so that a checkbox could select its own label(s), and a label could select its checkbox. Given how many ARIA attributes are IDs or lists of IDs, this would significantly improve ARIA in general.

All that said, there are certainly some issues. Without JS there’s no way to manage state such as the aria-expanded attribute. Having some dedicated elements that have explicit behavior could simplify implementations, and make it easier for developers to build tabs correctly, rather than a bunch of <div>s with onclick.


Thanks for summarizing it in a terse list :blush:. Until, this gets standardized in HTML, the best solution for web developers is to use an “ARIA tabs” library, I think.


So you’d basically like to be able to simplify rules like these

#tab-1:checked ~ [role="tablist"] [for="tab-1"], 
#tab-2:checked ~ [role="tablist"] [for="tab-2"], 
#tab-3:checked ~ [role="tablist"] [for="tab-3"] {
  background-color: #FFF;
#tab-1:checked ~ #tab-panel-1, 
#tab-2:checked ~ #tab-panel-2, 
#tab-3:checked ~ #tab-panel-3 {
  display: block;

to something like this?

.tab:checked >>> label {
  background-color: #FFF;
.tab:checked >>> .panel {
  display: block;


Yes, although the syntax needs to be worked out.

Something like

label:attr(for, input:checked, id)

could work to select the labels where the [for] attributes matche the input:checked elements’ id attributes

In the example, this would replace

#tab-1:checked ~ [role="tablist"] [for="tab-1"] {
  background-color: #FFF;
#tab-1:checked ~ #tab-panel-1 {
  display: block;


.tabs__tab:attr(for, .tabs__radio:checked, id) {
  background-color: #FFF;
.tabs__panel:attr(id, .tabs__tab:attr(for, .tabs__radio:checked, id), aria-controls) {
  display: block;

I’m sure there are a bajillion reasons why this sort of selector can’t and won’t exist, and I have no idea what sort of specificity the selector would have, but I can guarantee that if I had access to something like it, I’d use it all the time.


I’m not sure what “trump the original claim” means. Does it mean that expanding another panel automatically collapses the previously expanded panel?

Yes, basically it is explaining what a single with options would do… If you say

If the markup is at odds, the last one wins.


JFTR, there used to be a solution for that in Selectors 4 drafts (subject indicator $ and reference combinator /…/), but the latter part is gone and the former has been replaced by :has().

$label /for/ input:checked {}
label:has(/for/ input:checked) {}