Kobalte

As a SolidJS user, you can rely on Kobalte's pre-built but unstyled components to create your own components faster and with great accessibility. In this guide I show a few code examples.

Get started

The idea is that you can copy the code examples, add your own CSS and then use the components with Modular Forms to create your forms. Please let us know via the issues if you run into any problems with the implementation or if any of the code samples are out of date.

To understand the code examples, it may help to read the guide about input components beforehand.

Installation

To get started, first add Kobalte to your project via npm, yarn or pnpm:

npm install @kobalte/core

Text Field

To capture a text input we use the Text Field component of Kobalte. Add the file TextField.tsx to your components directory and copy the following code into it:

import { TextField as Kobalte } from '@kobalte/core';
import { type JSX, Show, splitProps } from 'solid-js';

type TextFieldProps = {
  name: string;
  type?: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date' | undefined;
  label?: string | undefined;
  placeholder?: string | undefined;
  value: string | undefined;
  error: string;
  multiline?: boolean | undefined;
  required?: boolean | undefined;
  disabled?: boolean | undefined;
  ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
  onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
  onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
};

export function TextField(props: TextFieldProps) {
  const [rootProps, inputProps] = splitProps(
    props,
    ['name', 'value', 'required', 'disabled'],
    ['placeholder', 'ref', 'onInput', 'onChange', 'onBlur']
  );
  return (
    <Kobalte.Root
      {...rootProps}
      validationState={props.error ? 'invalid' : 'valid'}
    >
      <Show when={props.label}>
        <Kobalte.Label>{props.label}</Kobalte.Label>
      </Show>
      <Show
        when={props.multiline}
        fallback={<Kobalte.Input {...inputProps} type={props.type} />}
      >
        <Kobalte.TextArea {...inputProps} autoResize />
      </Show>
      <Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
    </Kobalte.Root>
  );
}

After adding your own styles, you can use the component together with Modular Forms as follows:

<Field name="email">
  {(field, props) => (
    <TextField
      {...props}
      type="email"
      label="Email"
      placeholder="jane@example.com"
      value={field.value}
      error={field.error}
      required
    />
  )}
</Field>

Select

To select a text value via a dropdown menu we use the Select component of Kobalte. Add the file Select.tsx to your components directory and copy the following code into it:

import { Select as Kobalte } from '@kobalte/core';
import { type JSX, Show, splitProps } from 'solid-js';

type Option = {
  label: string;
  value: string;
};

type SelectProps = {
  name: string;
  label?: string | undefined;
  placeholder?: string | undefined;
  options: Option[];
  value: string | undefined;
  error: string;
  required?: boolean | undefined;
  disabled?: boolean | undefined;
  ref: (element: HTMLSelectElement) => void;
  onInput: JSX.EventHandler<HTMLSelectElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLSelectElement, Event>;
  onBlur: JSX.EventHandler<HTMLSelectElement, FocusEvent>;
};

export function Select(props: SelectProps) {
  const [rootProps, selectProps] = splitProps(
    props,
    ['name', 'placeholder', 'options', 'required', 'disabled'],
    ['placeholder', 'ref', 'onInput', 'onChange', 'onBlur']
  );
  const [getValue, setValue] = createSignal<Option>();
  createEffect(() => {
    setValue(props.options.find((option) => props.value === option.value));
  });
  return (
    <Kobalte.Root
      {...rootProps}
      multiple={false}
      value={getValue()}
      onChange={setValue}
      optionValue="value"
      optionTextValue="label"
      validationState={props.error ? 'invalid' : 'valid'}
      itemComponent={(props) => (
        <Kobalte.Item item={props.item}>
          <Kobalte.ItemLabel>{props.item.textValue}</Kobalte.ItemLabel>
          <Kobalte.ItemIndicator>
            {/* Add SVG icon here */}
          </Kobalte.ItemIndicator>
        </Kobalte.Item>
      )}
    >
      <Show when={props.label}>
        <Kobalte.Label>{props.label}</Kobalte.Label>
      </Show>
      <Kobalte.HiddenSelect {...selectProps} />
      <Kobalte.Trigger>
        <Kobalte.Value<Option>>
          {(state) => state.selectedOption().label}
        </Kobalte.Value>
        <Kobalte.Icon>{/* Add SVG icon here */}</Kobalte.Icon>
      </Kobalte.Trigger>
      <Kobalte.Portal>
        <Kobalte.Content>
          <Kobalte.Listbox />
        </Kobalte.Content>
      </Kobalte.Portal>
      <Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
    </Kobalte.Root>
  );
}

After adding your own styles, you can use the component together with Modular Forms as follows:

<Field name="framework">
  {(field, props) => (
    <Select
      {...props}
      label="Framework"
      placeholder="Select a framework"
      options={[
        { label: 'SolidJS', value: 'solid' },
        { label: 'Qwik', value: 'qwik' },
        { label: 'Preact', value: 'preact' },
      ]}
      value={field.value}
      error={field.error}
      required
    />
  )}
</Field>

Checkbox

To capture a boolean or text value via a checkbox we use the Checkbox component of Kobalte. Add the file Checkbox.tsx to your components directory and copy the following code into it:

import { Checkbox as Kobalte } from '@kobalte/core';
import { type JSX, splitProps } from 'solid-js';

type CheckboxProps = {
  name: string;
  label: string;
  value?: string | undefined;
  checked: boolean | undefined;
  error: string;
  required?: boolean | undefined;
  disabled?: boolean | undefined;
  ref: (element: HTMLInputElement) => void;
  onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLInputElement, Event>;
  onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
};

export function Checkbox(props: CheckboxProps) {
  const [rootProps, inputProps] = splitProps(
    props,
    ['name', 'value', 'checked', 'required', 'disabled'],
    ['ref', 'onInput', 'onChange', 'onBlur']
  );
  return (
    <Kobalte.Root
      {...rootProps}
      validationState={props.error ? 'invalid' : 'valid'}
    >
      <Kobalte.Input {...inputProps} />
      <Kobalte.Control>
        <Kobalte.Indicator>{/* Add SVG icon here */}</Kobalte.Indicator>
      </Kobalte.Control>
      <Kobalte.Label>{props.label}</Kobalte.Label>
    </Kobalte.Root>
  );
}

After adding your own styles, you can use the component together with Modular Forms as follows:

<Field name="cookies" type="boolean">
  {(field, props) => (
    <Checkbox
      {...props}
      label="Yes, I want cookies"
      checked={field.value}
      error={field.error}
      required
    />
  )}
</Field>

Radio Group

For the selection of a single text value via radio buttons we use the Radio Group component of Kobalte. Add the file RadioGroup.tsx to your components directory and copy the following code into it:

import { RadioGroup as Kobalte } from '@kobalte/core';
import { type JSX, Show, splitProps, For } from 'solid-js';

type RadioGroupProps = {
  name: string;
  label?: string | undefined;
  options: { label: string; value: string }[];
  value: string | undefined;
  error: string;
  required?: boolean | undefined;
  disabled?: boolean | undefined;
  ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
  onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
  onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
};

export function RadioGroup(props: RadioGroupProps) {
  const [rootProps, inputProps] = splitProps(
    props,
    ['name', 'value', 'required', 'disabled'],
    ['ref', 'onInput', 'onChange', 'onBlur']
  );
  return (
    <Kobalte.Root
      {...rootProps}
      validationState={props.error ? 'invalid' : 'valid'}
    >
      <Show when={props.label}>
        <Kobalte.Label>{props.label}</Kobalte.Label>
      </Show>
      <div>
        <For each={props.options}>
          {(option) => (
            <Kobalte.Item value={option.value}>
              <Kobalte.ItemInput {...inputProps} />
              <Kobalte.ItemControl>
                <Kobalte.ItemIndicator />
              </Kobalte.ItemControl>
              <Kobalte.ItemLabel>{option.label}</Kobalte.ItemLabel>
            </Kobalte.Item>
          )}
        </For>
      </div>
      <Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
    </Kobalte.Root>
  );
}

After adding your own styles, you can use the component together with Modular Forms as follows:

<Field name="framework">
  {(field, props) => (
    <RadioGroup
      {...props}
      label="Framework"
      options={[
        { label: 'SolidJS', value: 'solid' },
        { label: 'Qwik', value: 'qwik' },
        { label: 'Preact', value: 'preact' },
      ]}
      value={field.value}
      error={field.error}
      required
    />
  )}
</Field>