Build Material UI Components#
The pn.ui components are built on Material UI, and sometimes you need a component that the library does not provide. MaterialUIComponent is the entry point for building your own: it is a ReactComponent that renders with the same Material UI library, React instance and theme as the built-in pn.ui components, so your component inherits theme_config, dark_theme and sx handling without any extra work.
This guide assumes you are familiar with the basics of ReactComponent. If you are not, start with Create Custom Widgets using ESM Components.
Import the base class#
MaterialUIComponent lives in panel.ui.base alongside the other base classes of the pn.ui components:
from panel.ui.base import MaterialUIComponent
Inside the ESM code you can then import Material UI components either one at a time or several at once, and they resolve to the copy that ships with Panel rather than being downloaded again:
import Button from "@mui/material/Button";
import {Stack, Typography} from "@mui/material";
The default import name has to match the name of the Material UI component, i.e. import Button from "@mui/material/Button" works but import MyButton from "@mui/material/Button" does not.
Theming#
Because the component renders inside the shared Material UI theme, color="primary" and friends follow the theme of your app, and a component level theme_config works exactly like it does for the built-in components:
RainbowButton(theme_config={"palette": {"primary": {"main": "#e91e63"}}})
See Customize Themes and Styles for the full set of options.
Use icons#
The Material Icons font is loaded together with the component, so the simplest way to add an icon is the Icon component with the icon name as its text. Set the font family explicitly so that the icon resolves inside the component’s shadow DOM; use "Material Icons Outlined" for the outlined variants. Here is a like button that toggles a liked parameter:
import param
import panel as pn
from panel.ui.base import MaterialUIComponent
pn.extension()
class LikeButton(MaterialUIComponent):
liked = param.Boolean(default=False)
_esm = """
import IconButton from "@mui/material/IconButton";
import Icon from "@mui/material/Icon";
export function render({ model }) {
const [liked, setLiked] = model.useState("liked");
return (
<IconButton color="primary" onClick={() => setLiked(!liked)}>
<Icon fontSize="large" style={{fontFamily: "Material Icons"}}>
{liked ? "favorite" : "favorite_border"}
</Icon>
</IconButton>
);
}
"""
like = LikeButton()
pn.Row(like, pn.bind(lambda liked: f"Liked: {liked}", like.param.liked))
If you prefer the SVG icons of @mui/icons-material, add an explicit import map entry for each icon you use and mark React as external with ?external=react, so the icon shares the React instance of the other components. The icon brings its own copy of the Material UI styles, which are not applied inside the component, so give it its dimensions and fill with inline styles:
import panel as pn
from panel.ui.base import MaterialUIComponent
pn.extension()
class DeleteButton(MaterialUIComponent):
_importmap = {
"imports": {
"@mui/icons-material/Delete": "https://esm.sh/@mui/icons-material@9.0.1/Delete?external=react",
}
}
_esm = """
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
export function render({ model }) {
return (
<IconButton color="primary">
<DeleteIcon style={{width: 35, height: 35, fill: "currentColor"}} />
</IconButton>
);
}
"""
DeleteButton()
Import map entries are global to the page, so two components that map the same icon to different URLs conflict. Keep the URL for each icon identical across your components.
Move the code into a separate file#
Once a component grows beyond a few lines, move the JSX into its own file next to the Python module. MaterialUIComponent reads the source of a file from _esm_base, given as a string path relative to the Python file:
class RainbowButton(MaterialUIComponent):
...
_esm_base = "rainbow_button.jsx"
// rainbow_button.jsx
import Button from "@mui/material/Button";
import confetti from "canvas-confetti";
export function render({ model }) {
...
}
Then serve the app as usual with panel serve app.py.
Distribute your component#
When your component is ready to share, package it as a Python extension as described in Publish a Panel Extension. The panel-extensions organization on GitHub has examples of published extensions built on this pattern.