Text

Link

A text link that navigates through your router. Internal hrefs render through OuroProvider's linkComponent, the same path Button with href takes, and external opens a new tab safely and says so.

Braise as in our braising guide, then rest overnight.

Adapted from the original recipe (opens in a new tab).

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

See it live, no sign-in needed: Home · A recipe · Recipes

In its source: app/components/auth/auth-page.tsx · app/routes/my-recipes.tsx · app/components/cooking-now/CookingNow.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/link#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/link

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/link#v1.2.13 ourostack/ouro-ui/link-tests#v1.2.13
import { Link } from "@/components/ouro/link/link";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Link href="/recipes">Browse recipes</Link>
<Link href="https://ouro.bot" external>Ouro</Link>
<Link href="/mcp" reloadDocument>MCP server metadata</Link>
<Text>Read the <Link href="/terms">terms</Link> first.</Text>
<Text size="sm"><Link href="/forgot" inline={false}>Forgot password?</Link></Text>

Usage

Link is for text links only: it sets the text color and the underline, so wrapping a card, a list row or a tab in it restyles their text. Inside running text, put the Link in a Text.

On a touch screen a link that stands alone is a box at least 44px tall and wide, with its text centred, so it is easy to tap and never overlaps a neighbour. A link in running text (a <p>, a <label>, a Text, a field's description or error) stays as tall as its line; inline says the same anywhere else. With a mouse every link is as tall as its text.

Keyboard focus draws an outline around a link that stands alone.

Use instead when

  • Use Button with href instead when the link is the main action and should look like a button.
  • Use Card with href instead when a whole card is the link, Item with href for a whole list row, and Table with rowHref (or TableRow with href) for a table row: each makes the surface one link with one tab stop, through the same router link, and keeps its own look.
  • Use Navbar or Sidebar instead for the links that move between an app's pages, which mark the current one: Tabs and SegmentedControl switch a view in place and take no href.
  • In running text it thickens the underline in the ring's colour instead, since an outline would cover the next character.

Props

Type: LinkProps.

children (required)
Type: ReactNode
The link text. Make it say where the link goes: "Browse recipes", not "click here".
className
Type: string
Layout only: margins, display, grid placement (ms-2). Appearance classes warn in development; use tone.
external
Type: boolean
Opens the link in a new tab: sets target="_blank", adds rel="noopener noreferrer" (keeping your own rel tokens) and an icon, and tells screen readers "opens in a new tab".
href (required)
Type: string
Where the link goes. Internal paths render through OuroProvider's linkComponent (your router's link), so navigation stays client-side; http(s)://, mailto: and other absolute URLs render as a plain <a>. Router links that take to take href here: the adapter maps it.
inline
Type: boolean
Whether the link sits in running text. On a touch screen a link that stands alone ("Forgot password?", "Back to recipes", a link in a list item or a table cell) is a box at least 44px tall and wide with its text centred, which takes its room in the layout; a link in running text stays as tall as its line. Unset, a link is in running text inside a <p>, a <label>, a <dt> or <dd>, a Text, and Ouro's own text (a field's description and error, a Form's message, an Alert's text, an Item's description). Pass inline for a sentence in another element, and inline={false} for a link that is alone in its Text.
ref
Type: Ref<HTMLAnchorElement>
Reaches the rendered element.
reloadDocument
Type: boolean
Navigates with a full page load: renders a plain <a> for an internal path too, skipping OuroProvider's linkComponent, so your router never handles it. Use it for a path your router has no page for: a resource route (/mcp, /feed.xml), a file download, a sign-in that starts on the server (/auth/github). A link with download does the same.
tone
Type: "primary" | "secondary" | "destructive" | "success" | "warning"
Default: "primary"
Text color from the theme, named after its token. secondary is the page's foreground, for links in navigation and lists; the underline still marks it as a link. primary reads --primary, not a theme's --ouro-action: a link is text in a sentence and the action colour is a fill. For a link that looks like the page's main action, use <Button href>.

Native attributes

Also accepts the attributes in React's AnchorHTMLAttributes and HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • color: Not a prop. Ouro UI uses tone: <Link tone="destructive">.
  • isExternal: Not a prop. Ouro UI uses external: <Link href="https://…" external>.
  • to: Not a prop. Ouro UI uses href, for router links too: <Link href="/recipes">.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Link merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Claim sources