Description
Buttons let users take actions and make choices. They initiate tasks, submit forms, trigger dialogs, and navigate between states. A consistent button vocabulary reduces cognitive load — when primary, secondary, and tertiary have predictable visual weight, users can identify the most important action in any context at a glance.
Appears in forms, dialogs, toolbars, cards, empty states, and page headers.
A shared button vocabulary means every screen in Open Point and Social Point uses the same visual hierarchy. Users learn once that the high-emphasis button is primary, and that knowledge transfers to every context.
Built on Web Awesome (<wa-button>) — framework-agnostic web component that works across Open Point (Angular), Social Point (Vue), and any HTML context. Pairs with ButtonGroup for grouped actions.
Anatomy
| Part | Required? | Notes |
|---|---|---|
| Label | Required (or icon) | Sentence case. Verb + object preferred. 1–3 words, 5 maximum. |
| Leading icon | Optional | 16×16px. Reinforces intent — don't use decoration-only icons. |
| Trailing icon | Optional | 16×16px. Use for affordances like dropdowns (chevron) or external links. |
| Loading indicator | Optional | Spinner replaces or accompanies label during async operations. |
Variants
Brand — Accent
The primary call-to-action for a view or dialog. Highest visual weight, in the brand colour.
Use once per view — if two Accent buttons appear together, one should be downgraded.
Neutral — Accent
A solid, high-commitment action rendered in neutral colour.
Actions that need to read as committed or final, but shouldn't compete with the view's Brand action.
Neutral — Filled-Outlined
A brand-coloured action with reduced visual weight.
Actions that relate to the primary flow but shouldn't compete with the view's Brand button — e.g. a secondary option alongside the main call-to-action that's still tied to the same task.
Neutral — Outlined
A bordered, no-fill action with moderate visual definition.
Standard secondary actions that need to be distinguishable but not prominent.
Neutral — Plain
The lowest-emphasis action available. Text only, no fill or border.
Dismissive, cancel, or "skip this" actions that shouldn't draw the eye.
Disabled
50% opacity, not interactive.
Avoid where possible. Use only when an action cannot be taken and the reason is visually clear. Prefer aria-disabled over the HTML disabled attribute if keyboard reach is needed.
Loading
Spinner replaces or precedes the label. Button dimensions preserved.
After the user triggers an async action — form submit, save, delete. Remove the loading state once the action resolves.
Intent variants
Danger — Accent
A destructive or irreversible action (delete, remove). Highest warning weight — solid fill in the danger colour.
Should almost always sit behind a confirmation step.
Danger — Outlined
A destructive action surfaced inline, without a dialog to carry the weight.
A row, a card, a toolbar — or as a secondary option next to a Danger/Accent confirming action.
Danger — Plain
A text-only destructive action with the lowest warning weight.
Used inline where a full-weight danger button would be excessive. Pending Ysa's audit.
Success — Accent
Confirms or completes a positive action.
The confirming action inside a dialog or notification modal — e.g. "Publish," "Mark complete." Not used inline.
Warning — Accent
Proceeding despite a caution.
The confirming action inside a dialog — e.g. "Continue anyway," "Overwrite." Not used inline.
Info — Accent
Acknowledging or dismissing informational content.
The acknowledging action inside an informational dialog or notification modal — e.g. "Got it," "Learn more." Not used inline.
Inverse surfaces
Outlined — Inverse
White border and label on dark surfaces.
Secondary actions on dark backgrounds — nav bars, dark banners, inverse panels.
Plain — Inverse
White label, no border or fill, on dark surfaces.
Tertiary actions on dark surfaces alongside an Outlined Inverse button.
Sizes
States
| State | Behaviour |
|---|---|
| Default | Resting state. Full colour, full opacity. |
| Hover | Slightly darker or lighter depending on variant. Cursor — pointer. |
| Focus | 2px solid focus ring, 2px offset. Always visible — never suppressed. |
| Disabled | Avoid where possible — disabled buttons are not in the tab order and give users no feedback about why the action is unavailable. Prefer showing the button in an error or inactive state with an explanation. When disabled is necessary: 50% opacity, cursor: not-allowed. Use aria-disabled="true" instead of the HTML disabled attribute if the button must remain keyboard-reachable. |
| Active / Pressed | Depressed visual on press. |
| Loading | Spinner visible. Button dimensions preserved. Optional label persists ("Saving…"). |
Usage guidelines
How to choose a button
Every button's role comes from two independent decisions: how much visual weight it should
carry (appearance), and what it means semantically (variant). Pick both, then check
the component reference below for the exact wording of that combination's role.
1. Appearance — how much weight?
| Appearance | Look | Choose this when… |
|---|---|---|
| Accent | Solid fill | This is the one committed action in a self-contained decision point — a view's main call-to-action, or a dialog's confirming action. |
| Filled-Outlined | Tinted, light fill | The action relates to the primary task but shouldn't compete with it. |
| Outlined | Bordered, no fill | A standard action that needs to be visible but not prominent. |
| Plain | Text only | A dismissive, cancel, or low-stakes inline action. |
2. Variant — what does it mean?
| Variant | Meaning |
|---|---|
| Brand | The primary task or flow |
| Neutral | No brand or intent meaning attached |
| Danger | Destructive or irreversible |
| Success | Confirms or completes a positive action |
| Warning | Proceeding despite a caution |
| Info | Acknowledging or dismissing informational content |
The rule that trips people up: Accent is capped at one per view
Accent is a visual weight, not a colour-specific thing — a solid-fill button competes with any other solid-fill button nearby, regardless of colour. Practically, that means:
- Brand/Accent is the one recommended action in a view. The brand colour is what signals "this is the recommended path."
- Neutral/Accent is not a partner to Brand/Accent — it's a substitute for it. Use Neutral/Accent only in views that have no Brand/Accent button at all, where something still needs to read as solid and final without any brand emphasis (an internal tool screen, a settings save, a neutral system dialog).
If a view already has a Brand/Accent button and a second action still needs to feel final rather than soft, drop that second action to Filled-Outlined or Outlined — never bump it up to Neutral/Accent. Two Accent-tier buttons side by side will always compete for attention, no matter what colours they use.
Note: appearance="filled" is a separate, valid value outside this role system — it's used for pressed/active states in segmented controls and toggle button groups (see ButtonGroup), not for standalone button roles.
Component reference
Brand
| Component | Role |
|---|---|
| Brand / Accent | The one recommended action in a view or dialog. Use once per view. Never pair with another Accent-tier button of any colour. |
Brand/Filled-Outlined and Brand/Outlined are not currently in the system — considered and deliberately excluded, since Neutral/Filled-Outlined already covers "tied to the primary flow, reduced weight," and no concrete screen has needed a third brand-weighted step. Revisit if a real use case comes up.
Neutral
| Component | Role |
|---|---|
| Neutral / Accent | A committed, final action in a view with no Brand/Accent button present — a substitute for Brand/Accent, not a companion to it. |
| Neutral / Filled-Outlined | A brand-coloured action with reduced visual weight, tied to the primary flow but not competing with it — e.g. a secondary option alongside the main CTA that's still part of the same task. |
| Neutral / Outlined | A standard secondary action that needs to be distinguishable but not prominent. |
| Neutral / Plain | The lowest-emphasis action available — dismissive, cancel, or "skip this." |
Danger
| Component | Role |
|---|---|
| Danger / Accent | A destructive or irreversible action (delete, remove). Highest warning weight — solid fill in the danger colour. Should almost always sit behind a confirmation step. |
| Danger / Outlined | A destructive action surfaced inline — a row, a card, a toolbar — without a dialog to carry the weight, or as a secondary option next to a Danger/Accent confirming action. |
| Danger / Plain | A text-only destructive action with the lowest warning weight, used inline where a full-weight danger button would be excessive. Pending Ysa's audit for the same reason. |
Success
| Component | Role |
|---|---|
| Success / Accent | The confirming action inside a dialog or notification modal for a positive, completing action — e.g. "Publish," "Mark complete." |
| Success / Outlined | A positive action surfaced inline, or a secondary option next to a Success/Accent confirming action. |
| Success / Plain | A low-emphasis, inline positive action. |
Warning
| Component | Role |
|---|---|
| Warning / Accent | The confirming action inside a dialog that proceeds despite a caution — e.g. "Continue anyway," "Overwrite." |
| Warning / Outlined | A cautionary action surfaced inline, or a secondary option next to a Warning/Accent confirming action. |
| Warning / Plain | A low-emphasis, inline cautionary action. |
Info
| Component | Role |
|---|---|
| Info / Accent | The acknowledging action inside an informational dialog or notification modal — e.g. "Got it," "Learn more." |
| Info / Outlined | An informational action surfaced inline, or a secondary option next to an Info/Accent action. |
| Info / Plain | A low-emphasis, inline informational action. |
Note: rendered as variant="brand" data-color="info" — this installed version of Web Awesome has no native "info" button variant. See component-overrides.css for the mapping.
Content guidelines
- Sentence case. "Save changes," not "Save Changes" or "SAVE CHANGES."
- *Product entity names stay capitalised (pending review).* Stakeholder, Property, Event, Consultation, Response (Open Point) and Project, Team, Team Leader, Campaign (Social Point) are capitalised in button labels, as with any other officially-named product term: "Add Stakeholder," "Create Property," "Delete Event," not "add a stakeholder," "create a property," "delete an event." This is the current standard, applied consistently across both products — see Capitalisation for the full reasoning and its open questions, which are still under review with marketing.
- Verb + object. Tell the user what happens: "Delete Project," "Add Stakeholder," "Send invitation." Avoid vague labels like "OK," "Yes," or "Submit" — the label should tell users exactly what will happen without needing to read surrounding context.
- 1–3 words, 5 maximum. Longer labels wrap or truncate at small sizes and slow down scanning.
- State the consequence directly for destructive actions. "Delete Consultation," not "Remove?" or "Are you sure?" — the confirmation dialog, not the button label, is where you explain what's irreversible.
- Match the label to the actual outcome. If a "Save" button also triggers a validation step, "Save" still describes the outcome correctly — but if it navigates elsewhere or triggers a side effect the user wouldn't expect, name that instead.
- Loading labels use present continuous. "Saving…," "Deleting…," "Publishing…" — not "Save" with a spinner, which reads as unresponsive rather than in progress.
- Never rely on colour alone to convey meaning. A Danger button's label should already say "Delete," "Remove," or "Revoke" — the red colour reinforces the label, it doesn't replace it.
When to use
- To trigger an action: submitting a form, opening a dialog, deleting a record.
- Wherever a user needs a clear affordance to do something.
When not to use
- For navigation between pages or views — use a link (
<a>) instead. Buttons set user expectations of action, not destination. - When multiple primary buttons would appear simultaneously — rethink the hierarchy.
Do / Don't
Do
Use sentence case for button labels, capitalising product entity names: "Save changes", "Add Stakeholder", "Delete Project".
Don't
Use vague labels like "OK", "Yes", or "Submit". The label should tell users exactly what will happen.
Do
Preserve button dimensions during loading states — prevent layout shift when the spinner appears.
Don't
Soften destructive actions with vague labels like "Remove?" — state the consequence directly: "Delete Consultation".
Do
Use aria-disabled="true" on buttons that should remain keyboard-reachable even when inactive.
Don't
Place multiple primary buttons in the same context. If two actions feel equally important, reconsider the hierarchy.
Layout & Spacing
Buttons use horizontal padding from the spacing scale. Heights are fixed per size to ensure consistent alignment with other form elements.
| Size | Height | Padding (H) | Font size | Min touch target |
|---|---|---|---|---|
| Extra small | 24px | 0.75rem | xs | 44×44px (add invisible target zone) |
| Small | 32px | 1rem | sm | 44×44px (add invisible target zone) |
| Medium (default) | 40px | 1.25rem | base | 44×44px native |
| Large | 48px | 1.5rem | lg | 48×48px native |
Full-width buttons (width: 100%) are appropriate in forms, mobile viewports, and single-column card layouts. Avoid them in toolbars or inline contexts.
Tokens
| Part | Token | Value |
|---|---|---|
| Strong background (accent/filled) | --wa-color-fill-loud | Set per variant via component-overrides.css. Brand → --op-color-green-60 (#BACD42) |
| Soft background (outlined/subtle) | --wa-color-fill-quiet | Set per variant. Neutral → transparent at rest |
| Border | --wa-color-border-loud | Set per variant via variants.styles.ts. Neutral outlined → --wa-color-neutral-border-loud (#726d66) — not --op-color-border-default, which is Orbit's own site-chrome token and unrelated to this component |
| Text on strong background | --wa-color-on-loud | Brand → --op-color-neutral-05 (#101C07). Inverse → #fff |
| Text on soft background | --wa-color-on-quiet | Neutral → --op-color-text-primary. Inverse → #fff |
| Focus ring | --wa-color-focus | Currently hardcoded to #24a4fc (blue) in wa-theme.generated.css — doesn't yet resolve from --op-color-interactive-focus/--op-color-green-60 as documented here. Flagged for the token pipeline, not yet fixed. |
| Disabled opacity | — | 0.5 |
| Border radius | --wa-border-radius-m | --op-radius-md → 8px |
Engineering notes
- Always use
<button>for actions, not<a>or<div>. Buttons are keyboard-focusable and activate with both Enter and Space by default. - Prefer
type="button"on all non-submit buttons inside forms to prevent accidental form submission. - For icon-only buttons, always provide an accessible label — either visible text,
aria-label, oraria-labelledby. - Avoid using
pointer-events: noneas a disabled pattern — it removes keyboard accessibility. Use thedisabledattribute oraria-disabled="true"instead. - For async actions (save, submit), disable or show a loading state on click to prevent double-submission. Preserve button dimensions during loading to avoid layout shift.
Keyboard interaction
| Key | Action |
|---|---|
| Tab | Moves focus to the button. Skips buttons with the HTML disabled attribute — use aria-disabled="true" if the button must remain reachable. |
| Shift + Tab | Moves focus to the previous focusable element. |
| Enter | Activates the button. |
| Space | Activates the button. |
Why it matters
A significant proportion of Open Point and Social Point users navigate by keyboard or assistive technology. A button that can only be activated with a mouse excludes these users entirely.
Focus
All button variants display a 2px solid focus ring in --wa-color-focus with a 2px offset, visible on both light and dark backgrounds. The ring must have at least 3:1 contrast against colours adjacent to it.
Use :focus-visible rather than :focus to suppress the ring on mouse click while preserving it for keyboard navigation.
After activation, focus must follow the outcome:
- Opening a dialog → focus moves to the first focusable element inside the dialog.
- Closing a dialog → focus returns to the button that triggered it.
- No context change (e.g. toggling state) → focus stays on the button.
Watch out
The most common button accessibility mistake is removing the focus outline for visual polish without replacing it. If the default ring doesn't fit your design, replace it — never remove it. A button with no visible focus state is unusable for keyboard users.
ARIA
| Role or attribute | When to use | Example |
|---|---|---|
aria-label | Icon-only buttons. The label must describe the action, not the icon name. | aria-label="Close dialog" |
aria-pressed | Toggle buttons with two states (on/off, active/inactive). Update dynamically when state changes. | aria-pressed="true" |
aria-expanded | Buttons that show or hide content (dropdowns, accordions, drawers). | aria-expanded="false" |
aria-controls | Associates a button with the panel it controls. Pair with aria-expanded. | aria-controls="filter-panel" |
aria-haspopup | Menu buttons that open a menu, listbox, tree, grid, or dialog. | aria-haspopup="menu" |
aria-disabled="true" | When a button must remain keyboard-focusable but must not activate. Prevent the action in the event handler. | aria-disabled="true" |
aria-describedby | Connects a button to a nearby explanation when the label alone doesn't communicate the consequence. | aria-describedby="delete-warning" |
How to apply it
If a button has a visible text label, the accessible name must contain that exact text — either as the button's content or within aria-label. When the two don't match, voice control users can't activate the button by speaking what they see, and screen reader users hear something different from what's on screen.
Contrast
Normal-weight text requires 4.5:1 contrast. Large text (18px regular or 14px bold) requires 3:1. Focus indicators and UI boundaries require 3:1 against adjacent colours. Disabled states are exempt.
| Variant | Text / Background | Contrast ratio | Result |
|---|---|---|---|
| Primary | #101C07 on #BACD42 |
8.1:1 | Pass |
| Secondary label | #BACD42 on white |
~3.7:1 | Fails for normal-weight text under 18px — large text only |
| Destructive | #FFFFFF on --op-color-red-40 |
4.6:1 | Pass |
| Disabled (all variants) | 50% opacity applied | Varies | Exempt |
Watch out
The secondary label contrast (~3.7:1) is insufficient for normal-weight text under 18px. This must be resolved before the component is marked stable. Never use colour alone to distinguish button states — always pair a colour change with another visual indicator such as an icon, label, or pattern change.
Touch targets
The minimum touch target is 24×24px. The recommended target is 44×44px — use this as the standard for all interactive elements.
The medium button (40px height) meets the minimum and approaches the recommendation with horizontal padding included. The small variant (32px height) requires an invisible touch target extension to reach 44×44px.
On Social Point community-facing surfaces, prefer medium or large buttons. Community participants include people with motor impairments for whom small tap targets are a significant barrier.
Things to avoid
- Using a non-button element for a button action —
<div>,<span>, or<a>styled to look like a button won't be announced correctly and won't respond to Space or Enter. Use a native<button>. - Removing focus indicators without replacement —
outline: noneoroutline: 0with no equivalent visible focus style makes keyboard navigation impossible. - Accessible name that doesn't match the visible label — if a button has visible text, the programmatic name must contain that text. When they differ, voice control users can't activate the button by speaking what they see.
- Colour alone to convey state — a colour change is not enough to show that a button is active, selected, or in an error state. Always pair it with another visual differentiator: an icon, a pattern, or a text change.
- Changing the visible label on a toggle button — when using
aria-pressed, only the attribute should change. Swapping the label alongside it creates a mismatch between what the screen reader announces and what the user sees. - Pointer-only interactions — binding only a click handler without keyboard equivalents means keyboard users can't activate the button.
- A focus ring with insufficient contrast — the ring must have at least 3:1 contrast against colours adjacent to it, both on the button surface and the page background.