Adapt
Re-render a component's contents into a different container per breakpoint or platform
Adapt lets one component render its contents somewhere else. A Select that is a dropdown on desktop becomes a Sheet on touch devices, without duplicating the contents or the open state.
Features
Switch containers by media query, by platform, or both
Contents move, they are not re-declared
Open state, dismissal and exit animations hand off to the new container
Built into Dialog, Popover and Select
Scoped, so nested adapting components don't capture each other
Installation
Adapt ships with tamagui, or you can install it independently:
yarn
npm
bun
pnpm
How it works
Adapt is two markers used together:
<Adapt>says when to adapt and what to adapt into.<Adapt.Contents />says where inside that new container the original contents should appear.
The contents are not re-rendered from scratch into the new tree. They are
published to a slot and read by Adapt.Contents, so state inside them survives
the switch, and the adapting component keeps owning open / onOpenChange.
When the max-md media query is inactive, the <Adapt> subtree renders nothing
and Select.Content renders in place as usual. When it becomes active, the
Sheet mounts and Select.Content moves into it.
<Adapt> must be a child of a component that supports adapting. Rendering it
anywhere else throws. Dialog, Popover and Select also expose it as a static
property (<Select.Adapt>, <Dialog.Adapt>, <Popover.Adapt>), which is the
same component.
Choosing when to adapt
when and platform are both optional, but at least one is required: with
neither set, Adapt is never active.
when takes any media query key from your config, or a boolean. platform
takes 'web', 'native', 'ios', 'android' or 'touch'.
When you set both, both must pass. <Adapt when={true} platform="touch"> is on
for every width, but only on touch devices.
Because when reads media state, you can also drive it from your own logic:
Adapting Dialog and Popover
Dialog and Popover use the same handoff. For both, put Adapt.Contents inside
the Sheet’s scrollable area:
Adapted content stays mounted through the Sheet’s exit animation and only unmounts once the Sheet reports that it is fully hidden, so closing looks the same whether or not the component is adapted.
Parts that only make sense in the unadapted layout drop out while adapted:
Popover.Arrow, Dialog.Overlay and Dialog.Close all render null, and
Dialog’s content renders without its own frame, focus scope or dismissable
layer, so the Sheet’s take over. Dialog.Close takes displayWhenAdapted if
you do want it inside the Sheet.
Scoping
Leave scope off and <Adapt> binds to the nearest adapting parent, which is
what you want nearly always. Nesting is already safe: a parent only reads an
<Adapt> from its own direct children (descending into fragments), so a Dialog
rendered inside a Popover’s content does not steal the Popover’s adaptation, or
hand it its own.
The scope on <Adapt> is not the scope you pass to Dialog, Popover or
Select. It is that component’s internal adapt scope, which is its own scope
prefixed by the component name:
| Component | Adapt scope |
|---|---|
| Dialog | DialogAdapt<scope> |
| Popover | PopoverAdapt<scope> |
| Select | AdaptSelect<scope> |
- Component
- Dialog
- Adapt scope
DialogAdapt<scope>- Component
- Popover
- Adapt scope
PopoverAdapt<scope>- Component
- Select
- Adapt scope
AdaptSelect<scope>
An unscoped Dialog is therefore DialogAdapt, and <Dialog scope="settings">
is DialogAdaptsettings. The same string goes on <Adapt.Contents /> if you
scope it.
Adapting into something other than a Sheet
Anything can be an adapt target. The render-callback form of children gives
you the contents plus the parent’s open state, so you can render your own
container:
If your container is a component of its own, read the same state with
useAdaptTarget() instead, which is what Sheet does:
handoff is how the parent and the target agree on timing. handoff.hidden
tells the target to play its exit, and calling handoff.onTransition with the
completed transition lets the parent unmount the contents at the right moment
instead of on a timer.
API reference
Adapt
Props
when
MediaQueryKey | boolean | null
A media query key from your config, or a boolean. Active when the query matches; true is always active.
platform
'web' | 'native' | 'ios' | 'android' | 'touch' | null
Restricts the adaptation to a platform. When combined with when, both must pass.
scope
string
Binds this Adapt to a specific adapting parent. Defaults to the nearest one.
children (required)
JSX.Element | ((contents: React.ReactNode, adapt: AdaptRenderState) => React.ReactNode)
The container to adapt into, which should contain an Adapt.Contents. As a function, it receives the contents element and { active, open, onOpenChange, state, handoff } so you can render your own container.
Adapt.Contents
Marks where the adapted contents render inside the new container. Exactly one should be rendered while an adaptation is active; in development, zero targets or more than one logs an error.
Props
scope
string
Binds to a specific adapting parent, matching the scope on Adapt.
useAdaptIsActive
useAdaptIsActive(scope?: string): boolean
Whether an adaptation is currently active. Use it to branch rendering inside content that is sometimes adapted.
useAdaptTarget
useAdaptTarget<State>(scope?: string): AdaptTarget<State> | null
Returns null unless an adaptation is active and the caller is under its
<Adapt> element. Pass scope to target that adaptation explicitly from
outside the element. When it is a target, the hook returns
{ open, onOpenChange, state, handoff }. Calling it also registers the caller
as the adapt target for development-time validation.
handoff is { hidden: boolean, skipNextAnimation?: boolean, onTransition: (e) => void },
where the transition event is { phase: 'start' | 'end', cause: 'open' | 'close' | 'snap', finished?: boolean }.
state is whatever the adapting component chose to pass through. Dialog passes
its internal context; Popover and Select pass nothing.
Troubleshooting
“You’re rendering a Tamagui <Adapt /> component without nesting it inside a
parent that is able to adapt.” <Adapt> or <Adapt.Contents /> is outside
a Dialog, Popover or Select. If the component uses scope, pass the matching
scope to <Adapt> too.
“Adapt is active but no target registered” the adaptation turned on but
nothing consumed the contents. Add an <Adapt.Contents /> inside the container
you adapt into.
“Adapt expected exactly one target” two containers are claiming the same
scope. Render one <Adapt.Contents /> per active adaptation.