Skip to content

Tab

<loomi-tabs> builds a heading bar from its <loomi-tab> children and toggles which panel is visible. There is no separate heading/body/content wiring to keep in sync — each <loomi-tab> carries its own heading (label/icon) and its own panel content together, so there’s nothing to name-match by hand.

Terminal window
npm install @loomidev/tab lit
import "@loomidev/tab";

Wrap any number of <loomi-tab> elements in a <loomi-tabs>. The tab marked active is selected by default — it doesn’t have to be the first one. If none is marked active, the first non-disabled tab is selected automatically.

Account profile

Update the public name, team role, and contact email shown across the workspace.

Save profile

Security

Require two-step verification and review the devices that are currently signed in.

Manage devices

Notifications

Choose which product updates, approvals, and billing alerts should send email.

<loomi-tabs>
<loomi-tab label="Profile" active>
<h3>Account profile</h3>
<p>Update the public name, team role, and contact email shown across the workspace.</p>
<loomi-button size="small">Save profile</loomi-button>
</loomi-tab>
<loomi-tab label="Security">
<h3>Security</h3>
<p>Require two-step verification and review the devices that are currently signed in.</p>
<loomi-button size="small" type="secondary">Manage devices</loomi-button>
</loomi-tab>
<loomi-tab label="Notifications">
<h3>Notifications</h3>
<p>Choose which product updates, approvals, and billing alerts should send email.</p>
<loomi-toggle label="Weekly summary" checked></loomi-toggle>
</loomi-tab>
</loomi-tabs>

Listen for loomi-tab-change on <loomi-tabs> if you need to react to the switch (e.g. lazy- loading a panel’s data):

document.querySelector("loomi-tabs").addEventListener("loomi-tab-change", (e) => {
console.log(e.detail.label); // the newly active tab's label
});

The active tab’s underline (or background, depending on style — see below) uses the default. Set color on <loomi-tabs> to pick a different one; it applies to every child tab.

2 open incidents are affecting checkout. The team was paged 6 minutes ago.

14 incidents were resolved this week, with a median time-to-fix of 22 minutes.

All 312 checks are passing on the main branch as of the last deploy.

4 checks are skipped for this branch because they only run on release tags.

<loomi-tabs color="error">
<loomi-tab label="Incidents" active>
<p>2 open incidents are affecting checkout. The team was paged 6 minutes ago.</p>
</loomi-tab>
<loomi-tab label="Resolved">
<p>14 incidents were resolved this week, with a median time-to-fix of 22 minutes.</p>
</loomi-tab>
</loomi-tabs>
<loomi-tabs color="success">
<loomi-tab label="Passing" active>
<p>All 312 checks are passing on the main branch as of the last deploy.</p>
</loomi-tab>
<loomi-tab label="Skipped">
<p>4 checks are skipped for this branch because they only run on release tags.</p>
</loomi-tab>
</loomi-tabs>

Common semantic colors: primary secondary info success error warning gray.

<loomi-tabs> comes in three styles, set via tab-style. The default is simple (an underlined heading row, as in every example above).

When the active tab changes, the heading indicator slides to the next tab. In simple style the underline moves; in system and pills styles the selected surface moves.

A segmented-control look — the active tab gets a raised pill inside a tinted track.

<loomi-tabs tab-style="system">
<loomi-tab label="Monthly" active></loomi-tab>
<loomi-tab label="Yearly"></loomi-tab>
</loomi-tabs>

The same segmented-control look as system, with fully rounded tab buttons.

24 open conversations 5 conversations need a reply 128 resolved conversations
<loomi-tabs tab-style="pills">
<loomi-tab label="All" active>24 open conversations</loomi-tab>
<loomi-tab label="Unread">5 conversations need a reply</loomi-tab>
<loomi-tab label="Archived">128 resolved conversations</loomi-tab>
</loomi-tabs>

Set icon on a <loomi-tab> to prefix its heading with an icon from the shared @loomidev/icons registry. Works in any of the three styles.

Your workspace is on the Team plan with 8 of 10 seats in use.

Ada invited a new member and Sara updated the billing contact today.

Two-step verification is required for all admins and enabled for 6 of 8 members.

<loomi-tabs>
<loomi-tab label="Overview" icon="information-circle" active>
<p>Your workspace is on the Team plan with 8 of 10 seats in use.</p>
</loomi-tab>
<loomi-tab label="Activity" icon="bell-alert">
<p>Ada invited a new member and Sara updated the billing contact today.</p>
</loomi-tab>
<loomi-tab label="Security" icon="lock-closed">
<p>Two-step verification is required for all admins and enabled for 6 of 8 members.</p>
</loomi-tab>
</loomi-tabs>

Need an icon that isn’t built in? Register your own — no need to fork the registry:

import { registerLoomiIcon } from "@loomidev/icons";
import { svg } from "lit";
registerLoomiIcon("rocket", svg`<path d="…" />`);
<loomi-tab label="Launches" icon="rocket"></loomi-tab>

Set disabled to fade out a tab and ignore clicks on it (and skip it during keyboard navigation — see below):

Arrives in 5-7 business days. Free on orders over $50.

Not available for this address yet.

<loomi-tabs>
<loomi-tab label="Standard Shipping" active>
<p>Arrives in 5-7 business days. Free on orders over $50.</p>
</loomi-tab>
<loomi-tab label="Same-Day Delivery" disabled>
<p>Not available for this address yet.</p>
</loomi-tab>
</loomi-tabs>

Set url instead of relying on the built-in panel switching to make a tab behave like a plain link — clicking it navigates via location.href rather than showing a panel:

Quick glance: 1,284 active users, 42 open invites, $12.8k in monthly spend.

<loomi-tabs>
<loomi-tab label="Dashboard" active>
<p>Quick glance: 1,284 active users, 42 open invites, $12.8k in monthly spend.</p>
</loomi-tab>
<loomi-tab label="Full Settings →" url="/settings"></loomi-tab>
</loomi-tabs>

<loomi-tabs> implements the WAI-ARIA APG “tabs” pattern out of the box: once a tab heading has focus, / (or /) move between tabs and switch the active panel immediately (automatic activation), and Home/End jump to the first/last enabled tab. Disabled tabs are skipped. No setup required — this works the same in every style.

  • See APG link in README.

For the library-wide baseline, see Foundations — Accessibility.

For the shared container and viewport rules, see Foundations — Responsive behavior.

For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.

AttributeDefaultDescription
colorprimaryActive-tab color. Any loomi color.
tab-stylesimplesimple | system | pills
AttributeDefaultDescription
label(blank)Heading text.
icon(blank)Heading icon name (see @loomidev/icons).
activefalseSelected by default. (boolean)
disabledfalseDisabled tab — faded out, ignores clicks and keyboard focus. (boolean)
url(blank)Navigate to this URL instead of switching panels.
SlotDescription
(default)Content placed inside the component.
EventDescription
loomi-tab-changeFired when the tab changes.

Revenue is up 12% this month and all scheduled payouts have cleared.

Three payment disputes need a response before Friday.

Nothing archived yet.

<loomi-tabs color="success" tab-style="pills">
<loomi-tab label="Overview" icon="information-circle" active>
<p>Revenue is up 12% this month and all scheduled payouts have cleared.</p>
</loomi-tab>
<loomi-tab label="Activity" icon="bell-alert">
<p>Three payment disputes need a response before Friday.</p>
</loomi-tab>
<loomi-tab label="Archived" disabled>
<p>Nothing archived yet.</p>
</loomi-tab>
<loomi-tab label="Full report →" url="/reports/full"></loomi-tab>
</loomi-tabs>

<loomi-tab> and <loomi-tabs> are standard custom elements, so the browser can use them in plain HTML, Blade, React, Vue, Angular, Svelte, Astro, and most other frameworks. The important beginner rule is: install the package, import it once before the tag is rendered, then write the Loomi tag in your template.

Run install commands from the app where you want to use this component. That means the folder that contains that app’s package.json. Do not run these install commands from packages/tab unless you are editing LoomiUI itself.

Terminal window
cd /path/to/your-app
npm install @loomidev/tab lit

If you are contributing to LoomiUI itself, first move to the top-level components folder. That is where the main package.json for all packages lives, and pnpm --filter ... commands should be run from there:

Terminal window
cd /path/to/your-copy-of-loomiui/components
pnpm --filter @loomidev/tab build
pnpm --filter @loomidev/tab typecheck

Use the CDN version for prototypes, documentation pages, or a quick reproduction. The import map tells the browser where to find Lit, which Loomi components use internally.

<script type="importmap">
{ "imports": { "lit": "https://esm.sh/lit@3.3.3", "lit/": "https://esm.sh/lit@3.3.3/" } }
</script>
<script type="module" src="https://esm.sh/@loomidev/tab"></script>
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>

In Vite, Webpack, Parcel, Rollup, or a framework build pipeline, install the package and import it once in your main app JavaScript file. After that, you can use the Loomi tag anywhere in your app.

import "@loomidev/tab";

Run the install command from your Laravel project root, then import the component in resources/js/app.js. If your project uses Laravel Vite, npm run dev and npm run build should also be run from the Laravel project root.

Terminal window
cd /path/to/your-laravel-app
npm install @loomidev/tab lit
npm run dev
resources/js/app.js
import "@loomidev/tab";
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>

React can render Loomi tags directly. If you are on React 18, or if you need to pass arrays, objects, or functions, use a ref and assign those values after the component mounts.

import "@loomidev/tab";
export function LoomiExample() {
return (
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>
);
}

If TypeScript does not recognize the Loomi tag in JSX, add it to your app’s JSX type declarations.

Import the package in the component that uses it, or once in your main Vue file. Vue templates can use Loomi tags directly. For arrays, objects, or functions, pass the value as a JavaScript property instead of as plain text.

<script setup>
import "@loomidev/tab";
</script>
<template>
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>
</template>

If Vue warns that the tag is an unknown component, configure compilerOptions.isCustomElement for tags that start with loomi- in your Vite or Vue config.

Import the package once and tell Angular to allow custom HTML tags with CUSTOM_ELEMENTS_SCHEMA. For NgModule apps, add the schema to the module instead of the standalone component.

app.component.ts
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@loomidev/tab";
@Component({
selector: "app-root",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>
`,
})
export class AppComponent {}

Svelte can import the package inside a component script. Astro can import it in the frontmatter of the page or layout where the tag appears.

<script>
import "@loomidev/tab";
</script>
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>
---
import "@loomidev/tab";
---
<loomi-tabs>
<loomi-tab label="Overview" active>Account summary</loomi-tab>
<loomi-tab label="Invoices">Recent invoices</loomi-tab>
</loomi-tabs>

Frameworks such as Next.js, Nuxt, SvelteKit, and Astro sometimes render HTML on the server before browser-only code runs. If your framework complains, move the Loomi import to client-side code. In Next.js, that usually means a component with "use client"; in Nuxt, it often means a .client.ts plugin.

  • @loomidev/core
  • @loomidev/icons