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.
- 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.
Do
Before you start, read the descaling guide and check the water hardness of your area.
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
To see our warranty, click here.
Do
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
Read our accessibility statement.
Do
Downloads: say what and how big
Don’t
Do
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.
| The file… | Element | Why |
|---|---|---|
| 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
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><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.
| Default | Hover | Focus | Pressed | Disabled | Loading | |
|---|---|---|---|---|---|---|
| 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
Do
Don’t
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"Do
No aria-label, or aria-label="Send message"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
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>witharia-expanded. - Jump to a section of the same page: an
<a>withhref="#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>witharia-haspopupandaria-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: nonewithout 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.Spacescrolls the page instead. - A button is activated with
EnterorSpace.
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.