Link
The anchor voice of the Navigation family: a link in running prose, or a handle in a nav strip. Always an <a>, never polymorphic.
Playground
<script lang="ts">
import { Link } from '@urbicon-ui/blocks';
const href = '/projects/42';
</script>
<Link
{href}
>
Project settings
</Link>01 Examples
A link in prose
--color-text-link token plus an underline. Two cues, because colour alone is not one for every reader — and the token is the single lever for restyling links project-wide. Hover moves the underline, not the text colour, so a paragraph of links does not flicker under the pointer.Read the Breadcrumb page before wiring a trail, or jump straight to the token reference.
<p>
Read the <Link href="/blocks/primitives/breadcrumb">Breadcrumb page</Link> before
wiring a trail, or jump straight to the
<Link href="/customization/tokens">token reference</Link>.
</p>Route tabs
active writes the aria-current the strip needs; the URL holds the rest, so there is no component state to keep in step. The strip below is live: these are real pages, and the lit handle is the one you are reading. One difference between it and the snippet: this docs app routes its hrefs through resolve(), which the built site renders relative to the current page, so the preview resolves each href against page.url before comparing. An app with absolute hrefs uses the comparison as written here.<script lang="ts">
import { Link } from '@urbicon-ui/blocks';
import { page } from '$app/state';
const tabs = [
{ label: 'Breadcrumb', href: '/blocks/primitives/breadcrumb' },
{ label: 'Link', href: '/blocks/primitives/link' },
{ label: 'Pagination', href: '/blocks/primitives/pagination' }
];
</script>
<nav
aria-label="Related component pages"
class="flex flex-wrap gap-4 py-[calc(var(--blocks-focus-ring-width)+var(--blocks-focus-ring-offset))] text-sm"
>
{#each tabs as tab (tab.href)}
<Link variant="standalone" href={tab.href} active={page.url.pathname === tab.href}>
{tab.label}
</Link>
{/each}
</nav>A link you cannot follow yet
disabled keeps the address on the element and takes the link out of the tab order: it is dimmed, cancels every activation — pointer, Enter, assistive technology — and announces itself as aria-disabled. Reach for it where the target exists but is not reachable in this state; the one below would open the Pagination page.<Link href="/blocks/primitives/pagination" disabled>
Pagination (unlocked once the list has more than one page)
</Link>02 Accessibility
Always an anchor
Link renders an <a href> and never swaps its root element, so
browser history, middle-click, copy-link, prefetch and the no-JavaScript fallback all come
for free. A link that should look like a button stays a thin wrapper over buttonVariants() in your own app.
The current page announces itself
active renders aria-current="page", which is how assistive
technology reads back which handle in a strip is the one you are on. It is the shorthand for
the page case and wins where both are given — for "step" in a wizard trail, or "true" for a non-page target, pass aria-current yourself and leave active unset.
A disabled link answers nothing, but keeps its address
disabled writes aria-disabled="true" and tabindex="-1", and cancels the navigation in a click
handler — so a pointer, Enter on a focused link and an assistive-technology activation all
do nothing, and your own onclick is not called
either. pointer-events-none alone would only stop the mouse.
The href stays on the element, so the address is still readable
and copyable, and keyboard users skip past the link rather than landing on a control that answers
nothing.
Give the focus ring room in a scrolling strip
Focus is drawn with outline on :focus-visible only, following the link's line
fragments rather than boxing the whole paragraph, and taking its width, offset and colour
from the --blocks-focus-ring-* tokens. A horizontally
scrolling container clips at its padding box, so give it padding computed from those same
tokens — py-[calc(var(--blocks-focus-ring-width)+var(--blocks-focus-ring-offset))] — rather than a fixed value: under prefers-contrast: more the ring grows, and a hand-counted
padding clips it exactly for the readers who need it most.
03 API Reference
11 propsProp | Type | Default | Description | |
|---|---|---|---|---|
children required | Snippet | — | The link text. Required — the anchor's accessible name is what it renders. | |
href required | string | — | The address the link points at. Opaque to the library — it is written to the href
attribute unchanged, so resolve(), a base path and the internal-vs-external decision stay
in app code. Required: a Link without an address is not a Link. | |
active | boolean | false | Mark this link as the one the current page is at: it renders aria-current="page", lifts
the link to document ink at font-medium and drops its hover state — you are already
there. Written for the standalone voice, where a current handle in a strip is the point;
on an inline link it also changes the weight mid-sentence, so where only the attribute is
wanted pass aria-current="page" directly instead. active is the shorthand for the page
case and wins where both are given, so the other aria-current values ("step" in a wizard
trail, "true" for a non-page target) are reached by passing aria-current and leaving
active unset. | |
class | string | — | Additional CSS class merged onto the root <a>. | |
disabled | boolean | false | Render the link inert: aria-disabled="true", tabindex="-1", pointer-events-none and a
click handler that cancels the navigation — so a pointer, Enter on a focused link and an
assistive-technology activation all do nothing, and a consumer's own onclick is not called
either. Drawn at reduced opacity. The href stays on the element: a disabled link keeps its
address readable and copyable, it just leaves the tab order and answers nothing. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Link: {...} }}>.
Prefer this over class overrides when a project wants one link look everywhere —
presets keep the hover and dark-mode logic coherent and reusable. | |
slotClasses | Partial<Record<LinkSlots, string>> | — | Per-slot class overrides. Slots: base | |
unstyled | boolean | — | Strip all default styles; combine with slotClasses to rebuild from scratch. | |
variant | inlinestandalone | 'inline' | Link voice. inline sits in running prose, painted in --color-text-link and underlined;
standalone is a handle in a <nav>, a filter row or a table header — no underline, and it
takes its colour from the ink ramp rather than the link token. | |
...HTMLAnchorAttributes inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class' | 'href') | |
...LinkVariants variant | VariantProps | — | Styling variants from LinkVariants |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
LinkProps | interface | props | 0 | — | |
LinkVariants | type | variant | 1 | — | |
LinkSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. |
05 Installation
Import
import { Link } from '@urbicon-ui/blocks';