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

> Documentation Index
> Fetch the complete documentation index at: https://gistui.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Radix UI

For React apps that use [Radix UI](https://www.radix-ui.com/primitives) primitives directly, styled your own way. If you use shadcn/ui, which is built on Radix, start with [shadcn/ui](/docs/ui-libraries/shadcn-ui/) instead.

```sh
npm i @gistui/react @gistui/styles @gistui/catalog radix-ui
yarn add @gistui/react @gistui/styles @gistui/catalog radix-ui
pnpm add @gistui/react @gistui/styles @gistui/catalog radix-ui
bun add @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

```tsx title="GistSwitch.tsx"
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

```tsx title="GistSelect.tsx"
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

```tsx
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](/docs/guides/theming/) 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](/docs/ui-libraries/shadcn-ui/#3-map-components).

Source: https://gistui.com/docs/ui-libraries/radix/index.mdx
