Skip to content

Button

Five variants, three sizes, and an icon-only form that insists on a name.

Preview

Code

The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.

<button class="wsu-btn wsu-btn--primary" type="button">Add course</button>

<button class="wsu-btn wsu-btn--outline" type="button">
  <svg class="wsu-icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-download"/></svg>
  Export
</button>

<!-- Icon only: the name is not optional -->
<button class="wsu-btn wsu-btn--icon wsu-btn--ghost" type="button">
  <span class="wsu-sr-only">Settings</span>
  <svg class="wsu-icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-settings"/></svg>
</button>

<!-- Navigating, so an anchor -->
<a class="wsu-btn wsu-btn--primary" href="/courses/new">Add course</a>

Why it works this way

  • A control that navigates must be an a, and a control that acts must be a button. Styling one to look like the other is fine. Using one where the other belongs breaks middle-click, open-in-new-tab, the screen reader's list of links and the difference between Enter and Space. 1.x shipped <a class="btn" href="#" onclick=…> throughout.
  • type="button" is the default. An unqualified button inside a form is a submit button, and every Cancel that silently submits the form it was meant to abandon is that defect.
  • An icon-only button without a label is meant to carry a .wsu-sr-only name instead of rendering unnamed. Blade raises InvalidArgumentException at render time, a hard failure a fresh template has no published callers to break. Vue cannot make label required without breaking every already-published caller that omits it on a plain, labeled button, so it instead warns in development when icon is set with no label; unlike Blade, an unnamed control still renders if the warning goes unread.
  • Buttons take a 1px press on :active, at --wsu-duration-fast.

Accessibility

  • Minimum 44px target from --wsu-size-tap-min (2.5.5).
  • Icon-only buttons are meant to carry a .wsu-sr-only name (4.1.2); Blade enforces this at render time, Vue only warns about it in development.
  • A disabled link is not a thing in HTML, so the href is dropped and aria-disabled is set instead.

Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.

Props

Blade

AttributeDefaultWhat it does
variant'ghost'primary, accent, outline, ghost or danger. Anything else falls back to ghost.
sizenullsm or lg.
hrefnullRenders an anchor instead of a button.
icon / iconAfternullIcon name, before or after the label.
iconOnly / labelfalse / nulllabel is required when iconOnly is set, or the component throws.
type'button'Set to submit deliberately, never by omission.
disabledfalseSets the native disabled attribute on a button; on a link it drops href and sets aria-disabled instead, since a disabled link is not a thing in HTML.

Vue

PropType and defaultWhat it does
variantString, 'primary'primary, accent, outline, ghost or danger.
sizeString, ''sm or lg.
as / hrefString, 'button'Set as="a" to navigate.
typeString, 'button'Set to submit deliberately, never by omission.
disabledBoolean, falseSets the native disabled attribute on a button; on an anchor (as="a") it drops href and sets aria-disabled instead, so the control stays focusable.
icon / labelBoolean, false / String, ''Icon-only when icon is set; label becomes the accessible name, or overrides it on any button.
loadingBoolean, falseAdds a spinner and aria-busy. The label stays put so the name is stable.