For React apps built with MUI.
npm i @gistui/react @gistui/styles @gistui/catalogyarn add @gistui/react @gistui/styles @gistui/catalogpnpm add @gistui/react @gistui/styles @gistui/catalogbun add @gistui/react @gistui/styles @gistui/catalogMap components
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
requiredonTextFieldadds 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.