Skip to main content
Urbicon UI

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

Variant
<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

The default voice: link ink from the --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

A nav of standalone handles: no underline, tertiary at rest, document ink under the pointer, and the current one already there at medium weight — so hovering shows what arriving will look like. 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 props
11 props 2 required
Prop
Type
Default
Description

04 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

05 Installation

Import

import { Link } from '@urbicon-ui/blocks';