- Status: Approved
- Date: 2026-04-01
- Decision makers: EDS Core Team
Some EDS 2.0 components render interactive elements (<a>, <button>) that consumers may need to swap for framework-specific alternatives — for example, rendering a Next.js <Link> or React Router <Link> instead of a plain <a> tag. This is commonly known as polymorphic rendering.
While building the Link component (#4601), we discovered the need for this pattern. Button will also need it (#4746), and more components are likely to follow — so we need a consistent approach across the library.
Note: The as prop on Input ('input' | 'textarea') is a different use case — it selects between two native elements within the same component. That pattern is not affected by this decision.
- TypeScript types must be correct and maintainable without complex generics
- Prop collisions between parent and child must be predictable and well-defined
- The pattern must work with any consumer component (router links, custom wrappers, etc.)
- API should be simple to understand and hard to misuse
- Must align with the rest of the EDS 2.0 conventions (vanilla CSS,
forwardRef, named exports)
The component accepts an as prop that specifies which element or component to render:
<Link as={RouterLink} to="/about">About</Link>Pros:
- Familiar to developers coming from styled-components or Chakra UI
- Concise API for simple cases
Cons:
- Complex TypeScript generics needed to infer the correct props for the rendered element — fragile and hard to maintain
- Prop collisions: parent and child props share a flat namespace, making it unclear which component receives which prop
- The
ascomponent's own props are mixed into the parent's prop type, leading to confusing autocomplete
The component accepts an asChild boolean. When true, it renders a Slot that merges the component's props onto the single child element:
<Link asChild>
<RouterLink to="/about">About</RouterLink>
</Link>Pros:
- Clean TypeScript — no generics needed; the child component keeps its own types
- No prop collisions — parent and child props are explicitly separated in JSX
- Works with any component, including those with incompatible prop types
- Well-established pattern (Radix UI) with broad community adoption
- Composable — can be nested and combined with other patterns
Cons:
- Slightly more verbose than
as(two elements instead of one) - Requires a
Slotutility to merge props correctly - Developers unfamiliar with Radix may need to learn the pattern
The component exposes a render prop or expects consumers to wrap it:
<Link renderAs={(props) => <RouterLink {...props} to="/about" />}>
About
</Link>Pros:
- Full control for consumers
- No magic — explicit prop passing
Cons:
- Worst DX of the three — verbose and harder to read
- Inconsistent with how the rest of the React ecosystem handles polymorphism
- Callback-heavy API conflicts with EDS 2.0's goal of simple, flat props
Use asChild + Slot for polymorphic rendering in EDS 2.0.
This pattern satisfies the key decision drivers: TypeScript types stay simple and correct, prop handling is predictable (merge behavior is documented in the Slot utility), and it works with any consumer component without complex generics.
The Slot utility lives in packages/eds-core-react/src/components/next/Slot/ and handles prop merging with these rules:
className— concatenatedstyle— shallow-merged (child wins on conflicts)- Event handlers — composed (child runs first, then parent)
- Other props — parent wins (component's semantic props take precedence)
Should support: Components that render a single interactive element consumers commonly need to swap — Link, Button, and similar navigation/action elements.
Should not support: Form inputs (use as for element variants like input/textarea), icons, layout primitives, and components where swapping the root element does not make sense.
- Good, because TypeScript types are simple — no generics, no type parameter forwarding
- Good, because the pattern is well-established (Radix UI) and documented
- Good, because consumers can use any router or custom component without EDS needing to know about it
- Good, because prop merge behavior is explicit and predictable
- Bad, because the
asChildAPI is slightly more verbose thanas(two JSX elements) - Bad, because developers unfamiliar with Radix need to learn the pattern (mitigated by documentation)
- Code reviews verify that new interactive components support
asChildwhere appropriate - The
Slotutility is shared — components must not implement their own prop merging - Components that add
asChildmust include tests for prop merging (className, events, ref forwarding)
- ADR-0004: Component conventions for EDS 2.0
- GitHub issue #4747 — ADR tracking issue
- GitHub issue #4746 — Button
asChildimplementation - GitHub PR #4601 — Link component (first
asChildimplementation)