Overview
Mantine provides an infrastructure to build 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 and productivity apps
mantine style is opinionated and cohesive
Mantine brings a set of cohesive style defaults.
If we aim to override some style, we should make it compatible with the Mantine overall look.
Some components, such as layout components and Mantine hooks, don't have a visible style, so we may use them in any design or project.
mantine's packages
Mantine provides distinct packages. The main ones are Mantine core and Mantine hooks.
terminology: components and component variants
Mantine provides components, and for some of them, variants. For example, it provides the Button component, for which it provides the outline and filled variants.
terminology: component's inner-elements
A component is constructed through several, named, inner-elements. For example, Slider is built upon a mark, a track and a bar. We may style the inner-elements distinctively.
customization patterns
We have three main patterns:
- (Local) customization of an instance: the scope of the change is local and limited.
- (Global) override of a Mantine component: it affects all sites that use these components.
- (Global) customization of style primitives, also called theming, which affects several components.
Customize an instance
overview
We want to customize a single instance of a component.
We do that by adding inline style, classes, or provide props.
inner workings
Technically, this pattern resolves to:
- style that lives in the element's style attribute
- markers set on the element, such as a class, an id, an attribute or a custom attribute, along with some CSS targeting those markers.
- inner DOM elements being added.
the limits of setting style or className attributes directly
We may provide quick inline style in the style attribute or classes to the className attribute, but there is no granularity into which inner-element we target. The style we provide usually applies to the root inner-element.
While this is possible, it's not a pattern recommended by Mantine, because it doesn't let Mantine act as an intermediary.
mantine patterns
Mantine offers patterns that are more powerful, and for them, allow to target inner-elements.
props that we may set on any Mantine component instance.
Mantine's style props are a set of props that we may set on any Mantine component, and which aim to set a single style aspect. The style targets the top-level element.
<Box m={4}></Box>
props that are specific to one or more components
Some props are unique to some components. They may determine the style, but also determine the component's inner structure, such as toggling inner-elements. For example, the TextInput's label prop adds a label element in addition to the input element.
<TextInput label="Username"></TextInput>
// adds:
<label>Username</label>
target an inner-element
We target an inner-element by specifying its Mantine-defined name, and provide a style object (styles pattern) or one or more classes (classNames pattern).
<Button
styles={{
root: { backgroundColor: 'red', height: 16},
label: { color: 'blue' },
}}
<Button
classNames={{
root: "primarybutton-root",
label: "primarybutton-label",
}}
/>
class naming pattern
The classes we provide in styles are to customize an instance of a given Mantine component. But we may wrap the instance into a new component, effectively creating a variant of the given Mantine component.
Given that, we may use the variant's name in all classes related to it, as a prefix, such as pinkbutton-* if we are making a Button variant. if we are to target an inner component, we use its name as a suffix.
.pinkbutton-root
.pinkbutton-label
Universal style props
Mantine components accept a series of props that let us control various style aspects:
spacing
| Prop | CSS Property | Theme key |
|---|---|---|
| m | margin | theme.spacing |
| mt | marginTop | theme.spacing |
| mb | marginBottom | theme.spacing |
| ml | marginLeft | theme.spacing |
| mr | marginRight | theme.spacing |
| mx | marginRight, marginLeft | theme.spacing |
| my | marginTop, marginBottom | theme.spacing |
| p | padding | theme.spacing |
| pt | paddingTop | theme.spacing |
| pb | paddingBottom | theme.spacing |
| pl | paddingLeft | theme.spacing |
| pr | paddingRight | theme.spacing |
| px | paddingRight, paddingLeft | theme.spacing |
| py | paddingTop, paddingBottom | theme.spacing |
typography
| Prop | CSS Property | Theme key |
|---|---|---|
| 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 | Theme key |
|---|---|---|
| w | width | theme.spacing |
| miw | minWidth | theme.spacing |
| maw | maxWidth | theme.spacing |
| h | height | theme.spacing |
| mih | minHeight | theme.spacing |
| mah | maxHeight | theme.spacing |
position
| Prop | CSS Property | Theme key |
|---|---|---|
| pos | position | – |
| top | top | – |
| left | left | – |
| bottom | bottom | – |
| right | right | – |
| inset | inset | – |
other props
| Prop | CSS Property | Theme key |
|---|---|---|
| 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 | – |
responsiveness: provide alternative values.
Instead of a single value, we provide an object with alternative values. Most of the time, we only provide two values.
- the base value provides the mobile-centric, default value. Its exact scope depends on which other breakpoints we define.
- The xs value activates at 576p. It excludes most smartphones. For reference, iPhones are under 450p width, with standard-size iPhones under 400p. If we want to exclude phablets as well, we use sm instead.
- The sm value activates at 768p (48em), which excludes phablets, and includes tablets and wider screens. For reference, iPads start at 768px.
<Flex
direction= {{ base: 'column',sm: 'row' }}
gap= {{ base: 'sm', sm: 'lg' }}
justify= {{ base: 'sm', sm: 'lg' }}
>
Global customization
Global customization, also called theming, aims to override some style-primitives to affect several components, or, with a smaller reach, to override a component to affect all instances.
customize style primitives
#todo
implement Mantine-provided, component-scoped empty classes
Mantine sticks a series of empty classes on each of its built-in components. We may implement them to customize a given aspect of a given component.
(Implementing such class affects all instances)
For example, Mantine sticks mantine-Button-root and mantine-Button-label on inner-elements of the Button component.
.mantine-Button-root {
border-width: 0.5px;
}
empty classes specificity
Those classes have the same specificity than Mantine's implemented internal classes. One technique to make them win is to import our stylesheet after Mantine's one.
CSS Module pattern
css module pattern overview
A processor parses a given stylesheet to scan the classes and expose them to JS. It generates a globally unique name for each of them. It packs the developer-defined classes and globally unique classes in a dictionary object:
export const classes = {
supercool: "supercool_5cEkq2n0x1",
supernice: "supernice_1kmox6oL39",
superGreat: "superGreat_1kmox6oL39",
"super-awesome": "super-awesome_1kmox6oL39",
} // conceptual // class dictionary
We import the dictionary.
import classes from "xxx.module.css";
<Button
classNames={{
root: classes.xxx, // resolves to the processed class
label: classes.xxx, // resolves to the processed class
}}
>
pattern: a module targets a Mantine component custom variant
In this pattern, the module's purpose is to define the style of a Mantine component custom variant for which we create a name. For example, we may create a PrimaryButton variant of Button. We name the CSS Module with the variant name: PrimaryButton.module.css
We name the classes according to the inner-element they target, such as root or label.
/* PrimaryButton.module.css */
.root {
}
.root:hover {
}
.label {
}
We may then use the classes to inline-customize a Mantine's component instance, or to create a fully-fledged React component variant.
import PrimaryButtonClassNames from "PrimaryButton.module.css";
{/* provide properties */}
<Button
classNames={{
root: PrimaryButtonClassNames.root,
label: PrimaryButtonClassNames.label,
}}
>
provide the classes object directly
When we follow the inner-element-as-a-classname naming pattern, we may give the CSS-module object directly to classNames, since classNames expects an object with inner-element named properties.
<Button classNames={primaryButtonClassNames}>
Mantine's internal styling
case study: Mantine's implementation of Button
The Button source code includes the typescript file (Button.tsx) and the stylesheet (Button.module.css).
HTML element structure
There are several, nested elements:
- the root button is the top level container.
- the inner span is an intermediate container for the label and the sections
- a section is a container for an icon.
- the label span contains the button's text.
- the loader span is an intermediate container for a loader.
/* root */
<button>
{/* 1.0 loader container*/}
<span />
{/* 2.0 inner */}
<span>
{/* 2.1 section */}
<span>
<svg /> {/* 2.1.0 icon */}
</span>
{/* 2.2 label */}
<span>{/* 2.2.0 text */}</span>
</span>
</button>
Both the mantine-implemented internal classes and empty classes are present, on each inner-element:
<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>
some inner-elements have some data-attributes, which may be shared across inner-elements of distinct components, such as the mantine-active and mantine-focus-auto attributes.
For example, mantine-active allows to receive style that activates when the element is active. The attribute is always there, and the stylesheet discriminates to the :active state.
mantine-active:active {
transform: translateY(calc(0.0625rem));
}
Button's stylesheet
The stylesheet is a CSS module and follows the pattern where classes are named after inner-elements of the Button component. We reproduce parts of the root class.
The .tsx file imports the CSS module.
style shipped on npm
the style is mostly the same. It is probably processed by PostCSS. It does not use CSS nesting.
It is then processed even more and merged to a gigantic 232kb stylesheet, shipped on npm too, the one we import from our app.
Light mode, dark mode
Mantine defaults to light mode: defaultColorScheme defaults to light.
```We may change it to darkorauto. auto is better because it follows the user's color scheme.
<MantineProvider defaultColorScheme="auto"></MantineProvider>
Default theme
The default theme spreads over several categories.
spacing
space between elements and inside elements.
xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32px
used for or usable by:
- gap for Group, Flex, Stack.
- padding props.
- margin props, notably for Divider
- width and height props such as
wandh, notably for Space.
radius
xs: 2px
sm: 4px (d)
md: 8px
lg: 16px
xl: 32px
used for:
- Paper
- Dialog, Modal, ..
- Button, Tooltip..
we may set the default with defaultRadius:
"defaultRadius": "sm",
breakpoints
a set of alternative thresholds that we may target 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 specify one or more thresholds in the styles-props alternative-values pattern.
font sizes: a set of 5 distinct sizes
"fontSizes": {
"xs": 12px
"sm": 14px
"md": 16px,
"lg": 18px
"xl": 20px
},
Those sizes do not affect headings. We may use them in:
- the fz style prop, on any component.
line heights
"lineHeights": {
xs: 1.4,
sm: 1.45,
md: 1.55,
lg: 1.6,
xl: 1.65
},
headings
family, weight and wrap behavior:
"headings": {
"fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji",
"fontWeight": "700",
"textWrap": "wrap",
},
size and line height:
"sizes": {
"h1": {
"fontSize": "calc(2.125rem * var(--mantine-scale))",
"lineHeight": "1.3"
},
"h2": {
"fontSize": "calc(1.625rem * var(--mantine-scale))",
"lineHeight": "1.35"
},
"h3": {
"fontSize": "calc(1.375rem * var(--mantine-scale))",
"lineHeight": "1.4"
},
"h4": {
"fontSize": "calc(1.125rem * var(--mantine-scale))",
"lineHeight": "1.45"
},
"h5": {
"fontSize": "calc(1rem * var(--mantine-scale))",
"lineHeight": "1.5"
},
"h6": {
"fontSize": "calc(0.875rem * var(--mantine-scale))",
"lineHeight": "1.5"
}
}
Gray Palette
description and use
It is a slate gray: it has a blue tint.
Mantine's light-mode theme makes use of this palette: it offers several whites for backgrounds, and a few dark grays for text. It does not include pure white, instead, it starts at #F8F9FA.
The following consideration usually apply for light-mode:
-
The background may be pure white. It may be the app/site global background of the controls background.
-
The background may go darker on hover. This is the case for buttons, and for the select's options:
gray.1 -
The text is black-ish or gray-ish depending on importance and role:
- pure black text:
- some headings
- body text
- input label
- button label
- gray 9 (readable black for body text)
- gray 8 (readable black)
- dark 7 (dimmed black)
- gray 6 (gray) for secondary text, and input description
- pure black text:
values
gray.0white: #F8F9FAgray.1white: #F1F3F5gray.2white gray: #E9ECEF- ...
gray.6gray: #868e96gray.7dark gray: #495057gray.8very dark gray: #343A40gray.9: very dark gray: #212529
Dark Palette
the dark palette
It is a neutral gray palette. It was a slate-gray palette (blue-tint) before Mantine 7.3.
Mantine's dark-mode theme makes use of this palette. It offers several darks for backgrounds, grays for controls, and whites for text.
use in dark mode
- The app background is dark 7. We may go darker with dark 8.
- Controls have lighter backgrounds: dark 6, and even lighter on hover: dark 5. Sometimes, hover goes to a darker background instead: such as the hovered option in a select control: dark 8.
- The text is light. It uses one of those 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 input's label is dark 0, the input's description is dark 2.
- The dimmed text is dark 2, such as secondary label, or secondary text in general.
values
dark.0: very light gray close to white: #C9C9C9.- ..
dark.9: black: #141414
revert to blue-tinted dark palette
const theme = createTheme({
colors: {
dark: [
"#C1C2C5",
"#A6A7AB",
"#909296",
"#5c5f66",
"#373A40",
"#2C2E33",
"#25262b",
"#1A1B1E",
"#141517",
"#101113",
],
},
})
Global Style: Colors
#todo
--mantine-color-scheme: dark;
--mantine-primary-color-contrast: var(--mantine-color-white);
--mantine-color-bright: var(--mantine-color-white);
--mantine-color-text: var(--mantine-color-dark-0);
--mantine-color-body: var(--mantine-color-dark-7);
--mantine-color-error: var(--mantine-color-red-8);
--mantine-color-placeholder: var(--mantine-color-dark-3);
--mantine-color-anchor: var(--mantine-color-blue-4);
--mantine-color-default: var(--mantine-color-dark-6);
--mantine-color-default-hover: var(--mantine-color-dark-5);
--mantine-color-default-color: var(--mantine-color-white);
--mantine-color-default-border: var(--mantine-color-dark-4);
--mantine-color-dimmed: var(--mantine-color-dark-2);
--mantine-color-dark-text: var(--mantine-color-dark-4);
--mantine-color-dark-filled: var(--mantine-color-dark-8);
--mantine-color-dark-filled-hover: var(--mantine-color-dark-7);
--mantine-color-dark-light: rgba(36, 36, 36, 0.15);
--mantine-color-dark-light-hover: rgba(36, 36, 36, 0.2);
--mantine-color-dark-light-color: var(--mantine-color-dark-3);
--mantine-color-dark-outline: var(--mantine-color-dark-4);
--mantine-color-dark-outline-hover: rgba(36, 36, 36, 0.05);
--mantine-color-gray-text: var(--mantine-color-gray-4);
--mantine-color-gray-filled: var(--mantine-color-gray-8);
--mantine-color-gray-filled-hover: var(--mantine-color-gray-9);
--mantine-color-gray-light: rgba(134, 142, 150, 0.15);
--mantine-color-gray-light-hover: rgba(134, 142, 150, 0.2);
--mantine-color-gray-light-color: var(--mantine-color-gray-3);
--mantine-color-gray-outline: var(--mantine-color-gray-4);
--mantine-color-gray-outline-hover: rgba(206, 212, 218, 0.05);
--mantine-color-red-text: var(--mantine-color-red-4);
Global style: Others
line-height defaults to 1.55
--mantine-line-height: 1.55;
Flex
Use a bare-bones flex container, with nothing set except display: flex.
override CSS defaults
- Switch the
direction, which is horizontal (row) by default, to vertical (column). It's best to switch conditionally. - 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:
<Flex
direction={{ base: "column", sm: "row" }}
gap={{ base: "sm", sm: "lg" }}
justify={{ base: "flex-start", sm: "space-between" }}
/>
Group
Layout elements horizontally and add space between them. The container spans the whole line. It uses a horizontal flex under the hood.
The differences with a stock horizontal flex:
- It adds some
gapbetween elements (defaults to md, aka 16px). - It centers elements vertically, with
align, instead of stretching them. - It wraps when needed (multi-line flex) (set
wrapto either mono-line (nowrap) or multi-line (wrap), which sets the underlying flex-wrap property).
overrides
-
Make children fill the container with
grow.- It sets flex-grow: 1 on children.
- By default, it makes them same-width (by capping them to a max-width) and put them on a single line (because it caps children to a share of the line).
- We can reestablish multi-line by turning off
preventGrowOverflowwhich turns off capping. wrapping was never disabled in this scenario. - If we also add nowrap, it's back a mono-line flex where children grow proportionally to their flex-basis (It's convoluted)
-
(non-Group specific) Pick horizontal alignment with
justify. It defaults to flex-start like stock flexes.
Stack
Layout elements vertically and add space between them.
The container spans the whole line (because its width is still auto). It uses a vertical flex under the hood. Items default to spread horizontally within it.
Differences with a stock vertical flex:
- It adds a
gap(16px by default).
Other settings to override CSS default values:
justify: elements sit at the top by default (flex-start)align: elements fill the stack horizontally by default (stretch)
SimpleGrid
Set up a column layout with a given number of equal-width columns.
The grid container spans the whole line and the columns together span the whole container.
The grid container adds space between tracks by default (16px) (both horizontal and vertical spacing aka spacing and verticalSpacing).
We set the number of columns with cols. It defaults to a single track. Best Practice: adapt the column count based on screen size, by providing at least two values.
<SimpleGrid cols={3}>
{/* items */}
</SimpleGrid>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
{/* items */}
</SimpleGrid>
Container
A wrapper that centers the content and keeps it aways from the screen edges. For example, we wrap the main column of a website, to have centering in desktop and padding on mobile.
max-width version
The default version comes with a max-width (for desktop) and some padding (for mobiles). 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. If we are to provide an arbitrary numeric value, we may as well set maw directly to remove indirection.
The default padding is 16px, aka md. We change it with the regular px style 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. A Box with horizontal padding has the same effect.
grid version
The grid version doesn't use padding and doesn't center with max-width. Instead, it sets a grid layout and center stuff with grid positioning.
Center
Center the child or the group of children horizontally and vertically.
Under the hood, it uses a flex container that justify and align the group of children to the center.
vertical-centering and height
Vertical centering will only come into play if Center's height is bigger than the inner content.
horizontal-centering and width.
The container flex spreads horizontally (width: auto), but compacts its children to max-content. We can prevent compaction by setting the group of children to width: 100%.
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 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 a 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 (--mantine-color-body).
- In light mode, it is white #FFF.
- In dark mode, it is dark.7.
We can override's Paper background with bg.
Paper also adds a radius (theme.defaultRadius) which defaults to 4px.
Paper specific customizations
- Add a pre-styled border with
withBorder(boolean) - Add a pre-styled shadow with
shadow. We specify a named size (e.g. sm), or a box-shadow CSS literal.
Accordion
Establish one or more expandable items that disclose more content when interacted with.
item: show a preview and enable disclosure of a panel
The Accordion contains one or several items. An item starts as a clickable preview (Accordion.Control) and embeds a panel, that starts as hidden, and that is toggled on click.
Mantine mounts all panels immediately, regardless of their toggle state. 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>
items appearance
By default, items are separated by a thin line. We can set variant to separated instead, or other variants.
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)
header: Appshell.Header
The header manages:
- the burger button
- the logo
- the dark mode / light mode toggle (potentially)
- the user button (potentially)
- a set of links (potentially)
configure the header
We build a header config and provide it to AppShell as a header prop. We commonly set the height.
<AppShell /* ... */ header={{ height: 60 }} />
navbar: Appshell.Navbar
The navbar stands on the left side. It is transient on mobile devices. On desktop it is commonly always-on, but Mantine supports having a conditional collapse as well, following a distinct logic and state variable.
We build a navbar config and provide it to AppShell as a navbar prop. collapsed aims to control the display of the navbar, while breakpoint sets the breakpoint from which we go from mobile to desktop logic.
<AppShell /* ... */ navbar={{}} />
collapsedreceives up to two boolean state variables: one for mobile and one for desktop. Most of the time, we only set mobile.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. We hide the mobile burger button with
hiddenFrom. If desktop supports collapse too, we use a distinct burger button (see below).
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>
)
navbar: allow collapse on desktop
We may allow the collapse behavior on Desktop as well: we create an additional, distinct boolean state variable, and provide it to the desktop property of collapsed. We also add a dedicated desktop burger button, which commonly keeps the same appearance regardless of the collapse state (we don't show a close icon because there is no overlay). This burger button limits its display to 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: other configuration
navbar={{ width: 300, /* ... */ }}
widthcontrols the width in desktop mode (it is always full-screen in mobile). We provide an immediate value or an object with two or more values.
navbar: implement top and bottom sections
We may split the navbar into AppShell.Sections, and one of them may grow.
<AppShell.Navbar>
<AppShell.Section grow> </AppShell.Section>
<AppShell.Section> </AppShell.Section>
</AppShell.Navbar>
Appshell.Main
The container for the main panel.
We may set the panel background color. The default light theme sets a white background while the dark theme sets a dark.7 background.
Override example: we may pick a light gray background such as gray.1, so that inner containers may stand out as white. Similarly, we may change the background to dark.8 so that inner elements may standout as dark.7.
const backgroundColor = colorScheme === "dark" ? "dark.8" : "gray.1"
synopsis
the configuration objects are required if we use the matching element, except for main which may not be configured with this pattern.
<AppShell header={{}} navbar={{}} aside={{}} footer={{}}>
<AppShell.Header />
<AppShell.Navbar />
<AppShell.Aside />
<AppShell.Main />
<AppShell.Footer />
</AppShell>
Tabs
Table data
abstract
A table works with an array of items, where each item is a row, and each property or member is a cell.
An item may come as an object or as an array.
pattern: the item is 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" },
]
pattern: the item is 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"],
]
We may come with this data by transforming an array of objects to an array of arrays:
const items = players.map((p) => [p.name, p.level, p.class])
headers
An array usually comes with headers. We may provide them in an array.
const headers = ["Element position", "Atomic mass", "Symbol", "Element name"]
Table with explicit structure
structure preview
<Table>
<Table.Thead>
{/* head */}
<Table.Tr>
<Table.Th>{col1Label}</Table.Th>
<Table.Th>{col2Label}</Table.Th>
<Table.Th>{col3Label}</Table.Th>
</Table.Tr>
</Table.Thead>
{/* body */}
<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 from an array of objects
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>
))
build rows 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>
))
headers and body
Thead is the container for the headers.
<Table.Thead>
<Table.Tr>
<Table.Th>{col1Label}</Table.Th>
<Table.Th>{col2Label}</Table.Th>
<Table.Th>{col3Label}</Table.Th>
</Table.Tr>
</Table.Thead>
Tbody is the container for rows.
<Table.Tbody>{rows}</Table.Tbody>
Table from data prop
We provide the complete data as a single object for a single prop:
- The
captionproperty defines the table's title - The
headproperty is the array of headers. - The
bodyarray is the list of items. Each item is an array with bare values
const tableData: 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"],
],
}
data prop
We provide such object to the data prop. Mantine then creates the markup automatically.
<Table data={tableData} />
TextInput
TextInput is an abstraction over <input type=text>. It inherits most props from <Input>. We describe all props here.
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.
NumberInput
NumberInput is a <input type=text> under the hood. This enables more freedom into what is displayed in the input. For example, we can display formatting commas.
The value we get is not consistently a number. For example, we get an empty string when the input is empty.
options
- Forbid decimal numbers with
allowDecimal. They are allowed by default. - Forbid negative numbers with
allowNegative. They are allowed by default. - Clamp the value between
minandmax. Determine how the value should clamp withclampBehavior:- Forbid values outside the limits at all times with
strict. - Clamp after input on
blur. This is the default. - Never clamp (
none)
- Forbid values outside the limits at all times with
As a text input, it supports a label, a description (secondary label) and a placeholder.
state
Work with the string and number type union:
const [value, setValue] = useState<string | number>("")
return <NumberInput value={value} onChange={setValue} />
Work with a number only. In this case, we only update state if the value is a number. When the NumberInput is empty, the state may have a ghost value. We clear the ghost value on blur if the field is empty.
const [value, setValue] = useState<number>(0)
return (
<NumberInput
value={value}
onChange={(value) => {
if (typeof value === "number") setValue(value)
}}
onFocus={(input) => {
/* clear field to prepare for input if value was 0 */
if (input.target.value === "0") {
input.target.value = ""
}
}}
onBlur={(input) => {
/* reset field to 0 if empty on leaving the field */
if (input.target.value === "") {
onChange(0)
}
}}
/>
)
FileInput
FileInput is a control that triggers the OS' file selection dialog. It shows information about the current selection. We can customize the flow:
- Allow selecting multiple files at once with
multiple. It defaults to false. - Restrict selection to a given file type in
accept, such as"image/png" - Add a clear button on the right with
clearable - 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 users close the platform's native File picker or clear the selection.
Other techniques
- Limit the input's width to truncate long file names with the regular maw prop
- 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.
FileInput inherits props from Input
<FileInput
size="xs"
maw={150}
onChange={onChange}
placeholder="Reference Image"
leftSection={<IconPhoto size={18} stroke={1.5} />}
clearable
variant="unstyled"
/>
Native Select
An abstraction over a <select> that embeds some <option> elements.
We collect options in an array, and assign them to the data attribute. We describe each option with a label and a value.
const options = [
{ label: "Death Knight", value: "deathKnight" },
{ label: "Demon Hunter", value: "demonHunter" },
]
<NativeSelect data={options} />
If the value works as a label, such as when we want to surface the value in the UI, we can replace the objects with a single string instead. The string acts both as the value and as the label.
const options = ["deathKnight", "demonHunter"]
SegmentedControl
SegmentedControl allows picking a value among a set of options. The options always stay on screen (Radio button). In that it is different from a Select.
The selected option has a highlight overlay ontop. As the selection changes, Mantine moves the overlay with a smooth animation.
Each option comes with a label, which is the user-facing description, and a value, which is its unique identifier and which we store in state. We make value conform to a type union.
We store the options in data. We set the initial selection in value.
onChange is called on user selection and before browser paint. React prevents any change unless we mutate the state accordingly.
type AIProvider = "GPT" | "IMAGEN"
const [provider, setProvider] = useState<AIProvider>("GPT")
<SegmentedControl
radius="md"
size="sm"
value={provider}
bg={"transparent"}
onChange={(v) => setProvider(v as AIProvider)}
data={[
{ label: openAILabel, value: "GPT" },
{ label: geminiLabel, value: "IMAGEN" },
]}
/>
Button
An abstraction over a <button>.
button specifics
-
Pick the overall appearance with
variant:- filled uses bright colors. (default)
- light uses toned down colors.
- outline
- subtle
- gradient
- default
-
Pick the
colortint. It defaults to the theme's primaryColor, aka Mantine's blue. -
Opt for a fluid button (fill the container) with
fullWidth. -
Opt for compact version with
size, and control the font size. -
Show the loading spinner appearance (and disable the button) with
loading. We customize the loader withloaderProps. (see Loader component) -
disabled, sets the appearance and behavior to disabled. the disabled appearance is the same across all variants. -
gradient, sets the gradient's colors and direction. It requires using the gradient variant.
other options
leftSection,rightSection, for example for an icon.
<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 pressed inside a <form>. We set type=submit if needed.
providing an onClick handler
The handler specifies which action to perform on click. This action usually does not depend on any information related to the click event.
Still, the handler receives the event as an argument. If we define the handler somewhere else than inline, we must indicate its typing. The argument it takes is of type Event.
More specifically, the event is of type
React.MouseEvent<HTMLButtonElement >.
The HTMLButtonElement type parameter indicates the type of the
target of the event, which in this case is a <button> element.
The handler is not supposed to return anything.
Handler type signature
onClick: React.MouseEvent<HTMLButtonElement> => void
shortcut type
onClick: React.MouseEventHandler<HTMLButtonElement>
Button.Group
We create a button cluster that glues up the buttons 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 button
ActionIcon serves the "icon as a button" pattern. UI-wise, the idea is that the icon is expressive enough to be self-explanatory, with no need for accompanying text, for example the "trash" icon refers to the "delete" action.
Technically it is a container that is distinct from the icon, that must be added as a child.
It gives its child icon the behaviour of a button, and it customizes its appearance according to the selected variant.
variant: same variants as Button's variants. To get a naked icon effect, we select subtle or transparent.size: the container's size impacts the hover and active zone, and the icon scale.
required child icon
As a wrapper, it impacts the concrete child icon.
We may still set size directly on the child Icon.
<ActionIcon variant='default'>
<IconMail size={14} stroke={..?}/>
</ActionIcon>
Pagination
we provide:
- the
totalnumber of pages - the
valueof the cursor aka selected page number. - the
onChangehandler - (optional)
withControls, to show previous and next arrows, defaults to true. - (optional)
withEdges, to show first and last arrows, default to false. - (optional)
size, to get bigger or smaller pagination control, defaults to md - (optional)
radius, to get rounder or squarer controls, defaults to md
const [activePage, setActivePage] = useState<number>(1)
<Pagination total={10} value={activePage} onChange={setActivePage} />
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 a boolean state variable to control the display (plugged on the opened prop).
Modal also works with an onClose handler, called on dismissal attempts, and responsible for toggling back the state to closed, or for denying the dismissal.
reminder: Mantine's useDisclosure provides a boolean state variable, and open and close state-mutation functions, which are fitting for a modal:
const [opened, { open, close }] = useDisclosure(false);
The following actions qualify as attempts to dismiss:
- press the escape key
- click outside
- click the close button (if displayed)
We usually open the modal with a button:
<Button onClick={open}>
Open modal
</Button>
The inner content is commonly form elements: some inputs and a submit button.
defaults
- the modal has a top section with a close button
radiusadheres to theme.defaultRadius which defaults to 4px.
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
A menu that gathers a set of controls, that we display conditionally. Menu manages the opened state automatically.
The user toggles the menu by clicking the control that lives in Menu.Target.
We add a set of Menu.Item which are <button> under the hood.
We may separate two different sections with Menu.Divider. We may give a section a label with Menu.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 some text content.
It is a <p> element under the hood by default, but we may set it to be another element such as an anchor tag or a span with the component prop.
It removes default margins by setting the (vertical) margin to 0.
It comes with some style affecting the font size, the font family and the line height, which is 1.55 by default. We may set it to inherit all those styles instead by adding the inherit prop.
<Text c="dimmed">Dimmed text</Text>
Box
A neutral wrapper, similar in essence to div or span, which may affect its children through Mantine style props:
<Box fz={12}>/* */</Box>
Bar Chart
packages
@mantine/charts
recharts
stylesheet
import "@mantine/charts/styles.css"
bar-chart logic
for each index, display one or several bars. We assume one bar for each index.
data for a given index
The chart library expects an object for each index. The object contains raw data. Among this data, we select which property acts as the label. The remaining properties may act as magnitude.
{month: "Jan", workouts: 17},
{month: "Feb", workouts: 18}
indicate data's shape
We indicate:
- the property that acts as the label, here month
- for each property that we want to act as a magnitude, we indicate: the property's name (here workouts) the color for that bar, and the label for that bar.
In the following example, workouts provides the magnitude for the unique serie.
<BarChart
data={}
h={300}
dataKey="month" // key that provides label
series={[
// bar(s) configuration
{ name: "workouts", color: "blue.6", label: "Workouts" },
]}
/>
Each magnitude's property, as it spreads over all the indexes, makes a distinct serie. For example, the ipadSales, the iponeSales, and the macSales make each one a distinct serie.
Progress
Progress is a replacement for the native <progress> element. It displays a linear bar. It uses a regular div under the hood.
We may opt for a striped bar, or even an animated striped bar.
simple progress bar
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} />
compound progress bar • segmented bar
We may have several segments.
- The container is
Progress.Root - Each segment is a
Progress.Section - We may add a segment label with
Progress.Label
We may make the label have a smart color with autoContrast.
<Progress.Root>
<Progress.Section value={20} color="indigo">
<Progress.Label>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, segmented bar.
In the simplest form, we may 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 and has a dedicated bar.
<RingProgress
sections={[{ value: 35, color: progressColor, tooltip: "35%" }]}
rootColor={trackColor}
size={30}
thickness={4}
roundCaps
/>
The Donut chart version has multiple sections:
sections={[
{ value: 40, color: 'cyan' },
{ value: 15, color: 'orange' },
{ value: 15, color: 'grape' },
]}
Avatar
The Avatar component may work with the user profile's image or may generate an avatar based on its name's initials. In absence of valid image or name, it falls back to a generic user icon automatically.
The border is round by default, as it sets the border-radius to 1000px.
The avatar size is 36px by default.
Mantine ensures the avatar remains square, cropping the image if needed, and that it covers the whole area, thanks to object-fit: cover.
provide an image
<Avatar src={imageURL} alt="John's portrait" />
provide a name
<Avatar name={name} />
request a generic icon intentionally
if we omit src and name, we always get a generic icon.
<Avatar />
Notifications
setup
npm install @mantine/notifications
import "@mantine/notifications/styles.css" // here
<MantineProvider>
<Notifications />
{/* Your app here */}
</MantineProvider>
import { notifications } from "@mantine/notifications"
create notification
we create a notification. We may set:
- an
id, if we intend to update this notification later on loadingto display a spinnerautoCloseto false, to keep the notification on-screen until dismissed. By default, the notification closes automatically. We may set the delay by providing a duration in milliseconds.positionto set it somewhere else than bottom-right.
message is required, while title is optional.
notifications.show({
id: task.id,
color: "white",
title: "Generating...",
message: task.prompt,
autoClose: false,
loading: true,
position: "top-right",
radius: "xl",
withCloseButton: false,
})
update notification with id
If the update indicates completion, we may show a completion icon, provide a completion text and plan for an auto-close.
notifications.update({
id: task.id,
color: "gray",
title: "Image successfully created",
message: task.prompt,
icon: <IconCheck size={18} />,
loading: false,
autoClose: 2500,
radius: "xl",
})
useForm
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's useForm provides a convenient way to specify the form's initial data and the validation and/or transformation to perform on submit.
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
useLocalStorage exposes an API similar to useState, but uses the local storage as a single source of truth. It requires a key that describes the local storage entry, and a defaultValue for when such entry is missing.
const config = {
key: "default-ai-provider",
defaultValue: "GPT",
}
synchronous version (opt-in)
When the component mounts, the hook attempts to read a persisted value from the local storage, and initializes the state with it. If the entry is missing, it creates one with defaultValue, and initialize the state with it.
const config = {
// ..
// opt-in for the synchronous version
getInitialValueInEffect: false,
}
asynchronous version (default)
In the asynchronous version, the hook immediately initializes the state with the default value, to avoid delaying the first paint. The local storage lookup is implemented as an effect that runs later on, after the first paint. Only then it may push the persisted value to the state.
Having the first paint ignoring the persisted value is not always desirable, that's why we may opt-in for the synchronous version.
exposed API
The hook exposes the state value and the dispatch method. It also exposes a method to remove the entry:
const [provider, setProvider, removeEntry] = useLocalStorage<AIProvider>(config)
The hook listens for changes in state and updates the local storage accordingly.
useDisclosure
A wrapper around useState<boolean>. It exposes a boolean state variable and provides helper methods which use terminology related to UI elements being opened or closed, such as open() and close(), in place of setIsOpened(true/false).
const [opened, { open, close, toggle }] = useDisclosure(false)
useMantineColorTheme
We want to read the current colorScheme, or set it to a value: dark or light.
Mantine saves the selected colorScheme in localStorage.
Before the App appears on-screen, Mantine checks the localStorage for any saved colorScheme, so that the first paint is done with the correct colorScheme.
const { colorScheme, setColorScheme } = useMantineColorScheme()
// also: toggleColorScheme and clearColorScheme