[:octicons-file-code-24:][_tabbed]{: .source-link }

# Tabbed

## Overview

!!! new "New 7.0"
    Tabbed has been newly added in 7.0.

Tabbed provides a syntax to easily add tabbed Markdown content. The Tabbed extension can be included in Python Markdown
by using the following:

```py3
import markdown
md = markdown.Markdown(extensions=['pymdownx.tabbed'])
```

## Syntax

Tabs start with `===` to signify a tab followed by a quoted title. Consecutive tabs are grouped into a tab set.

!!! example "Example Tabs"

    === "Output"
        === "Tab 1"
            Markdown **content**.

            Multiple paragraphs.

        === "Tab 2"
            More Markdown **content**.

            - list item a
            - list item b

    === "Markdown"
        ```
        === "Tab 1"
            Markdown **content**.

            Multiple paragraphs.

        === "Tab 2"
            More Markdown **content**.

            - list item a
            - list item b
        ```

In the rare case that you want to follow two separate tab sets right after each other, you can explicitly mark the start
of a new tab set with `!`.


!!! example "Example Tab Breaks"

    === "Output"
        === "Tab 1"
            Markdown **content**.

            Multiple paragraphs.

        === "Tab 2"
            More Markdown **content**.

            - list item a
            - list item b

        ===! "Tab A"
            Different tab set.

        === "Tab B"
            ```
            More content.
            ```

    === "Markdown"

        ```
        === "Tab 1"
            Markdown **content**.

            Multiple paragraphs.

        === "Tab 2"
            More Markdown **content**.

            - list item a
            - list item b

        ===! "Tab A"
            Different tab set.

        === "Tab B"
            ```
            More content.
            ```
        ```

## Styling with CSS

In order to use tabbed blocks, some additional CSS is needed. You can check out the configuration below which will
show the CSS and the HTML it targets. Keep in mind that the CSS provided below is just the minimum needed to get you
started. You can tweak it and modify it to get it how you like it.

In general, tabbed controls are wrapped in a `#!html <div>` with the class `tabbed-set`. They contain an
`#!html <input>` with an ID of `__tabbed_<tab_set_number>_<tab_number>`. All the `#!html <input>` elements from a
specific tab set will use the name `__tabbed_<tab_set_number>`. Particularly, a user should be mindful of the ID to keep
from explicitly using a conflicting ID. Auto-generated slugs shouldn't conflict though.


!!! settings "Tabbed Code Setup"
    This is a very basic setup. Tabs can be styled in different ways, but this shows how to get it functionally working.
    Here we show what it visually looks like, what the generated HTML looks like, and what the CSS looks like.

    === "Preview"
        ![Tabbed Style](../images/tabbed-default.png)

    === "HTML"
        ```html
        <div class="tabbed-set" data-tabs="1:3"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><label for="__tabbed_1_1">Tab with a really long title 1</label><div class="tabbed-content">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        <input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><label for="__tabbed_1_2">Tab with a really long title 2</label><div class="tabbed-content">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        <input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><label for="__tabbed_1_3">Tab with a really long title 3</label><div class="tabbed-content">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        </div>
        ```

    === "CSS"
        ```{.css .md-max-height}
        /* General environment setup */
        html {
          background-color: black;
          height: 100%;
        }
        body {
          font-size: 120%;
          width: 100%;
          padding: 1em 2em;
          margin: 0 auto;
          background-color: white;
          height: 100%;
        }

        /* Tab style starts here */
        .tabbed-set {
          position: relative;
          display: flex;
          flex-wrap: wrap;
          margin: 1em 0;
          border-radius: 0.1rem;
        }

        .tabbed-set > input {
          display: none;
        }

        .tabbed-set label {
          width: auto;
          padding: 0.9375em 1.25em 0.78125em;
          font-weight: 700;
          font-size: 0.84em;
          white-space: nowrap;
          border-bottom: 0.15rem solid transparent;
          border-top-left-radius: 0.1rem;
          border-top-right-radius: 0.1rem;
          cursor: pointer;
          transition: background-color 250ms, color 250ms;
        }

        .tabbed-set .tabbed-content {
          width: 100%;
          display: none;
          box-shadow: 0 -.05rem #ddd;
        }

        .tabbed-set input {
          position: absolute;
          opacity: 0;
        }

        .tabbed-set input:checked:nth-child(n+1) + label {
          color: red;
          border-color: red;
        }

        @media screen {
          .tabbed-set input:nth-child(n+1):checked + label + .tabbed-content {
            order: 99;
            display: block;
          }
        }

        @media print {
          .tabbed-content {
            display: contents;
          }
        }
        ```

## Linked Tabs

Some sites will link tabs across tab containers. The easiest way to explain this is by example. Click a tab below and
see how they seem linked:

=== "Python"

=== "JavaScript"

===! "Python"

=== "JavaScript"

People ask about how to get this functionality from time to time, and the answer is JavaScript. It is the only real way
to get such functionality. While this can be achieved with varying degrees of intelligence -- save preferences across
pages for example -- we've provided a very basic example of how to accomplish this below.

The function, when called, will setup click events for every tab. When clicked, it will look for tabs of the same name
on the page and ensure they also get selected. It works for the default style and the new [alternate style](#alternate-style).

```js
const tabSync = () => {
  const tabs = document.querySelectorAll(".tabbed-set > input")
  for (const tab of tabs) {
    tab.addEventListener("click", () => {
      const current = document.querySelector(`label[for=${tab.id}]`)
      const pos = current.getBoundingClientRect().top
      const labelContent = current.innerHTML
      const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label')
      for (const label of labels) {
        if (label.innerHTML === labelContent) {
          document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true
        }
      }

      // Preserve scroll position
      const delta = (current.getBoundingClientRect().top) - pos
      window.scrollBy(0, delta)
    })
  }
}
```

## Alternate Style

!!! new "New 9.0"

!!! warning "Experimental Feature"

The original idea behind the Tabbed extension was to provide a tabbed interface in Markdown that could be driven purely
by CSS. If JavaScript was disabled or unavailable, tab functionality would remain.

@squidfunk (Martin Donath), the author and maintainer of the [MkDocs Material theme][mkdocs-material], who originally
collaborated on the first Tabbed style, approached us again with a newer style.

The aim was to solve one big problem, namely that on narrower screen sizes (like mobile), tabs are broken onto separate
lines, like here for example.

![Tabbed Style Narrow](../images/tabbed-default-narrow.png)

And this is indeed a real issue with the current style. The bonuses of the current style is that it needs absolutely no
JavaScript and is driven by CSS alone. It supports as many tabs as you can throw at it but the downside is that when all
the tabs do not fit on a single line, they'll wrap which is not aesthetically pleasing in all themes or interfaces.

So, a new style was proposed, and we once again worked to hammer out the details, and in the end, we think we have
something that is better in many ways. The new style works with only CSS and does not actually need any JavaScript to be
functional, but it is ideally enhanced with a little JavaScript. The reason why is that when too many tabs are on a
single line, you can overflow them with CSS and make the tab container scrollable. This can provide a much more
aesthetically pleasing interface, but there are two caveats though:

1. When tabs overflow, if you want a visual indicator that there are more tabs, it requires a little JavaScript.
2. The number of tabs is limited by how many tabs you define in the CSS.

As for the first point, this seems an acceptable restraint. When JavaScript is available, you will get a nice visual
indicator, but if JavaScript is ever not available, or disabled, tab functionality will still work, just without the
indicators.

As for the second point, this also seems acceptable as most pages will usually have some upper limit of tabs that are
ever used. Pages can become quite cluttered when using too many tabs, and it seems reasonable that most would limit them
to some practical number.

!!! settings "Alternate Tabbed Code Setup"
    The example below styles the tabs, adds indicators that work as buttons to navigate to the next tab when the there
    are overflowed tabs, and scrolls tabs into view smoothly.

    === "Preview"
        ![Tabbed Style Alternate](../images/tabbed-alternate.png)

    === "HTML"
        ```html
        <div class="tabbed-set tabbed-alternate" data-tabs="1:3"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Tab with a really long title 1</label><label for="__tabbed_1_2">Tab with a really long title 2</label><label for="__tabbed_1_3">Tab with a really long title 3</label></div>
        <div class="tabbed-content">
        <div class="tabbed-block">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        <div class="tabbed-block">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        <div class="tabbed-block">
        <p>Lorem ipsum ullamco ea aute do sint cupidatat elit nostrud exercitation dolore culpa aliquip nisi commodo nisi qui
        magna non laborum proident id voluptate in cupidatat duis.</p>
        </div>
        </div>
        </div>
        ```

    === "CSS"
        ```{.css .md-max-height}
        /* General environment setup */
        html {
          background-color: black;
          height: 100%;
        }
        body {
          font-size: 120%;
          width: 20em;
          padding: 1em 2em;
          margin: 0 auto;
          background-color: white;
          height: 100%;
        }

        /* Tab style starts here */
        .tabbed-alternate {
          position: relative;
          display: flex;
          flex-wrap: wrap;
          flex-direction: column;
          margin: 1em 0;
          border-radius: 0.1rem;
        }

        .tabbed-alternate > input {
          display: none;
        }

        .tabbed-labels {
          display: flex;
          width: 100%;
          overflow: auto;
          box-shadow: 0 -0.1rem #ddd inset;
          scrollbar-width: none;
        }
        .tabbed-labels::-webkit-scrollbar {
          display: none;
        }
        .tabbed-labels > label {
          width: auto;
          padding: 0.9375em 1.25em 0.78125em;
          font-weight: 700;
          font-size: 0.84em;
          white-space: nowrap;
          border-bottom: 0.1rem solid transparent;
          scroll-snap-align: start;
          border-top-left-radius: 0.1rem;
          border-top-right-radius: 0.1rem;
          cursor: pointer;
          transition: background-color 250ms, color 250ms;
        }
        .tabbed-labels > label:hover {
          color: red;
        }

        .tabbed-labels.tabbed-scroll-left::before {
          display: inline-block;
          font-size: 0.9em;
          position: absolute;
          top: 0.75em;
          left: 0;
          padding-right: 0.64em;
          color: gray;
          background: linear-gradient(to right, rgb(255, 255, 255) 75%, rgba(255, 255, 255, 0));
          content: "\25C0";
          cursor: pointer;
        }

        .tabbed-labels.tabbed-scroll-right::after {
          display: inline-block;
          font-size: 0.9em;
          position: absolute;
          top: 0.75em;
          right: 0;
          padding-left: 0.64em;
          color: gray;
          background: linear-gradient(to right, rgba(255, 255, 255, 0), rgb(255, 255, 255) 25%);
          content: "\25B6";
          cursor: pointer;
        }

        .tabbed-alternate .tabbed-content {
          width: 100%;
        }
        .tabbed-alternate input:nth-child(1):checked ~ .tabbed-content > :nth-child(1),
        .tabbed-alternate input:nth-child(2):checked ~ .tabbed-content > :nth-child(2),
        .tabbed-alternate input:nth-child(3):checked ~ .tabbed-content > :nth-child(3) {
          display: block;
        }
        .tabbed-alternate .tabbed-block {
          display: none;
        }
        @media screen {
          .tabbed-alternate input:nth-child(1):checked ~ .tabbed-labels > :nth-child(1),
          .tabbed-alternate input:nth-child(2):checked ~ .tabbed-labels > :nth-child(2),
          .tabbed-alternate input:nth-child(3):checked ~ .tabbed-labels > :nth-child(3) {
            color: red;
            border-color: red;
          }
        }
        @media print {
          .tabbed-labels {
            display: contents;
          }
          .tabbed-labels > label:nth-child(1) {
            order: 1;
          }
          .tabbed-labels > label:nth-child(2) {
            order: 2;
          }
          .tabbed-labels > label:nth-child(3) {
            order: 3;
          }

          .tabbed-alternate .tabbed-content {
            display: contents;
          }
          .tabbed-alternate .tabbed-block {
            display: block;
          }
          .tabbed-alternate .tabbed-block:nth-child(1) {
            order: 1;
          }
          .tabbed-alternate .tabbed-block:nth-child(2) {
            order: 2;
          }
          .tabbed-alternate .tabbed-block:nth-child(3) {
            order: 3;
          }
        }
        ```

    === "JS"
        ```{.js .md-max-height}
        // Identify whether a tab bar can be scrolled left or right and apply indicator classes 
        const tabOverflow = () => {
          const checkScroll = e => {
            // Use a margin as we just don't always align exactly on the right.
            const margin = 3
            const target = e.target
            if (!e.target.matches('.tabbed-labels')) {
              return
            }

            const scrollWidth = target.scrollWidth - target.clientWidth
            target.classList.remove('tabbed-scroll-left', 'tabbed-scroll-right')
            if (e.type === "resize" || e.type === "scroll") {
              if (scrollWidth === 0) {
                return
              }
              if (!target.scrollLeft) {
                target.classList.add('tabbed-scroll-right')
              } else if (target.scrollLeft < scrollWidth - margin){
                target.classList.add('tabbed-scroll-left', 'tabbed-scroll-right')
              } else {
                target.classList.add('tabbed-scroll-left')
              }
            }
          }

          // Change the tab to either the previous or next input - depending on which indicator was clicked.
          // Make sure the current, selected input is scrolled into view.
          const tabChange = e => {
            const target = e.target
            const selected = target.closest('.tabbed-set').querySelector('input:checked')
            let updated = null

            if (target.classList.contains('tabbed-scroll-right') && e.offsetX >= e.target.offsetWidth - 15) {
              const sib = selected.nextSibling
              updated = selected
              if (sib && sib.tagName === 'INPUT') {
                updated = sib
              }
            } else if (target.classList.contains('tabbed-scroll-left') && e.offsetX <= 15) {
              const sib = selected.previousSibling
              updated = selected
              if (sib && sib.tagName === 'INPUT') {
                updated = sib
              }
            }
            if (updated) {
              updated.click()
            }
          }

          const onResize = new ResizeObserver(entries => {
            entries.forEach(entry => {
              checkScroll({target: entry.target, type: 'resize'})
            })
          })

          const labels = document.querySelectorAll('.tabbed-alternate > .tabbed-labels')
          labels.forEach(el => {
            checkScroll({target: el, type: 'resize'})
            onResize.observe(el)
            el.addEventListener('resize', checkScroll)
            el.addEventListener('scroll', checkScroll)
            el.addEventListener('click', tabChange)
          })
        }

        // Smooth scroll tab into view when changed
        const tabScroll = () => {
          const tabs = document.querySelectorAll(".tabbed-alternate > input")
          for (const tab of tabs) {
            tab.addEventListener("change", () => {
              const label = document.querySelector(`label[for=${tab.id}]`)
              label.scrollIntoView({block: "nearest", inline: "nearest", behavior: "smooth"})
            })
          }
        }

        // Should run after document loaded, but for illustration purposes:
        tabOverflow()
        tabScroll()
        ```

## Options

Option            | Type | Default       | Description
----------------- | ---- | ------------- | -----------
`alternate_style` | bool | `#!py3 False` | Use the experimental, alternative style.

--8<-- "refs.txt"
