Skip to main content

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 MUI CssBaseline (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

Live examples omit ThemeProvider

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

Live Editor
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>
  );
}
Result
Loading...

Grey scale

Extended grey steps (25, 75, and 50900) are defined on the palette.

Live Editor
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>
  );
}
Result
Loading...

Typography

Typography variants (h1h6, subtitle1/subtitle2, body1/body2, caption, overline, button) come from the theme scale. Buttons also inherit textTransform: 'none' from typography.button.

Live Editor
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>
  );
}
Result
Loading...

Buttons (MuiButton, MuiButtonBase)

Contained, outlined, and text variants; sizes; semantic colors; disabled state.

Live Editor
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>
  );
}
Result
Loading...

Text fields (MuiOutlinedInput, MuiInputLabel, MuiFormLabel, MuiFormHelperText)

Labels sit above fields (bold, non-floating). Outlined inputs use rounded corners and primary focus rings.

Live Editor
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>
  );
}
Result
Loading...

Select (MuiSelect)

Select inherits outlined-input styling from the theme.

Live Editor
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>
  );
}
Result
Loading...

Date picker input (MuiPickersOutlinedInput)

Date pickers use the same outlined-input overrides as text fields.

Live Editor
function Example() {
  return (
    <LocalizationProvider dateAdapter={AdapterLuxon}>
      <Box sx={{ p: 3, maxWidth: 300 }}>
        <DatePicker label="Start date" />
      </Box>
    </LocalizationProvider>
  );
}
Result
Loading...

Table (MuiTable, MuiTableCell, MuiTableHead, MuiTableBody, MuiTableContainer, MuiTableRow)

Full-width layout, divider borders, header sizing, and row hover on grey.50.

Live Editor
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>
  );
}
Result
Loading...

Tabs (MuiTab)

Tab strip styling for navigation.

Live Editor
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>
  );
}
Result
Loading...

Dropdown menus with icon alignment.

Live Editor
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>
  );
}
Result
Loading...

Uses palette.link and global anchor styles from GlobalStyles.

Live Editor
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>
  );
}
Result
Loading...

Pagination (MuiPagination, MuiPaginationItem)

Page controls with primary, secondary, and outlined variants.

Live Editor
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>
  );
}
Result
Loading...

Circular progress (MuiCircularProgress)

Loading indicators across semantic colors.

Live Editor
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>
  );
}
Result
Loading...

Paper & card (MuiPaper, MuiCard)

Surfaces for layout sections and content cards.

Live Editor
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>
  );
}
Result
Loading...

Divider (MuiDivider)

Horizontal, vertical, and text-aligned dividers using palette.divider.

Live Editor
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>
  );
}
Result
Loading...