Design system Actions and links

Button

The action. Every pressable thing in the workspace that is not a link, an icon-only control, or a whole row that acts.

.mk-btn.mk-split-btn

Overview

A button acts on this page. A link navigates away from it. That line decides which component to reach for before anything about size or tier matters.

Usage

  • Trigger a deliberate actionUse a button when pressing changes data, submits input, starts a process, or opens an action surface. Use a link when the result is navigation only.
  • Express action priorityReserve Primary for the single expected next action in its region. Use Secondary and quieter variants for valid alternatives and routine controls.
  • Expose consequence and progressDestructive actions use the error treatment. While work is running, the same button reports progress and prevents a duplicate press.

Anatomy

1
2 3
  1. 1Container. Height comes from the size ladder. Every tier carries a 1px border, transparent where it does not show, so a Primary and a Secondary of the same size measure the same.
  2. 2Leading icon slot. Optional. order: -1 holds it left of the label even when a runtime template writes it after. It scales with the size ladder and inherits the label's ink.
  3. 3Label. The verb and its object. A button without the icon slot keeps exactly the same geometry.

Variants and states

Priority tiers

One Primary per view. The accent is the workspace's only saturated action colour and spending it twice on one screen means neither instance is the next step.

TierFaceThe job
Primary.mk-btn-primary The action the agent is expected to take next.
Secondary.mk-btn-secondary A real alternative to the Primary, wearing the brand-gold outline.
Quiet.mk-btn-ghost Accent ink on nothing, but still a button: the same height and the same 44px target as the two above, so it can stand in a row with them.
Destructive.mk-btn-destructive Removal and cancellation. The functional error tone, never brand red. Last in the row, and never the Primary.

Padding

Padding is a property of the Quiet tier, not another priority. Keep the standard padding when it stands with other buttons; remove it when the same quiet action must align flush with a line of facts. Both rows below start at the same cell edge, so the only thing that moves is the label.

PaddingQuietWhat changes
Standard20px The label sits in from the edge it shares with the column, which is what keeps it looking like a button among buttons.
Flush0 The label starts on that edge, so it lines up with the facts above it. The 44px hit area does not shrink with the padding — it reaches past the label instead.

The size ladder

Five heights on one scale. The bare component is M. Blocked is a state shown beside every size, never a size of its own.

SizeText onlyLeading iconBlocked
XS28px
S36px
M44px
L48px
XL56px

States

Every state below is the real component wearing that state, not a picture of it. The rows share one set of tiers so a state can be read across them.

StatePrimarySecondaryQuietWhat changes
Rest The tier's own face.
Hover Primary darkens one step. Secondary fills with the accent wash. Quiet slides in a hairline underline and deepens its ink.
Keyboard focus A 2px camel outline at 2px offset, identical on every tier and every size.
Blocked The gold leaves, the hover dies, the label speaks quiet. The control keeps its place in the tab order so its reason can be read.
Busy — — The face keeps its colour one shade toward the ink. Busy is not blocked, and it returns to rest the moment the answer lands. Only the tier doing the work wears it, which is the Primary.

Split button

One action with an attached menu of alternatives, worn as two sibling buttons in one group. The named left face performs its action immediately. Only the separated chevron face opens the menu, and the menu lists every action, including the current one, so the whole set is always one press away. If there is no ready default, this is not a split button: use a Menu behind an icon button.

The shell adds nothing of its own

Both faces take the ordinary button classes: the same tier, the same size, always together. A split where the halves disagree is two buttons pretending to be one. The toggle's square is as wide as the step is tall, and its filled chevron scales 12 / 14 / 16 / 18 / 20px up the same five-step ladder as the button.

States

StateSpecimenWhat changes
Rest Two ordinary buttons in one group: the tier's own face on both.
Hover Each face answers on its own — the pair never lights together.
Keyboard focus The 2px camel outline rings the focused face only, so Tab order stays legible.
Open The toggle carries aria-expanded="true" while the menu is up; the chevron turns with it.
Blocked Both faces block together — a menu of actions that cannot be performed is a trap, not an option set. The pair takes aria-disabled and the reason sits beside the group.

Do and don't

Dates are not set yet

Do

A blocked control stays reachable and says why beside itself.

Don't

The disabled attribute drops the control out of the tab order. A keyboard user never reaches it and never learns what is blocking them.

Do

The quiet tier is the editorial link. Accent ink, underline on hover, no fill.

Don't

Dashes mean emptiness, a drop target, a not-yet zone. On a control they read as a placeholder pretending to be one.

Do

A working primary keeps its colour one shade toward the ink and returns to its resting face the moment the answer lands.

Don't

Busy is not blocked. The disabled grey is reserved for a control that cannot be pressed and has a reason.

Do

The split's named face is a complete action. Press it and 24 hours are held; the menu holds the full 12 / 24 / 48 set.

Don't

A left face that itself opens a chooser makes the whole control a menu with a decorative seam.

Copy

The label answers what the agent is about to do. It is not a description of the control and never a category name.

  • Verb first, then the objectSend itinerary, not Itinerary and not Submit. A bare verb leaves the object to be guessed from position.
  • Name what changesA control that changes an object's state says which object and to what. Move enquiry #1119 to On hold… rather than Advance status.
  • The ellipsis promises a stepA trailing … means a confirmation or a form comes next. Without it the press commits.
  • Sentence caseOnly the first word and proper nouns are capitalised.
  • Leaving says soA control that opens another tab carries the external mark, and its accessible name says opens in a new tab in words.
  • No system dialectRefusal reasons are plain lowercase sentences. No codes, no vendor names, no caps.
  • The split face states the whole actionHold for 24 hours: verb, object, parameter. The press commits exactly what the label says.
  • Split menu items are the same sentenceEach alternative reads like the face it would become: Hold for 12 hours, not 12h.
  • The toggle names the choiceIts accessible name says what the menu chooses: Choose booking hold duration.

Accessibility

  • Target floor44px on touch. XS and S carry an invisible expander so their visible face can be smaller without shrinking the target.
  • Focus is never removedEvery tier and size shows the 2px camel outline at 2px offset.
  • Blocked, not removedaria-disabled="true" with the reason in a real element referenced by aria-describedby. Unblocking is announced through a polite live region, because a colour change is not an event.
  • The icon is decorationThe leading glyph carries aria-hidden="true". The label does the naming. An icon replaces a label or is absent — a row never ships both for the same fact.
  • MotionThe busy ring stops animating under prefers-reduced-motion and the busy state stays readable without it.
  • Split group, two namesrole="group" with a label naming the family; each face keeps its own accessible name. Tab reaches both faces in order, so the action face never surprises.
  • Split menu contractThe toggle carries aria-haspopup="menu" and aria-expanded; the menu is a real role="menu" with menuitems, closed on Escape and on choosing.

When to reach for something else

  • Going somewhereUse Link. A button that navigates lies about what will happen.
  • Only a glyph fitsUse Icon button, which carries the accessible name a bare mark cannot.
  • The whole row actsUse Row action, so the target is the row and not a small face inside it.
  • Padding is too loudUse the quiet tier with .mk-btn-flush. It is still the same button priority and state contract.

Markup

<!-- the action -->
<button class="mk-btn mk-btn-primary" type="button">Send itinerary</button>

<!-- with a leading icon, at L -->
<button class="mk-btn mk-btn-secondary mk-btn-l" type="button">
  <svg class="mk-btn-icon" data-mk-icon="plus" aria-hidden="true"></svg>Add property
</button>

<!-- blocked, with the reason reachable -->
<button class="mk-btn mk-btn-secondary" type="button"
        aria-disabled="true" aria-describedby="why">Hold rooms</button>
<p class="mk-claim" id="why">Dates are not set yet</p>

<!-- busy -->
<button class="mk-btn mk-btn-primary" type="button" aria-busy="true">
  <span class="mk-btn-spin" aria-hidden="true"></span>Checking availability
</button>

<!-- the quiet tier, aligned flush with an inline context -->
<button class="mk-btn mk-btn-ghost mk-btn-flush mk-btn-inline" type="button">Edit</button>

<!-- phone: every button is width:100% below 768px, from the component itself.
     .mk-btn-inline is the written opt-out for a control that truly shares a row -->
<button class="mk-btn mk-btn-secondary mk-btn-s mk-btn-inline" type="button">Edit</button>

<!-- split button: one default action, the menu of alternatives behind the toggle -->
<span class="mk-menu-wrap">
  <span class="mk-split-btn" role="group" aria-label="Booking hold actions">
    <button type="button" class="mk-btn mk-btn-primary mk-btn-s mk-split-btn-action">
      Hold for 24 hours
    </button>
    <button type="button" class="mk-btn mk-btn-primary mk-btn-s mk-split-btn-toggle"
            aria-haspopup="menu" aria-expanded="false"
            aria-label="Choose booking hold duration">
      <svg class="mk-split-btn-chevron" data-mk-icon="chevron" aria-hidden="true"></svg>
    </button>
  </span>
  <div class="mk-menu" role="menu" aria-label="Booking hold duration" hidden>
    <button type="button" class="mk-menu-item" role="menuitem">Hold for 12 hours</button>
    <button type="button" class="mk-menu-item" role="menuitem">Hold for 24 hours</button>
    <button type="button" class="mk-menu-item" role="menuitem">Hold for 48 hours</button>
  </div>
</span>