Skip to main content

Creating customizable Admin components

MUI components support multiple ways of customization. You can override the styles of individual component instances or style them globally using the theme. You can also use the theme to override those components' default props.

All Comet Admin components should also support these features.

Those features should be usable as described in Customization and Styling.

Basics

Generally, a Comet Admin component should export the following in the index.ts file:

  • The component itself, with a unique name
  • The props type, named as the component, followed by Props
  • The class key type, named as the component, followed by ClassKey
export { MyComponent, MyComponentProps, MyComponentClassKey } from "./src/MyComponent";

Class keys

A component's ClassKey type defines all the individual elements (or slots) of a component and all modifiers/alternative styles of a component.

The outermost slot of a component is generally called root.

export type MyComponentClassKey = "root" | "title";

Slots

Each element or subcomponent of a Comet Admin component is defined as a slot.

A slot is created and styled by using createComponentSlot() and passing in the HTML element or component you want to base your slot on.

Additionally, the component's name must be passed in, as well as the slot's name, as defined in the class key type.

const Root = createComponentSlot("div")<MyComponentClassKey>({
componentName: "MyComponent",
slotName: "root",
})(
({ theme }) => css`
background-color: ${theme.palette.primary.main};
`,
);

const Title = createComponentSlot(Typography)<MyComponentClassKey>({
componentName: "MyComponent",
slotName: "title",
})(css`
color: lime;
`);

Defining the props type

The props type of Comet Admin components must extend ThemedComponentBaseProps. As a generic, an object must be passed in with the slot's name as the keys and the type of their base element or component as values.

export interface MyComponentProps
extends ThemedComponentBaseProps<{
root: "div";
title: typeof Typography;
}> {
variant?: "primary" | "secondary";
children?: React.ReactNode;
}

Using props

Props should not be used directly but through the useThemeProps hook instead. This ensures that the component's defaultProps from the theme are merged with the passed-in props.

Note that the name passed to the useThemeProps hook must be the same as the component's name, prefixed with CometAdmin.

export function MyComponent(inProps: MyComponentProps) {
const { children, variant } = useThemeProps({ props: inProps, name: "CometAdminMyComponent" });
return (
// ...
)
}

slotProps and restProps

To allow overriding the props of each slot, the slotProps object should be spread into each slot. Additionally, the sx and className props should be passed into the root slot, for example with ...restProps.

slotProps and restProps should typically be spread after other props to allow overriding of the slot's hardcoded props. Exceptions would be for props that should never be overridden.

export function MyComponent(inProps: MyComponentProps) {
const { slotProps, ...restProps } = useThemeProps({
props: inProps,
name: "CometAdminMyComponent",
});
return (
<Root {...slotProps?.root} {...restProps}>
<Title {...slotProps?.title} />
</Root>
);
}

Owner state

The ownerState is an object that includes all values that can impact the component's styling. It is passed into every slot that requires any of these values for its styling.

type OwnerState = {
highlighted: boolean;
};

const Root = createComponentSlot("div")<MyComponentClassKey, OwnerState>({
componentName: "MyComponent",
slotName: "root",
})(
({ theme, ownerState }) => css`
${ownerState.highlighted &&
css`
background-color: ${theme.palette.primary.main};
`}
`,
);

export function MyComponent(inProps: MyComponentProps) {
// ...
const ownerState: OwnerState = {
highlighted: getHighlightedState(),
};

return <Root ownerState={ownerState} {...slotProps?.root} {...restProps} />;
}

A slot's classesResolver

By default, a slot's class name consists of the component and slot names, prefixed with CometAdmin. For example, the root slot of MyComponent would have the class name CometAdminMyComponent-root, while the title slot would have the class name CometAdminMyComponent-title.

Additional class names can be added to a slot by returning an array of class keys in classesResolver(). This allows easy customization using the resulting class name in a selector outside the component.

In the following example, a component has a conditional highlighted state. If true, the class name CometAdminMyComponent-highlighted is added to the root slot by returning the highlighted class key in classesResolver. If false, the classesResolver does not return the class key, so no additional class name is added.

export type MyComponentClassKey = "root" | "highlighted";

type OwnerState = {
highlighted: boolean;
};

const Root = createComponentSlot("div")<MyComponentClassKey, OwnerState>({
componentName: "MyComponent",
slotName: "root",
classesResolver(ownerState) {
return [ownerState.highlighted && "highlighted"];
},
})(css`
// ...
`);

Overridable icons

When using icons inside a component, make sure they can be replaced by custom icons when using the component. Generally, this is done by defining an iconMapping prop as an object, for which the key clearly defines what the icon is used for.

export interface MyComponentProps {
// ...
iconMapping?: {
fullscreenButton?: React.ReactNode;
closeDialog?: React.ReactNode;
};
}

The icons can then be used by destructuring them, renaming them with Icon as a suffix, and setting a default icon.

export function MyComponent(inProps: MyComponentProps) {
const { iconMapping = {}, ...restProps } = useThemeProps({
props: inProps,
name: "CometAdminMyComponent",
});
const {
fullscreenButton: fullscreenButtonIcon = <Expand />,
closeDialog: closeDialogIcon = <Close color="inherit" />,
} = iconMapping;

return (
<>
<Button startIcon={fullscreenButtonIcon}>Fullscreen</Button>
<Dialog {...}>
<IconButton>{closeDialogIcon}</IconButton>
{/* ... */}
</Dialog>
</>
);
}

Adding the component to MUI's theme type

To allow setting the components defaultProps and styleOverrides in the theme, the component must be added to the ComponentsPropsList, ComponentNameToClassKey, and Components interfaces. The key should be the component name, prefixed with CometAdmin.

Note that the defaultProps inside the Components interface should be a Partial<> of the ComponentsPropsList type. This is because defaultProps are optional changes to the component's props, so nothing is required to be passed in.

declare module "@mui/material/styles" {
interface ComponentsPropsList {
CometAdminMyComponent: MyComponentProps;
}

interface ComponentNameToClassKey {
CometAdminMyComponent: MyComponentClassKey;
}

interface Components {
CometAdminMyComponent?: {
defaultProps?: Partial<ComponentsPropsList["CometAdminMyComponent"]>;
styleOverrides?: ComponentsOverrides<Theme>["CometAdminMyComponent"];
};
}
}