Linking

Pass href and you're done. Roadie picks the right element, applies the right target / rel defaults, and routes through your app's configured client router via RoadieLinkProvider. Use onClick instead of href and you get a real <button> back. Everything that's link-shaped — Button, IconButton, Card, Breadcrumb.Link, Carousel.TitleLink, Tabs.Tab — speaks the same vocabulary.

Quick start

Mount RoadieLinkProvider once at your app root, alongside ThemeProvider. Pass next/link directly, or any wrapper that takes href + children.

import NextLink from 'next/link'
import { RoadieLinkProvider, ThemeProvider } from '@oztix/roadie-components'
export function Providers({ children }) {
return (
<RoadieLinkProvider Link={NextLink}>
<ThemeProvider>{children}</ThemeProvider>
</RoadieLinkProvider>
)
}

Now every Roadie component that accepts href routes through Next's client navigation automatically — prefetch, scroll restoration, view transitions all preserved.

How href is resolved

The same rules apply to every link-bearing component. The decision is pure and SSR-safe — no hydration mismatches, no client-only checks.

href shapeRenders asDefaults applied
undefined<button> (or <div> for Card without onClick)
/events/123, ./local, #sectionConfigured Link (or <a> if no provider)
https://…, http://…, //…<a>target='_blank' rel='noopener noreferrer'
mailto:, tel:, sms:<a>None — no target, no rel

Override per call with external (boolean), target, or rel. Pass external={false} on an https://oztix.com.au/x URL to force internal routing through the provider; pass external on an internal redirect path to open in a new tab.

Components

Every link-bearing component below accepts the same href / external / target / rel props. The escape hatch column shows the API for cases where href isn't enough (custom elements, full render control).

ComponentDefault elementEscape hatch
Button, IconButton<button>render prop
Card<div> (or routed <a> when href is set)render prop
Breadcrumb.Link<a>render prop
Carousel.TitleLink<a>render prop
Tabs.Tab<button> (or routed <a> when href is set)render prop

Examples

Internal navigation

<Button href='/events/123'>View event</Button>
<Card href='/events/123'>{/* whole-card link */}</Card>
<Breadcrumb.Link href='/events'>Events</Breadcrumb.Link>

External link

<Button href='https://stripe.com/docs'>Stripe docs</Button>
// Renders <a target='_blank' rel='noopener noreferrer'>

Email / phone

<Button href='mailto:hello@oztix.com.au'>Email us</Button>
<IconButton aria-label='Call' href='tel:+61400000000'>
<PhoneIcon />
</IconButton>

Force external / internal

{/* First-party URL that should still open in a new tab: */}
<Button href='https://oztix.com.au/checkout' external>
Checkout
</Button>
{/* Internal-looking redirect that bounces through the SPA: */}
<Button href='/redirect/foo' external={false}>
Continue
</Button>

Escape hatch — render

The href path covers the happy case. For full control over the rendered element, every Roadie component accepts the same render prop — element form, component form, or function form. The contract mirrors Base UI's render prop.

Element form

{/* Card as a clickable button (no href, full element swap): */}
<Card render={<button type='button' onClick={handleSelect} />}>
</Card>
{/* Button: typed access to anchor-only DOM props */}
<Button render={<a href='/file.pdf' download='spec.pdf' />}>
Download spec
</Button>
{/* Breadcrumb.Link with a custom analytics-aware wrapper: */}
<Breadcrumb.Link render={<MyTrackedLink analyticsId='breadcrumb' href='/x' />}>
Events
</Breadcrumb.Link>

Function form

Receive the default props and return any element. Useful for state-aware rendering or attribute composition.

<Tabs.Tab
value='events'
render={(props, state) => (
<a {...props} data-active={state.selected} href='/events' />
)}
>
Events
</Tabs.Tab>

When you pass both href and render, render wins — Roadie's smart routing is silently disabled for that call. Button logs a one-shot dev warning so the conflict can't ship by accident. Pick one.

Legacy as prop: Card, Breadcrumb.Link, and Carousel.TitleLink previously exposed an as prop for polymorphism. It continues to work for back-compat but is @deprecated as of v2.6 and will be removed in v3.0.0 — migrate to render.

Link tabs — Tabs.Tab href

Tabs work as link-tabs out of the box — pass href on each Tabs.Tab and the rendered anchor participates in the tab list's roving tabindex group. Arrow keys still move focus across mixed button + anchor tabs.

<Tabs value={value} onValueChange={setValue}>
<Tabs.List>
<Tabs.Tab value='overview' href='/account/overview'>Overview</Tabs.Tab>
<Tabs.Tab value='events' href='/account/events'>Events</Tabs.Tab>
<Tabs.Tab value='settings' href='/account/settings'>Settings</Tabs.Tab>
</Tabs.List>
</Tabs>

Gotcha: pressing Enter on a focused link-tab triggers native browser navigation immediately. If you derive Tabs.value from controlled local state, you can see a brief flicker between selection and route change. Recommended pattern: derive value from the route itself (e.g. via usePathname()) so route is the source of truth.

Tracking actions

Roadie ships zero analytics code — tracking taxonomy is product-shaped (event names, page sections, vendor choices). The recommended pattern is a small consumer-app wrapper that reads currentTarget.href / aria-label off the rendered element, so a single <Tracked> works on top of every Roadie action.

// app-level component, lives in your app — not in Roadie
'use client'
import { useTracking } from '@/utils/tracking'
export function Tracked({ trackEvent = 'link', pageSection, children }) {
const { trackClick } = useTracking()
return React.cloneElement(children, {
onClick: (e) => {
const el = e.currentTarget
trackClick(trackEvent, {
pageSection,
href: el.getAttribute('href') ?? undefined,
label: el.getAttribute('aria-label') ?? el.textContent?.trim()
})
children.props.onClick?.(e)
}
})
}
{/* Wrap any Roadie action — no tracking knowledge inside Roadie. */}
<Tracked pageSection='cart-checkout'>
<Button href={checkoutUrl} intent='accent' emphasis='strong'>
Checkout
</Button>
</Tracked>

Migration

LinkButton and LinkIconButton remain exported and continue to work, but new code should prefer Button / IconButton with href. They are scheduled for removal in v3.0.0.

// Before
<LinkButton href='/events' intent='accent'>Events</LinkButton>
<LinkIconButton href='/cart' aria-label='Cart' size='icon-md'>
<CartIcon />
</LinkIconButton>
// After
<Button href='/events' intent='accent'>Events</Button>
<IconButton href='/cart' aria-label='Cart' size='md'>
<CartIcon />
</IconButton>