Skip to content
Playground

Custom components

You can create your own form components. Any built-in component can be replaced with a custom one, giving you full control over rendering and behavior.

To create a component, you just need to create a Svelte component with a compatible $props type. The easiest way to do this is to use the ComponentProps property registry as follows:

import type { ComponentProps } from "@sjsf/form";
let {
value = $bindable(),
config,
handlers,
}: ComponentProps["numberWidget"] = $props();

You will then be able to replace numberWidget with your component via the UI schema:

import type { Schema, UiSchema } from "@sjsf/form";
import MyNumberWidget from "./my-number-widget";
const schema: Schema = {
type: "number"
}
const uiSchema: UiSchema = {
"ui:components": {
"numberWidget": MyNumberWidget
}
}

You can also register a new or overwrite an old component in the selected theme as follows:

import { extendByRecord, overrideByRecord } from "@sjsf/form/lib/resolver";
import { theme } from "@sjsf/some-theme";
import MyNumberWidget from "./my-number-widget";
// Register a new component
declare module "@sjsf/form" {
interface ComponentProps {
// NOTE: Prefer to declare new components using some prefix to avoid
// conflicts in the future
myNumberWidget: ComponentProps["numberWidget"];
}
interface ComponentBindings {
myNumberWidget: "value";
}
}
export const myTheme = extendByRecord(theme, { myNumberWidget: MyNumberWidget });
// Override the default component
export const myTheme = overrideByRecord(theme, { numberWidget: MyNumberWidget })

Each component is responsible for constructing the set of attributes it needs. This decouples the SJSF from any specific UI library.

In simple cases, you can use information from config and the uiOption function to generate attributes:

const { config, uiOption }: ComponentProps["arrayTemplate"] = $props();
const description = $derived(uiOption("description") ?? config.schema.description);

You can define new UI options as follows:

declare module "@sjsf/form" {
interface UiOptions {
// NOTE: Prefer to declare new UI options using some prefix to avoid
// conflicts in the future
myUiOption?: boolean;
}
}

Then use the uiOption function to get the value of the UI option:

const { uiOption } = $props();
const value = $derived(uiOption("myUiOption") ?? false);
// Equivalent to:
import { retrieveUiOption } from "@sjsf/form";
const { config } = $props();
const ctx = getFormContext();
const value = $derived(retrieveUiOption(ctx, config, "myUiOption") ?? false);

When uiOption is used, the value defined via extraUiOptions will replace the value from the UI schema. You can use the uiOptionProps function to merge object values from the UI schema and extraUiOptions:

import type { RadioGroupItemProps, WithoutChildrenOrChild } from 'bits-ui';
import { getFormContext, uiOptionProps, type ComponentProps } from '@sjsf/form';
declare module '@sjsf/form' {
interface UiOptions {
shadcnRadioItem?: Omit<WithoutChildrenOrChild<RadioGroupItemProps>, 'value'>;
}
}
const ctx = getFormContext();
const { config, handlers }: ComponentProps['radioWidget'] = $props();
const itemAttributes = $derived(
uiOptionProps('shadcnRadioItem')(
{
onclick: handlers.oninput,
onblur: handlers.onblur
},
config,
ctx
)
);

For more complex interactive components, you may need to consider many properties and their priorities. The library provides a set of functions for forming attributes for both standard HTML elements and custom components. These functions can be categorized into two categories based on their level of abstraction:

  1. properties

These functions are designed to form a set of properties by combining them in the desired order, have the suffix Prop or Props. Example:

import type { HTMLButtonAttributes } from "svelte/elements";
import {
composeProps,
disabledProp,
getFormContext,
uiOptionProps,
uiOptionNestedProps,
type ComponentProps,
} from "@sjsf/form";
import type { ButtonType } from "@sjsf/form/fields/components";
declare module "@sjsf/form" {
interface UiOptions {
button?: HTMLButtonAttributes;
buttons?: {
[B in ButtonType]?: HTMLButtonAttributes;
};
}
}
const { type, onclick, config, disabled }: ComponentProps["button"] = $props();
const ctx = getFormContext();
const props = $derived(composeProps(
ctx,
config,
{
disabled,
type: "button",
onclick,
} satisfies HTMLButtonAttributes,
uiOptionProps("button"),
uiOptionNestedProps("buttons", (p) => p[type]),
disabledProp
))
  1. attributes

These functions are pre-prepared compositions of functions from the previous category, have the suffix Attributes. Examples:

import type { HTMLInputAttributes } from "svelte/elements";
declare module "@sjsf/form" {
interface UiOptions {
number?: HTMLInputAttributes;
}
}
import { getFormContext, inputAttributes, type ComponentProps } from "@sjsf/form";
let {
value = $bindable(),
config,
handlers,
}: ComponentProps["numberWidget"] = $props();
const ctx = getFormContext();
const attributes = $derived(
inputAttributes(ctx, config, "number", handlers, {
type: "number",
style: "flex-grow: 1",
})
);

If you need to use theme components inside your custom component, you have two options:

  1. Import the component directly
import Button from "@sjsf/your-theme/components/button.svelte";
  1. Use the getComponent function
import { getComponent, getFormContext } from "@sjsf/form";
const { config } = $props();
const ctx = getFormContext();
const Button = $derived(getComponent(ctx, "button", config));

To use getComponent with a custom component, add it to the FoundationalComponents registry:

declare module "@sjsf/form" {
interface FoundationalComponents {
myNumberWidget: {};
}
}
// Now the following code works:
import { getComponent, type UiSchema } from "@sjsf/form";
const Widget = $derived(getComponent(ctx, "myNumberWidget", config));
const uiSchema: UiSchema = {
"ui:components": {
myNumberWidget: "numberWidget"
}
};

You can also use the getFieldComponent function to get the component responsible for displaying/processing the current value:

<script lang="ts">
import { getFormContext, getFieldComponent } from "@sjsf/form";
let { value = $bindable(), config, uiOption } = $props();
const ctx = getFormContext();
const Field = $derived(getFieldComponent(ctx, config));
</script>
<Field type="field" bind:value={value as undefined} {config} {uiOption} />

To use a custom component in a resolver function - it must be declared as FoundationalComponentType (keyof FoundationalComponents) and its properties must be compatible with the FieldCommonProps<any> type.

The compatibility is checked as follows:

type IsFoundationalField<T extends FoundationalComponentType> =
FieldCommonProps<any> extends ComponentProps[T]
? ComponentProps[T] extends FieldCommonProps<any>
? true
: false
: false;