Skip to content

MUI

Render GistUI answers with MUI (Material UI) components in React - Card, Button and TextField - with forms, validation, actions and dialogs still working.

Updated View as Markdown

For React apps built with MUI.

npm i @gistui/react @gistui/styles @gistui/catalog

Map components

gistui-mui.tsxtsx
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.

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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close