Files
2026-08-28 09:33:37 -07:00

9.4 KiB
Raw Permalink Blame History

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:

  1. MarkdownLink override — @theme/components/MarkdownLink.tsx replaces Redocly's default renderer for markdoc's [text](url) syntax (registered in @theme/markdoc/components.tsx). It still renders Redocly's own routing Link (so client-side navigation keeps working) but stamps it with Link's class scheme via the exported linkClassName() helper, forced to intention="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.

  2. CSS-cascade fallback — styles/_content.scss's a: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 the MarkdownLink override doesn't reach: raw HTML <a> embedded directly in a .md file (markdoc passes those through as literal HTML, bypassing the link node 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.

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. See shared/components/Icons/shared.scss for the bds-icon-engaged contract and /icon-demo for 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 variant
  • legacy/BdsLink.tsx, legacy/LinkArrow.tsx — old component, unused, kept for reference