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-uiyarn add @gistui/react @gistui/styles @gistui/catalog radix-uipnpm add @gistui/react @gistui/styles @gistui/catalog radix-uibun add @gistui/react @gistui/styles @gistui/catalog radix-uiWhy 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
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
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-uipackage. With the older scoped packages, the imports are@radix-ui/react-switchand@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 youbuttonPropsto spread on any button element: see shadcn/ui.