@loomidev/tab
v0.3.0
Published
<loomi-tabs>/<loomi-tab> — tabbed content (simple/system/pills styles).
Maintainers
Readme
@loomidev/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.
npm install @loomidev/tab litimport "@loomidev/tab";Basic Usage
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.
<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
});Different Colors
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.
<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.
Other Tab Styles
<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.
System Tab Style
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>Pills Tab Style
The same segmented-control look as system, with fully rounded tab buttons.
<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>With Icons
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.
<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>Disabled Tabs & Tabs That Navigate
Set disabled to fade out a tab and ignore clicks on it (and skip it during
keyboard navigation — see below):
<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:
<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>Keyboard Navigation
<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.
Accessibility
- See APG link in README.
For the library-wide baseline, see Foundations — Accessibility.
Responsive behavior
For the shared container and viewport rules, see Foundations — Responsive behavior.
Dark mode
For theme activation, token overrides, and contrast guidance, see Foundations — Dark mode.
Attributes
<loomi-tabs>
| Attribute | Default | Description |
| ----------- | ------- | ---------------------------------- |
| color | primary | Active-tab color. Any loomi color. |
| tab-style | simple | simple | system | pills |
<loomi-tab>
| Attribute | Default | Description |
| ---------- | --------- | ------------------------------------------------------------------------ |
| label | (blank) | Heading text. |
| icon | (blank) | Heading icon name (see @loomidev/icons). |
| active | false | Selected by default. (boolean) |
| disabled | false | Disabled tab — faded out, ignores clicks and keyboard focus. (boolean) |
| url | (blank) | Navigate to this URL instead of switching panels. |
Slots
| Slot | Description | | ----------- | ------------------------------------ | | (default) | Content placed inside the component. |
Events
| Event | Description |
| ------------------ | --------------------------- |
| loomi-tab-change | Fired when the tab changes. |
Full Example
<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>Framework integration
<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.
Where to run commands
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.
cd /path/to/your-app
npm install @loomidev/tab litIf 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:
cd /path/to/your-copy-of-loomiui/components
pnpm --filter @loomidev/tab build
pnpm --filter @loomidev/tab typecheckChoose your framework
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/[email protected]", "lit/": "https://esm.sh/[email protected]/" } }
</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.
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>Server-side rendering notes
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.
Dependencies
@loomidev/core@loomidev/icons
