PettyUI/.firecrawl/kobalte-polymorphism.md
Mats Bosson db906fd85a Fix linting config and package fields
- Replace .eslintrc.cjs with eslint.config.mjs (ESLint 9 flat config)
  using direct eslint-plugin-solid + @typescript-eslint/parser approach
- Add @typescript-eslint/parser to root devDependencies
- Add main/module/types top-level fields to packages/core/package.json
- Add resolve.conditions to packages/core/vite.config.ts
- Create packages/core/tsconfig.test.json for test type-checking
- Remove empty paths:{} from packages/core/tsconfig.json
2026-03-29 02:35:57 +07:00

355 lines
11 KiB
Markdown

# Polymorphism
All component parts that render a DOM element have an `as` prop.
## The `as` prop
For simple use cases the `as` prop can be used, either with native HTML elements or custom Solid components:
```
Copytsx
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>
);
}
```
```
Copytsx
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>
);
}
```
## The `as` prop callback
For more advanced use cases the `as` prop can accept a callback.
The main reason to use a callback over the normal `as` prop is being able to set props without interfering with Kobalte.
When using this pattern the following rules apply to the callback:
- You must spread the props forwarded to your callback onto your node/component.
- Custom props are passed as is from the parent.
- Kobalte options are not passed to the callback, only the resulting html attributes.
- You should set your event handlers on the parent and not inside your callback.
```
Copytsx
import { Tabs } from "@kobalte/core/tabs";
import { MyCustomButton } from "./components";
function App() {
return (
<Tabs>
<Tabs.List>
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
<Tabs.Trigger value="one" as={MyCustomButton}>
A Trigger
</Tabs.Trigger>
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
<Tabs.Trigger
value="one"
as={props => (
// The `value` prop is directly passed to MyCustomButton
<MyCustomButton value="custom" {...props} />
)}
>
Custom Button Trigger
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="one">Content one</Tabs.Content>
</Tabs>
);
}
```
```
Copytsx
import { Tabs } from "@kobalte/core/tabs";
import { MyCustomButton } from "./components";
function App() {
return (
<Tabs>
<Tabs.List>
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
<Tabs.Trigger value="one" as={MyCustomButton}>
A Trigger
</Tabs.Trigger>
{/* The `value` prop is used by Kobalte and not passed to MyCustomButton */}
<Tabs.Trigger
value="one"
as={props => (
// The `value` prop is directly passed to MyCustomButton
<MyCustomButton value="custom" {...props} />
)}
>
Custom Button Trigger
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="one">Content one</Tabs.Content>
</Tabs>
);
}
```
You can optionally use a type helper to get the exact types passed to your callback:
```
Copytsx
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>;
```
```
Copytsx
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
Setting custom event handlers on component will call your custom handler before Kobalte's.
## Types
This section is mainly for library author that want to build on top of Kobalte and expose the correct types
to your end users.
Every component that renders an HTML element has the following 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`
This type contains all custom props consumed by Kobalte, these props do not exist in HTML.
These are not passed to the HTML element nor to the `as` callback.
### `ComponentCommonProps<T>`
This type contains HTML attributes optionally accepted by the Kobalte component and will
be forwarded to the rendered DOM node. These are managed by Kobalte but can be customized by the end
user. It includes attributes such as `id`, `ref`, event handlers, etc. The generic is used by `ref` and event handlers,
by default it is `HTMLElement`.
### `ComponentRenderProps`
This type extends `ComponentCommonProps` and additionally contains attributes that are passed
to the DOM node and fully managed by Kobalte. You should never assign these yourself or set them on
the Kobalte component. Modifying these props will break your component's behavior and accessibility.
### `ComponentProps<T>`
This is the final type exported by components, it is equal to `ComponentOptions & Partial<ComponentCommonProps>`.
It combines all props expected by Kobalte's component. The generic is used by the CommonProps, by default it is `HTMLElement`.
### `PolymorphicProps<T, ComponentProps>`
If you're writing a custom component and want to expose Kobalte's `as` prop to the end user
and keep proper typing, be sure to use `PolymorphicProps<T, ComponentProps>` for your props type.
```
Copytsx
import { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
// Optionally extend `TabsTriggerProps` if you wish to
// expose Kobalte props to your end user.
interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {
variant: "default" | "outline";
}
// Your generic `T` should extend ValidComponent and have a default value of the default DOM node.
function CustomTabsTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, CustomProps<T>>,
) {
// Typescript degrades typechecking when using generics, as long as we
// spread `others` to our element, we can effectively ignore them.
const [local, others] = splitProps(props as CustomProps, ["variant"]);
return (
<Tabs.Trigger
// Optional, will default to Kobalte otherwise.
// This should match with your generic `T` default.
as="button"
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
// Make sure to spread these props!
{...others}
/>
);
}
```
```
Copytsx
import { Tabs, TabsTriggerProps } from "@kobalte/core/tabs";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
// Optionally extend `TabsTriggerProps` if you wish to
// expose Kobalte props to your end user.
interface CustomProps<T extends ValidComponent = "button"> extends TabsTriggerProps<T> {
variant: "default" | "outline";
}
// Your generic `T` should extend ValidComponent and have a default value of the default DOM node.
function CustomTabsTrigger<T extends ValidComponent = "button">(
props: PolymorphicProps<T, CustomProps<T>>,
) {
// Typescript degrades typechecking when using generics, as long as we
// spread `others` to our element, we can effectively ignore them.
const [local, others] = splitProps(props as CustomProps, ["variant"]);
return (
<Tabs.Trigger
// Optional, will default to Kobalte otherwise.
// This should match with your generic `T` default.
as="button"
class={local.variant === "default" ? "default-trigger" : "outline-trigger"}
// Make sure to spread these props!
{...others}
/>
);
}
```
If you do not wish to allow changing the element type, you can simplify your types by making
props: `OverrideComponentProps<"button", CustomProps>`, replace `"button"` with the correct
tagname for other components, imported from `"@kobalte/utils"`.
If you also want to export exact types, you can re-export and extends component types:
```
Copytsx
export interface CustomTabsTriggerOptions extends TabsTriggerOptions {
variant: "default" | "outline";
}
export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement> extends TabsTriggerCommonProps<T> {
// If you allow users to set classes and extend them.
//class: string;
}
export interface CustomTabsTriggerRenderProps
extends CustomTabsTriggerCommonProps,
TabsTriggerRenderProps {
// If you do not allow users to set classes and manage all of them.
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>,
) {}
```
```
Copytsx
export interface CustomTabsTriggerOptions extends TabsTriggerOptions {
variant: "default" | "outline";
}
export interface CustomTabsTriggerCommonProps<T extends HTMLElement = HTMLElement> extends TabsTriggerCommonProps<T> {
// If you allow users to set classes and extend them.
//class: string;
}
export interface CustomTabsTriggerRenderProps
extends CustomTabsTriggerCommonProps,
TabsTriggerRenderProps {
// If you do not allow users to set classes and manage all of them.
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>` is a helper from `"@kobalte/core/polymorphic"` that converts a tag name into its element
(e.g. `ElementOf<"button"> = HTMLButtonElement`).
Previous[←Animation](https://kobalte.dev/docs/core/overview/animation)Next[Server side rendering→](https://kobalte.dev/docs/core/overview/ssr)