CSC Design System Skills & Best Practices
CS
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

Default
Primary
Secondary
Outline
Ghost
Soft
Text
Destructive
Success
Icon-only
Split
Group

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

BTN-001RequiredHTML

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.

Source

BTN-002RequiredHTML

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.

Source

BTN-003RequiredHTML

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.

Source

BTN-004RequiredWCAG

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.

Source

BTN-005RequiredWCAG

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.

Source

BTN-006RequiredHTML

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.

Source

BTN-007RequiredWCAG

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.

Source

BTN-008RequiredWCAG

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.

Source

BTN-009RecommendedProposed CSC convention

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.

BTN-010RequiredARIA

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.

Source

BTN-011RequiredWCAG

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.

Source

BTN-012ConditionalSecurity

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.

BTN-013RecommendedFramework

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.

BTN-014ConditionalSecurity

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.

BTN-015RecommendedProposed CSC convention

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.

BTN-016RecommendedProposed CSC convention

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.

BTN-017RecommendedProposed CSC convention

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.

BTN-018RequiredWCAG

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.

Source

BTN-019ConditionalProposed CSC convention

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.

Source

BTN-020ProhibitedProposed CSC convention

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 and cancelCorrect implementation · TSX

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>
  );
}
Corrected save buttonCorrect implementation · TSX

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>
  );
}
Icon-only searchAccessibility implementation · TSX

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>
  );
}
Loading submitCorrect implementation · TSX

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>
  );
}
Destructive actionCorrect implementation · TSX

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>
  );
}
Full-width mobile actionResponsive implementation · TSX

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>
  );
}
Button with an iconIntegration example · TSX

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

Clickable divIncorrect implementation · TSX · Illustrative

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.