Theme
@nodeblocks/frontend-theme is the shared Material UI design system used by Nodeblocks frontend blocks. It ships a pre-configured defaultTheme, a root ThemeProvider that registers global styles and CSS baseline, and token modules (palette, typography, spacing, components) you can merge when branding an app.
Mount ThemeProvider once at the application root so every block and MUI component shares the same colors, typography, spacing, shadows, and per-component style overrides.
Install
npm install @nodeblocks/frontend-theme @mui/material @emotion/react @emotion/styled
Usage
Wrap your app (or Storybook preview) with ThemeProvider. All frontend blocks expect this context to be present.
import { ThemeProvider, defaultTheme } from '@nodeblocks/frontend-theme';
export function App() {
return (
<ThemeProvider theme={defaultTheme}>
{/* routes, blocks, and other MUI UI */}
</ThemeProvider>
);
}
ThemeProvider accepts:
theme— MUI theme object (default:defaultTheme)enableCssBaseline— render MUICssBaseline(default:true)injectMuiStylesFirst— allow custom CSS to override MUI (default:true)
Customize the theme
Merge overrides into defaultTheme so you keep existing component styles and only change what you need.
Palette and tokens
import { createTheme } from '@mui/material';
import { ThemeProvider, defaultTheme } from '@nodeblocks/frontend-theme';
const brandTheme = createTheme(defaultTheme, {
palette: {
primary: {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#ffffff',
},
},
});
<ThemeProvider theme={brandTheme}>{/* ... */}</ThemeProvider>;
Component overrides — spread the exported components map, then patch individual keys:
import { createTheme } from '@mui/material';
import { ThemeProvider, defaultTheme, components } from '@nodeblocks/frontend-theme';
const customTheme = createTheme(defaultTheme, {
components: {
...components,
MuiButton: {
styleOverrides: {
root: { borderRadius: 12 },
},
},
},
});
Rebuild from exported modules
import { createTheme } from '@mui/material';
import { palette, typography, spacing, components, defaultTheme } from '@nodeblocks/frontend-theme';
const theme = createTheme(defaultTheme, {
palette: { ...palette, primary: { ...palette.primary, main: '#006ead' } },
typography,
spacing,
components,
});
Exported API: defaultTheme, ThemeProvider, useTheme, GlobalStyles, palette, typography, spacing, components, and ThemeProviderProps.
Themed components
ThemeProviderEvery tsx live snippet on this page is rendered inside the documentation Playground, which internally wraps the preview in ThemeProvider with defaultTheme. The examples therefore skip that wrapper so the UI is easier to read. In your own application, you still need to mount ThemeProvider at the root as shown in Usage.
The sections below mirror the blocks/frontend-theme Storybook preview (SharedTheme.stories.tsx). Each snippet is a tsx live example showing styled output for the MUI components that have theme overrides in components.ts.
Color palette
Palette tokens drive primary, secondary, semantic colors, backgrounds, and text.
function Example() { const colors = ['primary', 'secondary', 'error', 'warning', 'info', 'success']; const shades = ['main', 'light', 'dark']; return ( <Stack direction="row" spacing={2} sx={{ p: 3, flexWrap: 'wrap', gap: 2 }}> {colors.map((color) => ( <Card key={color} sx={{ minWidth: 160 }}> <CardContent> <Typography variant="h6" gutterBottom sx={{ textTransform: 'capitalize' }}> {color} </Typography> <Stack direction="row" spacing={1}> {shades.map((shade) => ( <Box key={shade} sx={{ width: 48, height: 48, bgcolor: `${color}.${shade}`, borderRadius: 1 }} title={`${color}.${shade}`} /> ))} </Stack> </CardContent> </Card> ))} </Stack> ); }
Grey scale
Extended grey steps (25, 75, and 50–900) are defined on the palette.
function Example() { const shades = ['25', '50', '75', '100', '200', '300', '400', '500', '600', '700', '800', '900']; return ( <Stack spacing={1} sx={{ p: 3, maxWidth: 400 }}> {shades.map((shade) => ( <Stack key={shade} direction="row" spacing={2} alignItems="center"> <Box sx={{ width: 120, height: 40, bgcolor: `grey.${shade}`, borderRadius: 1, border: '1px solid', borderColor: 'divider', }} /> <Typography variant="body2" sx={{ minWidth: 32 }}> {shade} </Typography> </Stack> ))} </Stack> ); }
Typography
Typography variants (h1–h6, subtitle1/subtitle2, body1/body2, caption, overline, button) come from the theme scale. Buttons also inherit textTransform: 'none' from typography.button.
function Example() { const variants = [ { label: 'Heading 1', variant: 'h1' }, { label: 'Heading 2', variant: 'h2' }, { label: 'Heading 3', variant: 'h3' }, { label: 'Heading 4', variant: 'h4' }, { label: 'Heading 5', variant: 'h5' }, { label: 'Heading 6', variant: 'h6' }, { label: 'Subtitle 1', variant: 'subtitle1' }, { label: 'Subtitle 2', variant: 'subtitle2' }, { label: 'Body 1 — regular paragraph text', variant: 'body1' }, { label: 'Body 2 — smaller paragraph text', variant: 'body2' }, { label: 'Caption text', variant: 'caption' }, { label: 'Overline text', variant: 'overline' }, ]; return ( <Stack spacing={2} sx={{ p: 3 }}> {variants.map(({ label, variant }) => ( <Typography key={variant} variant={variant}> {label} </Typography> ))} </Stack> ); }
Buttons (MuiButton, MuiButtonBase)
Contained, outlined, and text variants; sizes; semantic colors; disabled state.
function Example() { return ( <Stack spacing={2} sx={{ p: 3 }}> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="contained" size="small">Small Contained</Button> <Button variant="contained" size="medium">Medium Contained</Button> <Button variant="contained" size="large">Large Contained</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="outlined" size="small">Small Outlined</Button> <Button variant="outlined" size="medium">Medium Outlined</Button> <Button variant="outlined" size="large">Large Outlined</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="text" size="small">Small Text</Button> <Button variant="text" size="medium">Medium Text</Button> <Button variant="text" size="large">Large Text</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="contained" color="primary">Primary</Button> <Button variant="contained" color="secondary">Secondary</Button> <Button variant="contained" color="error">Error</Button> <Button variant="contained" color="warning">Warning</Button> <Button variant="contained" color="info">Info</Button> <Button variant="contained" color="success">Success</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap' }}> <Button variant="contained" disabled>Disabled</Button> <Button variant="outlined" disabled>Disabled Outlined</Button> </Stack> </Stack> ); }
Text fields (MuiOutlinedInput, MuiInputLabel, MuiFormLabel, MuiFormHelperText)
Labels sit above fields (bold, non-floating). Outlined inputs use rounded corners and primary focus rings.
function Example() { return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 400 }}> <TextField label="Standard" variant="outlined" /> <TextField label="With Value" variant="outlined" defaultValue="Sample text" /> <TextField label="Disabled" variant="outlined" disabled /> <TextField label="Required" variant="outlined" required /> <TextField label="With Helper Text" variant="outlined" helperText="Some important helper text" /> <TextField label="Error State" variant="outlined" error helperText="Error message" /> </Stack> ); }
Select (MuiSelect)
Select inherits outlined-input styling from the theme.
function Example() { const [value, setValue] = React.useState(''); return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 300 }}> <FormControl fullWidth> <InputLabel id="theme-select-label">Option</InputLabel> <Select labelId="theme-select-label" value={value} label="Option" onChange={(e) => setValue(e.target.value)} > <MenuItem value="option1">Option 1</MenuItem> <MenuItem value="option2">Option 2</MenuItem> <MenuItem value="option3">Option 3</MenuItem> </Select> </FormControl> <FormControl fullWidth error> <InputLabel id="theme-select-error">Error State</InputLabel> <Select labelId="theme-select-error" value="" label="Error State"> <MenuItem value="option1">Option 1</MenuItem> </Select> </FormControl> <FormControl fullWidth disabled> <InputLabel id="theme-select-disabled">Disabled</InputLabel> <Select labelId="theme-select-disabled" value="" label="Disabled"> <MenuItem value="option1">Option 1</MenuItem> </Select> </FormControl> </Stack> ); }
Date picker input (MuiPickersOutlinedInput)
Date pickers use the same outlined-input overrides as text fields.
function Example() { return ( <LocalizationProvider dateAdapter={AdapterLuxon}> <Box sx={{ p: 3, maxWidth: 300 }}> <DatePicker label="Start date" /> </Box> </LocalizationProvider> ); }
Table (MuiTable, MuiTableCell, MuiTableHead, MuiTableBody, MuiTableContainer, MuiTableRow)
Full-width layout, divider borders, header sizing, and row hover on grey.50.
function Example() { return ( <TableContainer component={Paper} sx={{ m: 3 }}> <Table> <TableHead> <TableRow> <TableCell>Name</TableCell> <TableCell>Email</TableCell> <TableCell>Role</TableCell> <TableCell>Status</TableCell> </TableRow> </TableHead> <TableBody> <TableRow> <TableCell>John Doe</TableCell> <TableCell>john@example.com</TableCell> <TableCell>Admin</TableCell> <TableCell><Chip label="Active" color="success" size="small" /></TableCell> </TableRow> <TableRow> <TableCell>Jane Smith</TableCell> <TableCell>jane@example.com</TableCell> <TableCell>User</TableCell> <TableCell><Chip label="Pending" color="warning" size="small" /></TableCell> </TableRow> <TableRow> <TableCell>Bob Johnson</TableCell> <TableCell>bob@example.com</TableCell> <TableCell>User</TableCell> <TableCell><Chip label="Active" color="success" size="small" /></TableCell> </TableRow> </TableBody> </Table> </TableContainer> ); }
Tabs (MuiTab)
Tab strip styling for navigation.
function Example() { const [tab, setTab] = React.useState(0); return ( <Box sx={{ p: 3 }}> <Tabs value={tab} onChange={(_, v) => setTab(v)}> <Tab label="Tab 1" /> <Tab label="Tab 2" /> <Tab label="Tab 3" /> </Tabs> <Typography sx={{ mt: 2 }}>Content for Tab {tab + 1}</Typography> </Box> ); }
Menu (MuiMenu, MuiMenuItem, MuiListItemIcon)
Dropdown menus with icon alignment.
function Example() { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); return ( <Box sx={{ p: 3 }}> <Button onClick={(e) => setAnchorEl(e.currentTarget)}>Open Menu</Button> <Menu anchorEl={anchorEl} open={open} onClose={() => setAnchorEl(null)}> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Home fontSize="small" /></ListItemIcon> Home </MenuItem> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Person fontSize="small" /></ListItemIcon> Profile </MenuItem> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Settings fontSize="small" /></ListItemIcon> Settings </MenuItem> <Divider /> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Logout fontSize="small" /></ListItemIcon> Logout </MenuItem> </Menu> </Box> ); }
Link (MuiLink)
Uses palette.link and global anchor styles from GlobalStyles.
function Example() { return ( <Stack spacing={2} sx={{ p: 3 }}> <Stack direction="row" spacing={3} alignItems="center" sx={{ flexWrap: 'wrap' }}> <Link href="#">Default Link</Link> <Link href="#" sx={{ color: 'secondary.main' }}>Secondary Link</Link> <Link href="#" sx={{ color: 'error.main' }}>Error Link</Link> </Stack> <Stack direction="row" spacing={3} alignItems="center" sx={{ flexWrap: 'wrap' }}> <Link href="#">No Underline</Link> <Link href="#" underline="hover">Underline on Hover</Link> <Link href="#" underline="always">Always Underline</Link> </Stack> <Typography variant="body1"> Paragraph with an <Link href="#">inline link</Link> in the text. </Typography> </Stack> ); }
Pagination (MuiPagination, MuiPaginationItem)
Page controls with primary, secondary, and outlined variants.
function Example() { const [page, setPage] = React.useState(1); return ( <Stack spacing={2} sx={{ p: 3 }}> <Pagination count={10} page={page} onChange={(_, v) => setPage(v)} color="primary" /> <Pagination count={10} color="secondary" /> <Pagination count={10} variant="outlined" color="primary" shape="rounded" /> </Stack> ); }
Circular progress (MuiCircularProgress)
Loading indicators across semantic colors.
function Example() { return ( <Stack direction="row" spacing={4} alignItems="center" sx={{ p: 3, flexWrap: 'wrap' }}> <Box textAlign="center"> <CircularProgress size={40} /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>Default</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="secondary" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>Secondary</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="success" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>Success</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="error" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>Error</Typography> </Box> </Stack> ); }
Paper & card (MuiPaper, MuiCard)
Surfaces for layout sections and content cards.
function Example() { return ( <Stack direction="row" spacing={2} sx={{ p: 3, flexWrap: 'wrap', gap: 2 }}> <Card sx={{ minWidth: 200 }}> <CardContent> <Typography variant="h6" gutterBottom>Card Title</Typography> <Typography variant="body2" color="text.secondary"> Card component with themed surface. </Typography> </CardContent> </Card> <Card variant="outlined" sx={{ minWidth: 200 }}> <CardContent> <Typography variant="h6" gutterBottom>Outlined Card</Typography> <Typography variant="body2" color="text.secondary"> Outlined card variant. </Typography> </CardContent> </Card> <Paper sx={{ p: 2, minWidth: 200 }}> <Typography variant="h6" gutterBottom>Paper Component</Typography> <Typography variant="body2" color="text.secondary"> Paper surface for grouped content. </Typography> </Paper> </Stack> ); }
Divider (MuiDivider)
Horizontal, vertical, and text-aligned dividers using palette.divider.
function Example() { return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 400 }}> <Box sx={{ bgcolor: 'background.paper', p: 2 }}> <Typography>Content above</Typography> <Divider sx={{ my: 2 }} /> <Typography>Content below</Typography> </Box> <Divider>CENTER</Divider> <Divider textAlign="left">LEFT</Divider> <Divider textAlign="right">RIGHT</Divider> <Stack direction="row" spacing={2} sx={{ height: 40, alignItems: 'center' }}> <Typography>Item 1</Typography> <Divider orientation="vertical" flexItem /> <Typography>Item 2</Typography> <Divider orientation="vertical" flexItem /> <Typography>Item 3</Typography> </Stack> </Stack> ); }