Skip to content

Radix UI

Use Radix UI primitives to draw GistUI components in React, with form validation and bind:$var still working.

Updated View as Markdown

For React apps that use Radix UI primitives directly, styled your own way. If you use shadcn/ui, which is built on Radix, start with shadcn/ui instead.

npm i @gistui/react @gistui/styles @gistui/catalog radix-ui

Why Radix fits well

GistUI’s Form reads values from native form controls. Radix form primitives (Checkbox, Switch, Select, RadioGroup, Slider) render a hidden native input when you give them name, so they join the Form without extra work.

A switch

GistSwitch.tsxtsx
import { useId } from "react";
import { Switch } from "radix-ui";
import type { ComponentProps } from "@gistui/react";
import { useGistField } from "@gistui/react/ui";

export function GistSwitch(p: ComponentProps) {
  const id = useId();
  const field = useGistField(p, "bool"); // one on/off value
  const value = field.bound
    ? { checked: field.value === true, onCheckedChange: (on: boolean) => field.setValue(on) }
    : { defaultChecked: p.props.checked === true };
  return (
    <div className="switch-row">
      {/* With `name`, Radix renders a hidden input that the Form reads. */}
      <Switch.Root id={id} name={field.name} disabled={field.locked} className="switch" {...value}>
        <Switch.Thumb className="switch-thumb" />
      </Switch.Root>
      <label htmlFor={id}>{field.label}</label>
    </div>
  );
}

A select

GistSelect.tsxtsx
import { Select } from "radix-ui";
import type { ComponentProps } from "@gistui/react";
import { ui, useGistField } from "@gistui/react/ui";

// Radix's Select picks one value; GistUI's own Select handles `multiple`.
const BuiltinSelect = ui.components.get("Select")!;

export function GistSelect(p: ComponentProps) {
  if (p.props.multiple === true) return <BuiltinSelect {...p} />;
  return <Single {...p} />;
}

function Single(p: ComponentProps) {
  const field = useGistField(p);
  const options = Array.isArray(p.props.options) ? p.props.options.map(String) : [];
  const value = field.bound
    ? { value: field.value == null ? "" : String(field.value), onValueChange: (v: string) => field.setValue(v) }
    : { defaultValue: typeof p.props.value === "string" ? p.props.value : undefined };
  return (
    <label>
      {field.label}
      <Select.Root name={field.name} disabled={field.locked} {...value}>
        <Select.Trigger aria-invalid={field.error ? true : undefined}>
          <Select.Value placeholder={(p.props.placeholder as string) ?? "Select…"} />
        </Select.Trigger>
        <Select.Portal>
          <Select.Content>
            <Select.Viewport>
              {options.map((o) => (
                <Select.Item key={o} value={o}><Select.ItemText>{o}</Select.ItemText></Select.Item>
              ))}
            </Select.Viewport>
          </Select.Content>
        </Select.Portal>
      </Select.Root>
      {field.error && <small role="alert">{field.error}</small>}
    </label>
  );
}

Register them

import { GistUI } from "@gistui/react";
import { ui } from "@gistui/react/ui";

const lib = ui.extend({ Switch: GistSwitch, Select: GistSelect });

<GistUI library={lib} stream={stream} onAction={handle} />;

Notes

  • The examples import from the unified radix-ui package. With the older scoped packages, the imports are @radix-ui/react-switch and @radix-ui/react-select, and the code is the same.
  • Radix ships no styles. Style the primitives as you already do, and use tokens so the GistUI components you did not replace match.
  • For buttons, useGistButton(p) gives you buttonProps to spread on any button element: see shadcn/ui.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close