Overview

Mantine provides an infrastructure to build React apps efficiently, with pre-styled UI components and feature-rich utilities.

benefits

  • solid UI defaults: neutral, minimalist, functional style
  • easy-to-use, consistent API (DX)
  • rapid development of utilitarian and productivity apps

mantine style is opinionated and cohesive

Mantine brings a set of cohesive style defaults.

If we aim to do a partial style override, we should make it compatible with the Mantine overall look.

Some components, such as layout components and Mantine hooks, don't have a visible style, so we can use them in any design.

mantine's packages

Mantine provides distinct packages. The main ones are Mantine core and Mantine hooks.

terminology: components and component variants

Mantine provides components, and for some of them, variants. For example, it provides the Button component, for which it provides the outline and filled variants.

terminology: root element and inner-elements

A component is constructed through a top level element (root) and several, named, inner-elements. For example, Slider is built upon a root top level element, and has various inner elements such as mark, track and bar. We can style inner-elements.

When the component is polymorphic, it has a flexible top level element, that we can set through the component prop. For example, we can change the Button top element to be <a> instead.

customization patterns

We have three main patterns:

  • (Local change) customize a single instance: the scope of the change is local and limited to that instance.
  • (Global change) override a Mantine component: it affects all its instances.
  • (Global change) override style primitives, also called theming: it affects several components and their instances.

Customize a single instance

overview

We want to customize a single instance of a component.

We add props, classes or inline style.

props that work on all Mantine component

Mantine's style props are a set of props that work on all Mantine component, and which set a single style aspect. The style targets the top-level element, not the inner-elements:

<Box m={4}></Box>

props that are specific to one or more components

Some props are unique to one or more components. They determine the style or add to the component's inner structure, such as toggling inner-elements. For example, we add a <label> element by adding the label prop to TextInput.

<TextInput label="Username"></TextInput>
// adds:
<label>Username</label>

how it works, how it resolves

The props, classes and inline style resolve to:

  • style that lives in the style attribute
  • markers set on the element, such as classes, attributes, or an id, with CSS targeting them.
  • inner DOM elements being added.

the limits of the style and className attributes, and of style props

We provide quick inline style in the style attribute or add classes to className, but there is no granularity as to which inner-element we target. The style we provide usually applies to the component's top level element.

Style props are subject to the same constraint: they usually apply to the top level element.

Instead, Mantine recommends the pattern where we can declaratively target inner-elements.

target inner-elements

We target inner-elements by specifying their name, and by providing a style object (styles attribute) or classes (classNames attribute).

 <Button
	styles={{
        root: { backgroundColor: 'red', height: 16},
        label: { color: 'blue' },
      }}
<Button
    classNames={{
        root: "pinkbutton-root",
        label: "pinkbutton-label",
    }}
/>

how to name classes (non-CSS module)

This section describes creating CSS classes manually, outside of the CSS module pipeline. Otherwise, see CSS module patterns.

The classes we provide in classNames are to customize an instance. We can think about it as creating a variant of that component, either conceptually or because we actually create a custom component. Either way, we pick a name for that variant and use it in class names that apply to it.

Since we plug classes to the inner elements they apply to, we append the inner element name to the class name for clarity.

.pinkbutton-root
.pinkbutton-label

Global customization

Global customization, also called theming, overrides style-primitives, affecting several components as side-effects, or, with a more precise reach, overrides one more specific component to affect all instances.

We customize style primitives by setting the theme or by setting CSS variables. Setting the theme ultimately sets CSS variables

customize style primitives with the theme

For example, we set the value of --mantine-font-weight-medium for the whole app through fontWeights.medium:

const theme = createTheme({
    fontWeights: {
        medium: "500",
    },
})

customize style primitives with CSS variables

:root {
    --mantine-font-weight-medium: 500;
}

override Mantine components with component-scoped empty classes

Mantine adds a series of empty classes on all components. We can implement them to customize a given aspect of that component. (Implementing such classes affects all instances)

For example, Mantine adds mantine-Button-root to the Button's root element and mantine-Button-label on the label inner-element. We implement the class if needed:

.mantine-Button-root {
    border-width: 0.5px;
}

empty classes specificity

Mantine empty classes have the same specificity than Mantine's internal classes. As such, we make sure to import our stylesheet after Mantine's one.

CSS Module patterns

CSS module primer

CSS module overview

A processor parses the stylesheet to scan classes and exposes them to JS as a key-value map. While the values are globally unique class names, we only interact with keys.

We should avoid hyphens in the key because it makes it harder to access in it JS, as JS doesn't support hyphens in variable names. We would need to quote the key name on lookup. Instead, we use camelCase for class names.

Note that Vite has an option to automatically transform class names with hyphens to camel case, but this adds a layer of indirection.

Pipeline example:

/* xxx.module.css */
.supercool {
}
.supernice {
}
.superGreat {
}
.super-awesome {
}

We import the map:

import classes from "xxx.module.css";
/* {
    supercool: "supercool_5cEkq2n0x1",
    supernice: "supernice_1kmox6oL39",
    superGreat: "superGreat_1kmox6oL39",
    "super-awesome": "super-awesome_1kmox6oL39",
} */

<Button
classNames={{
        root: classes.supercool, // "supercool_5cEkq2n0x1"
        label: classes.supernice,	// "supernice_1kmox6oL39",
        section: classes.superGreat,
    		loader: classes['super-awesome']
    }}
  >

Mantine pattern

one module for one variant

In this pattern, the module's purpose is to define the style for one Mantine component custom variant. For example, we create a PrimaryButton variant of Button. We name the CSS Module: PrimaryButton.module.css

target inner elements

We name the classes according to the inner-element we target, such as root or label:

/* PrimaryButton.module.css */
.root {
}

.root:hover {
}

.label {
}

We then apply the classes on the Mantine's instance:

import PrimaryButtonClassNames from "PrimaryButton.module.css";

  {/* verbose */}
<Button
classNames={{
        root: PrimaryButtonClassNames.root,
        label: PrimaryButtonClassNames.label,
    }}
  >

When we follow the inner-element-as-a-classname pattern, we can give the map directly to classNames, since it matches exactly what is expected by classNames:

  {/* short */}
<Button classNames={PrimaryButtonClassNames}>

Universal style props

We control various style aspects, by adding the following props to any Mantine component (instance):

spacing

PropCSS PropertyTheme key
mmargintheme.spacing
mtmarginToptheme.spacing
mbmarginBottomtheme.spacing
mlmarginLefttheme.spacing
mrmarginRighttheme.spacing
mxmarginRight, marginLefttheme.spacing
mymarginTop, marginBottomtheme.spacing
ppaddingtheme.spacing
ptpaddingToptheme.spacing
pbpaddingBottomtheme.spacing
plpaddingLefttheme.spacing
prpaddingRighttheme.spacing
pxpaddingRight, paddingLefttheme.spacing
pypaddingTop, paddingBottomtheme.spacing

typography

PropCSS PropertyTheme key
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration

size

PropCSS PropertyTheme key
wwidththeme.spacing
miwminWidththeme.spacing
mawmaxWidththeme.spacing
hheighttheme.spacing
mihminHeighttheme.spacing
mahmaxHeighttheme.spacing

position

PropCSS PropertyTheme key
posposition
toptop
leftleft
bottombottom
rightright
insetinset

other props

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

responsiveness: provide alternative values.

Instead of providing a single value, we provide an object with alternative values. Most of the time, we provide two values:

<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'sm', 	sm: 'lg' }}
>
  • the base value provides the mobile-centric, default value. Its exact scope depends on which other breakpoints we use, but it always includes regular smartphones. For reference, iPhones are under 450p width, with standard-size iPhones under 400p.
    • The xs breakpoint activates at 576p. It doesn't apply to regular smartphones but applies to phablets and bigger devices.
    • The sm breakpoints activates at 768p (48em). It doesn't apply to phablets, but applies to tablets and wider screens. For reference, iPads start at 768px.

Set default Color Scheme

Mantine defaults to light mode.

We can change it to dark or auto. The latter conforms to the user's device color scheme:

<MantineProvider defaultColorScheme="auto"></MantineProvider>

Button case study

case study: Button implementation

The Button source code includes the TypeScript file (Button.tsx) and the CSS module stylesheet (Button.module.css).

HTML structure and inner elements

There are several elements:

  • the root <button> is the top level element.
  • the loader <span> is an intermediate container for a loader.
  • the inner <span> is an intermediate container for label and sections
  • a section is a container for a side icon.
  • the label hosts the button's text.
<button {/* root */}>
    <span {/* 1.0 loader */} />
    <span  {/* 2.0 inner */}>
        <span {/* 2.1 section  */}>
            <svg /> {/* icon */}
        </span>
        <span {/* 2.2 label  */}>
          {/* text */}
      </span>
    </span>
</button>

Mantine adds its internal classes (e.g. m-77c9d27d) and the non-implemented classes (e.g. mantine-Button-root) to 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>

Mantine adds component agnostic classes, such as mantine-active and mantine-focus-auto. They are placed preemptively, and their style trigger on specific states. For example, mantine-active activates its style on the active state:

mantine-active:active {
    transform: translateY(calc(0.0625rem));
}

Mantine also adds data-attribute for styling purpose, such as data-centered="true".

.m_a3c6e060[data-centered] {
    display: flex;
    justify-content: center;
    align-items: center;
}

CSS module source code

The stylesheet targets inner-elements with classes of the same name. For example:

.loader {
  position: absolute;
  left: 50%;
  top: 50%;
}

The .tsx file imports the classes object and plugs it to Button.classes.

style shipped on npm

The style shipped on npm has been processed by PostCSS. It doesn't use CSS nesting.

Mantine gathers all CSS file into a single 232kb stylesheet. Mantine also provides smaller stylesheets, that focus on one or more components.

Default theme

The default theme (v7 or v8) spreads over several categories.

spacing

Space between elements and space inside elements. It is used for:

  • gap, for Group, Flex, Stack.
  • padding props.
  • margin props, for example for Divider
  • width and height props such as w and h, for example for Space.
xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32p

radius

Border radius used for:

  • Paper
  • Dialog and Modal
  • Button and Tooltip

default radius:

"defaultRadius": "sm"

actual values:

xs: 2px
sm: 4px // (d)
md: 8px
lg: 16px
xl: 32px

breakpoints

A set of thresholds, that we target to activate style conditionally. We should keep those thresholds as-is:

  "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

A set of 5 sizes:

"fontSizes": {
    "xs": 12px
    "sm": 14px
    "md": 16px,
    "lg": 18px
    "xl": 20px
  },

Those sizes do not affect headings. We use them in:

  • the fz style prop.

line heights

  "lineHeights": {
    xs: 1.4,
    sm: 1.45,
    md: 1.55, // (d)
    lg: 1.6,
    xl: 1.65
  },

defaults to 1.55

--mantine-line-height: 1.55;

headings

family, weight and wrap behavior:

"headings": {
    "fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji",
    "fontWeight": "700",
    "textWrap": "wrap",
  },

size and line height:

  "sizes": {
      "h1": {
        "fontSize": "calc(2.125rem * var(--mantine-scale))",
        "lineHeight": "1.3"
      },
      "h2": {
        "fontSize": "calc(1.625rem * var(--mantine-scale))",
        "lineHeight": "1.35"
      },
      "h3": {
        "fontSize": "calc(1.375rem * var(--mantine-scale))",
        "lineHeight": "1.4"
      },
      "h4": {
        "fontSize": "calc(1.125rem * var(--mantine-scale))",
        "lineHeight": "1.45"
      },
      "h5": {
        "fontSize": "calc(1rem * var(--mantine-scale))",
        "lineHeight": "1.5"
      },
      "h6": {
        "fontSize": "calc(0.875rem * var(--mantine-scale))",
        "lineHeight": "1.5"
      }
    }

Gray Palette

overview

Mantine's gray is a slate gray palette (blue tint), and is designed for light mode:

  • It offers several whites for backgrounds (but doesn't include pure white, instead, it starts at #F8F9FA.)
  • It offers a few dark grays for text. It doesn't include pure black.
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

note on light mode use

The following consideration usually apply for light-mode:

  • The background can be pure white. It is the site global background or some controls background.

  • The background can go darker on hover. This is the case for buttons, and for the select's options: gray.1

  • The text is black-ish or gray-ish depending on 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:
      • some headings
      • body text
      • input label
      • button label

Dark Palette

overview

Mantine's dark is a neutral gray palette. It used to be a slate gray palette (blue tint) before Mantine 7.3.

It is designed for dark mode, and Mantine dark color scheme uses it. It offers several darks for backgrounds, grays for controls, and whites for text.

TokenLookAliasValueValue%
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 dark mode

  • The app background is dark 7. We may go darker with dark 8.
  • Controls have lighter backgrounds: dark 6, and even lighter on hover: dark 5. Sometimes, hover goes to a darker background instead: such as the hovered option in a select control: dark 8.
  • The text is light. It uses one of those colors:
    • pure white (bright white for some headings)
    • dark 0 (readable white for body text)
    • dark 1 (slightly dimmed)
    • dark 2 (dimmed, secondary text, matches c=dimmed)
  • The button label is pure white, both for filled and default variants.
  • The input's label is dark 0, the input's description is dark 2.
  • The dimmed text is dark 2, such as secondary label, or secondary text in general.

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",
        ],
    },
})

Colors and color schemes

dark color scheme

We study the CSS variables and their default value for the Mantine dark color scheme (dark theme).

Note: the CSS variables have the --mantine-color prefix, such as --mantine-color-bright, where bright is the suffix. We trimmed the prefix from the table for readability.

Some colors only trigger when we use the given variant or color prop, for example the default variant or the gray color. The general values apply when not overridden by a variant or a color.

general values

suffixvalue
schemedark
bright--mantine-color-white
text--mantine-color-dark-0
dimmed--mantine-color-dark-2
placeholder--mantine-color-dark-3
body--mantine-color-dark-7
anchor--mantine-color-blue-4
error--mantine-color-red-8
red-text--mantine-color-red-4

variant: "default"

note: it comes with a border.

While the default for hover is dark-5, we can also use dark-4 or dark-6.

suffixvalue
default-color--mantine-color-white
default--mantine-color-dark-6
default-hover--mantine-color-dark-5
default-border--mantine-color-dark-4

color: "dark"

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

suffixvalue
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"

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)

Flex

Use a bare-bones flex container, with nothing set except display: flex.

override CSS defaults

  • Switch the direction, which is horizontal (row) by default, to vertical (column).
  • 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

Layout elements horizontally and add space between them. The container spans the whole line. It uses a horizontal flex under the hood.

The differences with a stock horizontal flex:

  • It adds some gap between elements (defaults to md, aka 16px).
  • It centers elements vertically, with align, instead of stretching them.
  • It wraps when needed (multi-line flex) (We can set wrap to mono-line (nowrap) or multi-line (wrap), which sets the underlying flex-wrap).

the grow variant (override)

  • Make children fill the container with grow.

    • It sets flex-grow: 1 on children.
    • By default, it makes them same-width (by capping them to the same max-width), and pack them on a single line (since the max-width is a share of the line, not because of disabling wrap).
    • We can reestablish multi-line by turning off capping (set preventGrowOverflow to false): it now goes to the next line since wrapping was never disabled, but only if the elements do not fit on a single line (do not use)
    • If we also 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

  • Pick horizontal alignment 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 still auto). It uses a vertical flex under the hood. Items default to spread horizontally within it.

Differences with a stock vertical flex:

  • It adds a gap (16px by default).

Other settings to override CSS defaults:

  • justify: elements sit at the top by default (flex-start)
  • align: elements fill the stack horizontally by default (stretch)

SimpleGrid

Set up a column layout with a given number of equal-width columns:

<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 horizontal and vertical spacing aka spacing and verticalSpacing).

We set the number of columns with cols. It defaults to a single track. Best Practice: adapt the column count based on screen size, providing at least two values:

<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>{/* items */}</SimpleGrid>

Container

A wrapper that centers the content and keeps it aways from screen edges. For example, we wrap the main column of a website, to have padding on mobile and centering on desktop.

max-width version

The default version comes with a max-width (for desktop) and some padding (for mobiles). It centers the content with margin: auto.

The max-width defaults to md (aka 960px). We change it with size. For example, we can pick 720px (sm) instead. If we are to provide an arbitrary numeric value, we may as well set maw directly to remove indirection.

The default padding is 16px, aka md. We change it with the regular px style prop.

fluid version

We may opt for a fluid container, which expands horizontally, with the fluid prop. It only comes with some padding, at 16px. A Box with horizontal padding has the same effect.

grid version

The grid version doesn't use padding and doesn't center with max-width. Instead, it sets a grid layout and center stuff with grid positioning.

Center

Center the child or the group of children horizontally and vertically.

Under the hood, it uses a flex container that justify and align the group of children to the center.

vertical-centering and height

Vertical centering will only come into play if Center's height is bigger than the inner content.

horizontal-centering and width.

The container flex spreads horizontally (width: auto), but compacts its children to max-content. We can prevent compaction by setting the group of children to width: 100%.

Divider

Separate items or sections with a visible line, similar in essence to the <hr> element.

Under the hood, it is an empty element whose border acts as the visible line.

  • The line is horizontal by default, we can set it to vertical with orientation.
  • We set the line thickness with size. It defaults to 1px.
  • The line is solid by default. We switch to dashed or dotted with variant.
  • The line doesn't add margin around itself by default. We add margin with my or mx.
  • The line's color defaults to:
    • --mantine-color-dark-4: #424242; in dark mode (dark gray)
    • --mantine-color-gray-3: #dee2e6; in light mode (light gray)

Space

Separate items with some white space. Mantine uses an empty element with some explicit size. It defaults to a zero size. We set the height or the width:

  • Set vertical spacing with h, usually in a stack-like layout.
  • Set horizontal spacing with w, usually in a horizontal flex-like layout.

Paper

Encapsulate some UI to set it apart visually.

The Paper's default background is the same as the body's default background (--mantine-color-body).

  • In light mode, it is white #FFF.
  • In dark mode, it is dark.7.

We can override's Paper background with bg.

Paper also adds a radius (theme.defaultRadius) which defaults to 4px.

Paper specific customizations

  • Add a pre-styled border with withBorder (boolean)
  • Add a pre-styled shadow with shadow. We specify a named size (e.g. sm), or a box-shadow CSS literal.

Accordion

Establish one or more expandable items that disclose more content when interacted with.

item: show a preview and enable disclosure of a panel

The Accordion contains one or several items. An item starts as a clickable preview (Accordion.Control) and embeds a panel, that starts as hidden, and that is toggled on click.

Mantine mounts all panels immediately, regardless of their toggle state. The panel may be a form. In that case, we can set the chevron as a plus sign, to signal we add some data.

We set the chevron style at the Accordion root element's level, since all items use it.

<Accordion variant="separated" chevron={<IconPlus />}>
    <Accordion.Item value="item1">
        <Accordion.Control>Log Workout</Accordion.Control>
        <Accordion.Panel>Content</Accordion.Panel>
    </Accordion.Item>
</Accordion>

items appearance

By default, items are separated by a thin line. We can set variant to separated instead, or other variants.

Appshell

Appshell is a mega layout component that manages:

  • The header on-top of the page, also called the banner.
  • The main section, below the header
  • The left-side navbar and the right-side aside (if applicable)

header: Appshell.Header

The header manages:

  • the burger button
  • the logo
  • the dark mode / light mode toggle (potentially)
  • the user button (potentially)
  • a set of links (potentially)

configure the header

We build a header config and provide it to AppShell as a header prop. We commonly set the height.

<AppShell /* ... */ header={{ height: 60 }} />

navbar: Appshell.Navbar

The navbar stands on the left side. It is transient on mobile devices. On desktop it is commonly always-on, but Mantine supports having a conditional collapse as well, following a distinct logic and state variable.

We build a navbar config and provide it to AppShell as a navbar prop. collapsed aims to control the display of the navbar, while breakpoint sets the breakpoint from which we go from mobile to desktop logic.

<AppShell /* ... */ navbar={{}} />
  • collapsed receives up to two boolean state variables: one for mobile and one for desktop. Most of the time, we only set mobile.
  • 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. We hide the mobile burger button with hiddenFrom. If desktop supports collapse too, we use a distinct burger button (see below).
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()

return (
    <AppShell
        navbar={{
            collapsed: { mobile: !opened },
            breakpoint: "sm",
        }}
    >
        <AppShell.Header>
            <Burger opened={opened} onClick={toggle} hiddenFrom="sm" />
        </AppShell.Header>
    </AppShell>
)

navbar: allow collapse on desktop

We may allow the collapse behavior on Desktop as well: we create an additional, distinct boolean state variable, and provide it to the desktop property of collapsed. We also add a dedicated desktop burger button, which commonly keeps the same appearance regardless of the collapse state (we don't show a close icon because there is no overlay). This burger button limits its display to desktop with visibleFrom.

const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(false)
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)

return (
    <AppShell
        navbar={{
            collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
            breakpoint: "sm",
        }}
    >
        <AppShell.Header>
            {/* mobile */}
            <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" />
            {/* desktop */}
            <Burger opened={false} onClick={toggleDesktop} visibleFrom="sm" />
        </AppShell.Header>
    </AppShell>
)

navbar: other configuration

navbar={{ width: 300, /* ... */ }}
  • width controls the width in desktop mode (it is always full-screen in mobile). We provide an immediate value or an object with two or more values.

navbar: implement top and bottom sections

We may split the navbar into AppShell.Sections, and one of them may grow.

<AppShell.Navbar>
    <AppShell.Section grow> </AppShell.Section>
    <AppShell.Section> </AppShell.Section>
</AppShell.Navbar>

Appshell.Main

The container for the main panel.

We may set the panel background color. The default light theme sets a white background while the dark theme sets a dark.7 background.

Override example: we may pick a light gray background such as gray.1, so that inner containers may stand out as white. Similarly, we may change the background to dark.8 so that inner elements may standout as dark.7.

const backgroundColor = colorScheme === "dark" ? "dark.8" : "gray.1"

synopsis

the configuration objects are required if we use the matching element, except for main which may not be configured with this pattern.

<AppShell header={{}} navbar={{}} aside={{}} footer={{}}>
    <AppShell.Header />
    <AppShell.Navbar />
    <AppShell.Aside />
    <AppShell.Main />
    <AppShell.Footer />
</AppShell>

Tabs

Table data

abstract

A table works with an array of items, where each item is a row, and each property or member is a cell.

An item may come as an object or as an array.

pattern: the item is an object

const items = [
    { position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
    { position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
    { position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
    { position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
    { position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
]

pattern: the item is an array

const items = [
    [6, 12.011, "C", "Carbon"],
    [7, 14.007, "N", "Nitrogen"],
    [39, 88.906, "Y", "Yttrium"],
    [56, 137.33, "Ba", "Barium"],
    [58, 140.12, "Ce", "Cerium"],
]

We may come with this data by transforming an array of objects to an array of arrays:

const items = players.map((p) => [p.name, p.level, p.class])

headers

An array usually comes with headers. We may provide them in an array.

const headers = ["Element position", "Atomic mass", "Symbol", "Element name"]

Table with explicit structure

structure preview

<Table>
    <Table.Thead>
        {/* head */}
        <Table.Tr>
            <Table.Th>{col1Label}</Table.Th>
            <Table.Th>{col2Label}</Table.Th>
            <Table.Th>{col3Label}</Table.Th>
        </Table.Tr>
    </Table.Thead>

    {/* body */}
    <Table.Tbody>
        <Table.Tr>
            <Table.Td>{item1.name}</Table.Td>
            <Table.Td>{item1.level}</Table.Td>
            <Table.Td>{item1.class}</Table.Td>
        </Table.Tr>
        {/* ... more rows */}
    </Table.Tbody>
</Table>

build rows from an array of objects

const rows = items.map((item) => (
    <Table.Tr>
        <Table.Td>{item.name}</Table.Td>
        <Table.Td>{item.level}</Table.Td>
        <Table.Td>{item.class}</Table.Td>
    </Table.Tr>
))

build rows from an array of arrays

const rows = items.map((item) => (
    <Table.Tr>
        <Table.Td>{item[0]}</Table.Td>
        <Table.Td>{item[1]}</Table.Td>
        <Table.Td>{item[2]}</Table.Td>
    </Table.Tr>
))

headers and body

Thead is the container for the headers.

<Table.Thead>
    <Table.Tr>
        <Table.Th>{col1Label}</Table.Th>
        <Table.Th>{col2Label}</Table.Th>
        <Table.Th>{col3Label}</Table.Th>
    </Table.Tr>
</Table.Thead>

Tbody is the container for rows.

<Table.Tbody>{rows}</Table.Tbody>

Table from data prop

We provide the complete data as a single object for a single prop:

  • The caption property defines the table's title
  • The head property is the array of headers.
  • The body array is the list of items. Each item is an array with bare values
const tableData: TableData = {
    caption: "Some elements from periodic table",
    head: ["Element position", "Atomic mass", "Symbol", "Element name"], // headers
    body: [
        // items
        [6, 12.011, "C", "Carbon"],
        [7, 14.007, "N", "Nitrogen"],
        [39, 88.906, "Y", "Yttrium"],
        [56, 137.33, "Ba", "Barium"],
        [58, 140.12, "Ce", "Cerium"],
    ],
}

data prop

We provide such object to the data prop. Mantine then creates the markup automatically.

<Table data={tableData} />

TextInput

TextInput is an abstraction over <input type=text>. It inherits most props from <Input>. We describe all props here.

controlled:

<TextInput
    value={summary}
    onChange={(event) => setSummary(event.currentTarget.value)}
    data-autofocus
    label="Summary"
    placeholder="What did you work on?"
/>

uncontrolled:

<TextInput
    type="email"
    ref={mailInput}
    required
    label="Email"
    placeholder="john-appple@example.com"
/>

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.

NumberInput

NumberInput is a <input type=text> under the hood. This enables more freedom into what is displayed in the input. For example, we can display formatting commas.

The value we get is not consistently a number. For example, we get an empty string when the input is empty.

options

  • Forbid decimal numbers with allowDecimal. They are allowed by default.
  • Forbid negative numbers with allowNegative. They are allowed by default.
  • Clamp the value between min and max. Determine how the value should clamp with clampBehavior:
    • Forbid values outside the limits at all times with strict.
    • Clamp after input on blur. This is the default.
    • Never clamp (none)

As a text input, it supports a label, a description (secondary label) and a placeholder.

state

Work with the string and number type union:

const [value, setValue] = useState<string | number>("")
return <NumberInput value={value} onChange={setValue} />

Work with a number only. In this case, we only update state if the value is a number. When the NumberInput is empty, the state may have a ghost value. We clear the ghost value on blur if the field is empty.

const [value, setValue] = useState<number>(0)

return (
    <NumberInput
        value={value}
        onChange={(value) => {
            if (typeof value === "number") setValue(value)
        }}
        onFocus={(input) => {
            /* clear field to prepare for input if value was 0 */
            if (input.target.value === "0") {
                input.target.value = ""
            }
        }}
        onBlur={(input) => {
            /* reset field to 0 if empty on leaving the field */
            if (input.target.value === "") {
                onChange(0)
            }
        }}
    />
)

FileInput

FileInput is a control that triggers the OS' file selection dialog. It shows information about the current selection. We can customize the flow:

  • Allow selecting multiple files at once with multiple. It defaults to false.
  • Restrict selection to a given file type in accept, such as "image/png"
  • Add a clear button on the right with clearable
  • Work with the selected file by providing a handler to onChange. The handler receives a File object (or a File array). The handler is called when users close the platform's native File picker or clear the selection.

Other techniques

  • Limit the input's width to truncate long file names with the regular maw prop
  • Add a file icon to the leftSection.
  • Opt for the unstyled variant which looks similar to a transparent variant: there is no border or background fill.
  • Shrink the control with size.

FileInput inherits props from Input

<FileInput
    size="xs"
    maw={150}
    onChange={onChange}
    placeholder="Reference Image"
    leftSection={<IconPhoto size={18} stroke={1.5} />}
    clearable
    variant="unstyled"
/>

Native Select

An abstraction over a <select> that embeds some <option> elements.

We collect options in an array, and assign them to the data attribute. We describe each option with a label and a value.

const options = [
    { label: "Death Knight", value: "deathKnight" },
    { label: "Demon Hunter", value: "demonHunter" },
]
<NativeSelect data={options} />

If the value works as a label, such as when we want to surface the value in the UI, we can replace the objects with a single string instead. The string acts both as the value and as the label.

const options = ["deathKnight", "demonHunter"]

SegmentedControl

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.

We describe the options in data:

  • The option's label is the user-facing description: It is a string or a React node.
  • The option's value is its identifier. The state holds this identifier when the option is selected. A type union lists the possible identifiers.

We set the initial selection in value.

onChange is called on selection change attempt. The selection changes only if we mutate the state.

type AIModel = "GPT" | "IMAGEN"
const [model, setModel] = useState<AIModel>("GPT")

<SegmentedControl<AIModel>
    value={model}
    onChange={(v) => setModel(v)}
    data={[
        { label: GPTLabel, value: "GPT" },
        { label: IMAGENLabel, value: "IMAGEN" },
    ]}
    // size="sm"
    // radius="md"
    // bg={"transparent"}
/>

Button

An abstraction over a <button>.

button specifics

  • Pick the overall appearance with variant:

    • filled uses bright colors. (default)
    • light uses toned down colors.
    • outline
    • subtle
    • gradient
    • default
  • Pick the color tint. It defaults to the theme's primaryColor, aka Mantine's blue.

  • Opt for a fluid button (fill the container) with fullWidth.

  • Opt for compact version with size, and control the font size.

  • Show the loading spinner appearance (and disable the button) with loading. We customize the loader with loaderProps. (see Loader component)

  • disabled, sets the appearance and behavior to disabled. the disabled appearance is the same across all variants.

  • gradient, sets the gradient's colors and direction. It requires using the gradient variant.

other options

  • leftSection, rightSection, for example for an icon.
<Button rightSection={<IconDownload size={14} />}>Download</Button>

Button not submitting form by default

Mantine adds type=button on the underlying <button>. It prevents the button from triggering the form submit, when pressed inside a <form>. We set type=submit if needed.

providing an onClick handler

The handler specifies which action to perform on click. This action usually does not depend on any information related to the click event.

Still, the handler receives the event as an argument. If we define the handler somewhere else than inline, we must indicate its typing. The argument it takes is of type Event.

More specifically, the event is of type React.MouseEvent<HTMLButtonElement >.

The HTMLButtonElement type parameter indicates the type of the target of the event, which in this case is a <button> element.

The handler is not supposed to return anything.

Handler type signature

onClick: React.MouseEvent<HTMLButtonElement> => void

shortcut type

onClick: React.MouseEventHandler<HTMLButtonElement>

Button.Group

We create a button cluster that glues up the buttons horizontally (default), or vertically.

<Button.Group>
    <Button variant="default">First</Button>
    <Button variant="default">Second</Button>
    <Button variant="default">Third</Button>
</Button.Group>

ActionIcon

icon button

ActionIcon serves the "icon as a button" pattern. UI-wise, the idea is that the icon is expressive enough to be self-explanatory, with no need for accompanying text, for example the "trash" icon refers to the "delete" action.

Technically it is a container that is distinct from the icon, that must be added as a child.

It gives its child icon the behaviour of a button, and it customizes its appearance according to the selected variant.

  • variant: same variants as Button's variants. To get a naked icon effect, we select subtle or transparent.
  • size: the container's size impacts the hover and active zone, and the icon scale.

required child icon

As a wrapper, it impacts the concrete child icon.

We may still set size directly on the child Icon.

<ActionIcon variant='default'>
	<IconMail size={14} stroke={..?}/>
</ActionIcon>

Pagination

we provide:

  • the total number of pages
  • the value of the cursor aka selected page number.
  • the onChange handler
  • (optional) withControls, to show previous and next arrows, defaults to true.
  • (optional) withEdges, to show first and last arrows, default to false.
  • (optional) size, to get bigger or smaller pagination control, defaults to md
  • (optional) radius, to get rounder or squarer controls, defaults to md
const [activePage, setActivePage] = useState<number>(1)
<Pagination total={10} value={activePage} onChange={setActivePage} />

Modal

Overlay some UI on top of the application. Dim the rest of the app while opened.

<Modal opened={opened} onClose={close}>
    {/* Modal content */}
</Modal>

Modal works with a boolean state variable to control the display (plugged on the opened prop).

Modal also works with an onClose handler, called on dismissal attempts, and responsible for toggling back the state to closed, or for denying the dismissal.

reminder: Mantine's useDisclosure provides a boolean state variable, and open and close state-mutation functions, which are fitting for a modal:

const [opened, { open, close }] = useDisclosure(false)

The following actions qualify as attempts to dismiss:

  • press the escape key
  • click outside
  • click the close button (if displayed)

We usually open the modal with a button:

<Button onClick={open}>Open modal</Button>

The inner content is commonly form elements: some inputs and a submit button.

defaults

  • the modal has a top section with a close button
  • radius adheres to theme.defaultRadius which defaults to 4px.

overrides

  • remove the close button with withCloseButton, which can remove the header altogether.
  • center the modal with centered.
  • add a title, which can be any React node (not text-only).
  • control the dimming with overlayProps. The dimming is done by an underlying <Overlay>.
  • more overrides

Menu

A menu that gathers a set of controls, that we display conditionally. Menu manages the opened state automatically.

The user toggles the menu by clicking the control that lives in Menu.Target.

We add a set of Menu.Item which are <button> under the hood.

We may separate two different sections with Menu.Divider. We may give a section a label with Menu.Label.

overall structure

<Menu shadow="md">
    <Menu.Target>
        <Button>Toggle menu</Button>
    </Menu.Target>
    <Menu.Dropdown>/* */</Menu.Dropdown>
</Menu>

menu proper

<Menu.Dropdown>
    <Menu.Label>Application</Menu.Label>
    <Menu.Item leftSection={<IconSettings size={14} />}>Settings</Menu.Item>
    <Menu.Item leftSection={<IconMessageCircle size={14} />}>Messages</Menu.Item>
    <Menu.Divider />
    <Menu.Label>Danger zone</Menu.Label>
    <Menu.Item leftSection={<IconArrowsLeftRight size={14} />}>Transfer</Menu.Item>
    <Menu.Item leftSection={<IconTrash size={14} />}>Delete</Menu.Item>
</Menu.Dropdown>

Title

Thin abstraction over a heading element. It uses a HTML heading under the hood, h1 by default.

It removes the user agent stylesheet's default margin by setting them to 0. It keeps the bold font weight.

The font size, font weight, and line height, come from theme.headings.

The default theme defines five font sizes:

  • h5 comes at 16px
  • h4 comes at 18px
  • h3 comes at 22px
  • h2 comes at 26px
  • h1 comes at 34px

We set the heading as follows:

  • order: pick the underlying html element, such as 1 for h1. It defaults to h1. Affects the font-size by default.
  • size: override the font-size caused by order. we pick a size as a number or as a heading in the heading hierarchy such as h5.
  • lineClamp: set the maximum number n of lines. If not set, the browser allow multiple lines for a heading element. If truncation happens, it shows an ellipsis thanks to text-overflow: ellipsis.
<Title order={3} size={"h5"}>
    Daybreak
</Title>

Text

A container for some text content.

It is a <p> element under the hood by default, but we may set it to be another element such as an anchor tag or a span with the component prop.

It removes default margins by setting the (vertical) margin to 0.

It comes with some style affecting the font size, the font family and the line height, which is 1.55 by default. We may set it to inherit all those styles instead by adding the inherit prop.

<Text c="dimmed">Dimmed text</Text>

Box

A neutral wrapper, similar in essence to div or span, which may affect its children through Mantine style props:

<Box fz={12}>/* */</Box>

Bar Chart

packages

@mantine/charts
recharts

stylesheet

import "@mantine/charts/styles.css"

bar-chart logic

for each index, display one or several bars. We assume one bar for each index.

data for a given index

The chart library expects an object for each index. The object contains raw data. Among this data, we select which property acts as the label. The remaining properties may act as magnitude.

{month: "Jan", workouts: 17},
{month: "Feb", workouts: 18}

indicate data's shape

We indicate:

  • the property that acts as the label, here month
  • for each property that we want to act as a magnitude, we indicate: the property's name (here workouts) the color for that bar, and the label for that bar.

In the following example, workouts provides the magnitude for the unique serie.

<BarChart
    data={}
    h={300}
    dataKey="month" // key that provides label
    series={[
        // bar(s) configuration
        { name: "workouts", color: "blue.6", label: "Workouts" },
    ]}
/>

Each magnitude's property, as it spreads over all the indexes, makes a distinct serie. For example, the ipadSales, the iponeSales, and the macSales make each one a distinct serie.

Progress

Progress is a replacement for the native <progress> element. It displays a linear bar. It uses a regular div under the hood.

We may opt for a striped bar, or even an animated striped bar.

simple progress bar

  • value: from 0 to 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} />

compound progress bar • segmented bar

We may have several segments.

  • The container is Progress.Root
  • Each segment is a Progress.Section
  • We may add a segment label with Progress.Label

We may make the label have a smart color with autoContrast.

<Progress.Root>
    <Progress.Section value={20} color="indigo">
        <Progress.Label>Documents</Progress.Label>
    </Progress.Section>
    <Progress.Section value={50} color="orange">
        <Progress.Label>Photos</Progress.Label>
    </Progress.Section>
</Progress.Root>

RingProgress

RingProgress displays a circular, segmented bar.

In the simplest form, we may use it as a circular progress bar, with a single bar showing the progress over the background track.

We may also use it as a segmented, Donut Chart, where one section represents a quantity and has a dedicated bar.

<RingProgress
    sections={[{ value: 35, color: progressColor, tooltip: "35%" }]}
    rootColor={trackColor}
    size={30}
    thickness={4}
    roundCaps
/>

The Donut chart version has multiple sections:

 sections={[
        { value: 40, color: 'cyan' },
        { value: 15, color: 'orange' },
        { value: 15, color: 'grape' },
      ]}

Avatar

The Avatar component may work with the user profile's image or may generate an avatar based on its name's initials. In absence of valid image or name, it falls back to a generic user icon automatically.

The border is round by default, as it sets the border-radius to 1000px.

The avatar size is 36px by default.

Mantine ensures the avatar remains square, cropping the image if needed, and that it covers the whole area, thanks to object-fit: cover.

provide an image

<Avatar src={imageURL} alt="John's portrait" />

provide a name

<Avatar name={name} />

request a generic icon intentionally

if we omit src and name, we always get a generic icon.

<Avatar />

Notifications

setup

npm install @mantine/notifications
import "@mantine/notifications/styles.css" // here
<MantineProvider>
    <Notifications />
    {/* Your app here */}
</MantineProvider>
import { notifications } from "@mantine/notifications"

create notification

we create a notification. We may set:

  • an id, if we intend to update this notification later on
  • loading to display a spinner
  • autoClose to false, to keep the notification on-screen until dismissed. By default, the notification closes automatically. We may set the delay by providing a duration in milliseconds.
  • position to set it somewhere else than bottom-right.

message is required, while title is optional.

notifications.show({
    id: task.id,
    color: "white",
    title: "Generating...",
    message: task.prompt,
    autoClose: false,
    loading: true,
    position: "top-right",
    radius: "xl",
    withCloseButton: false,
})

update notification with id

If the update indicates completion, we may show a completion icon, provide a completion text and plan for an auto-close.

notifications.update({
    id: task.id,
    color: "gray",
    title: "Image successfully created",
    message: task.prompt,
    icon: <IconCheck size={18} />,
    loading: false,
    autoClose: 2500,
    radius: "xl",
})

useForm

Gather data from user and send it to a server. Send it as-is or after some (client-side) validation and/or transformation.

In React, a form uses either controlled or uncontrolled inputs. Mantine provides both options.

Mantine's useForm provides a convenient way to specify the form's initial data and the validation and/or transformation to perform on submit.

Overview

We work with a regular <form>, which embeds a set of inputs and a submit button.

We provide initial data in an object (initialValues). The user may edit data through the inputs.

On submit, Mantine collects the data and provide it to the validator and/or transformer. Then, it provides the processed data to the callback, in principle for it to send it to the server.

When an initial value has no matching input (invisible, uneditable property), Mantine still collects it.

main steps

  • we create a configuration object
  • We compute a configured form object, through useForm(configuration)
  • We generate the form-submit handler with form.onSubmit(f), where f is our callback to handle the processed data. The form-submit handler takes an event as argument, and is assigned to the <form> onSubmit attribute, which is the react version of standard onsubmit.
  • We add attributes to the form inputs so that they interoperate. we spread form.getInputProps(email) to add the relevant attributes on the input, depending if it is controlled or uncontrolled: value if it is controlled, initialValue and ref if it is uncontrolled. They also get onChange and onBlur.

init form

const form = useForm({ // config
  	initialValues: {x: "", y: ""},
	validate: f
	transformValues: (collected) => processed
})

form callback

function my_callback(data) {
    data.x
    data.y
}

const mantineEventHandler = form.onSubmit(my_callback)

wire up

form onSubmit={e => mantineEventHandler(e)}
	TextInput label="x" {...form.getInputProps('x')}
	TextInput label="x" {...form.getInputProps('y')}
	TextInput label="x" {...form.getInputProps('z')} // z is not part of initial values, but may still be included if input is changed}
/form

initial value or not.

Mantine collect properties from the initial values, even if there is no matching input, and even if the value is null or undefined, and provide them for processing.

If an input is connected to an initial value that does not exist, it is effectively initialized to undefined, which will generate a React warning. Mantine will collect the value of that input and associate with the key provided in form.getInputProps??

initial value for a TextInput

The initial value for a TextInput should no be null: it triggers a React warning. It should also not be set to undefined. Instead, we should initialize to empty string. We may do post-processing in the transformer to change the empty string to null.

utils

// get current form values
form.values

// Set all or some form values
form.setValues(values)

// Set all form values using the previous state
form.setValues((prev) => ({ ...prev, ...values }))

// Set value of single field
form.setFieldValue("path", value)

// Set value of nested field
form.setFieldValue("user.firstName", "Jane")

// Resets form.values to initialValues,
// clears all validation errors,
// resets touched and dirty state
form.reset()

// Sets initial values, used when form is reset
form.setInitialValues({ values: "object" })

useLocalStorage

useLocalStorage exposes an API similar to useState, but uses the local storage as a single source of truth. It requires a key that describes the local storage entry, and a defaultValue for when such entry is missing.

const config = {
    key: "default-ai-provider",
    defaultValue: "GPT",
}

synchronous version (opt-in)

When the component mounts, the hook attempts to read a persisted value from the local storage, and initializes the state with it. If the entry is missing, it creates one with defaultValue, and initialize the state with it.

const config = {
    // ..
    // opt-in for the synchronous version
    getInitialValueInEffect: false,
}

asynchronous version (default)

In the asynchronous version, the hook immediately initializes the state with the default value, to avoid delaying the first paint. The local storage lookup is implemented as an effect that runs later on, after the first paint. Only then it may push the persisted value to the state.

Having the first paint ignoring the persisted value is not always desirable, that's why we may opt-in for the synchronous version.

exposed API

The hook exposes the state value and the dispatch method. It also exposes a method to remove the entry:

const [provider, setProvider, removeEntry] = useLocalStorage<AIProvider>(config)

The hook listens for changes in state and updates the local storage accordingly.

useDisclosure

A wrapper around useState<boolean>. It exposes a boolean state variable and provides helper methods which use terminology related to UI elements being opened or closed, such as open() and close(), in place of setIsOpened(true/false).

const [opened, { open, close, toggle }] = useDisclosure(false)

useMantineColorTheme

We want to read the current colorScheme, or set it to a value: dark or light.

Mantine saves the selected colorScheme in localStorage.

Before the App appears on-screen, Mantine checks the localStorage for any saved colorScheme, so that the first paint is done with the correct colorScheme.

const { colorScheme, setColorScheme } = useMantineColorScheme()
// also: toggleColorScheme and clearColorScheme
earlymorning logo

© Antoine Weber 2026 - All rights reserved

Overview

Mantine provides an infrastructure to build React apps efficiently, with pre-styled UI components and feature-rich utilities.

benefits

  • solid UI defaults: neutral, minimalist, functional style
  • easy-to-use, consistent API (DX)
  • rapid development of utilitarian and productivity apps

mantine style is opinionated and cohesive

Mantine brings a set of cohesive style defaults.

If we aim to do a partial style override, we should make it compatible with the Mantine overall look.

Some components, such as layout components and Mantine hooks, don't have a visible style, so we can use them in any design.

mantine's packages

Mantine provides distinct packages. The main ones are Mantine core and Mantine hooks.

terminology: components and component variants

Mantine provides components, and for some of them, variants. For example, it provides the Button component, for which it provides the outline and filled variants.

terminology: root element and inner-elements

A component is constructed through a top level element (root) and several, named, inner-elements. For example, Slider is built upon a root top level element, and has various inner elements such as mark, track and bar. We can style inner-elements.

When the component is polymorphic, it has a flexible top level element, that we can set through the component prop. For example, we can change the Button top element to be <a> instead.

customization patterns

We have three main patterns:

  • (Local change) customize a single instance: the scope of the change is local and limited to that instance.
  • (Global change) override a Mantine component: it affects all its instances.
  • (Global change) override style primitives, also called theming: it affects several components and their instances.

Customize a single instance

overview

We want to customize a single instance of a component.

We add props, classes or inline style.

props that work on all Mantine component

Mantine's style props are a set of props that work on all Mantine component, and which set a single style aspect. The style targets the top-level element, not the inner-elements:

<Box m={4}></Box>

props that are specific to one or more components

Some props are unique to one or more components. They determine the style or add to the component's inner structure, such as toggling inner-elements. For example, we add a <label> element by adding the label prop to TextInput.

<TextInput label="Username"></TextInput>
// adds:
<label>Username</label>

how it works, how it resolves

The props, classes and inline style resolve to:

  • style that lives in the style attribute
  • markers set on the element, such as classes, attributes, or an id, with CSS targeting them.
  • inner DOM elements being added.

the limits of the style and className attributes, and of style props

We provide quick inline style in the style attribute or add classes to className, but there is no granularity as to which inner-element we target. The style we provide usually applies to the component's top level element.

Style props are subject to the same constraint: they usually apply to the top level element.

Instead, Mantine recommends the pattern where we can declaratively target inner-elements.

target inner-elements

We target inner-elements by specifying their name, and by providing a style object (styles attribute) or classes (classNames attribute).

 <Button
	styles={{
        root: { backgroundColor: 'red', height: 16},
        label: { color: 'blue' },
      }}
<Button
    classNames={{
        root: "pinkbutton-root",
        label: "pinkbutton-label",
    }}
/>

how to name classes (non-CSS module)

This section describes creating CSS classes manually, outside of the CSS module pipeline. Otherwise, see CSS module patterns.

The classes we provide in classNames are to customize an instance. We can think about it as creating a variant of that component, either conceptually or because we actually create a custom component. Either way, we pick a name for that variant and use it in class names that apply to it.

Since we plug classes to the inner elements they apply to, we append the inner element name to the class name for clarity.

.pinkbutton-root
.pinkbutton-label

Global customization

Global customization, also called theming, overrides style-primitives, affecting several components as side-effects, or, with a more precise reach, overrides one more specific component to affect all instances.

We customize style primitives by setting the theme or by setting CSS variables. Setting the theme ultimately sets CSS variables

customize style primitives with the theme

For example, we set the value of --mantine-font-weight-medium for the whole app through fontWeights.medium:

const theme = createTheme({
    fontWeights: {
        medium: "500",
    },
})

customize style primitives with CSS variables

:root {
    --mantine-font-weight-medium: 500;
}

override Mantine components with component-scoped empty classes

Mantine adds a series of empty classes on all components. We can implement them to customize a given aspect of that component. (Implementing such classes affects all instances)

For example, Mantine adds mantine-Button-root to the Button's root element and mantine-Button-label on the label inner-element. We implement the class if needed:

.mantine-Button-root {
    border-width: 0.5px;
}

empty classes specificity

Mantine empty classes have the same specificity than Mantine's internal classes. As such, we make sure to import our stylesheet after Mantine's one.

CSS Module patterns

CSS module primer

CSS module overview

A processor parses the stylesheet to scan classes and exposes them to JS as a key-value map. While the values are globally unique class names, we only interact with keys.

We should avoid hyphens in the key because it makes it harder to access in it JS, as JS doesn't support hyphens in variable names. We would need to quote the key name on lookup. Instead, we use camelCase for class names.

Note that Vite has an option to automatically transform class names with hyphens to camel case, but this adds a layer of indirection.

Pipeline example:

/* xxx.module.css */
.supercool {
}
.supernice {
}
.superGreat {
}
.super-awesome {
}

We import the map:

import classes from "xxx.module.css";
/* {
    supercool: "supercool_5cEkq2n0x1",
    supernice: "supernice_1kmox6oL39",
    superGreat: "superGreat_1kmox6oL39",
    "super-awesome": "super-awesome_1kmox6oL39",
} */

<Button
classNames={{
        root: classes.supercool, // "supercool_5cEkq2n0x1"
        label: classes.supernice,	// "supernice_1kmox6oL39",
        section: classes.superGreat,
    		loader: classes['super-awesome']
    }}
  >

Mantine pattern

one module for one variant

In this pattern, the module's purpose is to define the style for one Mantine component custom variant. For example, we create a PrimaryButton variant of Button. We name the CSS Module: PrimaryButton.module.css

target inner elements

We name the classes according to the inner-element we target, such as root or label:

/* PrimaryButton.module.css */
.root {
}

.root:hover {
}

.label {
}

We then apply the classes on the Mantine's instance:

import PrimaryButtonClassNames from "PrimaryButton.module.css";

  {/* verbose */}
<Button
classNames={{
        root: PrimaryButtonClassNames.root,
        label: PrimaryButtonClassNames.label,
    }}
  >

When we follow the inner-element-as-a-classname pattern, we can give the map directly to classNames, since it matches exactly what is expected by classNames:

  {/* short */}
<Button classNames={PrimaryButtonClassNames}>

Universal style props

We control various style aspects, by adding the following props to any Mantine component (instance):

spacing

PropCSS PropertyTheme key
mmargintheme.spacing
mtmarginToptheme.spacing
mbmarginBottomtheme.spacing
mlmarginLefttheme.spacing
mrmarginRighttheme.spacing
mxmarginRight, marginLefttheme.spacing
mymarginTop, marginBottomtheme.spacing
ppaddingtheme.spacing
ptpaddingToptheme.spacing
pbpaddingBottomtheme.spacing
plpaddingLefttheme.spacing
prpaddingRighttheme.spacing
pxpaddingRight, paddingLefttheme.spacing
pypaddingTop, paddingBottomtheme.spacing

typography

PropCSS PropertyTheme key
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration

size

PropCSS PropertyTheme key
wwidththeme.spacing
miwminWidththeme.spacing
mawmaxWidththeme.spacing
hheighttheme.spacing
mihminHeighttheme.spacing
mahmaxHeighttheme.spacing

position

PropCSS PropertyTheme key
posposition
toptop
leftleft
bottombottom
rightright
insetinset

other props

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

responsiveness: provide alternative values.

Instead of providing a single value, we provide an object with alternative values. Most of the time, we provide two values:

<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'sm', 	sm: 'lg' }}
>
  • the base value provides the mobile-centric, default value. Its exact scope depends on which other breakpoints we use, but it always includes regular smartphones. For reference, iPhones are under 450p width, with standard-size iPhones under 400p.
    • The xs breakpoint activates at 576p. It doesn't apply to regular smartphones but applies to phablets and bigger devices.
    • The sm breakpoints activates at 768p (48em). It doesn't apply to phablets, but applies to tablets and wider screens. For reference, iPads start at 768px.

Set default Color Scheme

Mantine defaults to light mode.

We can change it to dark or auto. The latter conforms to the user's device color scheme:

<MantineProvider defaultColorScheme="auto"></MantineProvider>

Button case study

case study: Button implementation

The Button source code includes the TypeScript file (Button.tsx) and the CSS module stylesheet (Button.module.css).

HTML structure and inner elements

There are several elements:

  • the root <button> is the top level element.
  • the loader <span> is an intermediate container for a loader.
  • the inner <span> is an intermediate container for label and sections
  • a section is a container for a side icon.
  • the label hosts the button's text.
<button {/* root */}>
    <span {/* 1.0 loader */} />
    <span  {/* 2.0 inner */}>
        <span {/* 2.1 section  */}>
            <svg /> {/* icon */}
        </span>
        <span {/* 2.2 label  */}>
          {/* text */}
      </span>
    </span>
</button>

Mantine adds its internal classes (e.g. m-77c9d27d) and the non-implemented classes (e.g. mantine-Button-root) to 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>

Mantine adds component agnostic classes, such as mantine-active and mantine-focus-auto. They are placed preemptively, and their style trigger on specific states. For example, mantine-active activates its style on the active state:

mantine-active:active {
    transform: translateY(calc(0.0625rem));
}

Mantine also adds data-attribute for styling purpose, such as data-centered="true".

.m_a3c6e060[data-centered] {
    display: flex;
    justify-content: center;
    align-items: center;
}

CSS module source code

The stylesheet targets inner-elements with classes of the same name. For example:

.loader {
  position: absolute;
  left: 50%;
  top: 50%;
}

The .tsx file imports the classes object and plugs it to Button.classes.

style shipped on npm

The style shipped on npm has been processed by PostCSS. It doesn't use CSS nesting.

Mantine gathers all CSS file into a single 232kb stylesheet. Mantine also provides smaller stylesheets, that focus on one or more components.

Default theme

The default theme (v7 or v8) spreads over several categories.

spacing

Space between elements and space inside elements. It is used for:

  • gap, for Group, Flex, Stack.
  • padding props.
  • margin props, for example for Divider
  • width and height props such as w and h, for example for Space.
xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32p

radius

Border radius used for:

  • Paper
  • Dialog and Modal
  • Button and Tooltip

default radius:

"defaultRadius": "sm"

actual values:

xs: 2px
sm: 4px // (d)
md: 8px
lg: 16px
xl: 32px

breakpoints

A set of thresholds, that we target to activate style conditionally. We should keep those thresholds as-is:

  "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

A set of 5 sizes:

"fontSizes": {
    "xs": 12px
    "sm": 14px
    "md": 16px,
    "lg": 18px
    "xl": 20px
  },

Those sizes do not affect headings. We use them in:

  • the fz style prop.

line heights

  "lineHeights": {
    xs: 1.4,
    sm: 1.45,
    md: 1.55, // (d)
    lg: 1.6,
    xl: 1.65
  },

defaults to 1.55

--mantine-line-height: 1.55;

headings

family, weight and wrap behavior:

"headings": {
    "fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji",
    "fontWeight": "700",
    "textWrap": "wrap",
  },

size and line height:

  "sizes": {
      "h1": {
        "fontSize": "calc(2.125rem * var(--mantine-scale))",
        "lineHeight": "1.3"
      },
      "h2": {
        "fontSize": "calc(1.625rem * var(--mantine-scale))",
        "lineHeight": "1.35"
      },
      "h3": {
        "fontSize": "calc(1.375rem * var(--mantine-scale))",
        "lineHeight": "1.4"
      },
      "h4": {
        "fontSize": "calc(1.125rem * var(--mantine-scale))",
        "lineHeight": "1.45"
      },
      "h5": {
        "fontSize": "calc(1rem * var(--mantine-scale))",
        "lineHeight": "1.5"
      },
      "h6": {
        "fontSize": "calc(0.875rem * var(--mantine-scale))",
        "lineHeight": "1.5"
      }
    }

Gray Palette

overview

Mantine's gray is a slate gray palette (blue tint), and is designed for light mode:

  • It offers several whites for backgrounds (but doesn't include pure white, instead, it starts at #F8F9FA.)
  • It offers a few dark grays for text. It doesn't include pure black.
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

note on light mode use

The following consideration usually apply for light-mode:

  • The background can be pure white. It is the site global background or some controls background.

  • The background can go darker on hover. This is the case for buttons, and for the select's options: gray.1

  • The text is black-ish or gray-ish depending on 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:
      • some headings
      • body text
      • input label
      • button label

Dark Palette

overview

Mantine's dark is a neutral gray palette. It used to be a slate gray palette (blue tint) before Mantine 7.3.

It is designed for dark mode, and Mantine dark color scheme uses it. It offers several darks for backgrounds, grays for controls, and whites for text.

TokenLookAliasValueValue%
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 dark mode

  • The app background is dark 7. We may go darker with dark 8.
  • Controls have lighter backgrounds: dark 6, and even lighter on hover: dark 5. Sometimes, hover goes to a darker background instead: such as the hovered option in a select control: dark 8.
  • The text is light. It uses one of those colors:
    • pure white (bright white for some headings)
    • dark 0 (readable white for body text)
    • dark 1 (slightly dimmed)
    • dark 2 (dimmed, secondary text, matches c=dimmed)
  • The button label is pure white, both for filled and default variants.
  • The input's label is dark 0, the input's description is dark 2.
  • The dimmed text is dark 2, such as secondary label, or secondary text in general.

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",
        ],
    },
})

Colors and color schemes

dark color scheme

We study the CSS variables and their default value for the Mantine dark color scheme (dark theme).

Note: the CSS variables have the --mantine-color prefix, such as --mantine-color-bright, where bright is the suffix. We trimmed the prefix from the table for readability.

Some colors only trigger when we use the given variant or color prop, for example the default variant or the gray color. The general values apply when not overridden by a variant or a color.

general values

suffixvalue
schemedark
bright--mantine-color-white
text--mantine-color-dark-0
dimmed--mantine-color-dark-2
placeholder--mantine-color-dark-3
body--mantine-color-dark-7
anchor--mantine-color-blue-4
error--mantine-color-red-8
red-text--mantine-color-red-4

variant: "default"

note: it comes with a border.

While the default for hover is dark-5, we can also use dark-4 or dark-6.

suffixvalue
default-color--mantine-color-white
default--mantine-color-dark-6
default-hover--mantine-color-dark-5
default-border--mantine-color-dark-4

color: "dark"

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

suffixvalue
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"

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)

Flex

Use a bare-bones flex container, with nothing set except display: flex.

override CSS defaults

  • Switch the direction, which is horizontal (row) by default, to vertical (column).
  • 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

Layout elements horizontally and add space between them. The container spans the whole line. It uses a horizontal flex under the hood.

The differences with a stock horizontal flex:

  • It adds some gap between elements (defaults to md, aka 16px).
  • It centers elements vertically, with align, instead of stretching them.
  • It wraps when needed (multi-line flex) (We can set wrap to mono-line (nowrap) or multi-line (wrap), which sets the underlying flex-wrap).

the grow variant (override)

  • Make children fill the container with grow.

    • It sets flex-grow: 1 on children.
    • By default, it makes them same-width (by capping them to the same max-width), and pack them on a single line (since the max-width is a share of the line, not because of disabling wrap).
    • We can reestablish multi-line by turning off capping (set preventGrowOverflow to false): it now goes to the next line since wrapping was never disabled, but only if the elements do not fit on a single line (do not use)
    • If we also 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

  • Pick horizontal alignment 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 still auto). It uses a vertical flex under the hood. Items default to spread horizontally within it.

Differences with a stock vertical flex:

  • It adds a gap (16px by default).

Other settings to override CSS defaults:

  • justify: elements sit at the top by default (flex-start)
  • align: elements fill the stack horizontally by default (stretch)

SimpleGrid

Set up a column layout with a given number of equal-width columns:

<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 horizontal and vertical spacing aka spacing and verticalSpacing).

We set the number of columns with cols. It defaults to a single track. Best Practice: adapt the column count based on screen size, providing at least two values:

<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>{/* items */}</SimpleGrid>

Container

A wrapper that centers the content and keeps it aways from screen edges. For example, we wrap the main column of a website, to have padding on mobile and centering on desktop.

max-width version

The default version comes with a max-width (for desktop) and some padding (for mobiles). It centers the content with margin: auto.

The max-width defaults to md (aka 960px). We change it with size. For example, we can pick 720px (sm) instead. If we are to provide an arbitrary numeric value, we may as well set maw directly to remove indirection.

The default padding is 16px, aka md. We change it with the regular px style prop.

fluid version

We may opt for a fluid container, which expands horizontally, with the fluid prop. It only comes with some padding, at 16px. A Box with horizontal padding has the same effect.

grid version

The grid version doesn't use padding and doesn't center with max-width. Instead, it sets a grid layout and center stuff with grid positioning.

Center

Center the child or the group of children horizontally and vertically.

Under the hood, it uses a flex container that justify and align the group of children to the center.

vertical-centering and height

Vertical centering will only come into play if Center's height is bigger than the inner content.

horizontal-centering and width.

The container flex spreads horizontally (width: auto), but compacts its children to max-content. We can prevent compaction by setting the group of children to width: 100%.

Divider

Separate items or sections with a visible line, similar in essence to the <hr> element.

Under the hood, it is an empty element whose border acts as the visible line.

  • The line is horizontal by default, we can set it to vertical with orientation.
  • We set the line thickness with size. It defaults to 1px.
  • The line is solid by default. We switch to dashed or dotted with variant.
  • The line doesn't add margin around itself by default. We add margin with my or mx.
  • The line's color defaults to:
    • --mantine-color-dark-4: #424242; in dark mode (dark gray)
    • --mantine-color-gray-3: #dee2e6; in light mode (light gray)

Space

Separate items with some white space. Mantine uses an empty element with some explicit size. It defaults to a zero size. We set the height or the width:

  • Set vertical spacing with h, usually in a stack-like layout.
  • Set horizontal spacing with w, usually in a horizontal flex-like layout.

Paper

Encapsulate some UI to set it apart visually.

The Paper's default background is the same as the body's default background (--mantine-color-body).

  • In light mode, it is white #FFF.
  • In dark mode, it is dark.7.

We can override's Paper background with bg.

Paper also adds a radius (theme.defaultRadius) which defaults to 4px.

Paper specific customizations

  • Add a pre-styled border with withBorder (boolean)
  • Add a pre-styled shadow with shadow. We specify a named size (e.g. sm), or a box-shadow CSS literal.

Accordion

Establish one or more expandable items that disclose more content when interacted with.

item: show a preview and enable disclosure of a panel

The Accordion contains one or several items. An item starts as a clickable preview (Accordion.Control) and embeds a panel, that starts as hidden, and that is toggled on click.

Mantine mounts all panels immediately, regardless of their toggle state. The panel may be a form. In that case, we can set the chevron as a plus sign, to signal we add some data.

We set the chevron style at the Accordion root element's level, since all items use it.

<Accordion variant="separated" chevron={<IconPlus />}>
    <Accordion.Item value="item1">
        <Accordion.Control>Log Workout</Accordion.Control>
        <Accordion.Panel>Content</Accordion.Panel>
    </Accordion.Item>
</Accordion>

items appearance

By default, items are separated by a thin line. We can set variant to separated instead, or other variants.

Appshell

Appshell is a mega layout component that manages:

  • The header on-top of the page, also called the banner.
  • The main section, below the header
  • The left-side navbar and the right-side aside (if applicable)

header: Appshell.Header

The header manages:

  • the burger button
  • the logo
  • the dark mode / light mode toggle (potentially)
  • the user button (potentially)
  • a set of links (potentially)

configure the header

We build a header config and provide it to AppShell as a header prop. We commonly set the height.

<AppShell /* ... */ header={{ height: 60 }} />

navbar: Appshell.Navbar

The navbar stands on the left side. It is transient on mobile devices. On desktop it is commonly always-on, but Mantine supports having a conditional collapse as well, following a distinct logic and state variable.

We build a navbar config and provide it to AppShell as a navbar prop. collapsed aims to control the display of the navbar, while breakpoint sets the breakpoint from which we go from mobile to desktop logic.

<AppShell /* ... */ navbar={{}} />
  • collapsed receives up to two boolean state variables: one for mobile and one for desktop. Most of the time, we only set mobile.
  • 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. We hide the mobile burger button with hiddenFrom. If desktop supports collapse too, we use a distinct burger button (see below).
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()

return (
    <AppShell
        navbar={{
            collapsed: { mobile: !opened },
            breakpoint: "sm",
        }}
    >
        <AppShell.Header>
            <Burger opened={opened} onClick={toggle} hiddenFrom="sm" />
        </AppShell.Header>
    </AppShell>
)

navbar: allow collapse on desktop

We may allow the collapse behavior on Desktop as well: we create an additional, distinct boolean state variable, and provide it to the desktop property of collapsed. We also add a dedicated desktop burger button, which commonly keeps the same appearance regardless of the collapse state (we don't show a close icon because there is no overlay). This burger button limits its display to desktop with visibleFrom.

const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(false)
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)

return (
    <AppShell
        navbar={{
            collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
            breakpoint: "sm",
        }}
    >
        <AppShell.Header>
            {/* mobile */}
            <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" />
            {/* desktop */}
            <Burger opened={false} onClick={toggleDesktop} visibleFrom="sm" />
        </AppShell.Header>
    </AppShell>
)

navbar: other configuration

navbar={{ width: 300, /* ... */ }}
  • width controls the width in desktop mode (it is always full-screen in mobile). We provide an immediate value or an object with two or more values.

navbar: implement top and bottom sections

We may split the navbar into AppShell.Sections, and one of them may grow.

<AppShell.Navbar>
    <AppShell.Section grow> </AppShell.Section>
    <AppShell.Section> </AppShell.Section>
</AppShell.Navbar>

Appshell.Main

The container for the main panel.

We may set the panel background color. The default light theme sets a white background while the dark theme sets a dark.7 background.

Override example: we may pick a light gray background such as gray.1, so that inner containers may stand out as white. Similarly, we may change the background to dark.8 so that inner elements may standout as dark.7.

const backgroundColor = colorScheme === "dark" ? "dark.8" : "gray.1"

synopsis

the configuration objects are required if we use the matching element, except for main which may not be configured with this pattern.

<AppShell header={{}} navbar={{}} aside={{}} footer={{}}>
    <AppShell.Header />
    <AppShell.Navbar />
    <AppShell.Aside />
    <AppShell.Main />
    <AppShell.Footer />
</AppShell>

Tabs

Table data

abstract

A table works with an array of items, where each item is a row, and each property or member is a cell.

An item may come as an object or as an array.

pattern: the item is an object

const items = [
    { position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
    { position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
    { position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
    { position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
    { position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
]

pattern: the item is an array

const items = [
    [6, 12.011, "C", "Carbon"],
    [7, 14.007, "N", "Nitrogen"],
    [39, 88.906, "Y", "Yttrium"],
    [56, 137.33, "Ba", "Barium"],
    [58, 140.12, "Ce", "Cerium"],
]

We may come with this data by transforming an array of objects to an array of arrays:

const items = players.map((p) => [p.name, p.level, p.class])

headers

An array usually comes with headers. We may provide them in an array.

const headers = ["Element position", "Atomic mass", "Symbol", "Element name"]

Table with explicit structure

structure preview

<Table>
    <Table.Thead>
        {/* head */}
        <Table.Tr>
            <Table.Th>{col1Label}</Table.Th>
            <Table.Th>{col2Label}</Table.Th>
            <Table.Th>{col3Label}</Table.Th>
        </Table.Tr>
    </Table.Thead>

    {/* body */}
    <Table.Tbody>
        <Table.Tr>
            <Table.Td>{item1.name}</Table.Td>
            <Table.Td>{item1.level}</Table.Td>
            <Table.Td>{item1.class}</Table.Td>
        </Table.Tr>
        {/* ... more rows */}
    </Table.Tbody>
</Table>

build rows from an array of objects

const rows = items.map((item) => (
    <Table.Tr>
        <Table.Td>{item.name}</Table.Td>
        <Table.Td>{item.level}</Table.Td>
        <Table.Td>{item.class}</Table.Td>
    </Table.Tr>
))

build rows from an array of arrays

const rows = items.map((item) => (
    <Table.Tr>
        <Table.Td>{item[0]}</Table.Td>
        <Table.Td>{item[1]}</Table.Td>
        <Table.Td>{item[2]}</Table.Td>
    </Table.Tr>
))

headers and body

Thead is the container for the headers.

<Table.Thead>
    <Table.Tr>
        <Table.Th>{col1Label}</Table.Th>
        <Table.Th>{col2Label}</Table.Th>
        <Table.Th>{col3Label}</Table.Th>
    </Table.Tr>
</Table.Thead>

Tbody is the container for rows.

<Table.Tbody>{rows}</Table.Tbody>

Table from data prop

We provide the complete data as a single object for a single prop:

  • The caption property defines the table's title
  • The head property is the array of headers.
  • The body array is the list of items. Each item is an array with bare values
const tableData: TableData = {
    caption: "Some elements from periodic table",
    head: ["Element position", "Atomic mass", "Symbol", "Element name"], // headers
    body: [
        // items
        [6, 12.011, "C", "Carbon"],
        [7, 14.007, "N", "Nitrogen"],
        [39, 88.906, "Y", "Yttrium"],
        [56, 137.33, "Ba", "Barium"],
        [58, 140.12, "Ce", "Cerium"],
    ],
}

data prop

We provide such object to the data prop. Mantine then creates the markup automatically.

<Table data={tableData} />

TextInput

TextInput is an abstraction over <input type=text>. It inherits most props from <Input>. We describe all props here.

controlled:

<TextInput
    value={summary}
    onChange={(event) => setSummary(event.currentTarget.value)}
    data-autofocus
    label="Summary"
    placeholder="What did you work on?"
/>

uncontrolled:

<TextInput
    type="email"
    ref={mailInput}
    required
    label="Email"
    placeholder="john-appple@example.com"
/>

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.

NumberInput

NumberInput is a <input type=text> under the hood. This enables more freedom into what is displayed in the input. For example, we can display formatting commas.

The value we get is not consistently a number. For example, we get an empty string when the input is empty.

options

  • Forbid decimal numbers with allowDecimal. They are allowed by default.
  • Forbid negative numbers with allowNegative. They are allowed by default.
  • Clamp the value between min and max. Determine how the value should clamp with clampBehavior:
    • Forbid values outside the limits at all times with strict.
    • Clamp after input on blur. This is the default.
    • Never clamp (none)

As a text input, it supports a label, a description (secondary label) and a placeholder.

state

Work with the string and number type union:

const [value, setValue] = useState<string | number>("")
return <NumberInput value={value} onChange={setValue} />

Work with a number only. In this case, we only update state if the value is a number. When the NumberInput is empty, the state may have a ghost value. We clear the ghost value on blur if the field is empty.

const [value, setValue] = useState<number>(0)

return (
    <NumberInput
        value={value}
        onChange={(value) => {
            if (typeof value === "number") setValue(value)
        }}
        onFocus={(input) => {
            /* clear field to prepare for input if value was 0 */
            if (input.target.value === "0") {
                input.target.value = ""
            }
        }}
        onBlur={(input) => {
            /* reset field to 0 if empty on leaving the field */
            if (input.target.value === "") {
                onChange(0)
            }
        }}
    />
)

FileInput

FileInput is a control that triggers the OS' file selection dialog. It shows information about the current selection. We can customize the flow:

  • Allow selecting multiple files at once with multiple. It defaults to false.
  • Restrict selection to a given file type in accept, such as "image/png"
  • Add a clear button on the right with clearable
  • Work with the selected file by providing a handler to onChange. The handler receives a File object (or a File array). The handler is called when users close the platform's native File picker or clear the selection.

Other techniques

  • Limit the input's width to truncate long file names with the regular maw prop
  • Add a file icon to the leftSection.
  • Opt for the unstyled variant which looks similar to a transparent variant: there is no border or background fill.
  • Shrink the control with size.

FileInput inherits props from Input

<FileInput
    size="xs"
    maw={150}
    onChange={onChange}
    placeholder="Reference Image"
    leftSection={<IconPhoto size={18} stroke={1.5} />}
    clearable
    variant="unstyled"
/>

Native Select

An abstraction over a <select> that embeds some <option> elements.

We collect options in an array, and assign them to the data attribute. We describe each option with a label and a value.

const options = [
    { label: "Death Knight", value: "deathKnight" },
    { label: "Demon Hunter", value: "demonHunter" },
]
<NativeSelect data={options} />

If the value works as a label, such as when we want to surface the value in the UI, we can replace the objects with a single string instead. The string acts both as the value and as the label.

const options = ["deathKnight", "demonHunter"]

SegmentedControl

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.

We describe the options in data:

  • The option's label is the user-facing description: It is a string or a React node.
  • The option's value is its identifier. The state holds this identifier when the option is selected. A type union lists the possible identifiers.

We set the initial selection in value.

onChange is called on selection change attempt. The selection changes only if we mutate the state.

type AIModel = "GPT" | "IMAGEN"
const [model, setModel] = useState<AIModel>("GPT")

<SegmentedControl<AIModel>
    value={model}
    onChange={(v) => setModel(v)}
    data={[
        { label: GPTLabel, value: "GPT" },
        { label: IMAGENLabel, value: "IMAGEN" },
    ]}
    // size="sm"
    // radius="md"
    // bg={"transparent"}
/>

Button

An abstraction over a <button>.

button specifics

  • Pick the overall appearance with variant:

    • filled uses bright colors. (default)
    • light uses toned down colors.
    • outline
    • subtle
    • gradient
    • default
  • Pick the color tint. It defaults to the theme's primaryColor, aka Mantine's blue.

  • Opt for a fluid button (fill the container) with fullWidth.

  • Opt for compact version with size, and control the font size.

  • Show the loading spinner appearance (and disable the button) with loading. We customize the loader with loaderProps. (see Loader component)

  • disabled, sets the appearance and behavior to disabled. the disabled appearance is the same across all variants.

  • gradient, sets the gradient's colors and direction. It requires using the gradient variant.

other options

  • leftSection, rightSection, for example for an icon.
<Button rightSection={<IconDownload size={14} />}>Download</Button>

Button not submitting form by default

Mantine adds type=button on the underlying <button>. It prevents the button from triggering the form submit, when pressed inside a <form>. We set type=submit if needed.

providing an onClick handler

The handler specifies which action to perform on click. This action usually does not depend on any information related to the click event.

Still, the handler receives the event as an argument. If we define the handler somewhere else than inline, we must indicate its typing. The argument it takes is of type Event.

More specifically, the event is of type React.MouseEvent<HTMLButtonElement >.

The HTMLButtonElement type parameter indicates the type of the target of the event, which in this case is a <button> element.

The handler is not supposed to return anything.

Handler type signature

onClick: React.MouseEvent<HTMLButtonElement> => void

shortcut type

onClick: React.MouseEventHandler<HTMLButtonElement>

Button.Group

We create a button cluster that glues up the buttons horizontally (default), or vertically.

<Button.Group>
    <Button variant="default">First</Button>
    <Button variant="default">Second</Button>
    <Button variant="default">Third</Button>
</Button.Group>

ActionIcon

icon button

ActionIcon serves the "icon as a button" pattern. UI-wise, the idea is that the icon is expressive enough to be self-explanatory, with no need for accompanying text, for example the "trash" icon refers to the "delete" action.

Technically it is a container that is distinct from the icon, that must be added as a child.

It gives its child icon the behaviour of a button, and it customizes its appearance according to the selected variant.

  • variant: same variants as Button's variants. To get a naked icon effect, we select subtle or transparent.
  • size: the container's size impacts the hover and active zone, and the icon scale.

required child icon

As a wrapper, it impacts the concrete child icon.

We may still set size directly on the child Icon.

<ActionIcon variant='default'>
	<IconMail size={14} stroke={..?}/>
</ActionIcon>

Pagination

we provide:

  • the total number of pages
  • the value of the cursor aka selected page number.
  • the onChange handler
  • (optional) withControls, to show previous and next arrows, defaults to true.
  • (optional) withEdges, to show first and last arrows, default to false.
  • (optional) size, to get bigger or smaller pagination control, defaults to md
  • (optional) radius, to get rounder or squarer controls, defaults to md
const [activePage, setActivePage] = useState<number>(1)
<Pagination total={10} value={activePage} onChange={setActivePage} />

Modal

Overlay some UI on top of the application. Dim the rest of the app while opened.

<Modal opened={opened} onClose={close}>
    {/* Modal content */}
</Modal>

Modal works with a boolean state variable to control the display (plugged on the opened prop).

Modal also works with an onClose handler, called on dismissal attempts, and responsible for toggling back the state to closed, or for denying the dismissal.

reminder: Mantine's useDisclosure provides a boolean state variable, and open and close state-mutation functions, which are fitting for a modal:

const [opened, { open, close }] = useDisclosure(false)

The following actions qualify as attempts to dismiss:

  • press the escape key
  • click outside
  • click the close button (if displayed)

We usually open the modal with a button:

<Button onClick={open}>Open modal</Button>

The inner content is commonly form elements: some inputs and a submit button.

defaults

  • the modal has a top section with a close button
  • radius adheres to theme.defaultRadius which defaults to 4px.

overrides

  • remove the close button with withCloseButton, which can remove the header altogether.
  • center the modal with centered.
  • add a title, which can be any React node (not text-only).
  • control the dimming with overlayProps. The dimming is done by an underlying <Overlay>.
  • more overrides

Menu

A menu that gathers a set of controls, that we display conditionally. Menu manages the opened state automatically.

The user toggles the menu by clicking the control that lives in Menu.Target.

We add a set of Menu.Item which are <button> under the hood.

We may separate two different sections with Menu.Divider. We may give a section a label with Menu.Label.

overall structure

<Menu shadow="md">
    <Menu.Target>
        <Button>Toggle menu</Button>
    </Menu.Target>
    <Menu.Dropdown>/* */</Menu.Dropdown>
</Menu>

menu proper

<Menu.Dropdown>
    <Menu.Label>Application</Menu.Label>
    <Menu.Item leftSection={<IconSettings size={14} />}>Settings</Menu.Item>
    <Menu.Item leftSection={<IconMessageCircle size={14} />}>Messages</Menu.Item>
    <Menu.Divider />
    <Menu.Label>Danger zone</Menu.Label>
    <Menu.Item leftSection={<IconArrowsLeftRight size={14} />}>Transfer</Menu.Item>
    <Menu.Item leftSection={<IconTrash size={14} />}>Delete</Menu.Item>
</Menu.Dropdown>

Title

Thin abstraction over a heading element. It uses a HTML heading under the hood, h1 by default.

It removes the user agent stylesheet's default margin by setting them to 0. It keeps the bold font weight.

The font size, font weight, and line height, come from theme.headings.

The default theme defines five font sizes:

  • h5 comes at 16px
  • h4 comes at 18px
  • h3 comes at 22px
  • h2 comes at 26px
  • h1 comes at 34px

We set the heading as follows:

  • order: pick the underlying html element, such as 1 for h1. It defaults to h1. Affects the font-size by default.
  • size: override the font-size caused by order. we pick a size as a number or as a heading in the heading hierarchy such as h5.
  • lineClamp: set the maximum number n of lines. If not set, the browser allow multiple lines for a heading element. If truncation happens, it shows an ellipsis thanks to text-overflow: ellipsis.
<Title order={3} size={"h5"}>
    Daybreak
</Title>

Text

A container for some text content.

It is a <p> element under the hood by default, but we may set it to be another element such as an anchor tag or a span with the component prop.

It removes default margins by setting the (vertical) margin to 0.

It comes with some style affecting the font size, the font family and the line height, which is 1.55 by default. We may set it to inherit all those styles instead by adding the inherit prop.

<Text c="dimmed">Dimmed text</Text>

Box

A neutral wrapper, similar in essence to div or span, which may affect its children through Mantine style props:

<Box fz={12}>/* */</Box>

Bar Chart

packages

@mantine/charts
recharts

stylesheet

import "@mantine/charts/styles.css"

bar-chart logic

for each index, display one or several bars. We assume one bar for each index.

data for a given index

The chart library expects an object for each index. The object contains raw data. Among this data, we select which property acts as the label. The remaining properties may act as magnitude.

{month: "Jan", workouts: 17},
{month: "Feb", workouts: 18}

indicate data's shape

We indicate:

  • the property that acts as the label, here month
  • for each property that we want to act as a magnitude, we indicate: the property's name (here workouts) the color for that bar, and the label for that bar.

In the following example, workouts provides the magnitude for the unique serie.

<BarChart
    data={}
    h={300}
    dataKey="month" // key that provides label
    series={[
        // bar(s) configuration
        { name: "workouts", color: "blue.6", label: "Workouts" },
    ]}
/>

Each magnitude's property, as it spreads over all the indexes, makes a distinct serie. For example, the ipadSales, the iponeSales, and the macSales make each one a distinct serie.

Progress

Progress is a replacement for the native <progress> element. It displays a linear bar. It uses a regular div under the hood.

We may opt for a striped bar, or even an animated striped bar.

simple progress bar

  • value: from 0 to 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} />

compound progress bar • segmented bar

We may have several segments.

  • The container is Progress.Root
  • Each segment is a Progress.Section
  • We may add a segment label with Progress.Label

We may make the label have a smart color with autoContrast.

<Progress.Root>
    <Progress.Section value={20} color="indigo">
        <Progress.Label>Documents</Progress.Label>
    </Progress.Section>
    <Progress.Section value={50} color="orange">
        <Progress.Label>Photos</Progress.Label>
    </Progress.Section>
</Progress.Root>

RingProgress

RingProgress displays a circular, segmented bar.

In the simplest form, we may use it as a circular progress bar, with a single bar showing the progress over the background track.

We may also use it as a segmented, Donut Chart, where one section represents a quantity and has a dedicated bar.

<RingProgress
    sections={[{ value: 35, color: progressColor, tooltip: "35%" }]}
    rootColor={trackColor}
    size={30}
    thickness={4}
    roundCaps
/>

The Donut chart version has multiple sections:

 sections={[
        { value: 40, color: 'cyan' },
        { value: 15, color: 'orange' },
        { value: 15, color: 'grape' },
      ]}

Avatar

The Avatar component may work with the user profile's image or may generate an avatar based on its name's initials. In absence of valid image or name, it falls back to a generic user icon automatically.

The border is round by default, as it sets the border-radius to 1000px.

The avatar size is 36px by default.

Mantine ensures the avatar remains square, cropping the image if needed, and that it covers the whole area, thanks to object-fit: cover.

provide an image

<Avatar src={imageURL} alt="John's portrait" />

provide a name

<Avatar name={name} />

request a generic icon intentionally

if we omit src and name, we always get a generic icon.

<Avatar />

Notifications

setup

npm install @mantine/notifications
import "@mantine/notifications/styles.css" // here
<MantineProvider>
    <Notifications />
    {/* Your app here */}
</MantineProvider>
import { notifications } from "@mantine/notifications"

create notification

we create a notification. We may set:

  • an id, if we intend to update this notification later on
  • loading to display a spinner
  • autoClose to false, to keep the notification on-screen until dismissed. By default, the notification closes automatically. We may set the delay by providing a duration in milliseconds.
  • position to set it somewhere else than bottom-right.

message is required, while title is optional.

notifications.show({
    id: task.id,
    color: "white",
    title: "Generating...",
    message: task.prompt,
    autoClose: false,
    loading: true,
    position: "top-right",
    radius: "xl",
    withCloseButton: false,
})

update notification with id

If the update indicates completion, we may show a completion icon, provide a completion text and plan for an auto-close.

notifications.update({
    id: task.id,
    color: "gray",
    title: "Image successfully created",
    message: task.prompt,
    icon: <IconCheck size={18} />,
    loading: false,
    autoClose: 2500,
    radius: "xl",
})

useForm

Gather data from user and send it to a server. Send it as-is or after some (client-side) validation and/or transformation.

In React, a form uses either controlled or uncontrolled inputs. Mantine provides both options.

Mantine's useForm provides a convenient way to specify the form's initial data and the validation and/or transformation to perform on submit.

Overview

We work with a regular <form>, which embeds a set of inputs and a submit button.

We provide initial data in an object (initialValues). The user may edit data through the inputs.

On submit, Mantine collects the data and provide it to the validator and/or transformer. Then, it provides the processed data to the callback, in principle for it to send it to the server.

When an initial value has no matching input (invisible, uneditable property), Mantine still collects it.

main steps

  • we create a configuration object
  • We compute a configured form object, through useForm(configuration)
  • We generate the form-submit handler with form.onSubmit(f), where f is our callback to handle the processed data. The form-submit handler takes an event as argument, and is assigned to the <form> onSubmit attribute, which is the react version of standard onsubmit.
  • We add attributes to the form inputs so that they interoperate. we spread form.getInputProps(email) to add the relevant attributes on the input, depending if it is controlled or uncontrolled: value if it is controlled, initialValue and ref if it is uncontrolled. They also get onChange and onBlur.

init form

const form = useForm({ // config
  	initialValues: {x: "", y: ""},
	validate: f
	transformValues: (collected) => processed
})

form callback

function my_callback(data) {
    data.x
    data.y
}

const mantineEventHandler = form.onSubmit(my_callback)

wire up

form onSubmit={e => mantineEventHandler(e)}
	TextInput label="x" {...form.getInputProps('x')}
	TextInput label="x" {...form.getInputProps('y')}
	TextInput label="x" {...form.getInputProps('z')} // z is not part of initial values, but may still be included if input is changed}
/form

initial value or not.

Mantine collect properties from the initial values, even if there is no matching input, and even if the value is null or undefined, and provide them for processing.

If an input is connected to an initial value that does not exist, it is effectively initialized to undefined, which will generate a React warning. Mantine will collect the value of that input and associate with the key provided in form.getInputProps??

initial value for a TextInput

The initial value for a TextInput should no be null: it triggers a React warning. It should also not be set to undefined. Instead, we should initialize to empty string. We may do post-processing in the transformer to change the empty string to null.

utils

// get current form values
form.values

// Set all or some form values
form.setValues(values)

// Set all form values using the previous state
form.setValues((prev) => ({ ...prev, ...values }))

// Set value of single field
form.setFieldValue("path", value)

// Set value of nested field
form.setFieldValue("user.firstName", "Jane")

// Resets form.values to initialValues,
// clears all validation errors,
// resets touched and dirty state
form.reset()

// Sets initial values, used when form is reset
form.setInitialValues({ values: "object" })

useLocalStorage

useLocalStorage exposes an API similar to useState, but uses the local storage as a single source of truth. It requires a key that describes the local storage entry, and a defaultValue for when such entry is missing.

const config = {
    key: "default-ai-provider",
    defaultValue: "GPT",
}

synchronous version (opt-in)

When the component mounts, the hook attempts to read a persisted value from the local storage, and initializes the state with it. If the entry is missing, it creates one with defaultValue, and initialize the state with it.

const config = {
    // ..
    // opt-in for the synchronous version
    getInitialValueInEffect: false,
}

asynchronous version (default)

In the asynchronous version, the hook immediately initializes the state with the default value, to avoid delaying the first paint. The local storage lookup is implemented as an effect that runs later on, after the first paint. Only then it may push the persisted value to the state.

Having the first paint ignoring the persisted value is not always desirable, that's why we may opt-in for the synchronous version.

exposed API

The hook exposes the state value and the dispatch method. It also exposes a method to remove the entry:

const [provider, setProvider, removeEntry] = useLocalStorage<AIProvider>(config)

The hook listens for changes in state and updates the local storage accordingly.

useDisclosure

A wrapper around useState<boolean>. It exposes a boolean state variable and provides helper methods which use terminology related to UI elements being opened or closed, such as open() and close(), in place of setIsOpened(true/false).

const [opened, { open, close, toggle }] = useDisclosure(false)

useMantineColorTheme

We want to read the current colorScheme, or set it to a value: dark or light.

Mantine saves the selected colorScheme in localStorage.

Before the App appears on-screen, Mantine checks the localStorage for any saved colorScheme, so that the first paint is done with the correct colorScheme.

const { colorScheme, setColorScheme } = useMantineColorScheme()
// also: toggleColorScheme and clearColorScheme