jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · General

link — the typographic lane, standing alone

A native <a> carrying the prose face's non-nav lane as its own utilities — primary ink, 4px underline offset, hover underline — so it works with no jx-pure scope in sight; inside one, the values coincide with the face's own channels (same property, same token, deterministic no-op). External detection is PATTERN-based, not origin-based: any absolute http(s) href is external — no window.location semantics dragged into SSR — and externals open a new tab with rel=noreferrer (the fleet convention; doc-link and PressButton both ship bare noreferrer, and modern browsers imply noopener from it). href, title and the external pair land AFTER the spread (the separator law): the contract is the component's, not overridable through rest props.

install — link

$npx jixoai-ui add link

https://ui.jixoai.com/r/link.json

native <a> http(s) = external · new tab rel=noreferrer (noopener implied) icon tri-state · undefined ≠ off app routes stay same-tab

overview

Overview

A native anchor carrying the prose face's non-nav lane as its own utilities; the href alone decides the external contract; the axes stamp a component whose paint is one ink, one offset, one hover.

Link renders one native <a> carrying the face B2 non-nav lane as its OWN utilities — --jx-primary ink, a 4px underline offset, underline on hover — so it stands alone outside any jx-pure scope; inside one, the values coincide (same property, same token — a deterministic no-op, and the face never declared a different value anyway). The external contract is PATTERN-based: any absolute http(s) href is external — target="_blank" + rel="noreferrer", landed after the spread (the separator law) — because window.location has no place in an SSR-safe registry component.

The suffix-icon lane is the tri-state input semantic-glyph law verbatim: undefined paints the default externalLink glyph IFF external (SSR-painted, hydration-matched), null is the explicit OFF, a snippet renders custom content — and internal links never carry the lane. The glyph is em-sized (0.8em) with the 0.2em gap owning the whole label-to-glyph distance, so the lane rides any ambient scale — which is exactly how the size axis reaches it.

A composed leaf, precisely: the markdown family maps prose links to it (grep receipt: markdown-node is the one component edge), and it composes nothing but the icon. The eight axes resolve all no-own on the anchor — size is the one CONSUMED voice (the em lanes), theme is the frozen pole (the pure-alias case: a dark island re-substitutes nothing), and six stamp-and-supply. Per-axis below; the shared grammar lives on the universal props page.

usage

Usage

Hand it an href — that alone decides the external contract. Everything else is the advisory and attribute passthrough.

Link usage
<script lang="ts">
import { Link } from '@ui/link';
</script>

<div class={cx(rt.flex, rt.wFull, rt.maxWXl, rt.col, rt.gap12, rt.lkBody)}>
  <p class={cx(rt.m0)}>
    Same document, different lanes: read
    <Link href="/docs/components/markdown.html" title="the markdown page">the markdown page</Link>
    here, or leave for
    <Link href="https://github.com/jixoai/ui" title="the repository">the repository</Link>
    in a new tab.
  </p>
  <p class={cx(rt.m0, rt.inkMuted)}>
    Prose composition — the offset keeps the underline off the descenders:
    <Link href="https://commonmark.org" title="the CommonMark spec">CommonMark</Link>,
    <Link href="https://tailwindcss.com/docs/typography-plugin" title="the Typography plugin">Tailwind Typography</Link>,
    and quiet reference ink inside a muted paragraph.
  </p>
</div>

link

The two lanes: app routes keep same-tab default navigation (a route change, not a departure), absolute http(s) hrefs open in a new tab with rel=noreferrer. Same component, same ink, one pattern deciding the contract.

Same document, different lanes: read the markdown page here, or leave for the repository in a new tab.

Prose composition — the offset keeps the underline off the descenders: CommonMark, Tailwind Typography, and quiet reference ink inside a muted paragraph.

link-usage.svelte svelte
<script lang="ts">
import { Link } from '@ui/link';
</script>

<div class={cx(rt.flex, rt.wFull, rt.maxWXl, rt.col, rt.gap12, rt.lkBody)}>
  <p class={cx(rt.m0)}>
    Same document, different lanes: read
    <Link href="/docs/components/markdown.html" title="the markdown page">the markdown page</Link>
    here, or leave for
    <Link href="https://github.com/jixoai/ui" title="the repository">the repository</Link>
    in a new tab.
  </p>
  <p class={cx(rt.m0, rt.inkMuted)}>
    Prose composition — the offset keeps the underline off the descenders:
    <Link href="https://commonmark.org" title="the CommonMark spec">CommonMark</Link>,
    <Link href="https://tailwindcss.com/docs/typography-plugin" title="the Typography plugin">Tailwind Typography</Link>,
    and quiet reference ink inside a muted paragraph.
  </p>
</div>

link

The suffix-icon lane — tri-state icon, the input semantic-glyph law verbatim: undefined is NOT off. Omitted paints the default externalLink glyph IFF external (an inline-core name answering getIcon synchronously — SSR paints it, hydration matches, no flash); null turns the lane OFF; a snippet renders custom content in the lane. Internal links never carry the lane at any setting.

undefined (omitted) — the default glyph on an external: the repository

null — the lane explicitly off: a quiet external (still target=_blank; only the glyph is gone)

snippet — custom content in the lane: leaving the document

internal — the lane never ships: staying in the document

link-icon-usage.svelte svelte
<script lang="ts">
import { Link } from '@ui/link';
</script>

<div class={cx(rt.flex, rt.wFull, rt.maxWXl, rt.col, rt.gap12, rt.lkBody)}>
  {#snippet arrowGlyph()}<span aria-hidden="true" class={cx(rt.fontMono)}>→</span>{/snippet}
  <p class={cx(rt.m0)}>
    undefined (omitted) — the default glyph on an external:
    <Link href="https://github.com/jixoai/ui">the repository</Link>
  </p>
  <p class={cx(rt.m0)}>
    null — the lane explicitly off:
    <Link href="https://github.com/jixoai/ui" icon={null}>a quiet external</Link>
    (still target=_blank; only the glyph is gone)
  </p>
  <p class={cx(rt.m0)}>
    snippet — custom content in the lane:
    <Link href="https://github.com/jixoai/ui" icon={arrowGlyph}>leaving the document</Link>
  </p>
  <p class={cx(rt.m0, rt.inkMuted)}>
    internal — the lane never ships:
    <Link href="/docs/components/markdown.html">staying in the document</Link>
  </p>
</div>

law

Why pattern, not origin

Origin comparison (href.origin !== window.location.origin) is the familiar heuristic — and it drags window.location into a server render, couples the component to the host it mounts in, and misfires on preview hosts and subpath deployments. The pattern reading is stricter by design: ANY absolute http(s) href is a departure from the document, which is exactly what the new-tab contract means. Relative routes, anchors and non-http schemes keep default navigation.

the external contract
// the whole external contract, as source:
const external = /^https?:\/\//i.test(href);

<a
  href={href}
  target={external ? '_blank' : undefined}
  rel={external ? 'noreferrer' : undefined}
  data-jx-link={external ? 'external' : 'internal'}
/>

api

API

Props extend the native HTMLAnchorAttributes minus color (the §1 collision rule) — href alone decides the external contract, icon decides only the decorative suffix lane; 15 meta props − 8 ambient axes = 7 rows, all curated.

Properties

PropertyTypeDefaultDescription
href * string—The link target — and the whole external contract: an absolute http(s) href renders target="_blank" + rel="noreferrer" and data-jx-link="external"; relative routes, anchors and non-http schemes keep same-tab navigation. Lands AFTER the spread (the separator law) — not overridable through rest.
title string—Advisory title, passthrough to the native attribute — also after the spread (the component's contract).
icon Snippet | null—The suffix-icon lane, TRI-STATE (undefined ≠ off): omitted paints the default externalLink glyph IFF the link is external (inline-core — SSR-painted, hydration-matched, 0.8em); null turns the lane off; a snippet renders custom content in the data-jx-link-icon span. Internal links never carry the lane at any setting.
children Snippet—The link label — the accessible name. Omit for attribute-only anchors; write the departure into the words, not a glyph.
class string''Forwarded to the anchor; consumer classes land last (tailwind-merge replaces, never stacks).
style unknown—Inline style passthrough — composed AFTER the family's carrier stamp (the #4 seam law: never clobbered, never dropped).
rest unknown (spread passthrough)—Every other anchor attribute passes through (Omit<HTMLAnchorAttributes, 'color'> — the §1 native-collision rule); href, title, target and rel land after the spread as the component's contract.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

axes

The eight axes on link

The prose link is one ink, one offset, one hover — so six axes stamp-and-supply with zero family readers (grep receipt). The exceptions: size is CONSUMED through the em voices (the label inherits the stamp; the 0.8em glyph rescales with it), and theme is the frozen pole's pure-alias case (the ink is defineVars-resolved at the stylex :root scope — a dark island re-substitutes nothing; forced-colors is the platform exception, media-gated). The carriers stamp the anchor root, greppable in the raw SSR.

Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.

PropertyTypeDefaultDescription
size 'small' | 'medium' | 'large' | 'auto' | number'auto'CONSUMED through the em voices — the §11 stamp sets the anchor's font-size and BOTH em-voiced surfaces follow: the label text (inherited — the anchor atom declares no font-size of its own) and the 0.8em suffix glyph (measured: 14px stamp → 14px anchor, 11.2px glyph; auto → the ambient scale). The underline offset is fixed 4px optical geometry and does NOT scale. Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — stamps --jx-shape-effective + --jx-radius-factor-effective; no family css reads them (grep receipt: zero carrier reads in ui/link/). The anchor draws no box. Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero readers). Nothing rounds. Number unit: px.
density 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings)'auto'SUPPLY-ONLY — the named rung stamps data-density on the anchor; the family reads none of the re-based channels (grep receipt: zero --jx-text/--jx-hit/--jx-inset reads — the icon lane's gap is the em-relative --link-icon-gap promotion seam, not a rung channel). The stamp supplies composed/guest content. Number unit: coefficient.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SUPPLY-ONLY — the instrument's ink is already --jx-primary (the alias), so the hue carrier adds nothing: --jx-color-effective stamps and zero family css reads it (grep receipt). Hue injection goes through the jx-hue-* utilities on the surrounding tree. Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'THE FROZEN POLE, the pure-alias case — measured: the ink is tokens['--jx-primary'], a stylex defineVars member declared (and therefore resolved) at the stylex :root theme scope; the anchor inherits the already-substituted light value, so a scoped .dark re-substitutes NOTHING (receipt: --primary flips on the anchor, --jx-primary does not; the computed color is byte-identical). A dark island keeps the light-profile accent — consumers compose dark links inside their own dark ground. The one platform exception is forced-colors: active, which swaps the ink to the LinkText system keyword by media query, not by the axis. light and system stamp nothing — tree inheritance.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (grep receipt: zero shadow declarations). Text casts no shadow. Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. The hover underline is instant (no transition declared); nothing animates. Number unit: coefficient.

Deviations, cited: the adoption is the census batch D row (the long tail — openspec/changes/explicit-props/research/migration-census.md). The §1 collision rule: the interface is Omit<HTMLAnchorAttributes, 'color'> — the native color attribute cleared for the hue lane; href, title, target and rel land after the spread as the component's contract. A composed leaf, precisely: markdown-node maps prose links here (the one component edge), and it composes nothing but the icon.

the eight axes on link
<!-- size: CONSUMED through the em voices — the stamp sets the
     anchor's font-size, the label text inherits it, and the 0.8em
     suffix glyph rescales with it -->
<Link href="https://github.com/jixoai/ui" size={14}>14px label · 0.8em glyph</Link>

<!-- theme: the FROZEN pole (the pure-alias case) — the ink is
     tokens['--jx-primary'], resolved at the stylex :root scope, so a
     dark island re-substitutes nothing: byte-identical ink -->
<Link href="/docs.html" theme="dark">dark island, same ink</Link>

link · the em voices and the frozen pole

size auto — the ambient scale ambient label ↗
size={14} — label 14px, glyph 0.8em → 11.2px (measured) stamped label ↗
light — the ambient accent light ink
theme="dark" — .dark stamps; the ink is byte-identical (the frozen pole) dark island, same ink

The size pair is the consumed lane, measured: the stamped anchor computes exactly 14px and the external glyph scales to 0.8 × 14 = 11.2px with it — while the underline offset stays the fixed 4px optical calibration. The theme pair is the pure-alias frozen pole: both anchors compute the same ink (the :root-resolved --jx-primary) even though --primary itself flips on the element — the substitution site decides, and here nothing rides the flipping var.

link-axes.svelte svelte
<!-- size: CONSUMED through the em voices — the stamp sets the
     anchor's font-size, the label text inherits it, and the 0.8em
     suffix glyph rescales with it -->
<Link href="https://github.com/jixoai/ui" size={14}>14px label · 0.8em glyph</Link>

<!-- theme: the FROZEN pole (the pure-alias case) — the ink is
     tokens['--jx-primary'], resolved at the stylex :root scope, so a
     dark island re-substitutes nothing: byte-identical ink -->
<Link href="/docs.html" theme="dark">dark island, same ink</Link>
one real query() case
<script lang="ts">
  import Link from '@ui/link';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- below 48rem the base (14px) applies; at 48rem+ the md case (18px)
     wins — label text and the 0.8em glyph step together -->
<Link href="/docs.html" size={query({ md: 18 }, 14)}>responsive label</Link>
responsive label

Media keys are min-width: below 48rem the base applies — 14px; at 48rem and wider the md case wins — 18px, label and glyph together. The number lane goes bare (results infer); string lanes take both generics. Resize across 48rem.

TokenDefault
the B2 non-nav lane--jx-primary ink · underline on hover
the offsettext-underline-offset: 4px
--link-icon-gap0.2em (promotion seam)
the optical baselinevertical-align: -0.125em
forced-colors inkLinkText
the hooksdata-jx-link={external|internal} · data-jx-link-icon

a11y

Accessibility

Native anchor semantics; the accessible name is the link text (write the departure into the words, not a glyph).

Keyboard

KeyAction
EnterActivates the anchor — native behavior, untouched

ARIA

AttributeValueDescription
anative elementThe platform announces the link role and its href; no extra ARIA.
target / relexternal pairtarget=_blank + rel=noreferrer land only on detected externals; the implied noopener is the fleet convention.
data-jx-link'external' | 'internal'Hook attribute naming which lane rendered — styling and tests read it without reverse-engineering the href.
data-jx-link-iconaria-hidden laneThe suffix-icon span is decorative: the accessible name is the link text alone, and the target/rel pair — never a glyph — carries the departure semantics.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h