Polymorphism
Kobalte components that render a DOM element support polymorphism via the as prop. This allows you to change the rendered element or component while preserving behavior, accessibility, and state management.
Polymorphism is useful when you want to:
- Change the underlying element (
button→a) - Integrate with your own design system components
- Control which props reach your component
Basic usage
Use as with a native element or a custom Solid component.
tsximport { Tabs } from "@kobalte/core/tabs";import { MyCustomButton } from "./components";function App() {return (<Tabs><Tabs.List>{/* Render an anchor tag instead of the default button */}<Tabs.Trigger value="one" as="a">A Trigger</Tabs.Trigger>{/* Render MyCustomButton instead of the default button */}<Tabs.Trigger value="one" as={MyCustomButton}>Custom Button Trigger</Tabs.Trigger></Tabs.List><Tabs.Content value="one">Content one</Tabs.Content></Tabs>);}
tsximport { Tabs } from "@kobalte/core/tabs";import { MyCustomButton } from "./components";function App() {return (<Tabs><Tabs.List>{/* Render an anchor tag instead of the default button */}<Tabs.Trigger value="one" as="a">A Trigger</Tabs.Trigger>{/* Render MyCustomButton instead of the default button */}<Tabs.Trigger value="one" as={MyCustomButton}>Custom Button Trigger</Tabs.Trigger></Tabs.List><Tabs.Content value="one">Content one</Tabs.Content></Tabs>);}
Kobalte consumes its own options internally and forwards valid HTML attributes to the rendered element.
Using as callbacks
For full control over which props are passed, as can also be a callback.
When using an as callback:
- Always spread the provided props
- Custom props are forwarded unchanged
- Kobalte options are not passed
- Event handlers must be defined on the parent
Violating these rules can break behavior or accessibility.
tsximport { Tabs } from "@kobalte/core/tabs";import { MyCustomButton } from "./components";function App() {return (<Tabs><Tabs.List><Tabs.Trigger value="one" as={MyCustomButton}>A Trigger</Tabs.Trigger><Tabs.Trigger value="one" as={props => <MyCustomButton value="custom" {...props} />}>Custom Button Trigger</Tabs.Trigger></Tabs.List><Tabs.Content value="one">Content one</Tabs.Content></Tabs>);}
tsximport { Tabs } from "@kobalte/core/tabs";import { MyCustomButton } from "./components";function App() {return (<Tabs><Tabs.List><Tabs.Trigger value="one" as={MyCustomButton}>A Trigger</Tabs.Trigger><Tabs.Trigger value="one" as={props => <MyCustomButton value="custom" {...props} />}>Custom Button Trigger</Tabs.Trigger></Tabs.List><Tabs.Content value="one">Content one</Tabs.Content></Tabs>);}
In this example:
value="one"is used by Kobalte and not passed toMyCustomButtonvalue="custom"is explicitly passed toMyCustomButton
Typing as callbacks
You can use PolymorphicCallbackProps to get exact typing for callback props:
tsximport { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";<Tabs.Triggervalue="one"as={(props: PolymorphicCallbackProps<MyCustomButtonProps,TabsTriggerOptions,TabsTriggerRenderProps>,) => (// The `value` prop is directly passed to MyCustomButton<MyCustomButton value="custom" {...props} />)}>Custom Button Trigger</Tabs.Trigger>;
tsximport { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";<Tabs.Triggervalue="one"as={(props: PolymorphicCallbackProps<MyCustomButtonProps,TabsTriggerOptions,TabsTriggerRenderProps>,) => (// The `value` prop is directly passed to MyCustomButton<MyCustomButton value="custom" {...props} />)}>Custom Button Trigger</Tabs.Trigger>;
Event lifecycle
Custom event handlers defined on a Kobalte component are called before Kobalte’s internal handlers.
Component Prop Types
This section is intended for library authors building on top of Kobalte.
Every Kobalte component that renders an element exposes four core types:
ComponentOptionsComponentCommonProps<T>ComponentRenderPropsComponentProps<T>
For example, Tabs.Trigger has the types TabsTriggerOptions, TabsTriggerCommonProps<T>,
TabsTriggerRenderProps and TabsTriggerProps<T>.
Components themselves accept props as PolymorphicProps<T, ComponentProps> where T is a generic
that extends ValidComponent and ComponentProps are the props of the Kobalte component.
This type allows components to accept Kobalte's props and all other props accepted by T.
ComponentOptions
Custom props consumed internally by Kobalte.
- Not valid HTML
- Not forwarded to the DOM
- Not passed to
ascallbacks
ComponentCommonProps<T>
Optional HTML attributes that wil be accepted and forwarded to the rendered DOM node.
- Includes
id,ref, and event handlers - Managed by Kobalte but customizable by the end user
- Generic is used by
refand event handlers, by default it isHTMLElement.
ComponentRenderProps
Extends ComponentCommonProps with attributes that are passed to the DOM node and fully controlled by Kobalte.
- These props must not be modified.
- Changing them will break behavior and accessibility.
ComponentProps<T>
Public props type exported by the component.
- Combines all props expected by Kobalte's component.
- Generic is used by
CommonProps, by default isHTMLElement.
Equivalent to ComponentOptions & Partial<ComponentCommonProps>.
PolymorphicProps<T, ComponentProps>
Use PolymorphicProps<T, ComponentProps> when you’re building a wrapper component and want to expose Kobalte’s as prop to end users with correct typing.
Example
tsximport { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";import { PolymorphicProps } from "@kobalte/core/polymorphic";import type { ValidComponent } from "@kobalte/utils";import { splitProps } from "solid-js";interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {variant: "default" | "outline";}function CustomTabsTrigger<T extends ValidComponent = "button">(props: PolymorphicProps<T, CustomProps<T>>,) {const [local, others] = splitProps(props as CustomProps, ["variant"]);return (<Tabs.Triggeras="button"class={local.variant === "default" ? "default-trigger" : "outline-trigger"}{...others}/>);}
tsximport { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";import { PolymorphicProps } from "@kobalte/core/polymorphic";import type { ValidComponent } from "@kobalte/utils";import { splitProps } from "solid-js";interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {variant: "default" | "outline";}function CustomTabsTrigger<T extends ValidComponent = "button">(props: PolymorphicProps<T, CustomProps<T>>,) {const [local, others] = splitProps(props as CustomProps, ["variant"]);return (<Tabs.Triggeras="button"class={local.variant === "default" ? "default-trigger" : "outline-trigger"}{...others}/>);}
When using generics, TypeScript can lose some precision; splitting local props and spreading the remaining props helps preserve usability.
Note:
Tshould extendValidComponentand default to the underlying element you render (eg."button").
Fixed element wrappers
If your wrapper should always render a specific element type, and you don’t want to support as, you can simplify typing with OverrideComponentProps from @kobalte/utils:
For example, use OverrideComponentProps<"button", CustomProps> where "button" is the tag name you want to render.
Exporting exact types
If you want to expose a strongly-typed surface area (eg. for downstream libraries), you can re-export and extend the underlying component types:
tsximport type { ValidComponent } from "@kobalte/utils";import type {TabsTriggerOptions,TabsTriggerCommonProps,TabsTriggerRenderProps,} from "@kobalte/core/tabs";import type { ElementOf, PolymorphicProps } from "@kobalte/core/polymorphic";export interface CustomTabsTriggerOptions extends TabsTriggerOptions {variant: "default" | "outline";}export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement>extends TabsTriggerCommonProps<T> {}export interface CustomTabsTriggerRenderPropsextends CustomTabsTriggerCommonProps,TabsTriggerRenderProps {class: string;}export type CustomTabsTriggerProps<T extends ValidComponent = "button"> = CustomTabsTriggerOptions &Partial<CustomTabsTriggerCommonProps<ElementOf<T>>>;export function CustomTabsTrigger<T extends ValidComponent = "button">(props: PolymorphicProps<T, CustomTabsTriggerProps<T>>,) {}
tsximport type { ValidComponent } from "@kobalte/utils";import type {TabsTriggerOptions,TabsTriggerCommonProps,TabsTriggerRenderProps,} from "@kobalte/core/tabs";import type { ElementOf, PolymorphicProps } from "@kobalte/core/polymorphic";export interface CustomTabsTriggerOptions extends TabsTriggerOptions {variant: "default" | "outline";}export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement>extends TabsTriggerCommonProps<T> {}export interface CustomTabsTriggerRenderPropsextends CustomTabsTriggerCommonProps,TabsTriggerRenderProps {class: string;}export type CustomTabsTriggerProps<T extends ValidComponent = "button"> = CustomTabsTriggerOptions &Partial<CustomTabsTriggerCommonProps<ElementOf<T>>>;export function CustomTabsTrigger<T extends ValidComponent = "button">(props: PolymorphicProps<T, CustomTabsTriggerProps<T>>,) {}
ElementOf<T> (from @kobalte/core/polymorphic) maps a tag name to its corresponding DOM element type (eg. ElementOf<"button"> → HTMLButtonElement).