UNPKG

opencode-agent-kit

Version:

Multi-stack OpenCode agent toolkit — 33+ specialized AI agents, 200+ skills, 46 commands, 8 MCP servers (Nuxt, React, Node.js, Laravel, CI3, Android, Flutter, DevOps, SEO, SonarQube, and more)

584 lines (448 loc) 13.9 kB
--- title: Polymorphism description: How to use the `as` prop to change the rendered HTML element while preserving component functionality. type: reference summary: The as prop pattern for polymorphic components, compared with Radix UI's Slot-based asChild approach with TypeScript best practices. prerequisites: - /composition - /types related: - /as-child - /accessibility --- The `as` prop is a fundamental pattern in modern React component libraries that allows you to change the underlying HTML element or component that gets rendered. Popularized by libraries like [Styled Components](https://styled-components.com/), [Emotion](https://emotion.sh/), and [Chakra UI](https://chakra-ui.com/), this pattern provides flexibility in choosing semantic HTML while maintaining component styling and behavior. The `as` prop enables polymorphic components - components that can render as different element types while preserving their core functionality: ```tsx <Button as="a" href="/home"> Go Home </Button> <Button as="button" type="submit"> Submit Form </Button> <Button as="div" role="button" tabIndex={0}> Custom Element </Button> ``` ## Understanding `as` The `as` prop allows you to override the default element type of a component. Instead of being locked into a specific HTML element, you can adapt the component to render as any valid HTML tag or even another React component. For example: ```tsx // Default renders as a div <Box>Content</Box> // Renders as a section <Box as="section">Content</Box> // Renders as a nav <Box as="nav">Content</Box> ``` This renders different HTML elements: ```html <!-- Default --> <div>Content</div> <!-- With as="section" --> <section>Content</section> <!-- With as="nav" --> <nav>Content</nav> ``` ## Implementation Methods There are two main approaches to implementing polymorphic components: a manual implementation and using Radix UI's `Slot` component. ### Manual Implementation The `as` prop implementation uses dynamic component rendering: ```tsx // Simplified implementation function Component({ as: Element = "div", children, ...props }) { return <Element {...props}>{children}</Element>; } // More complete implementation with TypeScript type PolymorphicProps<E extends React.ElementType> = { as?: E; children?: React.ReactNode; } & React.ComponentPropsWithoutRef<E>; function Component<E extends React.ElementType = "div">({ as, children, ...props }: PolymorphicProps<E>) { const Element = as || "div"; return <Element {...props}>{children}</Element>; } ``` The component: 1. Accepts an `as` prop with a default element type 2. Uses the provided element or fallback to default 3. Spreads all other props to the rendered element 4. Maintains type safety with TypeScript generics ### Using Radix UI Slot [Radix UI](https://www.radix-ui.com/) provides a `Slot` component that offers a more powerful alternative to the `as` prop pattern. Instead of just changing the element type, `Slot` merges props with the child component, enabling composition patterns. First, install the package: ```package-install npm install @radix-ui/react-slot ``` The `asChild` pattern uses a boolean prop instead of specifying the element type: ```tsx import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; const itemVariants = cva("rounded-lg border p-4", { variants: { variant: { default: "bg-white", primary: "bg-blue-500 text-white", }, size: { default: "h-10 px-4", sm: "h-8 px-3", lg: "h-12 px-6", }, }, defaultVariants: { variant: "default", size: "default", }, }); function Item({ className, variant = "default", size = "default", asChild = false, ...props }: React.ComponentProps<"div"> & VariantProps<typeof itemVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "div"; return ( <Comp data-slot="item" data-variant={variant} data-size={size} className={cn(itemVariants({ variant, size, className }))} {...props} /> ); } ``` Now you can use it in two ways: ```tsx // Default: renders as a div <Item variant="primary">Content</Item> // With asChild: merges props with child component <Item variant="primary" asChild> <a href="/home">Link with Item styles</a> </Item> ``` The `Slot` component: 1. Clones the child element 2. Merges the component's props (className, data attributes, etc.) with the child's props 3. Forwards refs correctly 4. Handles event handler composition ### Comparison: `as` vs `asChild` **`as` prop (manual implementation):** ```tsx // Explicit element type <Button as="a" href="/home">Link Button</Button> <Button as="button" type="submit">Submit Button</Button> // Simple, predictable API // Limited to element types ``` **`asChild` with Slot:** ```tsx // Implicit from child <Button asChild> <a href="/home">Link Button</a> </Button> <Button asChild> <button type="submit">Submit Button</button> </Button> // More flexible composition // Works with any component // Better prop merging ``` **Key differences:** | Feature | `as` prop | `asChild` + Slot | | ------------------------- | ------------------- | -------------------------------- | | **API Style** | `<Button as="a">` | `<Button asChild><a /></Button>` | | **Element Type** | Specified in prop | Inferred from child | | **Component Composition** | Limited | Full support | | **Prop Merging** | Basic spread | Intelligent merging | | **Ref Forwarding** | Manual setup needed | Built-in | | **Event Handlers** | May conflict | Composed correctly | | **Library Size** | No dependency | Requires `@radix-ui/react-slot` | ### When to Use Each Approach **Use `as` prop when:** - You want a simpler API surface - You're primarily switching between HTML elements - You want to avoid additional dependencies - The component is simple and doesn't need complex prop merging **Use `asChild` + Slot when:** - You need to compose with other components - You want automatic prop merging behavior - You're building a component library similar to Radix UI or shadcn/ui - You need reliable ref forwarding across different component types ## Key Benefits ### 1. Semantic HTML Flexibility The `as` prop ensures you can always use the most semantically appropriate HTML element: ```tsx // Navigation container <Container as="nav" className="navigation"> <NavItems /> </Container> // Main content area <Container as="main" className="content"> <Article /> </Container> // Sidebar <Container as="aside" className="sidebar"> <Widgets /> </Container> ``` ### 2. Component Reusability One component can serve multiple purposes without creating variants: ```tsx // Text component used for different elements <Text as="h1" size="2xl">Page Title</Text> <Text as="p" size="md">Body paragraph</Text> <Text as="span" size="sm">Inline text</Text> <Text as="label" size="sm">Form label</Text> ``` ### 3. Accessibility Improvements Choose elements that provide the best accessibility for each context: ```tsx // Link that looks like a button <Button as="a" href="/signup"> Sign Up Now </Button> // Button that submits a form <Button as="button" type="submit"> Submit </Button> // Heading with button styles <Button as="h2" role="presentation"> Section Title </Button> ``` ### 4. Style System Integration Maintain consistent styling while changing elements: ```tsx const Card = styled.div` padding: 1rem; border-radius: 8px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); `; // Same styles, different elements <Card as="article">Article content</Card> <Card as="section">Section content</Card> <Card as="li">List item content</Card> ``` ## Common Use Cases ### Typography Components Create flexible text components: ```tsx function Text({ as: Element = 'span', variant = 'body', ...props }) { const className = cn( 'text-base', variant === 'heading' && 'text-2xl font-bold', variant === 'body' && 'text-base', variant === 'caption' && 'text-sm text-gray-600', props.className ); return <Element className={className} {...props} />; } // Usage <Text as="h1" variant="heading">Title</Text> <Text as="p" variant="body">Paragraph</Text> <Text as="figcaption" variant="caption">Caption</Text> ``` ### Layout Components Build semantic layouts: ```tsx function Flex({ as: Element = 'div', ...props }) { return ( <Element className={cn('flex', props.className)} {...props} /> ); } // Semantic HTML <Flex as="header" className="justify-between"> <Logo /> <Navigation /> </Flex> <Flex as="main" className="flex-col"> <Content /> </Flex> ``` ### Interactive Elements Handle different interaction types: ```tsx function Clickable({ as: Element = 'button', ...props }) { const isButton = Element === 'button'; const isAnchor = Element === 'a'; return ( <Element role={!isButton && !isAnchor ? 'button' : undefined} tabIndex={!isButton && !isAnchor ? 0 : undefined} {...props} /> ); } // Various clickable elements <Clickable as="button" onClick={handleClick}>Button</Clickable> <Clickable as="a" href="/link">Link</Clickable> <Clickable as="div" onClick={handleClick}>Div Button</Clickable> ``` ## TypeScript Best Practices ### Generic Component Types Create fully type-safe polymorphic components: ```tsx type PolymorphicRef<E extends React.ElementType> = React.ComponentPropsWithRef<E>["ref"]; type PolymorphicProps<E extends React.ElementType, Props = {}> = Props & Omit<React.ComponentPropsWithoutRef<E>, keyof Props> & { as?: E; }; // Component with full type safety function Component<E extends React.ElementType = "div">({ as, ...props }: PolymorphicProps<E, { customProp?: string }>) { const Element = as || "div"; return <Element {...props} />; } ``` ### Inferring Props Automatically infer props based on the element: ```tsx // Props are inferred from the element type <Component as="a" href="/home">Home</Component> // ✅ href is valid <Component as="div" href="/home">Home</Component> // ❌ TS error: href not valid on div <Component as="button" type="submit">Submit</Component> // ✅ type is valid <Component as="span" type="submit">Submit</Component> // ❌ TS error ``` ### Discriminated Unions Use discriminated unions for element-specific props: ```tsx type ButtonProps = | { as: "button"; type?: "submit" | "button" | "reset" } | { as: "a"; href: string; target?: string } | { as: "div"; role: "button"; tabIndex: number }; function Button(props: ButtonProps & { children: React.ReactNode }) { const Element = props.as; return <Element {...props} />; } ``` ## Best Practices ### 1. Default to Semantic Elements Choose meaningful defaults that represent the most common use case: ```tsx // ✅ Good defaults function Article({ as: Element = "article", ...props }) {} function Navigation({ as: Element = "nav", ...props }) {} function Heading({ as: Element = "h2", ...props }) {} // ❌ Too generic function Component({ as: Element = "div", ...props }) {} ``` ### 2. Document Valid Elements Clearly specify which elements are supported: ```tsx interface BoxProps { /** * The HTML element to render as * @default 'div' * @example 'section', 'article', 'aside', 'main' */ as?: "div" | "section" | "article" | "aside" | "main" | "header" | "footer"; } ``` ### 3. Validate Element Appropriateness Warn when inappropriate elements are used: ```tsx function Button({ as: Element = "button", ...props }) { if (__DEV__ && Element === "div" && !props.role) { console.warn( 'Button: When using as="div", provide role="button" for accessibility', ); } return <Element {...props} />; } ``` ### 4. Handle Event Handlers Properly Ensure event handlers work across different elements: ```tsx function Interactive({ as: Element = "button", onClick, ...props }) { const handleKeyDown = (e: React.KeyboardEvent) => { if (Element !== "button" && (e.key === "Enter" || e.key === " ")) { onClick?.(e as any); } }; return ( <Element onClick={onClick} onKeyDown={Element !== "button" ? handleKeyDown : undefined} {...props} /> ); } ``` ## Common Pitfalls ### Invalid HTML Nesting Be careful about HTML nesting rules: ```tsx // ❌ Invalid - button inside button <Button as="button"> <Button as="button">Nested</Button> </Button> // ❌ Invalid - div inside p <Text as="p"> <Box as="div">Invalid nesting</Box> </Text> // ✅ Valid nesting <Text as="div"> <Box as="div">Valid nesting</Box> </Text> ``` ### Missing Accessibility Attributes Remember to add appropriate ARIA attributes: ```tsx // ❌ Missing accessibility <Box as="nav"> <MenuItems /> </Box> // ✅ Proper accessibility <Box as="nav" aria-label="Main navigation"> <MenuItems /> </Box> ``` ### Type Safety Loss Avoid using overly permissive types: ```tsx // ❌ Too permissive - no type safety function Component({ as: Element = "div", ...props }: any) { return <Element {...props} />; } // ✅ Type safe function Component<E extends React.ElementType = "div">({ as, ...props }: PolymorphicProps<E>) { const Element = as || "div"; return <Element {...props} />; } ``` ### Performance Considerations Be aware of re-render implications: ```tsx // ❌ Creates new component on every render function Parent() { const CustomDiv = (props) => <div {...props} />; return <Component as={CustomDiv} />; } // ✅ Stable component reference const CustomDiv = (props) => <div {...props} />; function Parent() { return <Component as={CustomDiv} />; } ```