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.
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then 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.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/linkIts 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.13import { 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
Buttonwithhrefinstead when the link is the main action and should look like a button. - Use
Cardwithhrefinstead when a whole card is the link,Itemwithhreffor a whole list row, andTablewithrowHref(orTableRowwithhref) 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
NavbarorSidebarinstead for the links that move between an app's pages, which mark the current one:TabsandSegmentedControlswitch a view in place and take nohref. - 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; usetone. external- Type:
boolean - Opens the link in a new tab: sets
target="_blank", addsrel="noopener noreferrer"(keeping your ownreltokens) 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'slinkComponent(your router's link), so navigation stays client-side;http(s)://,mailto:and other absolute URLs render as a plain<a>. Router links that taketotakehrefhere: 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>, aText, and Ouro's own text (a field's description and error, a Form's message, an Alert's text, an Item's description). Passinlinefor a sentence in another element, andinline={false}for a link that is alone in itsText. 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, skippingOuroProvider'slinkComponent, 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 withdownloaddoes the same. tone- Type:
"primary" | "secondary" | "destructive" | "success" | "warning" - Default:
"primary" - Text color from the theme, named after its token.
secondaryis the page's foreground, for links in navigation and lists; the underline still marks it as a link.primaryreads--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 usestone:<Link tone="destructive">.isExternal: Not a prop. Ouro UI usesexternal:<Link href="https://…" external>.to: Not a prop. Ouro UI useshref, 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
- layout-classes: test:registry/ouro/components/ouro/link/link.test.tsx::Link > lets app layout classes win
verify:testExists(registry/ouro/components/ouro/link/link.test.tsx::Link > lets app layout classes win)