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 usedIcon buttons — icon-only ALWAYS carries
title (auto-tooltip); with a label the icon takes .me-1Row actions — the three shapes + the round expand toggle (live on the data-list page);
.btn-contrast adapts to the themeDestructive — outline in rows/cards; solid
.btn-danger only as a dialog's confirm button- ONE
.btn-primaryper zone (page header, card, modal footer) — if two things feel primary, one of them is not. - 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-dangerkeeps its role on STANDALONE destructive row actions). - Outline buttons NEVER sit inside a
.btn-group— the shared internal borders read badly. Grouped buttons are SOLID (.btn-secondary,.btn-danger,.btn-contrast); a segmentedbtn-checkchoice 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-secondaryand 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-footerwhenever the form owns one (forms) — any other arrangement is custom, outside the standard. .btn-contrastis the theme-adaptive contrast button (dark on the light theme, light on the dark theme, same class): the standard for utility row controls — the kebab actions button, the split-dropdown pair, the round expand toggle.- Row/table actions come in THREE shapes — pick ONE per list and stay consistent: the inline
.btn-groupof.btn-smicon buttons; the split dropdown (primary action +.dropdown-toggle-splitcaret, both.btn-contrast); the actions-only kebab (.btn-contrast+ri-more-2-lineopening all actions). The expand toggle is never part of the group: a small round.btn-contrastat the row end (.btn-xs .btn-icon-round— the circle sizes from the size class). - 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-hiddenspan. - 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
Your trial ends in 5 days — pick a plan to keep your data.
Two invoices are overdue. Review them.
- 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-infoandalert-warningfor notices,alert-dangerfor blocking problems;alert-successis 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-*-emphasispair — solidbg-*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.successafter a save/delete,g.errorfor system failures,g.warningfor business refusals (res.error.code≥ 1000). - 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
-linevariants are the default everywhere (-fillonly 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 thetitle. - Keep one icon per concept app-wide (delete is always
ri-delete-bin-line, edit is alwaysri-pencil-line).