Mantine React Component Development Skill
When to Use This Skill
Apply this skill when working on:
- •Component Development: Creating or editing React components built on Mantine's factory pattern
- •TypeScript Patterns: Implementing polymorphic components, type-safe props, and CSS variables
- •Styles API: Configuring component styling through Mantine's Styles API system (selectors, vars, modifiers)
- •Component Composition: Building compound components with static sub-components (e.g.,
Component.Target) - •Context Management: Implementing safe context patterns for component state sharing
- •Accessibility: Ensuring ARIA compliance, keyboard navigation, and focus management
- •Code Review: Maintaining consistency with established patterns and conventions
- •Documentation: Writing component demos, MDX docs, and API references
Project Structure
Components are organized in a monorepo workspace structure:
- •
/package/src/: Main component source code- •
ComponentName.tsx: Main component implementation - •
ComponentName.module.css: Component-scoped styles - •
ComponentName.context.ts: Context providers (if needed) - •
ComponentName.errors.ts: Error messages - •
ComponentName.test.tsx: Jest + Testing Library tests - •
ComponentName.story.tsx: Storybook stories - •
SubComponent/SubComponent.tsx: Sub-components in their own folders - •
index.ts: Public exports
- •
- •
/docs/: Next.js documentation site- •
demos/ComponentName.demo.*.tsx: Interactive demos - •
styles-api/ComponentName.styles-api.ts: Styles API metadata - •
docs.mdx: Main documentation page
- •
Refer to existing components in ./package/src/ for implementation examples.
TypeScript Patterns
Component Factory Pattern
All components use Mantine's polymorphic factory pattern.
import { polymorphicFactory, PolymorphicFactory, useProps, useStyles, createVarsResolver } from '@mantine/core';
export type ComponentStylesNames = 'root' | 'element';
export type ComponentCssVariables = {
root: '--custom-var' | '--another-var';
};
export interface ComponentProps extends BoxProps, StylesApiProps<ComponentFactory> {
/** Prop description */
customProp?: string;
}
export type ComponentFactory = PolymorphicFactory<{
props: ComponentProps;
defaultComponent: 'div';
defaultRef: HTMLDivElement;
stylesNames: ComponentStylesNames;
vars: ComponentCssVariables;
staticComponents: {
SubComponent: typeof SubComponent;
};
}>;
const defaultProps: Partial<ComponentProps> = {
customProp: 'default',
};
const varsResolver = createVarsResolver<ComponentFactory>((_, { customProp }) => ({
root: {
'--custom-var': customProp,
},
}));
export const Component = polymorphicFactory<ComponentFactory>((_props, ref) => {
const props = useProps('Component', defaultProps, _props);
const { classNames, className, style, styles, unstyled, vars, customProp, ...others } = props;
const getStyles = useStyles<ComponentFactory>({
name: 'Component',
classes,
props,
className,
style,
classNames,
styles,
unstyled,
vars,
varsResolver,
});
return (
<Box ref={ref} {...getStyles('root')} {...others}>
{/* component content */}
</Box>
);
});
Component.displayName = '@your-scope/Component';
Component.SubComponent = SubComponent;
Key Requirements:
- •Use
unknownfor unconstrained generics; narrow with type guards - •Avoid
any; useReact.ReactNodefor children - •Define explicit type aliases for style names and CSS variables
- •Use
usePropshook for default prop merging - •Implement
varsResolverfor CSS custom properties
Context Pattern
For components requiring state sharing, use Mantine's safe context pattern.
import { createSafeContext } from '@mantine/core';
import { COMPONENT_ERRORS } from './Component.errors';
interface ComponentContext {
state: boolean;
setState: (value: boolean) => void;
}
export const [ComponentContextProvider, useComponentContext] = createSafeContext<ComponentContext>(
COMPONENT_ERRORS.context
);
Error Definitions in Component.errors.ts:
export const COMPONENT_ERRORS = {
context: 'Component was not found in the tree',
validation: 'Specific validation message',
};
Sub-Component Pattern
Sub-components access parent context and enforce constraints.
import { forwardRef, useProps, isElement, createEventHandler } from '@mantine/core';
import { useComponentContext } from '../Component.context';
export interface SubComponentProps {
children: React.ReactNode;
refProp?: string;
}
export const SubComponent = forwardRef<HTMLElement, SubComponentProps>((props, ref) => {
const { children, ...others } = useProps('SubComponent', {}, props);
if (!isElement(children)) {
throw new Error(COMPONENT_ERRORS.children);
}
const ctx = useComponentContext();
const onClick = createEventHandler(children.props.onClick, () => ctx.toggle());
return cloneElement(children, { onClick, ref });
});
Styles API
Every component exposes a Styles API for customization. Define in ./docs/styles-api/Component.styles-api.ts:
import type { ComponentFactory } from '@your-scope/component';
import type { StylesApiData } from '../components/styles-api.types';
export const ComponentStylesApi: StylesApiData<ComponentFactory> = {
selectors: {
root: 'Root element',
element: 'Specific child element',
},
vars: {
root: {
'--custom-var': 'Controls custom behavior',
'--another-var': 'Controls another aspect',
},
},
modifiers: [
{ modifier: 'data-active', selector: 'root', condition: '`active` prop is set' }
],
};
CSS Module (Component.module.css):
.root {
/* Use CSS custom properties from varsResolver */
property: var(--custom-var, fallback);
}
.element {
/* Scoped class name */
}
/* Data attribute modifiers */
.root[data-active] {
/* Active state */
}
Component Guidelines
Controlled vs Uncontrolled State
Use @mantine/hooks useUncontrolled for dual-mode state:
import { useUncontrolled } from '@mantine/hooks';
const [value, setValue] = useUncontrolled({
value: props.value,
defaultValue: props.defaultValue,
finalValue: undefined,
onChange: props.onChange,
});
Lifecycle Hooks
Use useDidUpdate from @mantine/hooks for effect-on-update patterns:
import { useDidUpdate } from '@mantine/hooks';
useDidUpdate(() => {
props.onStateChange?.(state);
}, [state]);
Props Validation
Validate props at runtime when type system isn't enough:
if (React.Children.count(children) !== 2) {
throw new Error('Component requires exactly two children');
}
Ref Forwarding
Always support ref forwarding for component composition:
export const Component = polymorphicFactory<ComponentFactory>((_props, ref) => {
return <Box ref={ref} {...others} />;
});
Coding Standards
Import Order
Prettier auto-sorts imports per ./.prettierrc.mjs:
- •CSS imports
- •React
- •Next.js (if applicable)
- •Built-in modules
- •Third-party modules
- •
@mantine/*packages - •Local imports (parent then sibling)
- •CSS modules last
Formatting
- •Print Width: 100 characters
- •Quotes: Single quotes
- •Trailing Commas: ES5-style
- •MDX: 70 character print width
Run npm run prettier:write before committing.
CSS Coding Standards
Naming Conventions:
- •Use kebab-case for all CSS identifiers:
- •Class names:
.component-name,.element-class - •Custom properties:
--variable-name,--led-color - •Keyframe names:
@keyframes slide-in,@keyframes led-pulse
- •Class names:
- •Never use camelCase or PascalCase in CSS (e.g.,
ledPulseis invalid, useled-pulse)
Property Order:
- •Avoid using CSS shorthand properties (like
background) after their longhand equivalents (likebackground-color) - •This causes the shorthand to override the longhand, leading to lint errors
- •Example of what NOT to do:
css
.element { background-color: red; /* This gets overridden */ background: blue; /* ERROR: shorthand overrides longhand */ } - •Instead, use only shorthand OR only longhand:
css
.element { background: blue; /* Correct: use shorthand only */ }
Validation:
- •Run
npm run lintto catch CSS linting errors - •stylelint enforces:
- •
declaration-block-no-shorthand-property-overrides: Prevents shorthand/longhand conflicts - •
keyframes-name-pattern: Enforces kebab-case for animation names
- •
Linting
ESLint config extends eslint-config-mantine:
import mantine from 'eslint-config-mantine';
import tseslint from 'typescript-eslint';
export default tseslint.config(...mantine, {
ignores: ['**/.next/**', '**/*.{mjs,cjs,js,d.ts,d.mts}']
});
Run npm run lint to check all rules.
TypeScript Configuration
Primary config (./tsconfig.json):
- •Target: ES2015
- •Module: ESNext with Node resolution
- •JSX: React (classic runtime)
- •Strict: Enabled (via mantine config)
- •Skip Lib Check: true (for faster builds)
Build config (tsconfig.build.json) isolates compilation scope.
Testing
Use @mantine-tests/core renderer with Testing Library.
import React from 'react';
import { render } from '@mantine-tests/core';
import { Component } from './Component';
describe('Component', () => {
it('renders without crashing', () => {
const { container } = render(<Component>Content</Component>);
expect(container).toBeTruthy();
});
it('applies custom className', () => {
const { container } = render(<Component className="custom" />);
expect(container.querySelector('.custom')).toBeTruthy();
});
it('forwards ref', () => {
const ref = React.createRef<HTMLDivElement>();
render(<Component ref={ref} />);
expect(ref.current).toBeTruthy();
});
});
Run tests with npm run jest.
Documentation
Demos
Create interactive demos in ./docs/demos/:
// Component.demo.basic.tsx
import { Component } from '@your-scope/component';
import { MantineDemo } from '@docs/components';
const code = `
import { Component } from '@your-scope/component';
function Demo() {
return <Component>Content</Component>;
}
`;
export const basic: MantineDemo = {
type: 'code',
component: Demo,
code,
};
Configurator demos use MantineDemo type 'configurator' with controls object.
MDX Pages
Main docs page at ./docs/docs.mdx:
import { InstallScript } from './components/InstallScript/InstallScript';
import * as demos from './demos';
## Installation
<InstallScript packages="@your-scope/component" />
## Usage
<Demo data={demos.basic} />
## Props
<PropsTable component="Component" />
## Styles API
<StylesApiTable component="Component" />
Refer to existing documentation structure in /docs for consistency.
Accessibility
ARIA Patterns
- •Use semantic HTML elements (
<button>,<nav>, etc.) over<div>with roles - •Add ARIA attributes only when semantic HTML is insufficient
- •Reference MDN ARIA practices: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
Keyboard Navigation
- •Ensure all interactive elements are keyboard-accessible
- •Use
tabIndex={-1}for programmatic focus management - •Implement arrow-key navigation for composite widgets
Focus Management
import { useFocusTrap } from '@mantine/hooks';
const focusTrapRef = useFocusTrap(active);
Data Attributes for State
Expose component state via data attributes for CSS and screen readers:
<div data-active={isActive} data-disabled={disabled} />
Security and Maintenance
No Hardcoded Secrets
- •Never commit API keys, tokens, or credentials
- •Use environment variables for sensitive configuration
- •Exclude
.envfiles in.gitignore
Dependency Hygiene
- •Keep
@mantine/coreand@mantine/hooksversions in sync (see./package/package.json) - •Use
peerDependenciesfor React and Mantine packages - •Run
npm run syncpackto verify version consistency
Least Privilege
- •Minimize scope of React Context providers
- •Avoid global state; prefer component-level state
- •Use
createSafeContextto enforce provider boundaries
Edge Cases and Troubleshooting
Module Resolution
- •Ensure all imports use relative paths for local modules
- •Check
package.jsonexportsfield for correct entry points - •Verify
tsconfig.jsonpathsif using aliases
CSS Modules
- •Class names are auto-scoped via
hash-css-selector(see./rollup.config.mjs) - •Import CSS modules as
import classes from './Component.module.css' - •Use
getStyleshelper for className merging
Polymorphic Components
- •Default component is
div; override withcomponentprop - •Ref type must match
defaultRefin factory definition - •Props are merged:
BoxProps & ComponentProps & { component?: any }
Testing Library Queries
- •Prefer
getByRoleovergetByTestId - •Use
screenfor global queries orcontainerfor scoped - •Mock
window.matchMediaif needed (see./jsdom.mocks.cjs)
Storybook Integration
- •Stories go in
Component.story.tsxalongside component - •Use CSF3 format with
MetaandStoryObjtypes - •Stories auto-generate docs from TypeScript types
File Organization
Maintain consistent file structure per component:
package/src/ ├── Component.tsx # Main component ├── Component.module.css # Scoped styles ├── Component.context.ts # Context (if needed) ├── Component.errors.ts # Error constants ├── Component.test.tsx # Tests ├── Component.story.tsx # Storybook ├── SubComponent/ │ └── SubComponent.tsx # Sub-component └── index.ts # Public API
Public API (index.ts):
export { Component } from './Component';
export type { ComponentProps, ComponentFactory } from './Component';
Commit Hygiene
- •Run
npm run testbefore committing (includes prettier, typecheck, lint, jest) - •Write clear commit messages:
fix: resolve prop merging issueorfeat: add keyboard navigation - •Avoid committing generated files (
dist/,.next/,out/) - •Use
.gitignoreto exclude build artifacts
References
See ./references/ for indexed documentation pointers.