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.
  • overriding style-primitives, affecting all components that depend on it:

    • We edit theme properties in createTheme({})
    • or we edit Mantine's CSS variables directly

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, sm or indigo. 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

PropCSS Propertyresolved alias
mmargintheme.spacing
mtmarginTop-
mbmarginBottom-
mlmarginLeft-
mrmarginRight-
mxmarginRight, marginLeft-
mymarginTop, marginBottom-

padding

PropCSS Propertyresolved alias
ppaddingtheme.spacing
ptpaddingTop-
pbpaddingBottom-
plpaddingLeft-
prpaddingRight-
pxpaddingRight, paddingLeft-
pypaddingTop, paddingBottom-

typography

PropCSS Propertyresolved alias
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration

size

PropCSS Propertyresolved alias
wwidththeme.spacing
miwminWidth-
mawmaxWidth-
hheight-
mihminHeight-
mahmaxHeight-

position

PropCSS Property
posposition
toptop
leftleft
bottombottom
rightright
insetinset

other props

PropCSS Propertyresolved alias
displaydisplay
flexflex
bdborder
bdrsborderRadius
bgbackgroundtheme.colors
bgszbackgroundSize
bgpbackgroundPosition
bgrbackgroundRepeat
bgabackgroundAttachment
ccolortheme.colors gray.5 blue
opacityopacity

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.

See also docs for v7 and v9.

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 w and h, 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

TokenDescriptionHex Value
gray.0white#F8F9FA
gray.1white#F1F3F5
gray.2white gray#E9ECEF
gray.6dim gray (secondary)#868e96
gray.7dim gray (secondary)#495057
gray.8very dark gray#343A40
gray.9very 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.

TokenLookAliasValueValueWhite %
dark.0whitishtext#c9c9c9#c979%
dark.1whitish#b8b8b8#b872%
dark.2dimmeddimmed#828282#8251%
dark.3dimmedplaceholder#696969#6941%
dark.4#424242#4226%
dark.5darkdefault-hover#3b3b3b#3b23%
dark.6darkdefault#2e2e2e#2e18%
dark.7darkbody#242424#2414%
dark.8black#1f1f1f#1f12%
dark.9black#141414#148%

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.
  • dimmed text is dark 2. It's used for secondary text and labels.

v6 values (slate)

The v6 palette had a blue tint:

Tokenv6 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: dark or light (see below).

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 suffixesvaluenote
--mantine-color-bright--mantine-color-whitetext
-text--mantine-color-dark-0text
-dimmed--mantine-color-dark-2text
-placeholder--mantine-color-dark-3text
-body--mantine-color-dark-7background
-anchor--mantine-color-blue-4link
-error--mantine-color-red-8form 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 suffixesvaluenote
default-color--mantine-color-whitetext
default--mantine-color-dark-6background
default-hover--mantine-color-dark-5background
default-border--mantine-color-dark-4border

color: "dark" aliases

it comes with values for several variants. It has no border.

semantic suffixesvalue
dark-text--mantine-color-dark-4
dark-filled--mantine-color-dark-8
dark-filled-hover--mantine-color-dark-7
dark-lightrgba(36, 36, 36, 0.15)
dark-light-hoverrgba(36, 36, 36, 0.2)
dark-light-color--mantine-color-dark-3
dark-outline--mantine-color-dark-4
dark-outline-hoverrgba(36, 36, 36, 0.05)

color: "gray" aliases

it comes with values for several variants.

suffixvalue
gray-text--mantine-color-gray-4
gray-filled--mantine-color-gray-8
gray-filled-hover--mantine-color-gray-9
gray-lightrgba(134, 142, 150, 0.15)
gray-light-hoverrgba(134, 142, 150, 0.2)
gray-light-color--mantine-color-gray-3
gray-outline--mantine-color-gray-4
gray-outline-hoverrgba(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 direction to 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 gap between 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 grow variant 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 preventGrowOverflow to 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.

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 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.

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:

  • collapsed receives 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.
  • breakpoint controls 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.Tab and Tabs.Panel share the same value.
  • (optional) We make the list mobile-friendly with Scroller: we wrap the .Tab components 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 the display=none HMTL attribute.

  • The keepMountedMode="display-none" option is special because, when combined with the default keepMounted={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:

  • body provides the rows: an array of arrays (an item is an array)
  • head provides the headers.
  • The caption property 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 leftSection and rightSection.
  • Mark the input as required. Mantine automatically adds an asterisk, unless we remove it with withAsterisk.
  • shrink or enlarge the input with size.

others

  • Add data-autofocus to 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 min and max. We determine how the value should clamp with clampBehavior:
    • 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

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:00 pr 16: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 label is the user-facing description: It is a string, or a React node, in particular when we want to add an icon.
  • The value is 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 with loaderProps (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 total number of pages
  • the value of the cursor aka selected page number.
  • the onChange handler, 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 md
  • radius, 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
  • radius adheres 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 of Menu.Item which are <button> under the hood.
  • We can separate sections with Menu.Divider. We can label section 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 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 16px and the line-height to 1.55 (using sm for both). We can revert those changes by adding the inherit prop 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 100
  • size: the vertical size, aka how thick it is. Defaults to 8px through default md.
  • radius: defaults to 4px through default sm. The default size and radius lead to a perfect semi circle border.
  • color: defaults to blue
  • animated: 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 with autoContrast.
<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
  • loading to display a spinner
  • autoClose: we keep the notification on-screen until dismissed (autoClose: false), or set an auto-close timer in milliseconds.
  • position to 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.

earlymorning logo

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.
  • overriding style-primitives, affecting all components that depend on it:

    • We edit theme properties in createTheme({})
    • or we edit Mantine's CSS variables directly

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, sm or indigo. 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

PropCSS Propertyresolved alias
mmargintheme.spacing
mtmarginTop-
mbmarginBottom-
mlmarginLeft-
mrmarginRight-
mxmarginRight, marginLeft-
mymarginTop, marginBottom-

padding

PropCSS Propertyresolved alias
ppaddingtheme.spacing
ptpaddingTop-
pbpaddingBottom-
plpaddingLeft-
prpaddingRight-
pxpaddingRight, paddingLeft-
pypaddingTop, paddingBottom-

typography

PropCSS Propertyresolved alias
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration

size

PropCSS Propertyresolved alias
wwidththeme.spacing
miwminWidth-
mawmaxWidth-
hheight-
mihminHeight-
mahmaxHeight-

position

PropCSS Property
posposition
toptop
leftleft
bottombottom
rightright
insetinset

other props

PropCSS Propertyresolved alias
displaydisplay
flexflex
bdborder
bdrsborderRadius
bgbackgroundtheme.colors
bgszbackgroundSize
bgpbackgroundPosition
bgrbackgroundRepeat
bgabackgroundAttachment
ccolortheme.colors gray.5 blue
opacityopacity

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.

See also docs for v7 and v9.

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 w and h, 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

TokenDescriptionHex Value
gray.0white#F8F9FA
gray.1white#F1F3F5
gray.2white gray#E9ECEF
gray.6dim gray (secondary)#868e96
gray.7dim gray (secondary)#495057
gray.8very dark gray#343A40
gray.9very 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.

TokenLookAliasValueValueWhite %
dark.0whitishtext#c9c9c9#c979%
dark.1whitish#b8b8b8#b872%
dark.2dimmeddimmed#828282#8251%
dark.3dimmedplaceholder#696969#6941%
dark.4#424242#4226%
dark.5darkdefault-hover#3b3b3b#3b23%
dark.6darkdefault#2e2e2e#2e18%
dark.7darkbody#242424#2414%
dark.8black#1f1f1f#1f12%
dark.9black#141414#148%

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.
  • dimmed text is dark 2. It's used for secondary text and labels.

v6 values (slate)

The v6 palette had a blue tint:

Tokenv6 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: dark or light (see below).

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 suffixesvaluenote
--mantine-color-bright--mantine-color-whitetext
-text--mantine-color-dark-0text
-dimmed--mantine-color-dark-2text
-placeholder--mantine-color-dark-3text
-body--mantine-color-dark-7background
-anchor--mantine-color-blue-4link
-error--mantine-color-red-8form 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 suffixesvaluenote
default-color--mantine-color-whitetext
default--mantine-color-dark-6background
default-hover--mantine-color-dark-5background
default-border--mantine-color-dark-4border

color: "dark" aliases

it comes with values for several variants. It has no border.

semantic suffixesvalue
dark-text--mantine-color-dark-4
dark-filled--mantine-color-dark-8
dark-filled-hover--mantine-color-dark-7
dark-lightrgba(36, 36, 36, 0.15)
dark-light-hoverrgba(36, 36, 36, 0.2)
dark-light-color--mantine-color-dark-3
dark-outline--mantine-color-dark-4
dark-outline-hoverrgba(36, 36, 36, 0.05)

color: "gray" aliases

it comes with values for several variants.

suffixvalue
gray-text--mantine-color-gray-4
gray-filled--mantine-color-gray-8
gray-filled-hover--mantine-color-gray-9
gray-lightrgba(134, 142, 150, 0.15)
gray-light-hoverrgba(134, 142, 150, 0.2)
gray-light-color--mantine-color-gray-3
gray-outline--mantine-color-gray-4
gray-outline-hoverrgba(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 direction to 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 gap between 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 grow variant 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 preventGrowOverflow to 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.

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 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.

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:

  • collapsed receives 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.
  • breakpoint controls 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.Tab and Tabs.Panel share the same value.
  • (optional) We make the list mobile-friendly with Scroller: we wrap the .Tab components 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 the display=none HMTL attribute.

  • The keepMountedMode="display-none" option is special because, when combined with the default keepMounted={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:

  • body provides the rows: an array of arrays (an item is an array)
  • head provides the headers.
  • The caption property 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 leftSection and rightSection.
  • Mark the input as required. Mantine automatically adds an asterisk, unless we remove it with withAsterisk.
  • shrink or enlarge the input with size.

others

  • Add data-autofocus to 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 min and max. We determine how the value should clamp with clampBehavior:
    • 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

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:00 pr 16: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 label is the user-facing description: It is a string, or a React node, in particular when we want to add an icon.
  • The value is 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 with loaderProps (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 total number of pages
  • the value of the cursor aka selected page number.
  • the onChange handler, 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 md
  • radius, 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
  • radius adheres 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 of Menu.Item which are <button> under the hood.
  • We can separate sections with Menu.Divider. We can label section 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 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 16px and the line-height to 1.55 (using sm for both). We can revert those changes by adding the inherit prop 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 100
  • size: the vertical size, aka how thick it is. Defaults to 8px through default md.
  • radius: defaults to 4px through default sm. The default size and radius lead to a perfect semi circle border.
  • color: defaults to blue
  • animated: 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 with autoContrast.
<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
  • loading to display a spinner
  • autoClose: we keep the notification on-screen until dismissed (autoClose: false), or set an auto-close timer in milliseconds.
  • position to 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.