Kobalte.v0.13.12

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 (buttona)
  • 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.

tsx
import { 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>
);
}
tsx
import { 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.

tsx
import { 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>
);
}
tsx
import { 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 to MyCustomButton
  • value="custom" is explicitly passed to MyCustomButton

Typing as callbacks

You can use PolymorphicCallbackProps to get exact typing for callback props:

tsx
import { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";
import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";
<Tabs.Trigger
value="one"
as={(
props: PolymorphicCallbackProps<
MyCustomButtonProps,
TabsTriggerOptions,
TabsTriggerRenderProps
>,
) => (
// The `value` prop is directly passed to MyCustomButton
<MyCustomButton value="custom" {...props} />
)}
>
Custom Button Trigger
</Tabs.Trigger>;
tsx
import { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs";
import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic";
<Tabs.Trigger
value="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:

  • ComponentOptions
  • ComponentCommonProps<T>
  • ComponentRenderProps
  • ComponentProps<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 as callbacks

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 ref and event handlers, by default it is HTMLElement.

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 is HTMLElement.

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

tsx
import { 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.Trigger
as="button"
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
{...others}
/>
);
}
tsx
import { 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.Trigger
as="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:

  • T should extend ValidComponent and 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:

tsx
import 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 CustomTabsTriggerRenderProps
extends 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>>,
) {}
tsx
import 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 CustomTabsTriggerRenderProps
extends 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).