Skip to content
TrademarkTrademark
Docs
Contexts

AdapterProvider

Updated: August 31, 2026
View on GitHub

Core no longer imports next/* at runtime. Four things a framework owns — rendering a link, rendering an image, reading the current path, and navigating — are read through adapters instead, and the defaults are plain DOM. That is what lets Once UI install and run in a Vite app, a Remix app, or a plain React app with no Next.js present.

Everything that routes or loads an image goes through this seam: SmartLink, ElementType — and so any Button, Card or ToggleButton with an href — Media, Logo, MegaMenu and Kbar.

In a Next.js app

Import LayoutProvider from @once-ui-system/core/next instead of the root. That is the entire migration: same props, same tree, and the Next adapters (next/link, next/image, next/navigation) are installed for you.

Miss this and nothing crashes — which is the trap. The DOM defaults take over, so internal links become full page loads and images stop being optimized. The app still works, just worse. If routing suddenly feels heavy after upgrading, this import is the first thing to check.

Apps that compose their own provider tree can keep using NextAdapterProvider directly; LayoutProvider from /next only removes the boilerplate.

In any other React app

Do nothing. The defaults render <a> and <img>, read window.location.pathname (popstate-aware, "" on the server) and navigate with window.location.assign.

Supplying your own

Pass adapters to AdapterProvider. It is a partial override merged onto whatever the nearest provider already supplies, so you can replace only the piece your framework does differently.

Because providers merge rather than replace, nesting one inside another is a legitimate way to override a single adapter for one subtree.

Why detection is not attempted

The obvious wish is for core to notice Next.js and wire itself up. It cannot, and the reason is worth knowing before you try it in your own app.

Two of the four adapters are hooks, and the two implementations call different numbers of hooks: the DOM useNavigate returns a closure and calls none, the Next one calls useRouter and useCallback. Swapping them after mount raises "Rendered more hooks than during the previous render." So resolution has to settle before the first render — and a browser bundle has no synchronous way to conditionally resolve an optional module. An explicit import is the bundler-independent version of the same thing.

Reading the adapters

useAdapters() returns the resolved set. Use it when you are building a component of your own that should route the same way core's do.

API reference

Type
Default
adaptersrequired
Overrides. Anything omitted keeps the value from the nearest provider, or the DOM default.
Partial<OnceUIAdapters>—
childrenrequired
React.ReactNode—

OnceUIAdapters

Type
Default
Link
Renders internal links.
ComponentType<AdapterLinkProps><a>
Image
Renders images. Lazy unless priority is set.
ComponentType<AdapterImageProps><img>
usePathname
Current pathname. Popstate-aware; "" on the server.
() => stringwindow.location.pathname
useNavigate
Imperative navigation.
() => (href: string) => voidwindow.location.assign

AdapterImageProps

The image adapter's contract follows next/image semantics so the Next implementation is a pass-through rather than a translation layer.

Type
Default
src
Image source. Required.
string—
alt
Alternative text. Required.
string—
fill
Absolutely fill the nearest positioned ancestor.
booleanfalse
priority
Eager-load and raise fetch priority.
booleanfalse
unoptimized
Skip framework image optimization. No-op for the DOM default.
booleanfalse
width
number—
height
number—
Edit this page on GitHub