The rules — tabs are peer views of ONE thing, inside a card
- Tabs ALWAYS come with a card — the card IS the tabbed container. A tab set without a card is a custom one-off, not the standard.
- The ONE tab pattern:
ul.nav.nav-pills.card-header-pillsinside the.card-header, panes in.card-body > .tab-content. No underline tabs, no standalone pills. - Pane content is NORMAL card-body content — no special background, no extra padding; it renders exactly as if it sat directly in the card body.
- Tabs are
<button>elements withdata-bs-toggle="tab"+ the aria set (role="tab",aria-controls,aria-selected) — never links withhref="#". - Each tab carries an icon (
.me-1,aria-hidden) + a short label; 2–5 tabs — more means the page is doing too much (split the page). - A tab may carry a notification COUNT:
.badge-count.ms-1after the label (see Invitations / Notifications here) — the framework's soft-secondary circle (subtle bg + emphasis text + subtle border, count dead-centered, aligned to the text middle; two digits stretch it into a pill). - The tab-pill law: a pill announces something NEW awaiting the user (a received invitation, an approval pending on you) — a plain SIZE (member count, row total) is never pill material; the label already says what the tab holds. When in doubt, no pill.
- Where the value comes from: the session context when it already carries the count (free); otherwise ONE cheap counts endpoint for the whole tabbed surface, called client-side — PER PAGE (each page owns its own, built when that page first needs a pill; there is no framework-wide counts endpoint), never a full list fetched just to count it, never one call per pill. Update the pill wherever the data changes (after mutations, or from the list's
lister:loaded). - Vertical variant: a
.nav-pills.flex-columncolumn beside the panes, both inside the SAME card body (aria-orientation="vertical"on the list) — use it when panes are tall and the tab set should stay in view. - Tabs switch peer views of the same object (a user's General / Security, a company's Details / Members). Different objects or workflows are different PAGES (routing), not tabs.
- Each pane is one logical section; a pane with a form carries its OWN submit (see settings) — one giant form across tabs is forbidden.
- The open tab is not URL-synced by default — tabs reset on reload. If a flow must deep-link a tab, that is a signal it wants to be a page.
The General pane — the first tab is .active and its pane .show.active. Pane content is plain card-body content; everything inside follows the normal standards (forms, tables, states).
The Invitations pane — a pane can hold a table or list; it still counts as ONE logical block. Its tab wears a .badge-count pill because pending invitations AWAIT the user — the tab-pill law in the rules above.
The Security pane — a pane with a form carries its own submit button, scoped to that pane's fields.
Vertical tabs — the pills column beside the panes
The Profile pane — the vertical set lives entirely in ONE card body: pills column left, panes right. Pane content stays plain card-body content, same as the horizontal form.
The Notifications pane — use the vertical variant when panes are tall and the tab set should stay in view while reading.
The Billing pane — the column is col-12 col-md-auto — it sizes to the widest label, and on small screens it stacks above the panes.