UI elements

Buttons, badges, alerts, toasts, icons — the vocabulary

Buttons — hierarchy first, color second

Hierarchy — ONE primary per zone, solid secondary for everything else
Sizes — default in headers/forms, .btn-sm inside cards and table rows; .btn-lg is not used
Icon buttons — icon-only ALWAYS carries title (auto-tooltip); with a label the icon takes .me-1
Row actions — ONE shape, .btn-actions-contrast (themable via private/config/theme.scss): a bare button when the row has one action, the same button + split caret when it has more; a row whose ONLY action is destructive is the one color exception (solid .btn-danger); the round expand toggle stays .btn-contrast chrome (live on the data-list page)
Destructive — the solid .btn-danger: the dialog's confirm button and the lone destructive ROW action (above); standalone destructive buttons OUTSIDE rows (a form's Delete) stay outline
  • ONE .btn-primary per zone (page header, card, modal footer) — if two things feel primary, one of them is not. Row actions never compete: they carry their own color, .btn-actions-contrast.
  • The default secondary is the SOLID .btn-secondary (steel blue) — the outline variants are not part of the standard hierarchy (they live in the catalog; .btn-outline-danger keeps its role on STANDALONE destructive buttons OUTSIDE list rows, e.g. a form's Delete — in rows the lone destructive action is the solid .btn-danger, lister.md "Row actions").
  • Outline buttons NEVER sit inside a .btn-group — the shared internal borders read badly. Grouped buttons are SOLID (.btn-actions-contrast — the row-action split, .btn-secondary, .btn-danger, .btn-contrast); a segmented btn-check choice uses .btn-soft-secondary.
  • Destructive actions always go through a dialog confirm; the confirm button inside the dialog is the solid .btn-danger.
  • Cancel is .btn-soft-outline-secondary and sits at the LEFT edge of the button row; the action buttons sit RIGHT-aligned, primary last. The row lives in the .card-footer / .modal-footer whenever the form owns one (forms) — any other arrangement is custom, outside the standard.
  • .btn-contrast is the theme-adaptive contrast button (dark on the light theme, light on the dark theme, same class): utility chrome — the round expand toggle rides it.
  • Row/table actions have ONE shape — and the standard governs EVERY list row, lister-built or hand-rendered (detail-tab .list-group-item rows included): a row with one action renders it as a bare .btn-sm .btn-actions-contrast button, a row with more renders the primary action + a .dropdown-toggle-split caret (both .btn-actions-contrast) opening the rest. The visible button AND every .dropdown-item carry icon + text; destructive items sit LAST as .dropdown-item.text-danger, below a .dropdown-divider when the set mixes them with harmless ones — and never as the visible half. THE ONE COLOR EXCEPTION: a row whose only action is DESTRUCTIVE renders it bare in the solid .btn-danger (the danger signal belongs on the button; row actions are never outline; g.confirm stays mandatory). .btn-actions-contrast is for ROW actions only — controls outside rows (card headers, toolbars) keep the normal hierarchy. The expand toggle is never part of the group: a small round .btn-contrast at the row end (.btn-xs .btn-icon-round).
  • The count follows the ROW (a status can leave a row one action — it renders bare while its siblings split); actions never wrap to a second line and the shape never changes at a breakpoint — the split's constant footprint IS the mobile behavior. The size is .btn-sm at EVERY width; never author .btn-xs on row actions — a list that genuinely crowds on mobile scopes the xs METRICS below the breakpoint via the --bs-btn-* variables in its route style.scss (lister.md has the snippet). The color is themable: $actions-contrast-* in the control panel, overridden per project in private/config/theme.scss (foundation "Project theming"). Full text: lister.md "Row actions".
  • Dropdown toggles NEVER carry title (the auto-tooltip and the dropdown are two Bootstrap instances on one element — Bootstrap allows only one); label them with a .visually-hidden span.
  • Buttons in a row sit in a flex container with gap-2 — no spacing utilities on the buttons themselves.

Alerts — persistent in-page notices, used sparingly

  • An alert is a persistent state the user must know about while on the page (trial ending, unpaid invoice, config missing) — not feedback for an action they just did (that is a toast).
  • Variants used: alert-info and alert-warning for notices, alert-danger for blocking problems; alert-success is almost never right (success is a toast).
  • Validation errors are NEVER alerts — the validator renders them inline at the fields.

Badges — statuses on the subtle/emphasis pair

Active Pending Blocked Trial Archived
  • A status badge is ALWAYS the bg-*-subtle text-*-emphasis pair — solid bg-* badges are not used for statuses (they shout and break in dark mode).
  • Semantics, fixed app-wide: success = active/ok, warning = transitional/attention, danger = blocked/failed, info = informational, secondary = neutral/inactive.
  • Same status = same color everywhere in the app — define the mapping once and stick to it.

Toasts — feedback for actions (live demo)

  • A toast answers "did my action work?": g.success after a save/delete, g.error for system failures, g.warning for business refusals (res.error.code ≥ 1000).
  • Errors are sticky — they stay until closed (the other types auto-hide after 5s). Point a failure at its place with the shake option; every variant live: notify & shake.
  • After a redirect/reload, pass { nextPage: true } and the toast shows on the page you land on.
  • Toasts never carry state the user must not miss — that is an alert on the page.
  • Full API and options: templates/app/docs/libs/genus.md "Toasts".

Icons — Remix Icon, line variants

  • The icon set is Remix Icon; the -line variants are the default everywhere (-fill only when a filled state means something, e.g. a toggled favorite).
  • Icons are decoration: aria-hidden="true" always; the meaning lives in the label or the title.
  • Keep one icon per concept app-wide (delete is always ri-delete-bin-line, edit is always ri-pencil-line).