Files
xrpl-dev-portal/shared/components/Link/Link.md
2026-08-28 09:33:37 -07:00

166 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`](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](../../../link-demo.page.tsx)),
each example labeled with the spec entry it matches.
## Usage
```tsx
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`](../../../styles/_link-tokens.scss)'s
`xrpl-link-colors($group)` mixin. `Link`'s own stylesheet
([`_link.scss`](_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`](../../../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`](../../../about/index.page.tsx)), you can opt a
specific link into a non-neutral treatment by writing `linkClassName()`'s
output literally into the string:
```html
<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. 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