Tabs

Peer views of one thing, inside a card

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-pills inside 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 with data-bs-toggle="tab" + the aria set (role="tab", aria-controls, aria-selected) — never links with href="#".
  • 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-1 after 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-column column 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.