---
title: "MUI"
description: "Render GistUI answers with MUI (Material UI) components in React - Card, Button and TextField - with forms, validation, actions and dialogs 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.

# MUI

For React apps built with [MUI](https://mui.com/material-ui/).

```sh
npm i @gistui/react @gistui/styles @gistui/catalog
yarn add @gistui/react @gistui/styles @gistui/catalog
pnpm add @gistui/react @gistui/styles @gistui/catalog
bun add @gistui/react @gistui/styles @gistui/catalog
```

## Map components

```tsx title="gistui-mui.tsx"
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import TextField from "@mui/material/TextField";
import type { ComponentProps } from "@gistui/react";
import { ui, useGistButton, useGistField } from "@gistui/react/ui";

const str = (v: unknown) => (typeof v === "string" && v !== "" ? v : undefined);

function MuiCard({ props, children }: ComponentProps) {
  return (
    <Card variant={props.v === "outline" ? "outlined" : "elevation"} data-gistui="Card">
      <CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>{children}</CardContent>
    </Card>
  );
}

const VARIANTS = { primary: "contained", accent: "contained", secondary: "outlined", ghost: "text", link: "text", danger: "contained" } as const;

function MuiButton(p: ComponentProps) {
  const b = useGistButton(p); // do: actions, submit and draft in a Form, opens: a Dialog, links
  const variant = VARIANTS[b.variant as keyof typeof VARIANTS] ?? "contained";
  const color = b.variant === "danger" ? "error" : "primary";
  if (b.href) return <Button variant={variant} color={color} href={b.href} target="_blank" rel="noopener noreferrer">{b.label}</Button>;
  return (
    <>
      <Button variant={variant} color={color} fullWidth={b.full} {...b.buttonProps}>{b.label}</Button>
      {b.dialog}
    </>
  );
}

function MuiInput(p: ComponentProps) {
  const field = useGistField(p); // validation, error, bind:$var
  const value = field.bound
    ? { value: field.value == null ? "" : String(field.value), onChange: (e: React.ChangeEvent<HTMLInputElement>) => field.setValue(e.target.value) }
    : { defaultValue: str(p.props.value) };
  return (
    <TextField
      name={field.name}
      label={field.label}
      type={str(p.props.type) ?? "text"}
      placeholder={str(p.props.placeholder)}
      required={field.required}
      disabled={field.locked}
      error={!!field.error}
      helperText={field.error ?? str(p.props.hint)}
      fullWidth
      {...value}
    />
  );
}

export const muiUI = ui.extend({ Card: MuiCard, Button: MuiButton, Input: MuiInput });
```

`TextField` renders a native `<input name>`, so the GistUI Form reads and validates it.

## Match the theme with tokens

Read the values from your MUI theme, so the GistUI components you did not replace (charts, tables, pickers) match.

```tsx
import { useTheme } from "@mui/material/styles";
import { GistUI, defineTheme } from "@gistui/react";
import { muiUI } from "./gistui-mui";

function Answer({ stream }: { stream: ReadableStream }) {
  const t = useTheme();
  const tokens = defineTheme({
    primary: t.palette.primary.main,
    accent: t.palette.primary.main,
    radius: Number(t.shape.borderRadius),
    font: t.typography.fontFamily,
  });
  return <GistUI library={muiUI} tokens={tokens} theme={t.palette.mode} stream={stream} />;
}
```

## Notes

- MUI's `required` on `TextField` adds its own asterisk; GistUI's Form still does the validating.
- `theme={t.palette.mode}` keeps GistUI's light or dark in step with MUI's.
- MUI uses Emotion, not cascade layers. GistUI's styles are in `@layer gistui`, so MUI's unlayered styles win where they overlap, which is what you want.

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