Tabs
JSTabs switch related panels in place: overview / usage / API, or three views of the same object. Don’t use them for wizard steps or for a sequence the reader has to walk in order.
Give the host an id, write the panels as #id-0, #id-1, and so on, and hand the element the labels:
<k-tabs id="sections" class="k-tabs" aria-label="Sections"></k-tabs><div id="sections-0">The first panel.</div><div id="sections-1">The second panel.</div>document.getElementById('sections').options = [ { label: 'Overview' }, { label: 'Usage' },];The panels are your HTML. They stay where you put them, so a panel can sit anywhere on the page, hold any markup, and be rendered by any framework. The element only builds the tablist and wires the ARIA. Panels that arrive late are picked up when they land.
Tabs sit flush and the selected chrome is primary. Switching a tab slides that fill through the tabs in between, then fades the panel. Hidden panels get hidden and inert. Setting options rebuilds the tablist; a class or aria-label change patches in place. Reduced motion drops the motion.
Classes
Section titled “Classes”| Class | Type | Description |
|---|---|---|
k-tabs | component | The one class you write. The element generates the list, tabs, and ink inside it. |
k-tabs--lg | modifier | Fixed 10rem tabs with larger padding and type. |
k-tabs--no-keyboard | modifier | Turns off arrow keys, Home, and End. |
Generated classes
| Class | Type | Description |
|---|---|---|
k-tabs__list | part | The tablist the element builds. |
k-tabs__ink | part | The primary fill that slides under the selected tab. |
k-tabs__tab | part | One tab button. |
k-tabs__panel | part | Put on each linked panel. Hidden ones carry hidden and inert. |
Options
Section titled “Options”options is an array, one entry per panel, in panel order. The length has to match the panels the element finds or it throws.
| Option | Type | Default | Description |
|---|---|---|---|
label | string | — | Tab text. Required. |
icon | KIconName | — | A kit icon name. The glyph sits before the label and shrinks to 1em. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
options | KTabItem[] | Read/write. Writing rebuilds the tablist. |
count | number | Read-only. Number of tabs. |
tabs | HTMLElement[] | Read-only copy of the tab buttons. |
labels | string[] | Read-only. Tab labels in order. |
selectedIndex | number | Read-only. Index of the open panel, or -1 before the element has content. |
Methods
Section titled “Methods”| Method | Returns | Description |
|---|---|---|
getSelected() | KTabsSelection | null | The open tab as { index, tab, panel, label }, or null when nothing is built. |
getTab(index) | HTMLElement | null | The tab button at an index. |
getPanel(index) | HTMLElement | null | The linked panel at an index. |
select(index, { focus }) | void | Opens a panel and fires k-change. Pass focus: true to move focus to the tab. |
selectByLabel(label, { focus }) | boolean | Opens the first panel whose label matches. Returns false on a miss. |
next({ wrap, focus }) | void | Opens the following panel. Wraps past the last one unless wrap is false. |
previous({ wrap, focus }) | void | Opens the preceding panel, wrapping the same way. |
refresh() | void | Relinks the panels and rebuilds the tablist. |
disconnect() | void | Removes listeners without removing the element from the page. |
Selecting a tab dispatches k-change with { index }, and the event bubbles. select() fires it too, so one listener covers clicks, keys, and your own calls.
Examples
Section titled “Examples”Three panels
Section titled “Three panels”Click a tab or move with the arrow keys once one is focused. The first panel holds a gauge, the last a form field. Panels are markup, so anything goes in them.
The other two panels are components. This one is a paragraph.
<k-tabs id="sections" class="k-tabs" aria-label="Sections"></k-tabs>
<div id="sections-0"><k-gauge id="upload" class="k-gauge"></k-gauge></div><div id="sections-1"><p>The other two panels are components. This one is a paragraph.</p></div><div id="sections-2"><div class="k-field"> <label class="k-label" for="email">Email</label> <input class="k-input" id="email" type="text" placeholder="maya@example.com" /></div><button type="button" class="k-btn k-btn--primary">Send</button></div>import 'k-web-ui/js';
document.getElementById('sections').options = [{ label: 'Progress' },{ label: 'Notes' },{ label: 'Invite' },];
document.getElementById('upload').options = {value: 64,max: 100,label: 'Upload',format: '%',};import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';import 'k-web-ui/js';
@Component({ selector: 'app-example', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <k-tabs id="sections" class="k-tabs" aria-label="Sections"></k-tabs>
<div id="sections-0"> <k-gauge id="upload" class="k-gauge"></k-gauge> </div> <div id="sections-1"> <p>The other two panels are components. This one is a paragraph.</p> </div> <div id="sections-2"> <div class="k-field"> <label class="k-label" for="email">Email</label> <input class="k-input" id="email" type="text" placeholder="maya@example.com" /> </div> <button type="button" class="k-btn k-btn--primary">Send</button> </div> `,})export class ExampleComponent implements AfterViewInit { ngAfterViewInit() { document.getElementById('sections').options = [ { label: 'Progress' }, { label: 'Notes' }, { label: 'Invite' }, ];
document.getElementById('upload').options = { value: 64, max: 100, label: 'Upload', format: '%', }; }}import { useEffect } from 'react';import 'k-web-ui/js';
export function Example() { useEffect(() => { document.getElementById('sections').options = [ { label: 'Progress' }, { label: 'Notes' }, { label: 'Invite' }, ];
document.getElementById('upload').options = { value: 64, max: 100, label: 'Upload', format: '%', }; }, []);
return ( <> <k-tabs id="sections" className="k-tabs" aria-label="Sections"></k-tabs>
<div id="sections-0"> <k-gauge id="upload" className="k-gauge"></k-gauge> </div> <div id="sections-1"> <p>The other two panels are components. This one is a paragraph.</p> </div> <div id="sections-2"> <div className="k-field"> <label className="k-label" htmlFor="email">Email</label> <input className="k-input" id="email" type="text" placeholder="maya@example.com" /> </div> <button type="button" className="k-btn k-btn--primary">Send</button> </div> </> );}With icons
Section titled “With icons”icon is a kit name like info.
The other two panels are components. This one is a paragraph.
<k-tabs id="icon-tabs" class="k-tabs" aria-label="Sections"></k-tabs><div id="icon-tabs-0">…</div><div id="icon-tabs-1">…</div><div id="icon-tabs-2">…</div>import 'k-web-ui/js';
document.getElementById('icon-tabs').options = [{ label: 'Progress', icon: 'info' },{ label: 'Notes', icon: 'success' },{ label: 'Invite', icon: 'warning' },];import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';import 'k-web-ui/js';
@Component({ selector: 'app-example', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <k-tabs id="icon-tabs" class="k-tabs" aria-label="Sections"></k-tabs> <div id="icon-tabs-0">…</div> <div id="icon-tabs-1">…</div> <div id="icon-tabs-2">…</div> `,})export class ExampleComponent implements AfterViewInit { ngAfterViewInit() { document.getElementById('icon-tabs').options = [ { label: 'Progress', icon: 'info' }, { label: 'Notes', icon: 'success' }, { label: 'Invite', icon: 'warning' }, ]; }}import { useEffect } from 'react';import 'k-web-ui/js';
export function Example() { useEffect(() => { document.getElementById('icon-tabs').options = [ { label: 'Progress', icon: 'info' }, { label: 'Notes', icon: 'success' }, { label: 'Invite', icon: 'warning' }, ]; }, []);
return ( <> <k-tabs id="icon-tabs" className="k-tabs" aria-label="Sections"></k-tabs> <div id="icon-tabs-0">…</div> <div id="icon-tabs-1">…</div> <div id="icon-tabs-2">…</div> </> );}k-tabs--lg makes every tab a fixed 10rem wide, with larger padding and type.
The other two panels are components. This one is a paragraph.
<k-tabs id="lg-tabs" class="k-tabs k-tabs--lg" aria-label="Sections"></k-tabs><div id="lg-tabs-0">…</div><div id="lg-tabs-1">…</div><div id="lg-tabs-2">…</div>import 'k-web-ui/js';
document.getElementById('lg-tabs').options = [{ label: 'Progress', icon: 'info' },{ label: 'Notes', icon: 'success' },{ label: 'Invite', icon: 'warning' },];import { Component, CUSTOM_ELEMENTS_SCHEMA, type AfterViewInit } from '@angular/core';import 'k-web-ui/js';
@Component({ selector: 'app-example', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <k-tabs id="lg-tabs" class="k-tabs k-tabs--lg" aria-label="Sections"></k-tabs> <div id="lg-tabs-0">…</div> <div id="lg-tabs-1">…</div> <div id="lg-tabs-2">…</div> `,})export class ExampleComponent implements AfterViewInit { ngAfterViewInit() { document.getElementById('lg-tabs').options = [ { label: 'Progress', icon: 'info' }, { label: 'Notes', icon: 'success' }, { label: 'Invite', icon: 'warning' }, ]; }}import { useEffect } from 'react';import 'k-web-ui/js';
export function Example() { useEffect(() => { document.getElementById('lg-tabs').options = [ { label: 'Progress', icon: 'info' }, { label: 'Notes', icon: 'success' }, { label: 'Invite', icon: 'warning' }, ]; }, []);
return ( <> <k-tabs id="lg-tabs" className="k-tabs k-tabs--lg" aria-label="Sections"></k-tabs> <div id="lg-tabs-0">…</div> <div id="lg-tabs-1">…</div> <div id="lg-tabs-2">…</div> </> );}Accessibility
Section titled “Accessibility”The element builds role="tablist" and role="tab" on the list and buttons it generates, and puts role="tabpanel", aria-labelledby, and k-tabs__panel on each linked panel. Tabs carry aria-selected and aria-controls. Closed panels get hidden and inert, so nothing inside them takes focus. Arrow keys, Home, and End move between tabs unless you add k-tabs--no-keyboard. aria-label on the host names the tablist; without one the element falls back to the host id. Decorative icons are aria-hidden, so keep the name in the tab text.
A popover or a <dialog> inside a closed panel is inert with it. Keep those as siblings of the host.
Dos and don’ts
Section titled “Dos and don’ts”Do
- Give the host a unique
idand number the panels from-0. - Set
optionsonce, with one entry per panel. - Use tabs for related views of the same object.
- Write panel content as plain markup, wherever it belongs on the page.
Don’t
- Hand-write the tablist. The element builds it.
- Skip a number. The element stops linking at the first gap.
- Use tabs as wizard steps.
- Rely on the icon alone for the tab name.