Skip to content

Theme System

The util-global-theme library provides Material UI theme configuration and utilities.

Overview

The theme system provides:

  • Light and dark mode support
  • High contrast accessibility theme
  • Cookie-based theme persistence
  • Server-side theme detection (Next.js)
  • Material UI integration

Using the Theme

ThemeProvider Setup

The theme is configured in the root layout:

apps/petfolio-business/src/app/layout.tsx
import { ThemeProvider } from '@util-global-theme';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Accessing Theme

Use the useTheme hook in components:

import { useTheme } from '@util-global-theme';

export function ThemedComponent() {
  const theme = useTheme();

  return (
    <div style={{ backgroundColor: theme.palette.primary.main }}>
      Themed content
    </div>
  );
}

Available Themes

Default Theme

Standard light/dark mode theme:

{
  palette: {
    primary: {
      main: '#1976d2',  // Blue
      light: '#42a5f5',
      dark: '#1565c0',
    },
    secondary: {
      main: '#dc004e',  // Pink
      light: '#f73378',
      dark: '#9a0036',
    },
  },
}

High Contrast Theme

Accessibility-focused theme with higher contrast ratios:

{
  palette: {
    primary: {
      main: '#000000',  // Black
    },
    secondary: {
      main: '#ffffff',  // White
    },
  },
}

Theme Switching

Client-Side Switching

import { useTheme } from '@util-global-theme';

export function ThemeToggle() {
  const { mode, toggleColorMode } = useTheme();

  return (
    <button onClick={toggleColorMode}>
      Switch to {mode === 'light' ? 'dark' : 'light'} mode
    </button>
  );
}

Theme preference is stored in the active-theme cookie:

active-theme=dark; Max-Age=31536000; Path=/

This allows server-side rendering with the correct theme.

Theme Structure

graph TD
    A[ThemeProvider] --> B{Theme Mode}
    B -->|light| C[Light Palette]
    B -->|dark| D[Dark Palette]
    B -->|high-contrast| E[High Contrast Palette]

    C --> F[MUI Theme]
    D --> F
    E --> F

    F --> G[Components]

    style A fill:#4fc3f7
    style F fill:#81c784
    style G fill:#ffb74d

Customizing Colors

Palette Customization

libs/util-global-theme/src/lib/themes/custom.ts
import { createTheme } from '@mui/material/styles';

export const customTheme = createTheme({
    palette: {
        primary: {
            main: '#6200ea', // Deep purple
        },
        secondary: {
            main: '#00bfa5', // Teal
        },
        background: {
            default: '#f5f5f5',
            paper: '#ffffff',
        },
    },
});

Typography Customization

export const customTheme = createTheme({
    typography: {
        fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
        h1: {
            fontSize: '2.5rem',
            fontWeight: 500,
        },
        body1: {
            fontSize: '1rem',
            lineHeight: 1.5,
        },
    },
});

Using MUI Components

Components automatically use the theme:

import { Button, Typography } from '@mui/material';

export function Example() {
  return (
    <>
      <Typography variant="h1" color="primary">
        Themed Heading
      </Typography>
      <Button variant="contained" color="secondary">
        Themed Button
      </Button>
    </>
  );
}

Emotion Styled Components

Use Emotion with theme:

import styled from '@emotion/styled';
import { Theme } from '@mui/material/styles';

interface CardProps {
    theme?: Theme;
}

export const Card = styled.div<CardProps>`
    background-color: ${({ theme }) => theme.palette.background.paper};
    color: ${({ theme }) => theme.palette.text.primary};
    padding: ${({ theme }) => theme.spacing(2)};
    border-radius: ${({ theme }) => theme.shape.borderRadius}px;
    box-shadow: ${({ theme }) => theme.shadows[2]};
`;

Responsive Design

Use theme breakpoints:

import { useTheme, useMediaQuery } from '@mui/material';

export function ResponsiveComponent() {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

  return (
    <div>
      {isMobile ? 'Mobile View' : 'Desktop View'}
    </div>
  );
}

Spacing System

Use theme spacing for consistency:

import { Box } from '@mui/material';

export function SpacedComponent() {
  return (
    <Box sx={{
      margin: 2,        // theme.spacing(2) = 16px
      padding: { xs: 1, md: 3 },  // Responsive spacing
    }}>
      Content
    </Box>
  );
}

Theme Utilities

Color Utilities

import { alpha, darken, lighten } from '@mui/material/styles';

const semiTransparent = alpha('#1976d2', 0.5); // 50% opacity
const darker = darken('#1976d2', 0.2); // 20% darker
const lighter = lighten('#1976d2', 0.2); // 20% lighter

Breakpoint Utilities

import { useTheme } from '@mui/material';

export function Example() {
  const theme = useTheme();

  const styles = {
    [theme.breakpoints.up('md')]: {
      fontSize: '1.5rem',
    },
    [theme.breakpoints.down('sm')]: {
      fontSize: '1rem',
    },
  };

  return <div css={styles}>Responsive text</div>;
}

Dark Mode Best Practices

Contrast Ratios

Ensure text is readable in both modes:

const theme = createTheme({
    palette: {
        mode: 'dark',
        primary: {
            main: '#90caf9', // Lighter blue for dark mode
        },
        text: {
            primary: '#ffffff',
            secondary: 'rgba(255, 255, 255, 0.7)',
        },
    },
});

Images and Icons

Adjust for dark mode:

export function Logo() {
  const theme = useTheme();
  const isDark = theme.palette.mode === 'dark';

  return (
    <img
      src={isDark ? '/logo-dark.svg' : '/logo-light.svg'}
      alt="Logo"
    />
  );
}

Accessibility

Color Contrast

Use MUI's built-in contrast helpers:

import { getContrastRatio } from '@mui/material/styles';

const ratio = getContrastRatio('#1976d2', '#ffffff');
// Should be >= 4.5:1 for normal text
// Should be >= 3:1 for large text

Focus Indicators

const theme = createTheme({
    components: {
        MuiButton: {
            styleOverrides: {
                root: {
                    '&:focus': {
                        outline: '2px solid',
                        outlineColor: theme.palette.primary.main,
                        outlineOffset: '2px',
                    },
                },
            },
        },
    },
});

Testing with Theme

Wrap components in tests:

import { ThemeProvider } from '@util-global-theme';
import { render } from '@testing-library/react';

const renderWithTheme = (component: React.ReactElement) => {
  return render(
    <ThemeProvider>
      {component}
    </ThemeProvider>
  );
};

test('renders with theme', () => {
  renderWithTheme(<MyComponent />);
});

Next Steps