Skip to main content
MQTT Explorer uses Material-UI (MUI) v7 with JSS styling for consistent, maintainable visual design across the application.

UI Framework

Stack:
  • @mui/material (v7) - Core components and theming
  • @mui/icons-material (v7) - Icon library
  • @mui/styles (v6) - JSS styling with withStyles HOC
  • @emotion/react & @emotion/styled - CSS-in-JS foundation
MQTT Explorer uses Material-UI v7, which is the latest major version with React 19 support.

Theming

Theme Configuration

Location: app/src/theme.ts
import { createTheme } from '@mui/material/styles'

const theme = createTheme({
  palette: {
    mode: 'light', // or 'dark'
    primary: {
      main: '#335C67', // Teal/blue-green
    },
    secondary: {
      main: '#FFA726', // Amber
    },
  },
  typography: {
    fontSize: 14, // 0.9rem base
    userSelect: 'none',
  },
})

Applying Theme

import { ThemeProvider } from '@mui/material/styles'
import { ThemeProvider as LegacyThemeProvider } from '@mui/styles'
import theme from './theme'

function App() {
  return (
    <ThemeProvider theme={theme}>
      <LegacyThemeProvider theme={theme}>
        {/* Your app */}
      </LegacyThemeProvider>
    </ThemeProvider>
  )
}
Both ThemeProvider components are required: one for Material-UI v7 components and one for legacy @mui/styles integration.

Colors

Theme Palette

Access theme colors through the palette:
backgroundColor: theme.palette.background.default  // #fff (light) / #121212 (dark)
backgroundColor: theme.palette.background.paper    // #fff (light) / #1e1e1e (dark)

Material-UI Color Palettes

Import color palettes for extended colors:
import { blueGrey, amber, green, red, orange } from '@mui/material/colors'

const styles = (theme: Theme) => ({
  lightShade: {
    backgroundColor: blueGrey[100],
  },
  darkShade: {
    backgroundColor: blueGrey[700],
  },
})
Available shades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, A100, A200, A400, A700

Theme-Conditional Colors

const color = theme.palette.mode === 'light' 
  ? blueGrey[300] 
  : theme.palette.primary.main

Code Editor Colors

Defined in app/src/components/Sidebar/CodeBlockColors.ts:
export const codeBlockColors = {
  keyword: '#0000ff',
  string: '#a31515',
  number: '#098658',
  comment: '#008000',
  // ...
}

Typography

Typography Variants

<Typography variant="h1">Heading 1</Typography>
<Typography variant="h2">Heading 2</Typography>
<Typography variant="h6">Heading 6</Typography>

Font Sizes

const styles = (theme: Theme) => ({
  text: {
    fontSize: theme.typography.pxToRem(15),  // 15px
  },
})

Monospace Font

font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace"
Used for code blocks, message payloads, and topic paths.

Spacing

8px Grid System

Material-UI uses an 8px grid system:
const styles = (theme: Theme) => ({
  container: {
    margin: theme.spacing(1),       // 8px
    padding: theme.spacing(2),      // 16px
    marginLeft: theme.spacing(1.5), // 12px (tree indentation)
  },
})
Common spacing values:
  • theme.spacing(0.5) = 4px
  • theme.spacing(1) = 8px
  • theme.spacing(1.5) = 12px
  • theme.spacing(2) = 16px
  • theme.spacing(3) = 24px

Border Radius

borderRadius: theme.shape.borderRadius  // 4px (default)

Component Styling

Primary Approach: withStyles HOC

The recommended way to style components:
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'

const styles = (theme: Theme) => ({
  root: {
    backgroundColor: theme.palette.background.default,
    padding: theme.spacing(2),
    borderRadius: theme.shape.borderRadius,
  },
  title: {
    color: theme.palette.text.primary,
    fontSize: theme.typography.pxToRem(20),
    marginBottom: theme.spacing(1),
  },
})

interface Props {
  classes: any
  title: string
}

function MyComponent({ classes, title }: Props) {
  return (
    <div className={classes.root}>
      <h2 className={classes.title}>{title}</h2>
    </div>
  )
}

export default withStyles(styles)(MyComponent)
Use withStyles for component-level styles that need theme access. Keep styles co-located with components.

Type Assertions

Some CSS properties require type assertions:
const styles = (theme: Theme) => ({
  node: {
    display: 'block' as 'block',
    whiteSpace: 'nowrap' as 'nowrap',
    overflow: 'hidden' as 'hidden',
    textOverflow: 'ellipsis',
  },
})

Responsive Styles

Use breakpoints for responsive design:
const styles = (theme: Theme) => ({
  sidebar: {
    display: 'none',
    [theme.breakpoints.up(750)]: {
      display: 'block',
    },
    [theme.breakpoints.down('sm')]: {
      width: '100%',
    },
  },
})

sx Prop (Simple Cases)

For simple, one-off styles:
<Button sx={{ color: 'primary.contrastText', mt: 2 }}>
  Click Me
</Button>
Use sx prop for simple styles. Use withStyles for complex component styles that need theme access.

Animations

CSS Animations

const styles = (theme: Theme) => ({
  '@keyframes updateLight': {
    '0%': { opacity: 0.3 },
    '50%': { opacity: 1 },
    '100%': { opacity: 0.3 },
  },
  updating: {
    animation: 'updateLight 0.5s ease-in-out',
  },
})

Theme Transitions

const styles = (theme: Theme) => ({
  drawer: {
    transition: theme.transitions.create('transform', {
      easing: theme.transitions.easing.sharp,
      duration: theme.transitions.duration.leavingScreen,
    }),
  },
})
Available easing functions:
  • theme.transitions.easing.easeInOut
  • theme.transitions.easing.easeOut
  • theme.transitions.easing.easeIn
  • theme.transitions.easing.sharp
Duration presets:
  • theme.transitions.duration.shortest (150ms)
  • theme.transitions.duration.shorter (200ms)
  • theme.transitions.duration.short (250ms)
  • theme.transitions.duration.standard (300ms)
  • theme.transitions.duration.leavingScreen (195ms)
  • theme.transitions.duration.enteringScreen (225ms)

Interactive States

Hover States

const styles = (theme: Theme) => ({
  treeNode: {
    cursor: 'pointer',
    '&:hover': {
      backgroundColor: theme.palette.mode === 'light'
        ? blueGrey[100]
        : theme.palette.primary.light,
    },
  },
})

Selection States

const styles = (theme: Theme) => ({
  selected: {
    backgroundColor: (theme.palette.mode === 'light'
      ? blueGrey[300]
      : theme.palette.primary.main) + ' !important',
    fontWeight: 'bold',
  },
})
Use !important sparingly and only when necessary to override Material-UI default styles.

Focus States

const styles = (theme: Theme) => ({
  input: {
    '&:focus': {
      borderColor: theme.palette.primary.main,
      outline: `2px solid ${theme.palette.primary.main}`,
    },
  },
})

Common Patterns

Tree Nodes

const styles = (theme: Theme) => ({
  node: {
    overflow: 'hidden' as 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap' as 'nowrap',
    cursor: 'pointer',
    padding: theme.spacing(0.5, 1),
    '&:hover': {
      backgroundColor: theme.palette.action.hover,
    },
  },
  subnodes: {
    marginLeft: theme.spacing(1.5), // 12px indentation
  },
})

Buttons

<Button variant="contained" color="primary">
  Submit
</Button>

Icons

import HomeIcon from '@mui/icons-material/Home'

<HomeIcon fontSize="inherit" />

Cards

import { Card, CardContent } from '@mui/material'

<Card>
  <CardContent>
    <Typography variant="h6">Card Title</Typography>
    <Typography variant="body2">Card content</Typography>
  </CardContent>
</Card>

Best Practices

DO ✅

  • ✅ Use theme variables (theme.palette.*, theme.spacing(), theme.typography.*)
  • ✅ Use withStyles HOC for component styles
  • ✅ Use theme.palette.mode for light/dark conditional styling
  • ✅ Import Material-UI color palettes for extended colors
  • ✅ Keep styles co-located with components
  • ✅ Test in both light and dark themes
  • ✅ Use responsive breakpoints for mobile compatibility
  • ✅ Follow accessibility guidelines (WCAG 2.1)

DON’T ❌

  • ❌ Hardcode colors or spacing values
  • ❌ Create global CSS files
  • ❌ Duplicate style definitions
  • ❌ Use inline styles for complex patterns
  • ❌ Ignore theme mode (always test both light/dark)
  • ❌ Use !important unless absolutely necessary
  • ❌ Override Material-UI component internals

Mobile Compatibility

MQTT Explorer targets Google Pixel 6 (412x915px viewport) for mobile optimization:

Touch-Friendly Targets

const styles = (theme: Theme) => ({
  button: {
    minHeight: '44px',  // Minimum tap target size
    minWidth: '44px',
  },
})

Responsive Layouts

const styles = (theme: Theme) => ({
  container: {
    flexDirection: 'column',
    [theme.breakpoints.up('sm')]: {
      flexDirection: 'row',
    },
  },
})
See MOBILE_COMPATIBILITY.md for mobile strategy.

Testing Styles

Visual Regression Testing

# Generate screenshots
yarn build
yarn test:demo-video

# Compare with baseline
# (Manual process - compare screenshots)

Theme Testing

import { renderWithProviders } from '../../utils/spec/testUtils'
import { createTheme } from '@mui/material/styles'

it('should render in dark theme', () => {
  const darkTheme = createTheme({ palette: { mode: 'dark' } })
  const { container } = renderWithProviders(<MyComponent />, {
    theme: darkTheme,
  })
  expect(container).to.exist
})

Accessibility Testing

  • Contrast ratio: Check WCAG 2.1 AA (4.5:1 for normal text)
  • Focus indicators: Visible focus states for keyboard navigation
  • Touch targets: Minimum 44x44px for mobile
Use browser DevTools accessibility inspector to validate ARIA labels and contrast ratios.

Resources

Material-UI Docs

Official Material-UI documentation

Color System

Material-UI color customization guide

Theming Guide

Complete theming documentation

Component API

Material-UI component API reference

Examples

Complete Component with Styles

import React from 'react'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { Card, CardContent, Typography, Button } from '@mui/material'
import { blueGrey } from '@mui/material/colors'

const styles = (theme: Theme) => ({
  card: {
    backgroundColor: theme.palette.background.paper,
    borderRadius: theme.shape.borderRadius,
    padding: theme.spacing(2),
    marginBottom: theme.spacing(2),
    '&:hover': {
      backgroundColor: theme.palette.mode === 'light'
        ? blueGrey[50]
        : theme.palette.primary.dark,
    },
  },
  title: {
    color: theme.palette.text.primary,
    fontSize: theme.typography.pxToRem(18),
    fontWeight: 'bold',
    marginBottom: theme.spacing(1),
  },
  content: {
    color: theme.palette.text.secondary,
    fontSize: theme.typography.pxToRem(14),
  },
  button: {
    marginTop: theme.spacing(2),
  },
})

interface Props {
  classes: any
  title: string
  content: string
  onAction: () => void
}

function InfoCard({ classes, title, content, onAction }: Props) {
  return (
    <Card className={classes.card}>
      <CardContent>
        <Typography className={classes.title}>{title}</Typography>
        <Typography className={classes.content}>{content}</Typography>
        <Button
          variant="contained"
          color="primary"
          className={classes.button}
          onClick={onAction}
        >
          Learn More
        </Button>
      </CardContent>
    </Card>
  )
}

export default withStyles(styles)(InfoCard)
This example demonstrates:
  • Theme variable usage
  • Spacing system
  • Typography
  • Hover states
  • Theme-conditional colors
  • Material-UI component integration

Build docs developers (and LLMs) love