Design System

Design System

Link and button

Built with: a href · button type

Overview

A link takes the user somewhere. A button does something in the current page.

How it works

A link redirects the user to a new location: another page, or another section of the current one through a # parameter. It can also download a file. It needs an href to carry the semantics of a link.

A button dispatches an action inside the page: open a modal, play a video, post a comment.

Forcing a link to behave like a button, or a button like a link, goes against the native behaviour of both. If you find yourself doing it, you need the other element.

Live example

Links

Types and states

Every link is underlined in every state. Hover, focus and visited are forced in the table so they can be compared; the live links further down behave the same way.

DefaultHoverFocusVisited
Inlinedescaling guidedescaling guidedescaling guidedescaling guide
StandaloneSee all machinesSee all machinesSee all machinesSee all machines
ExternalAccessibility statementAccessibility statementAccessibility statementAccessibility statement
DownloadAnnual report (PDF, 2 MB)Annual report (PDF, 2 MB)Annual report (PDF, 2 MB)Annual report (PDF, 2 MB)
  • DefaultLilac #5e548e, 7.1:1 on white. Underline 1 px, offset 3 px.
  • HoverUnderline grows to 3 px. Colour change alone would not be enough.
  • FocusYellow #ffe600 background, ink text and the site focus ring: 4 px #0066ff outline plus a light halo. Visible on any background.
  • VisitedOptional. Darker lilac. Never the only way to tell links apart.
Links in text must not rely on colour

Don’t

Before you start, read the descaling guide and check the water hardness of your area.

Colour is the only cue. Lilac on ink text is under 3:1, so people with low vision or colour blindness can’t find the link (1.4.1).

Do

Before you start, read the descaling guide and check the water hardness of your area.

Underlined in body text, in every state. The underline is the cue; colour is a bonus.

Navigation menus, footers and card titles can drop the underline because their position already says “this is a link”, but they still need a visible hover and focus style.

Link text says where it goes

Screen reader users often pull up a list of links, read out of context. Each link must make sense on its own, or at least with its sentence (2.4.4). Same text means same destination (3.2.4).

Don’t

“Click here”, repeated “Read more” and raw URLs. A links list reads “click here, read more, read more, read more, h t t p s…”.

Do

The destination is in the link text. Readable alone, short, front-loaded with the key words.
When the design insists on “Read more”
Make the card title the link

The best fix. The title is unique and descriptive; “Read more” becomes decorative or goes away. See the Interactive Card pattern on this page for the stretched-link technique.

Or add hidden context

Visible “Read more”, accessible name “Read more about descaling”. The name still starts with the visible words, so voice control works (2.5.3).

Read more about descaling
<a href="/descaling">
  Read more<span class="visually-hidden"> about descaling</span>
</a>
New tabs and external sites

Don’t

Opens a new tab with no warning. Screen reader and magnifier users lose the Back button and their place (G201, 3.2.5).

Do

Avoid new tabs. If you must: the external icon, aria-hidden, plus “(opens in new tab)” in the accessible name. Same for leaving the site.
Downloads: say what and how big

Don’t

Download what? In which format? On a slow connection, how long will it take?

Do

File name, type and size in the visible text. Use the download attribute only when the file already has a location, in the app or on the server, that the href can point to.

Only with a location download belongs on a link only when there is a file to point to: a URL in the app or on the server. If the file has to be generated, converted or zipped first, there is nowhere to go yet: it is an action, so a <button> builds the file and then saves it.

Link or button for downloads
The file…ElementWhy
Already exists at a URL (/report-2026.pdf, a stored asset)<a href="/report-2026.pdf" download>It goes somewhere: a link. Copy, open in new tab and the links list all work.
Must be generated, converted or zipped first (export orders to CSV, convert to PDF, zip photos)<button type="button">Export orders (CSV)</button>It does something: build the file, then save it. No location exists until the action runs.
Skip link
Skip to main content

The main content. The skip link moves focus here.

First focusable element on every page, visually hidden until it gets focus, then shown in the Focus state above. Press Tab in the mock header to reveal it. It points to <main id="main">, which takes focus with tabindex="-1" (2.4.1 Bypass Blocks).

A link can look like a button. A link without href can’t be a link.

Do

<a href="/shop" class="btn btn--primary">. Navigates, so it is a link styled as a button: the arrow icon (aria-hidden) tells people it takes them somewhere. Enter only; it shows in the links list.

Don’t

<a onclick="openMenu()">Menu</a><a href="#" onclick="save()">Save</a><a href="javascript:void(0)">Close</a>
No href, or an href that goes nowhere, to run a script. Use <button>.

Buttons

Variants and states

Hover, focus, pressed, disabled and loading are forced in the table so they can be compared; the live buttons further down behave the same way.

DefaultHoverFocusPressedDisabledLoading
primary
secondary
tertiary
  • HoverFill darkens. Never the only cue for anything: touch and keyboard users never see it.
  • FocusThe site focus ring: 4 px #0066ff outline, 3 px outside the button. The hard shadow drops so the ring is the only edge. Use :focus-visible. 3:1 against the page (2.4.7, 2.4.11).
  • PressedShadow drops and the button moves 4 px (translate). Pure feedback, no meaning.
  • DisabledDashed border plus muted text, not colour alone. See the disabled section before using it.
  • LoadingSpinner is aria-hidden, label stays, focus stays. Progress is announced in a live region (see Loading and feedback).
Accessible name: say what happens

The visible label is the accessible name. It names the action, ideally verb + object. Voice-control users say what they see, so the name must start with the visible text (2.5.3 Label in Name).

Don’t

Generic labels. Out of context (a screen reader buttons list) nobody knows what they do.

Do

Verb + object. Each button makes sense on its own.

Don’t

Icon-only button with no name. Screen readers say “button”. A trash icon can mean delete, discard or clear.

Do

aria-label="Delete draft" on the button, icon aria-hidden. The same text appears as a tooltip on hover and focus (1.4.13).

Don’t

aria-label="Submit the contact form"
The aria-label replaces the visible text. “Click Send” in voice control does nothing (2.5.3).

Do

No aria-label, or aria-label="Send message"
Let the visible text be the name. If you add context, start with the visible words.
Target size and spacing
  • 24×24 · minimum (2.5.8 AA)
  • 44×44 · our default (2.5.5 AAA)
Rules

Every button in this system is at least 44×44 px, including icon buttons and the tertiary variant. Smaller visual icons keep a 44 px hit area. Keep at least 8 px between adjacent targets so a 24 px circle around each one never overlaps. Text grows with rem; the button grows with it (1.4.4).

Focus must be visible and never clipped

Don’t

outline: none, or a parent with overflow: hidden cutting the ring. Keyboard users lose their place (2.4.7, 2.4.11).

Do

:focus-visible { outline: 4px solid #0066ff; outline-offset: 3px; } and enough room around the button for the ring. Press Tab to see it.
disabled vs aria-disabled
disabled

Removed from the tab order and ignored by click. Screen reader users browsing with Tab never find it, so they never learn why they cannot continue. Fine for controls that are irrelevant right now.

aria-disabled="true" + a reason

Stays focusable and is announced as “dimmed / unavailable”. On activation, do nothing and point to the reason. Best for submit buttons and steps.

Accept the terms to continue.

Better still: keep the button enabled and validate on submit, moving focus to the first error. Disabled styling is exempt from contrast rules, but ours stays readable (ink-soft on fill, 7:1) and adds a dashed border so it is not shown by colour alone (1.4.1).

Toggle and menu buttons: state comes from ARIA
Toggle · aria-pressed

The label never changes; the state does. Screen readers: “Favourite, toggle button, pressed”. Don’t also swap the label to “Unfavourite”: the state would be read twice and contradict itself.

Menu or disclosure · aria-expanded

“Options, button, collapsed”. aria-controls points to the menu. Esc closes the menu and returns focus here. The chevron is decorative.

Loading and feedback
Save changes

On activation the button keeps its label and its focus, gets aria-disabled="true" and a spinner (aria-hidden), and goes back to Default when it is done. A role="status" line announces “Changes saved”.

Don’t set disabled while loading: the focused element leaves the tab order and focus falls back to <body> (2.4.3). Don’t replace the label with only a spinner: the name becomes empty.

Usage

Which one to use

  • An expandable section: a <button> with aria-expanded.
  • Jump to a section of the same page: an <a> with href="#section".
  • Load more items into a list: a <button>, then place focus on the first new item.
  • Read more somewhere else: an <a> with an href.
  • Open a menu: a <button> with aria-haspopup and aria-expanded.
  • Play or pause a video: a <button>.
  • Download a file: an <a> with the download attribute.
  • A card that leads to a detail page: a <a> stretched with ::after.

Avoid

  • outline: none without a replacement focus style. Keyboard and screen reader users need a visible focus on every interactive element.

Accessibility

Keyboard

  • A link is activated with Enter. Space scrolls the page instead.
  • A button is activated with Enter or Space.

Screen readers

  • A link is announced as “Link, [name]” and a button as “Button, [name]”. A descriptive and correct accessible name is the most important thing on either.

Related

References

Read the post: Link or Button

On this page