9.4 KiB
Link
Link (shared/components/Link/Link.tsx) is the site's anchor component,
built from the BDS Link spec (components/link.md / link.json /
link-examples.md in pd-xrpl-developer-docs). It's the only anchor that
should be used for new JSX anchor tags going forward. The old design-system
link, BdsLink, is kept intact but unused — see
legacy/BdsLink.md.
A live reference of every variety (size × color group × with/without icon,
light and dark) is at /link-demo (link-demo.page.tsx),
each example labeled with the spec entry it matches.
Usage
import { Link } from 'shared/components/Link';
// or, in files that already import Redocly's routing Link:
import { Link as XrplLink } from 'shared/components/Link';
<Link href="/docs/concepts/" intention="brand" variation="standalone">
Read More
</Link>
<p>
Body copy with an <Link href="/docs/">inline link</Link> in a sentence.
</p>
Link always renders a real <a href> — but for anything that isn't an
external URL (http(s):/mailto:) or a bare #fragment, that <a> comes
from Redocly's own routing Link (@redocly/theme/components/Link/Link),
not a plain one. This matters, not just cosmetically: Redocly's Link
resolves relative paths against the current route, rewrites the path for the
current locale (so href="/docs/concepts/" correctly becomes
/ja/docs/concepts/ for a /ja/... visitor instead of silently bouncing
them to the English page), and drives client-side navigation plus
hover-prefetch instead of a full page load. Writing href="/docs/concepts/"
gets all of that automatically — there's no separate "internal" prop to set.
A control that acts rather than navigates is a Button, even if it's meant
to look like a link.
Props
| Prop | Values | Default | Notes |
|---|---|---|---|
intention |
brand | neutral |
brand |
brand = XRPL green. neutral = black/white, no distinct hover, sage :visited. |
context |
on-theme | on-inverse | on-saturated |
on-theme |
Which surface the link sits on — controls both color group and focus-ring color. neutral + on-saturated is a compile-time type error (no such combination exists in the spec). |
variation |
inline | standalone |
inline |
inline sits inside running text: no fixed size (inherits the parent's font-size/line-height), no icon, display: inline so it wraps. standalone is a link acting as its own element (a card's "Read More", a CTA under a paragraph): fixed size, optional trailing icon, display: inline-flex. |
size |
sm | md | lg |
md |
Only affects standalone — inline always inherits from its parent. |
iconEnd |
boolean |
false |
Trailing arrow, from the shared icon set (shared/components/Icons) -- XrplArrowInternalLinkIcon, or XrplArrowExternalLinkIcon when target="_blank" is set. Only rendered when variation="standalone"; ignored on inline. |
href |
string |
— | Required. |
Everything else (target, rel, onClick, aria-*, ...) passes through to
the <a>.
There's no disabled state — the old component had one, but the new spec
forbids it (ADR-XRPL-012): a disabled link is a contradiction, since a link
that can't be followed shouldn't render as a link.
Color groups → intention × context
intention |
context |
Result |
|---|---|---|
brand |
on-theme |
XRPL green rest/hover, sage :visited |
brand |
on-inverse |
Green-on-dark-surface variant (e.g. a link inside a dark card on a light page) |
brand |
on-saturated |
Black-on-green (e.g. a link inside a solid-green banner) |
neutral |
on-theme |
Black (light) / white (dark), flat across hover/active, sage :visited |
neutral |
on-inverse |
Neutral tuned for a dark card on a light page |
Every resolved value lives in one place: styles/_link-tokens.scss's
xrpl-link-colors($group) mixin. Link's own stylesheet
(_link.scss) just calls it per --intention class
(.xrpl-link--brand, .xrpl-link--neutral-on-inverse, etc.) — it never
hardcodes a color itself. Change a token once here and both Link and the
sitewide fallback (below) pick it up.
neutral's :hover/:active are intentionally flattened to match its rest
color — that's a deliberate design decision (a black link doesn't get a color
change on hover), not an incomplete state. :visited is not flattened for
either group and follows the spec's sage values: neutral uses $sage-11/
$sage-dark-11 (a muted gray, clearly distinct from rest), brand uses
$sage-12/$sage-dark-12 (legitimately near-black/near-white). Both are
correct per spec, not bugs, and can only be seen with real browsing history —
not in an incognito/sandboxed browser, and not on /link-demo's links
directly (see the reset button there, which points every demo link at a
fresh, never-visited URL instead).
What plain <a> tags get (the fallback)
Most of the site's links aren't JSX — they're [text](url) or raw
<a href="..."> inside .md/.mdx files. Two mechanisms make sure those
still get themed, without every markdown file needing to import a component:
-
MarkdownLinkoverride —@theme/components/MarkdownLink.tsxreplaces Redocly's default renderer for markdoc's[text](url)syntax (registered in@theme/markdoc/components.tsx). It still renders Redocly's own routingLink(so client-side navigation keeps working) but stamps it withLink's class scheme via the exportedlinkClassName()helper, forced tointention="neutral" variation="inline"— docs/technical body copy stays black, not brand green, per design direction. This covers the overwhelming majority of markdown links site-wide with no per-file changes. -
CSS-cascade fallback —
styles/_content.scss'sa:not(.xrpl-link)rule, scoped to[data-component-name="Markdown/Markdown"] article, applies the same neutral/on-theme mixins directly. This is the safety net for anything theMarkdownLinkoverride doesn't reach: raw HTML<a>embedded directly in a.mdfile (markdoc passes those through as literal HTML, bypassing thelinknode entirely) and tag components that render their own<a>(e.g.{% child-pages %}→ChildPages.tsx).
Both paths call the exact same _link-tokens.scss mixins Link itself uses,
so there's one source of truth for color/underline/focus-ring values —
nothing is hand-duplicated.
If you're building raw HTML strings that can't use JSX at all (e.g. a data
array rendered with dangerouslySetInnerHTML, like the FAQ answers in
about/index.page.tsx), you can opt a
specific link into a non-neutral treatment by writing linkClassName()'s
output literally into the string:
<a href="..." class="xrpl-link xrpl-link--inline xrpl-link--md xrpl-link--brand xrpl-link--ctx-on-theme">...</a>
Anything without an xrpl-link class at all still falls through to the
neutral fallback above — it never renders unstyled.
Adding a new :not(.xrpl-link) site
A handful of older, page/component-scoped stylesheets style <a> tags
directly (landing-page body copy, FAQ accordions, card descriptions, etc.),
predating this component. Each one needs a :not(.xrpl-link) exclusion so it
doesn't fight the component's own color rules on specificity — otherwise a
Link rendered inside that context can get silently overridden (this has
happened several times: _landings.scss, _light-theme.scss's separate
.landing block, _pages.scss's FAQ rule, CardTextIconCard.scss's
description-link rule). If you add a new CSS rule that targets a by tag
inside some content area, exclude .xrpl-link from it, the same way the
existing ones do.
Specificity gotcha: :where() and bds-theme-mode
_link-tokens.scss's mixins wrap both light and dark branches in an
explicit bds-theme-mode(light) / bds-theme-mode(dark) call — neither is
left as an unscoped default. This isn't stylistic: an unscoped
.xrpl-link--brand { color: ... } is (0,1,0) specificity, and it loses to
_light-theme.scss's generic html.light a:where(:not(.bds-link)...)
fallback, which resolves to (0,1,2) — :where() zeroes the specificity of
selectors inside it, but not html/.light sitting outside it. Wrapping
both branches the same way brings every Link color rule to (0,2,1),
which wins cleanly without !important. Keep this pattern if you touch these
mixins.
Files
Link.tsx— component, types,linkClassName()helper_link.scss— geometry, typography, display, triggers the icon's built-in motion (see below)../Icons—XrplArrowInternalLinkIcon/XrplArrowExternalLinkIcon, the trailing arrow itself: artwork, sizing (1em, tracks this element's own font-size), and the hover/focus-visible motion, none of which lives in this component. Seeshared/components/Icons/shared.scssfor thebds-icon-engagedcontract and/icon-demofor a live reference.../../../styles/_link-tokens.scss— color/underline/focus mixins (shared with the fallback)../../../@theme/components/MarkdownLink.tsx— markdown[text](url)override../../../styles/_content.scss— raw-HTML-in-markdown fallback../../../link-demo.page.tsx— visual reference of every variantlegacy/BdsLink.tsx,legacy/LinkArrow.tsx— old component, unused, kept for reference