Overview
Mantine provides infrastructure for building React apps efficiently, with pre-styled UI components and feature-rich utilities.
benefits
- solid UI defaults: neutral, minimalist, functional style
- easy-to-use, consistent API (DX)
- rapid development of utilitarian/productivity apps
mantine style is opinionated and cohesive
Mantine brings a set of cohesive style defaults: if we aim to do a partial style override, we should make it compatible with the Mantine overall look.
Layout components and hooks, on the other hand, can be used in any design.
mantine's packages
Mantine provides several packages. Mantine's core and hooks are the two main ones. Source code:
terminology: components and component variants
Mantine provides components, and for some of them, variants. For example, it provides the Button component, and for it, it provides the outline and filled variants.
terminology: inner-elements
Components are constructed through inner-elements, each with a name, the first being the root element.
For example, Slider is built on top of root (which happens to be a <div>), and various other inner-elements such as mark, track and bar. The point of having named inner-elements is for styling: we target inner-elements with their name.
In this document, and because the root element is special, we sometimes exclude it from the inner-elements category to treat it differently.
The component is polymorphic when we can change which HTML element is used as the component's root element (we set it in the component prop). For example, we can ask to use an anchor tag <a> for the root of a Button instead of <button> , when the action is really navigation.
customization routes
There are three main ways to customize style:
- Per-instance customization: the scope of the change is local to a single instance of a component.
- Component global override: override a Mantine component globally, affecting all instances.
- Global override of style primitives: override a style primitive, affecting all components (and instances) that depend on it.
shipped stylesheets
The stylesheets shipped on npm have been processed by PostCSS.
Mantine ships two kind of stylesheets:
- it ships a standalone 232kb stylesheet that includes style for all core elements.
- it also ships smaller stylesheets, focusing on one or more components.
Per-instance customization
per-instance customization (inline customization)
We want to customize a single instance of a component. We do so by settings props, classes or adding inline CSS:
<Button /* customize this very button */></Button>
props that work on all Mantine components
Mantine's style props are props that work on all Mantine components, and which set a single style aspect:
<Box m={4}></Box>
Note: The style targets the root element, with no granularity for other inner-elements.
props specific to one or more components
Some props are unique to one or more components. They either determine the style or add to the component's inner structure by toggling inner-elements. For example, TextInput accepts the label prop to add a <label> element to its inner-structure:
<TextInput label="Username"></TextInput>
// adds:
<label>Username</label>
target inner-elements with styles and classNames
We target inner-elements by specifying their name. For each inner-element, we pass either a style object or a set of classes, depending on the pattern picked:
<Button
styles={{ // styles pattern
root: { backgroundColor: 'red', height: 16},
label: { color: 'blue' },
}}
<Button
classNames={{
// classNames pattern
root: "pinkbutton-root",
label: "pinkbutton-label",
}}
/>
(fallback) undiscriminated styling with the style or className React props
When providing style with style or className, there is no granularity as to which inner-element we target: the style we provide applies to the component's root element, similar to style props.
class naming patterns (when not using CSS modules)
Since we target inner-elements:
- we add the inner-element's name to the class name,
- we namespace the class with the kind of variant we want to create, such as
.pinkbutton-:
.pinkbutton-root {
}
.pinkbutton-label {
}
Note: a cleaner approach is to a create a separate CSS module (PinkButton.module.css), having classes named purely based on inner-elements (such as .label), without a namespace (see CSS module patterns).
Global overrides
Global overrides, also called theming, can be done by:
-
overriding a component, affecting all instances across the app:
- We implement component-scoped pre-defined classes, such as
mantine-Button-root.
- We implement component-scoped pre-defined classes, such as
-
overriding style-primitives, affecting all components that depend on it:
- We edit theme properties in
createTheme({}) - or we edit Mantine's CSS variables directly
- We edit theme properties in
set theme properties
We create a custom theme. For example, we control font weights across the app with fontWeights. Under the hood, it sets the --mantine-font-weight-xxx CSS variables:
const theme = createTheme({
fontWeights: {
medium: "500",
},
})
set CSS variables directly
:root {
--mantine-font-weight-medium: 500;
}
implement component-scoped empty classes
Mantine adds empty classes to its components' inner-elements (including root). By implementing them, we override those inner-elements globally.
For example, Mantine adds mantine-Button-root to the Button's root element. We implement the class if needed:
.mantine-Button-root {
border-width: 2px;
}
component-scoped empty classes specificity
Empty classes have the same specificity than Mantine's internal classes. As such, we implement overrides in a stylesheet than we import after Mantine's one.
CSS Module patterns
CSS module overview
A CSS module is a regular stylesheet (CSS file) whose classes are subject to processing.
A processor parses classes in the stylesheet (such as superGreat) and derive unique names (such as superGreat_1kmox6oL39). It then produces a derived stylesheet using the unique class names:
.superGreat_1kmox6oL39 {
} /* post-processing */
the class map object
Since the developer only knows about the pre-processed class names (such as superGreat), the processor creates a JS object mapping the class names to their post-processed name (a class map object). The developer only interacts with this map. We often call it classes:
<div className={classes.superGreat}></div> // class="superGreat_1kmox6oL3"
When using a bundler that supports CSS modules, we import the map directly from the CSS module path:
import classes from "xxx.module.css"
Note: We avoid hyphens in class names, because it forces the use of raw strings in property lookups:
<div className={classes["super-great"]}></div> // superGreat_1kmox6oL3
Mantine patterns with CSS modules
targeting inner elements directly
In one CSS module, we target the inner-elements of a single Mantine component (virtually creating a variant of this component).
module name
As the module defines style for a custom variant, we name the module accordingly, e.g.: PrimaryButton.module.css.
name classes according to inner elements
We name classes according to the inner-element they target, such as root or label:
/* PrimaryButton.module.css */
.root {
}
.root:hover {
}
.label {
}
We then attach classes to the inner slots of classNames:
<Button
classNames={{
root: classes.root,
label: classes.label,
}}
>
Since the class map fields already take the name of the inner-elements, they cleanly match the API expected by classNames: as such, we provide the class map itself to classNames:
{/* cleaner */}
<Button classNames={classes}>
Style props
style props work on all Mantine components
Mantine's style props are props that work on all Mantine components. They set a single style aspect, and affect the root element, with no granularity for inner-elements:
<Box m={4}></Box>
immediate value or alias
When using a prop, we provide either:
- an immediate value, such as
4px, - or, for some props, an alias, such as
xl,smorindigo. The theme defines the resolved values. For example, the spacing aliases resolve to the following values by default:
// alias default values in theme:
spacing: {
xs: rem(10),
sm: rem(12),
md: rem(16), // i.e. 16px
lg: rem(20),
xl: rem(32),
},
Note: using an alias instead of an immediate value can make code less straightforward because the resolved value is not immediately clear.
responsiveness: provide alternative values
We can provide alternative values instead of a single one. This fits responsive designs, where mobile and desktop have different values:
<Flex
direction= {{ base: 'column',sm: 'row' }}
gap= {{ base: 'sm', sm: 'lg' }}
justify= {{ base: 'sm', sm: 'lg' }}
>
- base provides the default, mobile-centric value. Its exact scope depends on other breakpoints, but it always devices with screen under 576p width. For reference, all iPhones are under 450p width.
- The xs breakpoint activates at 576p. It targets phablets and bigger devices.
- The sm breakpoints activates at 768p (48em). It targets tablets and wider screens. For reference, iPads start at 768px.
list of style props
The resolved alias information only matters when we use aliases, such as sm or indigo.
margin
| Prop | CSS Property | resolved alias |
|---|---|---|
| m | margin | theme.spacing |
| mt | marginTop | - |
| mb | marginBottom | - |
| ml | marginLeft | - |
| mr | marginRight | - |
| mx | marginRight, marginLeft | - |
| my | marginTop, marginBottom | - |
padding
| Prop | CSS Property | resolved alias |
|---|---|---|
| p | padding | theme.spacing |
| pt | paddingTop | - |
| pb | paddingBottom | - |
| pl | paddingLeft | - |
| pr | paddingRight | - |
| px | paddingRight, paddingLeft | - |
| py | paddingTop, paddingBottom | - |
typography
| Prop | CSS Property | resolved alias |
|---|---|---|
| ff | fontFamily | |
| fz | fontSize | theme.fontSizes |
| fw | fontWeight | |
| lts | letterSpacing | |
| ta | textAlign | |
| lh | lineHeight | theme.lineHeights |
| fs | fontStyle | |
| tt | textTransform | |
| td | textDecoration | – |
size
| Prop | CSS Property | resolved alias |
|---|---|---|
| w | width | theme.spacing |
| miw | minWidth | - |
| maw | maxWidth | - |
| h | height | - |
| mih | minHeight | - |
| mah | maxHeight | - |
position
| Prop | CSS Property |
|---|---|
| pos | position |
| top | top |
| left | left |
| bottom | bottom |
| right | right |
| inset | inset |
other props
| Prop | CSS Property | resolved alias |
|---|---|---|
| display | display | |
| flex | flex | |
| bd | border | |
| bdrs | borderRadius | |
| bg | background | theme.colors |
| bgsz | backgroundSize | |
| bgp | backgroundPosition | |
| bgr | backgroundRepeat | |
| bga | backgroundAttachment | |
| c | color | theme.colors gray.5 blue |
| opacity | opacity |
Color scheme
Mantine defaults to a light color scheme regardless of the device's active color scheme.
We can change the default to dark or auto instead. auto conforms to the user's device color scheme:
<MantineProvider defaultColorScheme="auto"></MantineProvider>
Button case study
Button implementation
The Button source code includes the TypeScript file (Button.tsx) and the CSS stylesheet (Button.module.css).
HTML default structure (simplified)
- the root is a
<button>. - inner, section, label and loader are
<span>elements:- loader contains a loader, if any
- inner contains the label and sections
- the label contains the button's text.
- a section is a container for an icon, if applicable:
<button {/* root */}>
<span {/* 1.0 loader */} />
<span {/* 2.0 inner */}>
<span {/* 2.1 section (icons..) */}/>
<span {/* 2.2 label (button text) */}/>
</span>
</span>
</button>
Mantine adds its internal classes (e.g. m-77c9d27d) along with unimplemented empty classes (e.g. mantine-Button-root) to the inner-elements:
<button class="m-77c9d27d mantine-Button-root">
<span class="m-80f1301b mantine-Button-inner">
<span class="m-811560b9 mantine-Button-label">Save</span>
</span>
</button>
the stylesheet: a CSS module
The Button's CSS module stylesheet targets inner-elements with classes of the same name:
.loader {
position: absolute;
left: 50%;
top: 50%;
}
The .tsx file imports the classes object and plugs it to Button.classes.
(advanced) component-agnostic internal classes and data attributes
Mantine also adds component-agnostic internal classes, such as mantine-active and mantine-focus-auto. They are placed preemptively, and their style triggers on specific states. For example, mantine-active activates its style on the active state:
mantine-active:active {
transform: translateY(calc(0.0625rem));
}
Mantine can also add internal data-attributes such as data-centered="true" to set a specific style:
.m_a3c6e060[data-centered] {
display: flex;
justify-content: center;
align-items: center;
}
Default theme
The default theme covers multiple design areas and categories.
It defines some style primitives directly. It also defines some aliases (such as sm), and what value they resolve to.
spacing values
Spacing determines space between elements and space inside elements. It is used for:
- gap, for Group, Flex, and Stack.
- padding.
- margin, such as for Divider
- width and height props such as
wandh, such as for Space.
xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32p
radius values
The border radius is used for Paper, Dialog and Modal, Button and Tooltip.
"defaultRadius": "md" // changed in v9 from "sm" to "md"
xs: 2px
sm: 4px // former default
md: 8px // (d)
lg: 16px
xl: 32px
breakpoints
A set of thresholds, to activate style conditionally:
"breakpoints": {
"xs": "36em", // 576px
"sm": "48em", // 768px
"md": "62em",
"lg": "75em",
"xl": "88em"
},
The sm breakpoint targets tablets and wider devices.
We use those thresholds in styles props to provide alternative values.
font sizes
Those font sizes can be used in the fz style prop:
"fontSizes": {
"xs": 12px
"sm": 14px
"md": 16px,
"lg": 18px
"xl": 20px
},
headings have a different set of values.
weights
Note: medium was changed from 500 to 600 in Mantine v9.
"fontWeights": {
"regular": '400',
"medium": '600',
"bold": '700',
}
line heights
The aliases can be used in the lh style prop:
"lineHeights": {
xs: 1.4,
sm: 1.45,
md: 1.55, // (d)
lg: 1.6,
xl: 1.65
},
The default line height is 1.55:
--mantine-line-height: 1.55;
headings
headings: {
fontFamily: DEFAULT_FONT_FAMILY,
fontWeight: '700',
textWrap: 'wrap',
sizes: {
h1: { fontSize: rem(34), lineHeight: '1.3' }, // 34px
h2: { fontSize: rem(26), lineHeight: '1.35' },
h3: { fontSize: rem(22), lineHeight: '1.4' },
h4: { fontSize: rem(18), lineHeight: '1.45' },
h5: { fontSize: rem(16), lineHeight: '1.5' },
h6: { fontSize: rem(14), lineHeight: '1.5' },
},
},
Gray Palette
overview
Mantine's gray palette is specifically designed for light mode:
- It offers a large number of whites and light grays (6) for backgrounds. It starts at
#F8F9FA, not pure white. - It offers very few dark grays (3-4) for text. It doesn't include pure black.
It has a blue tint (slate gray).
values
| Token | Description | Hex Value |
|---|---|---|
gray.0 | white | #F8F9FA |
gray.1 | white | #F1F3F5 |
gray.2 | white gray | #E9ECEF |
gray.6 | dim gray (secondary) | #868e96 |
gray.7 | dim gray (secondary) | #495057 |
gray.8 | very dark gray | #343A40 |
gray.9 | very dark gray | #212529 |
use in backgrounds
-
Backgrounds can be pure white, such as the site's global background or controls' background.
-
Controls' background can go darker on hover. This is the case for buttons, and Select's options:
gray.1
use in text
The text is black-ish or gray-ish depending on its importance and role:
- gray 9 (readable black for body text)
- gray 8 (readable black)
- dark 7 (dimmed)
- gray 6 (gray) for secondary text, and input description
- pure black text (optional)
- some headings
- body text
- input label
- button label
Dark Palette
overview
Mantine's dark palette is designed for dark mode, and is used by the Mantine dark theme. It offers several darks and grays for backgrounds (6), and few whitish (2-3) for text.
It is a neutral gray palette, but used to be a slate gray palette (blue tint) before Mantine 7.3.
| Token | Look | Alias | Value | Value | White % |
|---|---|---|---|---|---|
dark.0 | whitish | text | #c9c9c9 | #c9 | 79% |
dark.1 | whitish | #b8b8b8 | #b8 | 72% | |
dark.2 | dimmed | dimmed | #828282 | #82 | 51% |
dark.3 | dimmed | placeholder | #696969 | #69 | 41% |
dark.4 | #424242 | #42 | 26% | ||
dark.5 | dark | default-hover | #3b3b3b | #3b | 23% |
dark.6 | dark | default | #2e2e2e | #2e | 18% |
dark.7 | dark | body | #242424 | #24 | 14% |
dark.8 | black | #1f1f1f | #1f | 12% | |
dark.9 | black | #141414 | #14 | 8% |
use in backgrounds
- The page's global background defaults to dark 7. We may go darker with dark 8.
- Controls have a lighter background: dark 6, and even lighter on hover: dark 5. Sometimes, hover goes to a darker background instead: such as for hovered options in a Select control: dark 8.
use in text
- The text uses light colors:
- pure white (bright white for some headings)
- dark 0 (readable white for body text)
- dark 1 (slightly dimmed)
- dark 2 (dimmed, secondary text, matches
c=dimmed)
- The Button label is pure white, both for filled and default variants.
- The inputs' labels are dark 0, the optional descriptions are dark 2, aka dimmed.
dimmedtext is dark 2. It's used for secondary text and labels.
v6 values (slate)
The v6 palette had a blue tint:
| Token | v6 value |
|---|---|
dark.0 | #C1C2C5 |
dark.1 | #A6A7AB |
dark.2 | #909296 |
dark.3 | #5c5f66 |
dark.4 | #373A40 |
dark.5 | #2C2E33 |
dark.6 | #25262b |
dark.7 | #1A1B1E |
dark.8 | #141517 |
dark.9 | #101113 |
revert to blue-tint dark palette
const theme = createTheme({
colors: {
dark: [
"#C1C2C5",
"#A6A7AB",
"#909296",
"#5c5f66",
"#373A40",
"#2C2E33",
"#25262b",
"#1A1B1E",
"#141517",
"#101113",
],
},
})
Color shades and aliases
overview
Mantine uses CSS variables to define color shades. CSS variables start with the --mantine-color prefix.
- Mantine defines 14 colors.
- Mantine defines 10 shades for each color, 0 to 9, following the pattern of
--mantine-color-dark-0. - It defines aliases, or semantic CSS variables, such as
--mantine-color-text, which resolve to a given shade, such as--mantine-color-dark-0.- The resolved value depends on the active color scheme:
darkorlight(see below).
- The resolved value depends on the active color scheme:
Some aliases are linked to a specific variant. For example, --mantine-color-default is linked to the default variant, and defines its background color.
dark color-scheme values
The aliases resolve to the following values:
| semantic suffixes | value | note |
|---|---|---|
| --mantine-color-bright | --mantine-color-white | text |
| -text | --mantine-color-dark-0 | text |
| -dimmed | --mantine-color-dark-2 | text |
| -placeholder | --mantine-color-dark-3 | text |
| -body | --mantine-color-dark-7 | background |
| -anchor | --mantine-color-blue-4 | link |
| -error | --mantine-color-red-8 | form error |
| -red-text | --mantine-color-red-4 | ? |
| -scheme | "dark" |
variant: "default" aliases
note: it comes with a border.
While the default for hover is dark-5, we can also use dark-4 or dark-6.
| semantic suffixes | value | note |
|---|---|---|
| default-color | --mantine-color-white | text |
| default | --mantine-color-dark-6 | background |
| default-hover | --mantine-color-dark-5 | background |
| default-border | --mantine-color-dark-4 | border |
color: "dark" aliases
it comes with values for several variants. It has no border.
| semantic suffixes | value |
|---|---|
| dark-text | --mantine-color-dark-4 |
| dark-filled | --mantine-color-dark-8 |
| dark-filled-hover | --mantine-color-dark-7 |
| dark-light | rgba(36, 36, 36, 0.15) |
| dark-light-hover | rgba(36, 36, 36, 0.2) |
| dark-light-color | --mantine-color-dark-3 |
| dark-outline | --mantine-color-dark-4 |
| dark-outline-hover | rgba(36, 36, 36, 0.05) |
color: "gray" aliases
it comes with values for several variants.
| suffix | value |
|---|---|
| gray-text | --mantine-color-gray-4 |
| gray-filled | --mantine-color-gray-8 |
| gray-filled-hover | --mantine-color-gray-9 |
| gray-light | rgba(134, 142, 150, 0.15) |
| gray-light-hover | rgba(134, 142, 150, 0.2) |
| gray-light-color | --mantine-color-gray-3 |
| gray-outline | --mantine-color-gray-4 |
| gray-outline-hover | rgba(206, 212, 218, 0.05) |
light color-scheme values
Flex
A bare-bones flex container, with nothing set besides display: flex.
override CSS defaults
We can override CSS defaults:
- Switch the
directionto vertical (column). It is horizontal (row) by default - Increase the
gap(defaults to 0) - Control
justify, aka main axis alignment, which default to flex-start. - Control
align, aka cross-axis alignment, which defaults to stretch.
Example of a responsive flex container, that is vertical in mobile:
<Flex
direction={{ base: "column", sm: "row" }}
gap={{ base: "sm", sm: "lg" }}
justify={{ base: "flex-start", sm: "space-between" }}
/>
Group
A layout component that layouts elements horizontally and adds space between them.
The container spans the whole line. It uses a horizontal flex under the hood.
Differences with a stock horizontal flex:
- It adds
gapbetween elements (defaults to md, aka 16px). - It centers elements vertically, with
align, instead of stretching them. - It wraps to the next line if needed (aka multi-line flex). We can revert to mono-line with (
wrap="nowrap"), which sets the underlying flex-wrap.
the grow variant (override)
- The
growvariant aims to fill the container horizontally by growing children:- It sets flex-grow: 1 on children.
- By default, it makes them same-width (by capping them to the same max-width, set to a share of the line, which in turns make them fit in one line).
- (advanced) If we turn off capping (set
preventGrowOverflowto false): elements can go to the next line if there is not enough room for all of them in the non-grown version. (do not use) - (advanced) If we add
nowrap, it's back a mono-line flex where children grow proportionally with their flex-basis (It's convoluted), cutting any overflow if the single line is not wide enough.
- (advanced) If we turn off capping (set
other overrides
- Align items horizontally with
justify. It defaults to flex-start (like default flexes).
Stack
Layout elements vertically and add space between them.
The container spans the whole line (because its width is auto). It uses a vertical flex under the hood. Items default to spread horizontally within the container.
Differences with a stock flex:
- It is vertical
- It sets a
gap(16px by default)
Other settings and their default:
justify: elements sit at the top by default (flex-start)align: elements fill the stack horizontally by default (stretch)
SimpleGrid
Set up a column-based layout with a given number of equal-width columns:
<SimpleGrid cols={3}>{/* items */}</SimpleGrid>
The grid container spans the whole line and the columns together span the whole line too.
The grid container adds space between tracks by default (16px), both horizontally and vertically, through spacing and verticalSpacing.
We set the number of columns with cols. It defaults to a single track. The best practice is to adapt the number of columns based on screen size:
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>{/* items */}</SimpleGrid>
Container
A wrapper that centers the content and keeps it aways from screen edges.
It can wrap the main column of a website, adding padding on mobile and centering on desktop.
grid version
The grid version uses a grid layout and centers stuff with grid positioning.
max-width version
The default version comes with a max-width (for desktop) and some padding (for mobile):
- It centers the content with margin: auto.
- The max-width defaults to md (aka 960px). We change it with
size. For example, we can pick 720px (sm) instead. - To remove indirection, we can also set max-width with
maw. - The default padding is 16px, aka md. We change it with the
pxstyle prop.
fluid version
We may opt for a fluid container, which expands horizontally, with the fluid prop. It only comes with some padding, at 16px.
Note: a Box with horizontal padding has the same effect than fluid.
Center
Center the child or the group of children horizontally and vertically.
Under the hood, it uses a flex container with justify and align to center the inner content.
vertical-centering and height
Vertical centering will only have an effect if Center's height is bigger than the inner-content.
width.
The container spreads horizontally (width: auto). It compacts its children to max-content, then centers them horizontally.
We can prevent compaction by setting the group of children to width: 100%. In this case, we lose horizontal centering.
Divider
Separate items or sections with a visible line, similar in essence to the <hr> element.
Under the hood, it is an empty element whose border acts as the visible line.
- The line is horizontal by default, we can set it to vertical with
orientation. - We set the line thickness with
size. It defaults to 1px. - The line is solid by default. We switch to dashed or dotted with
variant. - The line doesn't add margin around itself by default. We can add margin with my or mx.
- The line's color defaults to:
--mantine-color-dark-4: #424242;in dark mode (dark gray)--mantine-color-gray-3: #dee2e6;in light mode (light gray)
Space
Separate items with some white space. Mantine uses an empty element with some explicit size. It defaults to zero size. We set the height or the width:
- Set vertical spacing with
h, usually in a stack-like layout. - Set horizontal spacing with
w, usually in a horizontal flex-like layout.
Paper
Encapsulate some UI to set it apart visually.
The Paper's default background is the same as the body's default background, aka --mantine-color-body.
- In light mode, it is white #FFF.
- In dark mode, it is dark.7.
As such, we may have to override's Paper background with bg.
Paper also adds a radius (theme.defaultRadius) which defaults to 8px.
customizations
- Add a pre-styled border with
withBorder(boolean) - Add a pre-styled shadow with
shadow. We specify an alias (e.g.sm), or a CSS box-shadow literal.
Accordion
An accordion offers one or more items that can expand to disclose more content when interacted with.
item preview and panel disclosure
Accordion items start as clickable previews (Accordion.Control) and embed a panel, toggled on click.
Mantine mounts all panels immediately, regardless of their toggled state.
variants
By default, items are separated by a thin line (default). We can use other variants, such as separated.
behavior
By default, a single panel is displayed at a time. Clicking on another item closes the currently opened one. We can allow multiple concurrent panels with the multiple prop.
(misc) use a panel as a form
The panel may be a form. In that case, we can set the chevron as a plus sign, to signal we add some data.
We set the chevron style at the Accordion root element's level, since all items use it.
<Accordion variant="separated" chevron={<IconPlus />}>
<Accordion.Item value="item1">
<Accordion.Control>Log Workout</Accordion.Control>
<Accordion.Panel>Content</Accordion.Panel>
</Accordion.Item>
</Accordion>
Appshell
Appshell is a mega layout component that manages:
- The header on-top of the page, also called the banner.
- The main section, below the header
- The left-side navbar and the right-side aside (if applicable)
synopsis
<AppShell header={{}} navbar={{}} aside={{}} footer={{}}>
<AppShell.Header />
<AppShell.Navbar />
<AppShell.Aside />
<AppShell.Main />
<AppShell.Footer />
</AppShell>
Note: the configuration objects are set on AppShell as props. They are required when we use the corresponding component (except for AppShell.Main).
header
The header usually manages:
- the burger button
- the logo
- (optional) the dark mode toggle, the user button, a set of links.
<AppShell.Header>
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" />
</AppShell.Header>
configure the header
We commonly set the height:
<AppShell header={{ height: 60 }} /* ... */ />
navbar
<AppShell navbar={{}} /* ... */ />
The navbar stands on the left side. On mobile, it only appears in a transient fashion, and is collapsed by default. On desktop it is commonly always-on, though we sometimes offer to collapse it too.
We control the collapsing state with collapsed, and the breakpoint from mobile to desktop with breakpoint:
collapsedreceives up to two boolean state variables: one for mobile and one for desktop. Most of the time, we only use the mobile state variable, and keep desktop always opened.breakpointcontrols when the collapse logic switches from mobile to desktop, aka when to use the mobile opened state versus the desktop opened state.- The burger button is responsible for toggling the state on and off:
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
return (
<AppShell
navbar={{
collapsed: { mobile: !opened },
breakpoint: "sm",
}}
>
<AppShell.Header>
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" />
</AppShell.Header>
</AppShell>
)
We hide the mobile burger on desktop with hiddenFrom since it only controls the mobile state.
supporting collapse on desktop
If we support desktop collapse, we provide a separate burger button controlling the desktop state.
We use an additional boolean state variable, and provide it to collapsed.
Note: the desktop burger button commonly keeps the same appearance instead of showing a close icon, since we are not really closing anything, contrary to mobile.
We display it only on desktop with visibleFrom:
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(false)
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
return (
<AppShell
navbar={{
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
breakpoint: "sm",
}}
>
<AppShell.Header>
{/* mobile */}
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" />
{/* desktop */}
<Burger opened={false} onClick={toggleDesktop} visibleFrom="sm" />
</AppShell.Header>
</AppShell>
)
navbar: desktop width
In mobile, the opened navbar is always full screen.
On desktop, we control the width with width. We provide an immediate value or an object with two or more values:
navbar={{ width: 300, /* ... */ }}
navbar: top and bottom sections
We can split the navbar into AppShell.Sections. One of them can grow.
<AppShell.Navbar>
<AppShell.Section grow> </AppShell.Section>
<AppShell.Section> </AppShell.Section>
</AppShell.Navbar>
Appshell.Main
The container for the main panel.
Its background defaults to the body's background color (--mantine-color-body):
- In light mode, It is white. We can pick a light gray background such as gray.1, so that inner containers may stand out as white.
- In dark mode, it is dark.7. We can pick dark.8 so that inner elements may standout as dark.7.
const backgroundColor = colorScheme === "dark" ? "dark.8" : "gray.1"
Tabs
The tabs widget is primarily a navigation control, where we switch between sections.
We show a tab bar with Tabs.List:
<Tabs> {/* container */}
<Tabs.List> {/* header, horizontal or vertical */}
<Tabs.Tab> {/* tab */}
<Tabs.Tab> {/* tab */}
<Tabs.Tab> {/* tab */}
</Tabs.Tab>
<Tabs.Panel> {/* tab panel */}
<Tabs.Panel> {/* tab panel */}
<Tabs.Panel> {/* tab panel */}
uncontrolled mode
- We select the initial tab with
defaultValue. - Pairs of matching
Tabs.TabandTabs.Panelshare the samevalue. - (optional) We make the list mobile-friendly with
Scroller: we wrap the.Tabcomponents with it.
<Tabs defaultValue="first" color="dimmed">
<Tabs.List>
<Scroller>
<Tabs.Tab value="first">Proteins</Tabs.Tab>
<Tabs.Tab value="second">Workouts</Tabs.Tab>
</Scroller>
</Tabs.List>
<Tabs.Panel value="first">{/* Proteins */}</Tabs.Panel>
<Tabs.Panel value="second">{/* Workouts */}</Tabs.Panel>
</Tabs>
controlled mode
Note: In controlled mode, the selected tab derives from state. The state is subscribed to through value, while onChange handles tentative tab changes:
const [authMode, setAuthMode] = useState("login")
;<Tabs value={authMode} onChange={(v) => setAuthMode(v!)}>
<Tabs.Tab value="login">Login</Tabs.Tab>
<Tabs.Tab value="signup">Sign up</Tabs.Tab>
</Tabs>
Tabs.Panel are optional in control mode
A Tabs.Panel wraps its content as a div and doesn't bring style by default
When its value is not matched, it uses display:none to hide its content.
We technically don't need a Tabs.Panel: we can do the conditional display manually based on state, or use an always-on component whose data derives from the selected tab name (do not use).
panels default to lazy-mounted, and sticking around when unselected
A given panel mounts the first time it is selected.
-
By default, it doesn't unmount when unselected. It uses the React
<Activity>hidden feature to hide itself:- Its state is preserved.
- The Effects are destroyed, aka the effect cleanup functions run just the same as if the component had unmounted, and the effects run again when the tab is selected back. (the default is
keepMountedMode="activity")
-
We can ask a "real" unmount with
keepMounted={false}which adds thedisplay=noneHMTL attribute. -
The
keepMountedMode="display-none"option is special because, when combined with the defaultkeepMounted={true}, all tabs mount immediately (not lazily), and their effect all run at the same time. Since they stay mounted, the cleanup effect doesn't run when switching tabs and the one-time effect doesn't run when switching to a tab. They appear hidden only through CSS.
Tables introduction
abstract
A table works with an array of items, where each item is a row, and each item's property (or member) is a cell.
An item may come as an object or as an array. Mantine supports both:
the item as an object
const items = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
]
the item as an array
const items = [
[6, 12.011, "C", "Carbon"],
[7, 14.007, "N", "Nitrogen"],
[39, 88.906, "Y", "Yttrium"],
[56, 137.33, "Ba", "Barium"],
[58, 140.12, "Ce", "Cerium"],
]
Note: we can transform an array of objects to an array of arrays by mapping over it:
players.map((p) => [p.name, p.level, p.class])
headers
A table usually comes with a headers row. We store them in an array:
const headers = ["Element position", "Atomic mass", "Symbol", "Element name"]
Tables with explicit structure
overview
- The header uses Thead and Th
- The body uses Tbody and Td:
<Table>
{/* 1. header */}
<Table.Thead>
<Table.Tr>
<Table.Th>{col1Label}</Table.Th>
<Table.Th>{col2Label}</Table.Th>
<Table.Th>{col3Label}</Table.Th>
</Table.Tr>
</Table.Thead>
{/* 2. rows */}
<Table.Tbody>
<Table.Tr>
<Table.Td>{item1.name}</Table.Td>
<Table.Td>{item1.level}</Table.Td>
<Table.Td>{item1.class}</Table.Td>
</Table.Tr>
{/* ... more rows */}
</Table.Tbody>
</Table>
build rows in a separate variable
- from an array of object:
const rows = items.map((item) => (
<Table.Tr>
<Table.Td>{item.name}</Table.Td>
<Table.Td>{item.level}</Table.Td>
<Table.Td>{item.class}</Table.Td>
</Table.Tr>
))
- from an array of arrays:
const rows = items.map((item) => (
<Table.Tr>
<Table.Td>{item[0]}</Table.Td>
<Table.Td>{item[1]}</Table.Td>
<Table.Td>{item[2]}</Table.Td>
</Table.Tr>
))
We then use them in Tbody:
<Table.Tbody>{rows}</Table.Tbody>
Tables from data prop
In this version, the <Table> inner-markup is generated automatically, from the data we pass to the data prop:
<Table data={data} />
The data prop, of type TableData, mainly provides the head and body properties:
bodyprovides the rows: an array of arrays (an item is an array)headprovides the headers.- The
captionproperty defines the table's title
const data: TableData = {
caption: "Some elements from periodic table",
head: ["Element position", "Atomic mass", "Symbol", "Element name"], // headers
body: [
// items
[6, 12.011, "C", "Carbon"],
[7, 14.007, "N", "Nitrogen"],
[39, 88.906, "Y", "Yttrium"],
[56, 137.33, "Ba", "Barium"],
[58, 140.12, "Ce", "Cerium"],
],
}
TextInput
TextInput is an abstraction over <input type=text>. It inherits most props from Input. We describe both in this section.
controlled:
<TextInput
label="Summary"
placeholder="What did you work on?"
// state
value={summary}
onChange={(event) => setSummary(event.currentTarget.value)}
/>
uncontrolled:
<TextInput
label="Email"
placeholder="john-appple@example.com"
// ref
ref={mailInput}
/>
props
- Set the main label with
label. A label describes the input's purpose. - Add an optional secondary label with
description - Add some placeholder text with
placeholder. - Add icons, visually inside the input, with
leftSectionandrightSection. - Mark the input as
required. Mantine automatically adds an asterisk, unless we remove it withwithAsterisk. - shrink or enlarge the input with
size.
others
- Add
data-autofocusto focus on mount
NumberInput
NumberInput uses a <input type=text> under the hood: this allows Mantine to have more flexibility in displaying non-number characters compared to the HTML <input type=number>.
For example, Mantine can display a thousand separator, even a custom one.
Mantine's underlying managed value differs from what is displayed: Mantine tries to manage an underlying number every time it makes sense. The thousand separators are not part of the underlying managed value.
Yet, the managed value is not always a number: it is a string when Mantine cannot resolve it to a number without ambiguity:
- the input is empty: ""
- the input is a standalone negative sign: "-"
- the input's number is too big or too small to fit in as a JS number
- the input has a trailing decimal separator:
0. - the input has a trailing decimal zero, such as
0.00
Otherwise the value is a number.
As such, we work with a string and number type union:
// NumberInput managed value:
const [value, setValue] = useState<string | number>("")
return <NumberInput value={value} onChange={setValue} />
options and overrides
- forbid decimal numbers with
allowDecimal={false}. (They are allowed by default). - forbid negative numbers with
allowNegative={false}. (They are allowed by default.) - clamp the value in the range between
minandmax. We determine how the value should clamp withclampBehavior:- Clamp after input on
blur(after exiting the input). This is the default. - Forbid values outside the limits at all times with
strict. - Never clamp with
none
- Clamp after input on
other options (Input)
As an Input, it supports label, description (secondary label) and placeholder.
FileInput
FileInput is a control that triggers the OS dialog to pick a file.
On iOS, we first choose between taking a photo, opening the Photos library, or choosing a file from the Files app.
The control shows information about the current selection.
synopsis
We work with the selected file by providing a handler to onChange:
- The handler receives a File object (or a File array).
- The handler is called:
- when the user closes the platform's native File picker
- or when the user clears the selection.
<FileInput
onChange={onChange}
placeholder="Reference Image"
variant="unstyled"
leftSection={<IconPhoto size={18} stroke={1.5} />}
size="xs"
maw={150}
clearable
/>
main options
- Enable multiple file selection with
multiple. It defaults to false. - Restrict selection to a given file type with
accept, such as"image/png" - Add a clear button on the right with
clearable
other techniques
- Limit the input's max width to truncate long file names.
- Add a file icon to the leftSection.
- Opt for the unstyled variant which looks similar to a transparent variant: there is no border or background fill.
- Shrink the control with size.
Date and Time inputs
overview
setup
Mantine offers the date and time components in a separate package. We must import a separate stylesheet:
npm install @mantine/dates dayjs
import "@mantine/dates/styles.css"
date as strings
Mantine's date and time components natively work with date strings formatted as follows:
2026-04-24(ISO compliant)16:14:00pr16:14(ISO compliant)2026-04-24 16:14:00(sometimes called SQL format)
While full dates are represented with SQL format strings, the components are compatible with ISO date strings: we can store an ISO date string in state, initialize the component with it and update the component with it:
2026-04-24T13:10:22.592Z
In the following examples, we work with dates stored in state as ISO strings rather than SQL strings
luxon
In the following examples, we sometimes use Luxon's DateTime helper to create and/or format dates.
DateTimePicker
DateTimePicker aims to collect a full date and time string, using both a calender style picker and a time input.
It supports valueFormat to customize the displayed date. It does not dictate how the dates are stored.
We provide the underlying date in value and onChange. While it natively works with SQL format strings, we can also provide the date as an ISO string:
// 1.0 init
const [createdAtISO, setCreatedAtISO] = useState(new Date().toISOString())
// '2026-04-24T13:10:22.592Z'
// 2.0 in use
<DateTimePicker
// what is shown in the input
// e.g. April 24 — 16:14
valueFormat="MMMM DD [—] HH:mm"
// controlled value, compatible with ISO
value={completedAtIso}
// on change, the control provides a SQL formatted date
// e.g. 2026-04-24 16:14:00
onChange={(dateTimeSql) => {
if (!dateTimeSql) return
setCompletedAtIso(new Date(dateTimeSql).toISOString())
}}
// input label
label="Completed at"
/>
TimeInput
TimeInput works with HH:mm, HH:mm:ss and compatible variants. Note that those formats are ISO compliant:
const initTime = DateTime.local().toFormat("HH:mm")
const [timeIso, setTimeIso] = useState(initTime)
The event is of type React.ChangeEvent:
// "HH:mm"
<TimeInput
label="Time"
value={timeIso}
onChange={(event) => {
setTimeIso(event.target.value)
}}
/>
DatePickerInput
DatePickerInput is only concerned about the date part of the full date, aka the the yyyy-MM-dd part:
const initDate = DateTime.local().toFormat("yyyy-MM-dd")
const [datePart, setDatePart] = useState(initDate)
Note: onChange receive the candidate date part as a string:
// "yyyy-MM-dd"
<DatePickerInput
label="Date"
value={datePart}
onChange={(datePart) => {
// 2026-04-24
if (datePart) {
setDatePart(datePart)
}
}}
/>
Native Select
An abstraction over a <select> element, with inner <option> elements.
Instead of adding explicit markup, we define options and provide them to NativeSelect (through the data prop):
<NativeSelect data={data} />
For each option, we provide a label and a value:
const data = [
{ label: "Death Knight", value: "deathKnight" },
{ label: "Demon Hunter", value: "demonHunter" },
]
(fallback) use the value as a label
If the value can work as a label, or if we want to surface the value directly in the UI, we can provide an array of raw strings. The strings are used both as values and labels:
const data = ["deathKnight", "demonHunter"]
Select
Select is more capable than NativeSelect:
- The dropdown keeps Mantine style and can be customized
- The list of options is searchable
SegmentedControl
Pick a value among a set of options. The options always stay on screen (radio button).
The selected option is highlighted with an overlay. As we change the selection, Mantine moves the overlay with an animation.
Instead of explicit markup, we define the options and provide them to SegmentedControl. For each option, we provide:
- The
labelis the user-facing description: It is a string, or a React node, in particular when we want to add an icon. - The
valueis the option's string identifier. It must conform to the type union provided at the component level.
The state holds the selected identifier. The initial value determines which option is selected at the start:
type AIModel = "GPT" | "GEMINI"
const [model, setModel] = useState<AIModel>("GPT")
<SegmentedControl<AIModel>
value={model}
onChange={(v) => setModel(v)}
data={[
{ label: "ChatGPT", value: "GPT" },
{ label: "Gemini", value: "GEMINI" },
]}
/>
Button
An abstraction over a <button>.
variants
- Pick a
variant:- filled uses bright colors. (default)
- default
- light uses toned-down colors.
- outline
- subtle
- gradient
button-specific props
- Pick the button's primary
color. It defaults to the theme's primaryColor, aka Mantine's blue. It applies to several parts of the button, such as the border, the background and sometimes the text, unless the text is white or black. - Opt for a fluid button (fill the container) with
fullWidth. - Opt for compact version with
size, which changes the font size, the height and the horizontal padding. gradient, when using the gradient variant, sets the gradient's colors and direction,.
other options
disabled, sets the appearance and behavior to disabled. the disabled appearance is the same across all variants.leftSection,rightSection, for example for an icon.- Show the loading spinner appearance (and disable the button) with
loading. We add props to the loader withloaderProps(see Loader component).
<Button rightSection={<IconDownload size={14} />}>Download</Button>
Button not submitting form by default
Mantine adds type=button on the underlying <button>. It prevents the button from triggering the form submit, when inside a <form>. We set type=submit if needed.
providing an onClick handler
The handler receives the click event, of type React.MouseEvent<HTMLButtonElement, MouseEvent>.
We can ignore the event argument and perform an action regardless.
The handler is not supposed to return anything.
If we define the handler in a separate location, we can annotate its type:
React.MouseEvent<HTMLButtonElement> => void;
// shortcut version:
React.MouseEventHandler<HTMLButtonElement>
Button.Group
We create a button cluster that glues up the buttons together, either horizontally (default) or vertically:
<Button.Group>
<Button variant="default">First</Button>
<Button variant="default">Second</Button>
<Button variant="default">Third</Button>
</Button.Group>
ActionIcon
icon as a button
ActionIcon serves the "icon as a button" pattern. It requires a self-explanatory icon, which makes the text label not strictly needed. For example, the "trash" icon unambiguously conveys the meaning of the "delete" action.
Technically, the icon is added as child of ActionIcon:
<ActionIcon>
<IconMail size={14} stroke={...}/>
</ActionIcon>
The child icon now behaves like a button, and follows the variant appearance. To get a naked icon effect, we select the subtle or transparent variants.
We can set size at the ActionIcon level: it impacts the hover and active zone, and the
icon scale.
Pagination
The Pagination widget is a macro component built from multiple parts.
required props
- the
totalnumber of pages - the
valueof the cursor aka selected page number. - the
onChangehandler, which receives the tentative page number:
const [activePage, setActivePage] = useState<number>(1)
//
<Pagination total={10} value={activePage} onChange={setActivePage} />
other props
withControls, to show previous and next arrows, defaults to true.withEdges, to show "first page" and "last page" arrows, default to false.size, to get bigger or smaller pagination control, defaults to mdradius, to get rounder or squarer controls, defaults to md
Modal
Overlay some UI on top of the application. Dim the rest of the app while opened.
<Modal opened={opened} onClose={close}>
{/* Modal content */}
</Modal>
Modal works with some boolean state, that controls the display. We subscribe to the state through the opened prop.
Modal also wants an onClose handler, which it calls on dismissal attempts. The handler, if it authorizes the dismissal, changes the state back to its initial value.
Mantine's useDisclosure is a good fit to create the Modal's boolean state variable and the accompanying helper functions, open and close:
const [opened, { open, close }] = useDisclosure(false)
dismissal attempts
Modal considers the following actions as attempts to dismiss:
- press the escape key
- click outside
- click the close button (if displayed)
form elements
The inner content is commonly form elements: inputs and a submit button.
defaults
- the modal has a top section with a close button
radiusadheres to theme.defaultRadius.
overrides
- remove the close button with
withCloseButton, which can remove the header altogether. - center the modal with
centered. - add a
title, which can be any React node (not text-only). - control the dimming with
overlayProps. The dimming is done by an underlying<Overlay>. - more overrides
Menu
Menu aims to manage a floating menu of controls.
Menu manages both the menu proper (the dropdown) and the button that opens it. It manages the opened state.
- The user toggles the menu by clicking the control that lives in
Menu.Target. - In
Menu.DropDown, we add a set ofMenu.Itemwhich are<button>under the hood. - We can separate sections with
Menu.Divider. We can label section withMenu.Label.
overall structure
<Menu shadow="md">
<Menu.Target>
<Button>Toggle menu</Button>
</Menu.Target>
<Menu.Dropdown>/* */</Menu.Dropdown>
</Menu>
menu proper
<Menu.Dropdown>
<Menu.Label>Application</Menu.Label>
<Menu.Item leftSection={<IconSettings size={14} />}>Settings</Menu.Item>
<Menu.Item leftSection={<IconMessageCircle size={14} />}>Messages</Menu.Item>
<Menu.Divider />
<Menu.Label>Danger zone</Menu.Label>
<Menu.Item leftSection={<IconArrowsLeftRight size={14} />}>Transfer</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />}>Delete</Menu.Item>
</Menu.Dropdown>
Title
Thin abstraction over a heading element. It uses a HTML heading under the hood, h1 by default.
It removes the user agent stylesheet's default margin by setting them to 0. It keeps the bold font weight.
The font size, font weight, and line height, come from theme.headings.
The default theme defines five font sizes:
- h5 comes at 16px
- h4 comes at 18px
- h3 comes at 22px
- h2 comes at 26px
- h1 comes at 34px
We set the heading as follows:
order: pick the underlying html element, such as 1 for h1. It defaults to h1. Affects the font-size by default.size: override the font-size caused by order. we pick a size as a number or as a heading in the heading hierarchy such as h5.lineClamp: set the maximum number n of lines. If not set, the browser allow multiple lines for a heading element. If truncation happens, it shows an ellipsis thanks to text-overflow: ellipsis.
<Title order={3} size={"h5"}>
Daybreak
</Title>
Text
A container for text content.
By default, It uses a <p> element, but also changes some style aspects:
- It removes the default vertical margins (set to 0).
- It sets the font size to
16pxand the line-height to1.55(usingsmfor both). We can revert those changes by adding theinheritprop instead.
<Text c="dimmed">Dimmed text</Text>
Box
A neutral wrapper, similar in essence to div, but which supports Mantine style props and other styling APIs:
<Box fz={12}>{/* */}</Box>
Bar Chart
setup
npm install @mantine/charts recharts
import "@mantine/charts/styles.css"
bar chart
Mantine's BarChart components primarily displays vertical bars.
For each index, we display one or several bars, depending on how many data series we display at once.
data for a given index
The chart library expects an object for each index. We select the property that acts as the label. The remaining properties act as magnitudes:
{ month: "Jan", iPhoneSales: 1700, macSales: 17 }
one or more series
Magnitudes that spread over all indexes make a distinct series. For example, we could have the following magnitudes for each month:
- iponeSales
- macSales
At the BarChart level, we set
- the property that provides the index labels (here month)
- for each series, we provide:
- the series' name
- the color
- the label for the bar.
<BarChart
data={{ month: "Jan", iPhoneSales: 1700, macSales: 17 }, /* ... */}
dataKey="month" // key that provides index labels
h={300}
series={[
// series configuration
{ name: "iPhoneSales", color: "blue.6", label: "iPhone sales" },
{ name: "macSales", color: "green.6", label: "Mac sales" },
]}
/>
Progress
Progress is a replacement for the native <progress> element: It displays one or more bars over a horizontal track. It uses a regular div under the hood.
We may opt for a striped bar, or even an animated striped bar.
Progress's root is the track while Progress's section are the bar(s) displayed over it.
simple progress bar
We set one or more of the following props:
value: from 0 to 100size: the vertical size, aka how thick it is. Defaults to8pxthrough default md.radius: defaults to4pxthrough default sm. The default size and radius lead to a perfect semi circle border.color: defaults toblueanimated: enable a striped pattern and animate the stripes.striped: enable a striped pattern.transitionDuration: set the duration of the animation that occurs when the value changes, in ms. Defaults to 100.
<Progress value={50} />
opt for a compound progress bar, with several segments
We can split the bar into several sections, with subcomponents:
- The container is
Progress.Root - Each segment is a
Progress.Section - We may add a segment label with
Progress.Label. We can make the label have a smart text color withautoContrast.
<Progress.Root>
<Progress.Section value={20} color="indigo">
<Progress.Label autoContrast>Documents</Progress.Label>
</Progress.Section>
<Progress.Section value={50} color="orange">
<Progress.Label>Photos</Progress.Label>
</Progress.Section>
</Progress.Root>
RingProgress
RingProgress displays a circular ring, divided in several parts:
- We can use it as a circular progress bar, with a single bar showing the progress over the background track.
- We may also use it as a segmented, Donut Chart, where one section represents a quantity with its own bar.
Example of a circular progress bar:
<RingProgress
sections={[{ value: 35, color: progressColor, tooltip: "35%" }]}
rootColor={trackColor}
size={30}
thickness={4}
roundCaps
/>
Example of a donut chart with multiple sections:
sections={[
{ value: 40, color: 'cyan' },
{ value: 15, color: 'orange' },
{ value: 15, color: 'grape' },
]}
Avatar
avatar versions
The Avatar comes in three different versions:
The first version uses the user profile's image:
<Avatar src={imageURL} alt="John's portrait" />
The name version generates an avatar based on the name's initials:
<Avatar name={name} />
In absence of src or name, it falls back to a generic user icon:
<Avatar />
appearance
- It uses a circle border by default, as it sets the border-radius to 1000px.
- The default size is 36px
Mantine ensures the avatar keeps a square aspect ratio, cropping the image if needed, and that it covers the whole area, thanks to object-fit: cover.
Notifications
The notifications package aims to show floating notifications (notification toasts). Notifications can be customized, and dismissed automatically.
setup
It is a separate package with a separate stylesheet:
npm install @mantine/notifications
import "@mantine/notifications/styles.css" // here
We add the Notifications placeholder component somewhere in the app:
<MantineProvider>
<Notifications />
{/* Your app here */}
</MantineProvider>
Note:
- it must be placed within MantineProvider
- it doesn't wrap the app
- is a placeholder: Mantine only needs one Notifications component, and uses it to display one or more notification toasts
create and dismiss notification toasts
We use an imperative API to show and dismiss notifications toasts:
import { notifications } from "@mantine/notifications"
//
notifications.show()
notifications.update()
notifications.hide()
creating a notification
To create a notification, we set:
message: the body of the message (required)title: the title of the massage- an
id, if we intend to update this notification later on loadingto display a spinnerautoClose: we keep the notification on-screen until dismissed (autoClose: false), or set an auto-close timer in milliseconds.positionto set it somewhere else than bottom-right.
notifications.show({
id: task.id,
color: "white",
title: "Generating...",
message: task.prompt,
autoClose: false,
loading: true,
position: "top-right",
radius: "xl",
withCloseButton: false,
})
update a notification
To update the notification, we provide its id.
To convey completion, we can:
- show a completion message and icon
- plan for auto-close after a delay:
notifications.update({
id: task.id,
color: "gray",
title: "Image successfully created",
message: "..."
loading: false,
autoClose: 2500,
icon: <IconCheck size={18} />,
})
Mantine form
optional
Mantine form is a separate package. It aims to streamline the management of complex forms with initial data, data validation and data transforms.
It is not needed for simple forms, as it introduces some changes and boilerplate code compared to React state based form.
npm install @mantine/form
form conceptual
Gather data from user and send it to a server. Send it as-is or after some (client-side) validation and/or transformation.
In React, a form uses either controlled or uncontrolled inputs. Mantine provides both options.
Mantine form overview
We work with a regular <form>, which embeds a set of inputs and a submit button.
We provide initial data in an object (initialValues). The user may edit data through the inputs.
On submit, Mantine collects the data and provide it to the validator and/or transformer. Then, it provides the processed data to the callback, in principle for it to send it to the server.
When an initial value has no matching input (invisible, uneditable property), Mantine still collects it.
main steps
- we create a configuration object
- We compute a configured form object, through useForm(configuration)
- We generate the form-submit handler with form.onSubmit(f), where f is our callback to handle the processed data. The form-submit handler takes an event as argument, and is assigned to the <form> onSubmit attribute, which is the react version of standard onsubmit.
- We add attributes to the form inputs so that they interoperate. we spread form.getInputProps(email) to add the relevant attributes on the input, depending if it is controlled or uncontrolled: value if it is controlled, initialValue and ref if it is uncontrolled. They also get onChange and onBlur.
init form
const form = useForm({ // config
initialValues: {x: "", y: ""},
validate: f
transformValues: (collected) => processed
})
form callback
function my_callback(data) {
data.x
data.y
}
const mantineEventHandler = form.onSubmit(my_callback)
wire up
form onSubmit={e => mantineEventHandler(e)}
TextInput label="x" {...form.getInputProps('x')}
TextInput label="x" {...form.getInputProps('y')}
TextInput label="x" {...form.getInputProps('z')} // z is not part of initial values, but may still be included if input is changed}
/form
initial value or not.
Mantine collect properties from the initial values, even if there is no matching input, and even if the value is null or undefined, and provide them for processing.
If an input is connected to an initial value that does not exist, it is effectively initialized to undefined, which will generate a React warning. Mantine will collect the value of that input and associate with the key provided in form.getInputProps??
initial value for a TextInput
The initial value for a TextInput should no be null: it triggers a React warning. It should also not be set to undefined. Instead, we should initialize to empty string. We may do post-processing in the transformer to change the empty string to null.
utils
// get current form values
form.values
// Set all or some form values
form.setValues(values)
// Set all form values using the previous state
form.setValues((prev) => ({ ...prev, ...values }))
// Set value of single field
form.setFieldValue("path", value)
// Set value of nested field
form.setFieldValue("user.firstName", "Jane")
// Resets form.values to initialValues,
// clears all validation errors,
// resets touched and dirty state
form.reset()
// Sets initial values, used when form is reset
form.setInitialValues({ values: "object" })
useLocalStorage
interact with localStorage
We want to read localStorage, and/or persist a value in it.
We interact with localStorage with a getter and a setter, in a fashion similar to React's useState API:
const [foo, setFoo, removeEntry] = useLocalStorage<AiModel>(config)
removeEntry() aims to clear localStorage.
We must provide a config, with the local storage key. We can also provide an initial/fallback value to set when the localStorage is missing a value:
const config = {
key: "ai-provider",
defaultValue: "GPT",
}
synchronous version (opt-in)
In the synchronous version, the hook reads local storage before the first render. If the entry is missing, it creates one with defaultValue, and initialize state with it:
const config = {
// synchronous version
getInitialValueInEffect: false,
}
asynchronous version (default)
In the asynchronous version, the hook initializes state with the default value. The localStorage lookup runs later, as an effect, after the first paint, avoiding delay. Only then is the state changed to that value.
Having the first paint ignoring the persisted value is not always desirable.
useDisclosure
create some boolean state for use in disclosure elements
useDisclosure creates a boolean state variable: It is a wrapper around useState<boolean>. It also provides semantic setters, for use in elements that open and close, such as modals:
const [opened, { open, close, toggle }] = useDisclosure(false)
useMantineColorScheme
manage the active color scheme
We want to read or set the currently active colorScheme, aka dark or light mode. useMantineColorScheme provides a getter and a setter:
const { colorScheme, setColorScheme, toggleColorScheme } = useMantineColorScheme()
backed by localStorage
Mantine saves the active colorScheme in localStorage and reads from it.
Before the App appears on-screen, Mantine reads localStorage for any saved colorScheme, so the first paint is done with the correct colorScheme.
We can revert localStorage to the default color scheme, the one set by the Mantine provider:
const { clearColorScheme } = useMantineColorScheme()
separate from useColorScheme
useColorScheme is a different hook, that reads the device's OS color scheme, and that is used much less often.