On this page
Proposed CSC Standards · 0.1.0
Buttons
Rules for actions: submit, confirm, open a dialog, or run a command. Navigation to another page stays a link.
A button triggers an action. It does not take the person to a new URL.
What this describes
When to use
- Submitting a form
- Opening or closing a dialog
- Saving a record
- Confirming or cancelling an action
- Expanding content that is not a separate page
- Running a command such as export or approve
When not to use
- Navigating to another page or file. Use a link.
- Replacing a link that has a real href with a button that calls router.push for ordinary navigation.
- Making a div or span clickable when a button or link fits.
Allowed variants
Primary
The single commit action: submit, save, or continue.
- Hierarchy
- Highest. Filled CSC blue.
- Use in
- One per action group.
- Restriction
- Do not use two primary buttons side by side.
- Accessibility
- White text on #2A338F.
- Responsive
- On narrow screens it may become full width. That is optional.
Secondary
A supporting action that is still filled.
- Hierarchy
- Below primary. Neutral fill.
- Use in
- Toolbars and secondary steps.
- Restriction
- Do not use it for the only destructive action.
- Accessibility
- Do not rely on the gray fill alone. Keep a text label.
- Responsive
- Wraps with the group. It does not have to be full width.
Outline
Cancel, back, or an alternative that should not compete with primary.
- Hierarchy
- Medium. Border and transparent fill.
- Use in
- Paired with a primary submit.
- Restriction
- Do not outline a control that has no action.
- Accessibility
- The border needs 3:1 against the surface when it is the only boundary.
- Responsive
- Keeps its label on one or two lines at 320px.
Ghost
Low-emphasis actions in a dense header or table row.
- Hierarchy
- Low. No border.
- Use in
- Row actions and quiet toolbars.
- Restriction
- Do not use ghost for the page’s main submit.
- Accessibility
- The text color still needs 4.5:1 against the surface.
- Responsive
- Give icon ghost buttons a text name and enough padding.
Soft
A quiet filled action on a white page.
- Hierarchy
- Between outline and primary.
- Use in
- Secondary promotion, such as view details.
- Restriction
- Do not use soft for danger.
- Accessibility
- Check text contrast on the soft blue fill.
- Responsive
- Same wrapping rules as other labeled buttons.
Text
An action that should look like text but still not navigate.
- Hierarchy
- Lowest button emphasis. Underlined text.
- Use in
- Inline actions inside a sentence of instructions.
- Restriction
- Use a real link, not variant link, when the destination is a URL.
- Accessibility
- Underline is required so color is not the only cue.
- Responsive
- Stays inline. Do not stretch it full width.
Destructive
Delete, reject, or another destructive command.
- Hierarchy
- Equal visual weight to primary, in action red #C41222.
- Use in
- Confirm dialogs and explicit destructive rows.
- Restriction
- Do not paint brand red #EC1C2D behind white text. Confirm when the action cannot be undone.
- Accessibility
- The label names the destruction. Color is additional.
- Responsive
- Keep it visually separate from the safe action, including after wrap.
Success
A positive commit such as approve, when the product uses a green action.
- Hierarchy
- Filled green, same weight as a primary commit.
- Use in
- Approval queues.
- Restriction
- Do not use success styling only to mean the form is valid.
- Accessibility
- Pair the color with a word such as Approve.
- Responsive
- Same target-size rules as primary.
Icon-only
A compact action whose icon is familiar and whose name is in the accessibility tree.
- Hierarchy
- Square target, no visible text.
- Use in
- Search, close, and row tools.
- Restriction
- Do not ship an icon button without an aria-label.
- Accessibility
- aria-label states the action. The icon is aria-hidden.
- Responsive
- Prefer at least 44 CSS pixels on touch screens. This is proposed, not the WCAG AA floor.
Split
A default action plus a short menu of related actions.
- Hierarchy
- Primary label plus a chevron menu button.
- Use in
- Save with Save draft and Save and close.
- Restriction
- The menu button needs its own name. SplitButton uses "More actions".
- Accessibility
- The chevron button sets aria-expanded and aria-haspopup.
- Responsive
- The menu must remain on screen at 320px. Allow it to align to the right edge of the group.
Group
Two or three related actions announced as one group.
- Hierarchy
- One primary, then quieter buttons.
- Use in
- Dialog footers and wizard steps.
- Restriction
- Name the group "Actions". Do not put two filled blue buttons in it.
- Accessibility
- The wrapper is role="group" with aria-label="Actions".
- Responsive
- Let the row wrap. Do not force nowrap.
Mandatory rules
Use a semantic button for actions
An action that does not navigate to a URL is a button element, not a clickable div, span, or link.
Implementation detail
The button element exposes the button role, is in the tab order, and activates with Enter and Space.
Render a button element for the action. Use an anchor only when the control opens another page or resource.
Check: Inspect the DOM. The control’s tag name is BUTTON.
Set type="button" when the button must not submit
A button inside a form submits that form unless type is set to button or reset.
Implementation detail
The HTML missing value default for type on a button in a form is submit.
Set type="button" on Cancel, open-dialog, and toolbar actions so they do not submit the form.
Check: In a form, activate a non-submit button and confirm the form does not submit.
Set type="submit" for the control that submits the form
The action that sends the form uses type="submit" so Enter in a field can submit it.
Implementation detail
Native submit keeps keyboard submission and the form’s submit event.
Use one button with type="submit", white text, and a #2A338F fill for the commit action.
Check: Press Enter in a text field and confirm the intended submit handler runs.
Give every button an accessible name
The accessible name comes from the text content, aria-label, or aria-labelledby, and it describes the action.
Implementation detail
Without a name, assistive technology cannot identify the control.
Prefer visible text. An icon-only button sets aria-label to the action.
Check: Read the accessible name in browser accessibility properties. It matches the visible label, or the icon label when there is no visible text.
Keep keyboard focus visible
A button shows a visible focus indicator when it is focused from the keyboard.
Implementation detail
WCAG 2.2 success criteria 2.4.7 and 2.4.11 require a visible, unobscured focus indicator.
Do not remove the focus-visible outline. Use a 3px #85b6ff outline with 2px offset on button:focus-visible.
Check: Tab to the button. The outline is visible and not covered by a sticky bar.
Activate buttons with the keyboard through native behavior
Enter and Space activate a button. Do not replace that with a click-only handler on a non-button.
Implementation detail
Native buttons already implement activation keys. Rebuilding them on a div usually misses Space.
Use a button element and its click handler. Do not add onKeyDown unless Enter and Space still activate it.
Check: Focus the button and press Enter, then Space. Both run the action.
Meet text and non-text contrast
Button text against its fill is at least 4.5:1. The focus outline and icon against the adjacent color are at least 3:1.
Implementation detail
WCAG 2.2 1.4.3 covers text contrast. 1.4.11 covers the boundary and icon of a control.
Primary text is white on #2A338F. Destructive actions use #C41222, not brand red #EC1C2D, because white on #EC1C2D is below 4.5:1.
Check: Measure the computed text and background colors. Do not treat this page as a completed audit.
Meet the WCAG 2.2 target-size minimum
The target is at least 24 by 24 CSS pixels, except where WCAG 2.5.8 allows an exception such as inline text links or user-agent controls.
Implementation detail
WCAG 2.2 AA success criterion 2.5.8 Target Size (Minimum) is 24 CSS pixels, with listed exceptions. It is not 44.
The smallest size is 27px tall. Check its rendered box before using it in a dense toolbar. Do not describe 44 pixels as the AA minimum.
Check: Measure the border box at the rendered size. Record exceptions when a control qualifies for one.
Prefer a 44 by 44 CSS pixel touch target on mobile
On viewports under 768px, primary and destructive actions should use at least size lg, or padding that reaches 44 CSS pixels.
Implementation detail
This is a Proposed CSC Standard for touch. It is stronger than WCAG 2.5.8 and must not be cited as the AA minimum.
Use at least 43px min-height for the main mobile action, or leave a 24px gap around a smaller control.
Check: At 375px, measure the primary action. Prefer 44 CSS pixels on each side.
Name icon-only buttons in text
An icon-only button has an accessible name that states the action, such as "Search records" or "Close dialog".
Implementation detail
An icon has no text alternative unless the control provides one. aria-hidden on the icon avoids a duplicate name.
Set aria-label to the action. Put aria-hidden on the decorative icon.
Check: The accessibility tree name equals the label prop, and the icon is hidden from that name.
Do not use color as the only way to tell actions apart
Destructive, success, and disabled actions also differ by label, icon, or text treatment.
Implementation detail
WCAG 1.4.1 Use of Color applies when color carries information.
A danger button still says Delete, Remove, or Reject. Do not rely on red alone.
Check: Read the label with color removed. The action is still identifiable.
Block a second submit when the action is not safe to repeat
While a save, payment, or application submit is in flight, the submit control is disabled or ignored.
Implementation detail
A double click can create two applications or two approvals.
While the action is in flight, set disabled and aria-busy on the button.
Applies when: The operation creates or changes a record and repeating it would duplicate that change.
Check: Trigger the async action and activate the button again before it settles. The handler runs once.
Show a loading state for asynchronous actions
If the result is not immediate, the button shows a spinner and keeps its accessible name.
Implementation detail
A spinner without a name, or a button that looks idle, leaves the person unsure whether the click registered.
Show a spinner with aria-hidden, set aria-busy, and keep the visible label.
Check: During the request, the button is disabled, aria-busy is true, and the visible label remains.
Confirm or allow recovery for a consequential destructive action
Deleting a record, rejecting an application, or another hard-to-undo action asks for confirmation or offers undo.
Implementation detail
A single click on a danger button is not enough protection for irreversible records.
Open a confirm dialog, or provide an undo path when the product can reverse the action.
Applies when: The action removes data or changes an official status and cannot be undone from the same screen.
Check: Activate Delete. A confirm step or an undo path appears before the record is gone.
Keep labels short and verb-first
Labels name the action in one to three words, such as Submit application or Save draft.
Implementation detail
Proposed CSC Standard. Long labels wrap or overflow on a 320px screen.
Start with a verb. Put extra explanation in surrounding text, not inside the button.
Check: The label is a verb phrase and does not wrap onto a third line at 320px.
Use one primary action in an action group
A group has one primary button. The rest are outline, ghost, or secondary.
Implementation detail
Proposed CSC Standard. Two filled primary buttons compete for the same decision.
The group contains one filled #2A338F button. Cancel is an outline button with type="button".
Check: Count filled primary buttons in the group. The count is one.
Explain why an action is unavailable
Disabled styling is not the only message when a person needs to know how to proceed.
Implementation detail
A disabled button is often removed from the tab order, so the explanation must be visible text nearby.
Pair disabled with a hint or an alert that names the missing requirement.
Applies when: The person can resolve the block, such as a missing required field.
Check: The unavailable action has visible text that states what is missing.
Keep targets from colliding on small screens
Adjacent buttons keep enough gap that each target stays at least 24 CSS pixels, including the gap exception in WCAG 2.5.8.
Implementation detail
WCAG 2.5.8 allows a smaller target when a 24 pixel diameter circle fits in the spacing. Cramped groups fail that test.
Use flex-wrap and at least 8px gap. Stack full-width actions only when the group no longer fits.
Applies when: Two targets are closer than 24 CSS pixels at the rendered width.
Check: At 320px, wrapped buttons do not overlap and the page does not scroll sideways.
Honor reduced-motion preferences
The loading spinner stops animating when the user prefers reduced motion.
Implementation detail
WCAG 2.3.3 Animation from Interactions is Level AAA, so this is a Proposed CSC Standard, not an AA mandate. The stylesheet already disables .spin under prefers-reduced-motion.
Do not add a new infinite animation on a button without the same media query.
Applies when: The button shows a spinner or other non-essential animation.
Check: With prefers-reduced-motion: reduce, the spinner does not rotate.
Do not hardcode colors outside the token set
Button color comes from the CSC variants and tokens, not a one-off hex on a single screen.
Implementation detail
Proposed CSC Standard. Brand blue is #2A338F, brand red is #EC1C2D, and the action red for small white text is #C41222.
Use the documented fills. Do not set a new background color for a one-off screen.
Check: Search the screen styles for hex colors on button elements. New colors are added to tokens first.
Restrictions
- BTN-012 Block a second submit when the action is not safe to repeat. The operation creates or changes a record and repeating it would duplicate that change.
- BTN-014 Confirm or allow recovery for a consequential destructive action. The action removes data or changes an official status and cannot be undone from the same screen.
- BTN-019 Honor reduced-motion preferences. The button shows a spinner or other non-essential animation.
- BTN-020 Do not hardcode colors outside the token set. Button color comes from the CSC variants and tokens, not a one-off hex on a single screen.
Accessibility
- Name, role, and value come from the button element plus the visible label or aria-label.
- Focus uses :focus-visible. Do not set outline: none without a replacement.
- Contrast: white on #2A338F for primary. White on #C41222 for danger. Do not put white text on brand red #EC1C2D.
- Target size: 24 CSS pixels is the WCAG 2.2 AA minimum, with exceptions. 44 CSS pixels is the proposed mobile convention.
- Loading sets aria-busy and keeps the text label.
Responsive behavior
- Desktop: size md, 8px gaps, one primary action in the group.
- Tablet: keep the same hierarchy. Allow the group to wrap instead of shrinking type below the body size.
- Mobile: prefer size lg for the main action so the target is closer to 44 CSS pixels. This is a proposed convention, not the WCAG 2.5.8 minimum of 24 CSS pixels.
- Do not make every button full width. A single column action such as Continue may span the column.
- Wrap long labels. Do not clip them with overflow hidden.
- After wrap, the danger button stays visibly distinct from the safe action.
Component states
Default
Filled or outlined according to variant, ready for pointer and keyboard activation.
Hover
The fill darkens through the button CSS. Hover is not required for touch and is not the only state that shows the control is interactive.
Focus visible
Tabbing shows a 3px #85b6ff outline with 2px offset. Mouse click does not have to keep that ring.
Active
The pressed look uses the CSS active state. The action still fires once per activation.
Disabled
disabled removes activation. Pair it with nearby text when the person can fix the reason.
Loading
loading sets disabled and aria-busy and shows a spinner that respects reduced motion.
Success
variant success is a positive commit such as Approve, not a validation result.
Destructive
variant danger is the error or destructive treatment. The label still says what will happen.
Correct implementation
Submit uses type="submit" and a #2A338F fill. Cancel uses type="button" and an outline so it does not submit the form.
Imports: None. Dependencies: None. Rules: BTN-001, BTN-002, BTN-003, BTN-016.
export function ApplicationActions() {
return (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button type="submit" style={{ background: '#2A338F', color: '#fff', borderRadius: 8, minHeight: 38, padding: '9px 15px', fontWeight: 700 }}>
Submit application
</button>
<button type="button" style={{ background: '#fff', color: '#354966', border: '1px solid #D5DFEC', borderRadius: 8, minHeight: 38, padding: '9px 15px' }}>
Cancel
</button>
</div>
);
}The same action as a semantic button. It is focusable and activates from the keyboard.
Imports: None. Dependencies: None. Rules: BTN-001, BTN-002, BTN-006.
export function SaveControl() {
function saveRecord() {}
return (
<button type="button" onClick={saveRecord} style={{ background: '#2A338F', color: '#fff', borderRadius: 8, minHeight: 38, padding: '9px 15px', fontWeight: 700 }}>
Save
</button>
);
}The button name is the aria-label. The icon is hidden from that name.
Imports: None. Dependencies: None. Rules: BTN-004, BTN-010.
export function FindRecord() {
return (
<button type="button" aria-label="Search records" style={{ width: 40, height: 40, borderRadius: 8, border: 0, background: 'transparent', color: '#536C89' }}>
<span aria-hidden="true">Search</span>
</button>
);
}The busy button is disabled, aria-busy is true, and the visible label stays.
Imports: None. Dependencies: None. Rules: BTN-012, BTN-013.
export function SavingButton({ pending }: { pending: boolean }) {
return (
<button type="submit" disabled={pending} aria-busy={pending || undefined} style={{ background: '#2A338F', color: '#fff', borderRadius: 8, minHeight: 38, padding: '9px 15px', fontWeight: 700 }}>
{pending ? <span aria-hidden="true">… </span> : null}
Save record
</button>
);
}The fill is action red #C41222. The word Delete carries the meaning. Confirm before an irreversible delete.
Imports: None. Dependencies: None. Rules: BTN-011, BTN-014, BTN-007.
export function DeleteRecord() {
return (
<button type="button" style={{ background: '#C41222', color: '#fff', borderRadius: 8, minHeight: 38, padding: '9px 15px', fontWeight: 700 }}>
Delete record
</button>
);
}A single column action can span the width. Do not force every button to full width.
Imports: None. Dependencies: None. Rules: BTN-009, BTN-018.
export function MobileContinue() {
return (
<button type="submit" style={{ width: '100%', background: '#2A338F', color: '#fff', borderRadius: 8, minHeight: 43, padding: '10px 18px', fontWeight: 700 }}>
Continue
</button>
);
}The visible label is the accessible name. The icon is decorative.
Imports: None. Dependencies: None. Rules: BTN-004, BTN-015.
export function NextStep() {
return (
<button type="button" style={{ background: '#2A338F', color: '#fff', borderRadius: 8, minHeight: 38, padding: '9px 15px', fontWeight: 700 }}>
Next step <span aria-hidden="true">→</span>
</button>
);
}Incorrect implementation
A div has no button role, no tab stop, and no Enter or Space activation. This snippet is an anti-pattern, not production code.
Imports: None. Dependencies: None. Rules: BTN-001, BTN-006.
function SaveControl() {
function saveRecord() {}
return <div onClick={saveRecord}>Save</div>;
}Common mistakes
- Using a link styled as a button for an action that stays on the page.
- Omitting type inside a form, so Cancel submits.
- Icon buttons with no accessible name.
- Two primary buttons in one footer.
- Disabling a button with no explanation of the missing field.
- Hardcoding a new blue or red instead of variant.
Testing checklist
Agent prompts for this component are in the AI Development Hub.