Components / navigation
Link
The underline is on by default rather than off, which is the opposite of most systems' inline links. The reason is the one above: an inline link with no underline is identified by color alone.
Summary
A native anchor with the contract applied.
| APG pattern | Link |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
Use it for anything that navigates, and a Button for anything that acts.
<Link href="/projects/northlight">Northlight</Link>
<Link href="https://studioedit.app" external icon={<Icon path={ICONS.arrowRight} />}>studioedit.app</Link>
<Link href="/settings" emphasis="standalone" intent="neutral">Account settings</Link>
Anatomy
An <a data-se="link">. It owns its contents.
| Part | Element | Required |
|---|---|---|
icon | the caller's | the caller's |
item | the caller's | the caller's |
sr | <span> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
href | string | required | Required. An anchor without one is not a link — role generic, no focus, no activation. Use <Button> for something that acts rather than navigates. |
emphasis | inline · standalone | inline | inline sits in a sentence and stays underlined. standalone is the whole line. |
intent | accent · neutral | accent | accent for a link in the reading flow, neutral where a row of links would otherwise shout. |
external | boolean | none | Opens in a new tab, sets rel="noopener noreferrer", and says so to a screen reader. |
icon | React.ReactNode | none | An <Icon> marking an external destination. |
States
| State | Expressed as | |
|---|---|---|
| Pressed | :active | |
| Hover | :hover |
Behavior
It navigates. external sets rel="noopener noreferrer" and appends screen-reader text.
Accessibility
The link text is the accessible name, so it has to make sense read alone. A page of links all named "Read more" is a list of identical entries to anyone navigating by link.
external says so out loud. It appends "(opens in a new tab)" in data-part="sr" — a link that leaves the site without saying so is a small betrayal, and a reader who has lost their place cannot get it back with the back button.
Space does not activate a link, and Enter does. That is the platform, and it is the reason the Link/Button distinction is not cosmetic.
href is required, and it was not always. An anchor without one has role="generic", takes no focus and does nothing on Enter — while reading as ordinary text, which is why the omission survived review for so long.
Keyboard
| Key | Result |
|---|---|
Enter | Follows the link |
Space | Nothing — the platform reserves it for scrolling |
Roles, states and properties
_None written. The elements carry their own._
Focus
A normal tab stop. That Space does nothing here and everything on a Button is the clearest reason the two must not be swapped.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/apg/partials-sweep.test.jsxtest/browser/icon-box.spec.mjstest/browser/site.spec.mjstest/browser/upstream-natives.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-accent | color |
--se-border-width | text-decoration-thickness |
--se-duration-instant | transition |
--se-ease-standard | transition |
--se-mix-down | color |
--se-mix-shade | color |
--se-radius-sm | border-radius |
--se-space-1 | margin-inline-start |
--se-text | color |
--se-text-muted | color |
--se-weight-medium | font-weight |
Motion
Color crosses --se-duration-instant on hover. The underline does not animate — a moving underline under text being read is noise.
Content
Name the destination, not the act of going there. Account settings, not Click here for account settings.
Sentence case, and no terminal punctuation inside a sentence.
Usage
If it changes the address, it is a Link. A link that is really a button cannot be triggered with Space; a button that is really a link cannot be opened in a new tab. This is a governance question rather than a styling one.
emphasis="standalone" drops the underline and is only safe when the link is the whole of its line. Inside a sentence, keep it: color alone fails roughly one man in twelve.
When not to use it. For an action styled to look quiet. That is Button with emphasis="transparent".
It sets no size, and that is the feature. A link takes the family, size and weight of the text it sits in — verified in the specimen across lead, body, ui and eyebrow, including the narrow face the last of those switches to. So there is no size prop and there should not be one: a link in a heading is heading-sized because it is in a heading, and a prop would let the two disagree.
What it does set is the underline and the color, which are the only things that make it a link rather than the words around it.
Notes
The underline is on by default rather than off, which is the opposite of most systems' inline links. The reason is the one above: an inline link with no underline is identified by color alone.