# Tabs / Pestañas

> Switches views; the indicator slides under the active tab.

Page: https://componentes-lovat.vercel.app/c/tabs/  
License: MIT · Design: coss ui (MIT) · tabs.tsx  
Requires: tokens.css  
Context: ambos · Density: media · Tone: sobria

## Files

- Tokens (once): https://componentes-lovat.vercel.app/tokens.css
- HTML: https://componentes-lovat.vercel.app/raw/tabs/tabs.html
- CSS: https://componentes-lovat.vercel.app/raw/tabs/tabs.css
- JS: https://componentes-lovat.vercel.app/raw/tabs/tabs.js
- Everything in one file: https://componentes-lovat.vercel.app/r/tabs.html

Dark theme: add class `dark` to `<html>`.

## When to use

- For 2 to 6 views of one topic.

## When not to use

- If each tab changes the URL: those are links.

## Accessibility

Roles: tablist, tab, tabpanel

| Keys | Action |
|---|---|
| ← / → | Previous or next tab |
| Inicio / Fin | First or last |

- W3C pattern with automatic activation and roving tabindex.

## Code

```html
<div class="c-tabs" data-c-tabs>
  <div class="c-tabs__list" role="tablist" aria-label="Cuenta">
    <button class="c-tabs__tab" role="tab" type="button" id="tabs-account" aria-controls="tabs-account-panel" aria-selected="true">Cuenta</button>
    <button class="c-tabs__tab" role="tab" type="button" id="tabs-password" aria-controls="tabs-password-panel" aria-selected="false" tabindex="-1">Contraseña</button>
    <button class="c-tabs__tab" role="tab" type="button" id="tabs-team" aria-controls="tabs-team-panel" aria-selected="false" tabindex="-1">Equipo</button>
    <span class="c-tabs__indicator" aria-hidden="true"></span>
  </div>
  <div class="c-tabs__panel" role="tabpanel" id="tabs-account-panel" aria-labelledby="tabs-account" tabindex="0"><p>Cambia tu nombre, tu foto y tu correo.</p></div>
  <div class="c-tabs__panel" role="tabpanel" id="tabs-password-panel" aria-labelledby="tabs-password" tabindex="0" hidden><p>Actualiza tu contraseña y activa la verificación en dos pasos.</p></div>
  <div class="c-tabs__panel" role="tabpanel" id="tabs-team-panel" aria-labelledby="tabs-team" tabindex="0" hidden><p>Invita a tu equipo y define quién puede editar.</p></div>
</div>
```

```css
/* tabs · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).
   Generado por tools/build_css.py desde tabs.tw.json. No editar a mano. Necesita tokens.css. */
.c-tabs__indicator{position:absolute}
.c-tabs__list{position:relative}
.c-tabs__tab{position:relative}
.c-tabs__indicator{bottom:calc(var(--spacing)*0);left:calc(var(--spacing)*0);z-index:calc(1*-1)}
.c-tabs__list{z-index:0}
.c-tabs{display:flex}
.c-tabs__list{display:flex}
.c-tabs__tab{display:flex}
.c-tabs__indicator{height:var(--active-tab-height)}
.c-tabs__tab{height:calc(var(--spacing)*8.5)}
.c-tabs__indicator{width:var(--active-tab-width)}
.c-tabs__list{width:fit-content}
.c-tabs__panel{flex:1}
.c-tabs__tab{flex-shrink:0;flex-grow:1}
.c-tabs__indicator{--tw-translate-x:var(--active-tab-left);translate:var(--tw-translate-x)var(--tw-translate-y);--tw-translate-y:calc(var(--active-tab-bottom)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}
.c-tabs__tab{cursor:pointer}
.c-tabs{flex-direction:column}
.c-tabs__list{align-items:center}
.c-tabs__tab{align-items:center}
.c-tabs__list{justify-content:center}
.c-tabs__tab{justify-content:center;gap:calc(var(--spacing)*1.5)}
.c-tabs{gap:calc(var(--spacing)*2)}
.c-tabs__list{column-gap:calc(var(--spacing)*.5);border-radius:var(--radius)}
.c-tabs__indicator{border-radius:calc(var(--radius) - 2px)}
.c-tabs__tab{border-radius:calc(var(--radius) - 2px);border-style:var(--tw-border-style);border-width:1px;border-color:#0000}
.c-tabs__indicator{background-color:var(--background)}
.c-tabs__list{background-color:var(--muted);padding:calc(var(--spacing)*.5)}
.c-tabs__tab{padding-inline:calc(calc(var(--spacing)*2.5) - 1px);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}
.c-tabs__panel{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
.c-tabs__tab{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap}
.c-tabs__list{color:var(--muted-foreground)}
.c-tabs__panel{color:var(--muted-foreground)}
.c-tabs__list{color:var(--muted-foreground)}
@supports (color:color-mix(in lab, red, red)){
.c-tabs__list{color:color-mix(in oklab,var(--muted-foreground)72%,transparent)}
}
.c-tabs__indicator{--tw-shadow-alpha:5%;--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000000d),0 1px 2px -1px var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}
@supports (color:lab(0% 0 0)){
.c-tabs__indicator{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,lab(0% 0 0/.05)),0 1px 2px -1px var(--tw-shadow-color,lab(0% 0 0/.05))}
}
.c-tabs__tab{transition-property:color,background-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}
.c-tabs__indicator{transition-property:width,translate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s;--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}
.c-tabs__tab{--tw-outline-style:none;outline-style:none}
.c-tabs__panel{--tw-outline-style:none;outline-style:none}
@media (hover:hover){
.c-tabs__tab:hover{color:var(--muted-foreground)}
}
.c-tabs__tab:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--ring)}
.c-tabs__tab[aria-selected=true]{color:var(--foreground)}
.c-tabs__tab:disabled{pointer-events:none;opacity:.64}
@media (min-width:40rem){
.c-tabs__tab{height:calc(var(--spacing)*7.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
}
.c-tabs__indicator:is(.dark *){background-color:var(--input)}
.c-tabs__tab svg{pointer-events:none;flex-shrink:0}
.c-tabs__tab svg:not([class*=opacity-]){opacity:.8}
.c-tabs__tab svg:not([class*=size-]){width:calc(var(--spacing)*4.5);height:calc(var(--spacing)*4.5)}
@media (min-width:40rem){
.c-tabs__tab svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}
}
/* propio */
.c-tabs__tab{background:transparent}
.c-tabs__panel:focus-visible{outline:2px solid var(--ring);outline-offset:2px;border-radius:var(--radius)}
.c-tabs__indicator:not([style]){display:none}
.c-tabs__panel p{max-width:60ch;padding:.5rem .25rem}
```

```js
/* Pestañas (APG, activación automática) con el indicador que se desliza bajo la pestaña activa. */
(function () {
  const C = (window.Componentes = window.Componentes || {});

  function place(list, tab) {
    const ind = list.querySelector('.c-tabs__indicator');
    if (!ind || !tab) return;
    const lr = list.getBoundingClientRect(), tr = tab.getBoundingClientRect();
    ind.style.setProperty('--active-tab-left', (tr.left - lr.left - list.clientLeft) + 'px');
    ind.style.setProperty('--active-tab-width', tr.width + 'px');
    ind.style.setProperty('--active-tab-height', tr.height + 'px');
    ind.style.setProperty('--active-tab-bottom', (lr.bottom - tr.bottom - list.clientTop) + 'px');
  }

  function setup(root) {
    const list = root.querySelector('[role="tablist"]');
    const tabs = [...list.querySelectorAll('[role="tab"]')];
    if (!tabs.length) return;
    const select = (tab, focus) => {
      tabs.forEach((t) => {
        const on = t === tab;
        t.setAttribute('aria-selected', String(on));
        t.tabIndex = on ? 0 : -1;
        const panel = document.getElementById(t.getAttribute('aria-controls'));
        if (panel) panel.hidden = !on;
      });
      if (focus) tab.focus();
      place(list, tab);
    };
    select(tabs.find((t) => t.getAttribute('aria-selected') === 'true') || tabs[0], false);
    list.addEventListener('click', (e) => { const t = e.target.closest('[role="tab"]'); if (t && !t.disabled) select(t, false); });
    list.addEventListener('keydown', (e) => {
      const enabled = tabs.filter((t) => !t.disabled);
      const i = enabled.indexOf(document.activeElement);
      if (i < 0) return;
      const next = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: enabled.length - 1 }[e.key];
      if (next === undefined) return;
      e.preventDefault();
      select(enabled[(next + enabled.length) % enabled.length], true);
    });
    new ResizeObserver(() => place(list, tabs.find((t) => t.getAttribute('aria-selected') === 'true'))).observe(list);
  }

  function init(root) {
    (root || document).querySelectorAll('[data-c-tabs]:not([data-c-ready])').forEach((el) => {
      el.dataset.cReady = '';
      try { setup(el); } catch (e) { console.warn('[tabs]', e); }
    });
  }

  C.tabs = { init };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());
  else init();
})();
```

## Credits

- coss ui (MIT) · tabs.tsx: https://coss.com/ui
- W3C WAI-ARIA APG, Tabs: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
