Material UI with Astro and React
Nov 03, 2022
MUI and Tailwind CSS is my top pick when creating a web project. And when I tried Astro for the first time, I felt like a free bird who can fly anywhere across JavaScript frameworks.
So, I tried to run MUI with Astro and found some limitations that are yet to support. Astro has options for how your UI components will load on the browser called hydration directives.
For now, you can only use MUI with the client:only
directive. In the future, they might update the React integration package or add a new integration to support the other hydration directives.
You can follow this issue for more info and get updates. Or you can follow me on Twitter to get any updates on this post.
Creating the project
Let’s create an Astro project first with the empty project template and Strict
typescript option first.
# Using NPM
npm create astro@latest
# Using Yarn
yarn create astro
# Using PNPM
pnpm create astro@latest
This will create an Astro project without any heavy templates.
Now, Let’s add React integration to our project.
# Using NPM
npx astro add react
# Using Yarn
yarn astro add react
# Using PNPM
pnpm astro add react
It will ask for permission to install packages and update astro.config.mjs
and tsconfig.json
files. Press y
on your keyboard every time when it asks to continue the installation.
After the installation is completed, create a components
folder, create a Home.tsx
file inside the folder and then paste the code below.
export default function Home() {
return (
<div>
<h1>Hello World</h1>
</div>
);
}
Also, update the index.astro
by importing your Home
component to test if the React integration working perfectly.
---
import Home from "../components/Home";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<Home />
</body>
</html>
Now, restart your server if it’s already running and run the npm dev
command and see if your project running successfully.

Installing MUI packages
I am assuming you have at least basic knowledge about Material UI and React. If you are trying Material UI for the first time, I suggest you learn Material UI with React first.
Now let’s install all the necessary packages for Material UI including fonts, icons, etc.
# Using NPM
npm install @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material
# Using YARN
yarn add @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material
# Using PNPM
pnpm add @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material
Material UI components with Astro
We are going to make a single to-do form. So, Let’s paste the code below in Home.tsx
file and let me explain step by step.
import { ThemeProvider } from "@emotion/react";
import {
Box,
Button,
Container,
CssBaseline,
TextField,
Typography,
} from "@mui/material";
import { purple } from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
const theme = createTheme({
palette: {
primary: {
main: purple[500],
},
},
});
export default function Home() {
const handleTodo = (e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
todo: { value: string };
};
alert(target.todo.value);
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container maxWidth="xs" sx={{ my: 10 }}>
<Typography variant="h4" fontWeight={600} gutterBottom>
Add Todo
</Typography>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column", gap: 2 }}
onSubmit={handleTodo}
>
<TextField id="todo" name="todo" label="Add a Task" />
<Button variant="contained" type="submit">
Add Task
</Button>
</Box>
</Container>
</ThemeProvider>
);
}
Since Material UI depends on Roboto
font by default we imported Roboto
fonts. Then we created a theme config to modify the primary theme color to test if the ThemeProvider
component is working.
Then, we added Container
, Typography
, Box
, TextField
and Button
components to create our add-to-do form which will open up on the alert box with the input value.
Now, Let’s update our index.astro
file to add hydration directive client:only
which will render the component on the client side when the page loads.
---
import Home from "../components/Home";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<Home client:only="react" />
</body>
</html>
Save your files and check your browser to see if everything is working like below. Make sure you restarted your dev server after installing the MUI npm packages.

As you can see, the primary color of the button is changed because of the ThemeProvider
component.
State Management with Material UI, Astro, and React
Usually, we use React state hooks such as useState
to update the component state, for example, opening/closing Modal, Dialogue, Drawer, etc.
But Astro suggests a different solution to share the state between your components. With Nano Stores, you can share your state across multiple UI frameworks. It is also very lightweight.
Let’s see how we can use Nano Store with our Material UI components. But first, we have to install the npm packages.
# Using NPM
npm install nanostores @nanostores/react
# Using YARN
yarn add nanostores @nanostores/react
# Using PNPM
pnpm add nanostores @nanostores/react
Now, create a new file stores/todoStore.ts
in the src
folder. and paste the code below.
import { atom } from "nanostores";
export interface Todo {
title: string;
}
export const todosAtom = atom<Todo[]>([]);
Here we created an interface and atom for Todo. The Atom store is to store your data. Compared to [state, setState]
it has todosAtom.get()
and todosAtom.set()
methods to get and store the data. Nano Store has another hook called useStore
to only get the data.
Let’s update our Home.tsx
file too.
import { ThemeProvider } from "@emotion/react";
import {
Box,
Button,
Container,
CssBaseline,
Paper,
TextField,
Typography,
} from "@mui/material";
import { purple } from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import { useStore } from "@nanostores/react";
import { todosAtom } from "../stores/todoStore";
const theme = createTheme({
palette: {
primary: {
main: purple[500],
},
},
});
export default function Home() {
const todos = useStore(todosAtom);
const handleTodo = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const target = e.target as typeof e.target & {
todo: { value: string };
reset: () => void; // clear all input values in the form
};
const newTodo = { title: target.todo.value };
todosAtom.set(todos.concat(newTodo));
target.reset();
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container maxWidth="xs" sx={{ my: 10 }}>
<Typography variant="h4" fontWeight={600} gutterBottom>
Add Todo
</Typography>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column", gap: 2 }}
onSubmit={handleTodo}
>
<TextField id="todo" name="todo" label="Add a Task" />
<Button variant="contained" type="submit">
Add Task
</Button>
</Box>
<Box sx={{ my: 4 }}>
<Typography variant="h5" gutterBottom fontWeight={600}>
List of To Do's
</Typography>
<Box>
{todos.map((todo, index) => (
<Paper key={index} sx={{ px: 2, py: 1, mb: 2 }}>
{todo.title}
</Paper>
))}
</Box>
</Box>
</Container>
</ThemeProvider>
);
}
As you can see, we are getting the value by using the useStore
hook from Nano Store. Then, we are updating our state by adding new to-do. To clear the form input values, we are using the reset()
method. And below, we are returning our to-dos inside the Paper
component.
Try adding to-do’s and you should see the same as below.

Try running npm run build
and npm run preview
to see if your application has any issues with running builds and preview.
Conclusion
You can try other Material UI components like Drawers, Table, etc. But instead of the useState
hook, I would highly recommend using Nano Store. Because you may have to manage your state in the Astro component too.
In my opinion, you shouldn’t use MUI with Astro in production since there are still lots of issues to fix. The main reason I would use Astro is to ship with zero JavaScript which is not possible with Material UI yet.