Customize Material UI Components and Theme: A Modern Approach

Build an ultra-modular component library with Bit and Material UI

Eden Ella
Bits and Pieces

--

An ultra-modular library is not a library in the traditional sense but rather a collection of independent components. This kind of architecture offers numerous benefits.

Our “library” includes the following:

  1. Full type support
  2. Previews (similar to SB stories) and auto-documentation for every component
  3. A custom ‘create theme’ extended to include an additional typography variant
  4. A custom ‘default’ theme, ‘dark theme,’ ‘theme provider,’ and ‘dark mode toggle.’
  5. A ‘button’ component, a ‘typography’ component, and more
  6. A custom reusable ‘react development environment’
The components for the custom MUI library on Bit Platform

“Independent components” usually means Bit Components. That’s also the case in this blog, so make sure you have Bit installed and a Bit workspace initialized.

npx @teambit/bvm install
bit new basic my-workspace

Clone a full solution to your Bit workspace

To start quickly, fork (clone) the components in this scope to your own Bit workspace (make sure to create a Bit workspace and cd into it)

Replace MY_BIT_PLATFORM_ACCOUNT with your username or Bit organization name on the Bit Platform. Replace MY_SCOPE with your scope name. Create a new scope if you don’t have one already set up.

cd my-workspace
bit scope fork learnbit-react.custom-mui-lib MY_BIT_PLATFORM_ACCOUNT.MY_SCOPE

Run the following to explore the components in the Workspace UI:

bit start

Your workspace should now have all the components available to modify and export (push) to your own scope on Bit Platform.

bit tag -m "first version"
bit export

Let’s review the different components and understand the rationale behind their implementation.

Button

One principle that should guide you when customizing any third-party library is that you, the library's maintainer, should have full control over its interface and implementation.

You should be able to choose what can and cannot be customized by users. You should also be able to change your component’s implementation without introducing breaking changes to its interface.

/**
* @componentId: learnbit-react.custom-mui-lib/actions/button
* @filename: button.tsx
*/

import {
Button as BaseButton,
type ButtonProps as BaseButtonProps,
} from '@mui/material';

export type ButtonProps = {} & BaseButtonProps;

export function Button({ children, ...rest }: ButtonProps) {
return <BaseButton {...rest}>{children}</BaseButton>;
}

Component previews

The button component’s preview presents a few variations of it:

/**
* @componentId: learnbit-react.custom-mui-lib/actions/button
* @filename: button.compositions.tsx
*/

import { Button } from './button.js';

export const DefaultButton = () => {
return <Button>Click me</Button>;
};

export const ContainedButton = () => {
return <Button variant="contained">Click me</Button>;
};

export const OutlineddButton = () => {
return <Button variant="outlined">Click me</Button>;
};

Component documentation

Bit auto-generates the component’s API reference (see the ‘API Reference’ tab in the component page locally or on the Bit Platform). The auto-generated docs can be extended with manually written docs. This documentation uses a live playground to present different button usages.

/**
* @componentId: learnbit-react.custom-mui-lib/actions/button
* @filename: button.docs.mdx
*/

---
description: An MUI button
---

import { Button } from './button.js';

Provide the text you want to display inside the Button component:

```jsx live
() => <Button>Click me</Button>;
```

Independent versioning

Since our library is a collection of independent Bit components, changes to every component will be reflected in the component’s history log and semantic versioning ( MAJORE.MINOR.PATCH ).

An example of a new button component release:

bit tag actions/button -m "add custom docs" --patch
bit export

Typography

The previews for the typography components

Our typography component extends MUI’s default typography variations with an additional variation: handwriting.

Since this “library” is built to be modular and composable, we will not place type declarations ( d.ts) that support that extension in our project root, but rather, we’ll have the component responsible for this feature extend the default type. This ensures everything works smoothly, regardless of which specific project that uses these components.

/**
* @componentId: learnbit-react.custom-mui-lib/typography/typography
* @filename: create-theme.ts
*/

import type { CSSProperties } from 'react';
import {
Typography as BaseTypography,
type TypographyProps as TypographyPropsMUI,
type TypographyVariant as BaseTypographyVariant,
type TypographyVariantsOptions as BaseTypographyVariantsOptions,
} from '@mui/material';

/**
* the Typography component is extended with the 'handwriting' variant
*/

export type TypographyVariant = 'handwriting' | BaseTypographyVariant;

declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
handwriting: true;
}
}

export interface TypographyProps extends TypographyPropsMUI {
variant?: TypographyVariant;
}

export function Typography({ children, ...rest }: TypographyProps) {
return <BaseTypography {...rest}>{children}</BaseTypography>;
}

/**
* this interface can be used by the theme to define the typography variants
*/
export interface TypographyVariantsOptions
extends BaseTypographyVariantsOptions {
handwriting?: CSSProperties;
}

Create theme

Our custom ‘create theme’ component extends MUIs with additional theme options, in this case, a new typography variation. This would allow us (as well as others) to generate new themes according to our extended schema.

/**
* @componentId: learnbit-react.custom-mui-lib/theme/create-theme
* @filename: create-theme.ts
*/

import type { Theme } from "@mui/material";
import { createTheme as createThemeBase } from "@mui/material/styles";
import type { ThemeOptions } from "./theme-options.js";

export function createTheme(options: ThemeOptions, ...args: object[]): Theme {
return createThemeBase(options, ...args);
}

Our theme options are extended with the typography variations that our custom typography component offers.

/**
* @componentId: learnbit-react.custom-mui-lib/theme/create-theme
* @filename: theme-options.ts
*/

import type { ThemeOptions as BaseThemeOptions } from "@mui/material";
import type { TypographyVariantsOptions } from "@learnbit-react/custom-mui-lib.typography.typography";

/**
* extend the options of the theme with an additional typography variants
*/
export interface ThemeOptions extends BaseThemeOptions {
typography?: TypographyVariantsOptions;
}

Custom ‘default’ theme

Our custom theme uses our custom ‘create-theme’ component to generate a theme with additional properties (the new typography type):

/**
* @componentId: learnbit-react.custom-mui-lib/theme/default-theme
* @filename: default-theme.ts
*/

import {
createTheme,
type ThemeOptions,
} from "@learnbit-react/custom-mui-lib.theme.create-theme";
/** returns the `@import` statments that load our fonts */
import { getDefaultFonts } from "@learnbit-react/custom-mui-lib.typography.get-default-fonts";

export function defaultTheme(): ThemeOptions {
return createTheme({
components: {
MuiCssBaseline: {
/**
* global CSS overrides
* immediately load the default fonts
* */
styleOverrides: getDefaultFonts(),
},
},
palette: {
mode: "light",
primary: {
main: "#4d64a8",
// ...
},
typography: {
fontFamily: "Outfit, sans-serif",
/* this is our custom typography variant */
handwriting: {
fontFamily: "Handlee, cursive",
},
},
});
}

Custom ‘dark’ theme

The ‘dark’ theme consists of the values/design tokens that to be overridden in the theme it extends and customizes. In this case, the ‘default’ theme is extended by the ‘dark’ theme but the same pattern can be used to extend (and further extend) any theme to any theme flavor.

/**
* @componentId: learnbit-react.custom-mui-lib/theme/dark-theme
* @filename: dark-theme.ts
*/

/* import the theme to customize and extend */
import { defaultTheme } from "@learnbit-react/custom-mui-lib.theme.default-theme";
import {
createTheme,
type ThemeOptions,
} from "@learnbit-react/custom-mui-lib.theme.create-theme";

export function darkTheme(): ThemeOptions {
return createTheme(
/* the theme to extend */
defaultTheme(),
/* the custom values for this theme */
{
palette: {
type: "dark",
primary: {
main: "#6580f9",
},
// ...
},
});
}

Reusable React Development Environment

Bit components use reusable development environments (‘env’) that support their development with compilers, linters, testers, etc. This specific component development environment extends Bit’s out-of-the-box React development environment.

For our use case, we’re not required to change any of the default configurations. However, we would like to have our reusable env wrap every preview of our components with the theme to save us time manually setting the theme provider for every preview (as well as to ensure a standardized preview context).

/**
* @componentId: learnbit-react.custom-mui-lib/dev/react-mui
* @filename: preview/mounter.ts
*/

import React from 'react';
import { createMounter } from '@teambit/react.mounter';
/* import our custom theme provider */
import { ThemeProvider } from '@learnbit-react/custom-mui-lib.theme.theme-provider';

/**
* provide our component previews
* with the context they need to run.
* in this case, the custom MUI theme
*/
export function MyReactProvider({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}

We can set this env in our workspace.jsonc config file to ensure new components are created using the template provided by this env (as well as to auto-set them with this env as their env):

/**
* @filename: {workspace-root}/workspace.jsonc
*/

{
// ...
"teambit.generator/generator": {
"envs": [
/**
* MAKE SURE TO REPLACE `learnbit-react.custom-mui-lib`
* WITH YOUR OWN `BIT_CLOUD_ACCOUNT.SCOPE_NAME`
"learnbit-react.custom-mui-lib/dev/react-mui"
]
},
}

For example, this ‘slider’ component will be created using this dev environment:

$ bit create react actions/slider

The output should validate a component was created using the proper env:

1 component(s) were created

learnbit-react.custom-mui-lib/actions/slider
location: custom-mui-lib/actions/slider
env: learnbit-react.custom-mui-lib/dev/react-mui@0.0.4 (set by template)
package: @learnbit-react/custom-mui-lib.actions.slider

To learn more about design systems with Bit see:

https://bit.cloud/solutions/design-systems

--

--