Skip to content

Tabs

JS

Tabs 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.

ClassTypeDescription
k-tabscomponentThe one class you write. The element generates the list, tabs, and ink inside it.
k-tabs--lgmodifierFixed 10rem tabs with larger padding and type.
k-tabs--no-keyboardmodifierTurns off arrow keys, Home, and End.
Generated classes
ClassTypeDescription
k-tabs__listpartThe tablist the element builds.
k-tabs__inkpartThe primary fill that slides under the selected tab.
k-tabs__tabpartOne tab button.
k-tabs__panelpartPut on each linked panel. Hidden ones carry hidden and inert.

options is an array, one entry per panel, in panel order. The length has to match the panels the element finds or it throws.

OptionTypeDefaultDescription
labelstring—Tab text. Required.
iconKIconName—A kit icon name. The glyph sits before the label and shrinks to 1em.
PropertyTypeDescription
optionsKTabItem[]Read/write. Writing rebuilds the tablist.
countnumberRead-only. Number of tabs.
tabsHTMLElement[]Read-only copy of the tab buttons.
labelsstring[]Read-only. Tab labels in order.
selectedIndexnumberRead-only. Index of the open panel, or -1 before the element has content.
MethodReturnsDescription
getSelected()KTabsSelection | nullThe open tab as { index, tab, panel, label }, or null when nothing is built.
getTab(index)HTMLElement | nullThe tab button at an index.
getPanel(index)HTMLElement | nullThe linked panel at an index.
select(index, { focus })voidOpens a panel and fires k-change. Pass focus: true to move focus to the tab.
selectByLabel(label, { focus })booleanOpens the first panel whose label matches. Returns false on a miss.
next({ wrap, focus })voidOpens the following panel. Wraps past the last one unless wrap is false.
previous({ wrap, focus })voidOpens the preceding panel, wrapping the same way.
refresh()voidRelinks the panels and rebuilds the tablist.
disconnect()voidRemoves 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.

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>
</>
);
}

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>
</>
);
}

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.

Do

  • Give the host a unique id and number the panels from -0.
  • Set options once, 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.