Overview

Mantine provides pre-styled React UI components.

benefits

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

mantine style is opinionated and cohesive

Mantine brings a set of cohesive style defaults.

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

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

mantine's packages

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

terminology: components and component variants

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

terminology: component's inner-elements

A component is constructed through several, named, inner-elements. For example, Slider is built upon a mark, a track and a bar. We may style the inner-elements distinctively.

customization patterns

We have three main patterns:

  • (Local) customization of an instance: the scope of the change is local and limited.
  • (Global) override of a Mantine component: it affects all sites that use these components.
  • (Global) customization of style primitives, also called theming, which affects several components.

Customize an instance

overview

We want to customize a single instance of a component.

We do that by adding inline style, classes, or provide props.

inner workings

Technically, this pattern resolves to:

  • style that lives in the element's style attribute
  • markers set on the element, such as a class, an id, an attribute or a custom attribute, along with some CSS targeting those markers.
  • inner DOM elements being added.

the limits of setting style or className attributes directly

We may provide quick inline style in the style attribute or classes to the className attribute, but there is no granularity into which inner-element we target. The style we provide usually applies to the root inner-element.

While this is possible, it's not a pattern recommended by Mantine, because it doesn't let Mantine act as an intermediary.

mantine patterns

Mantine offers patterns that are more powerful, and for them, allow to target inner-elements.

props that we may set on any Mantine component instance.

Mantine's style props are a set of props that we may set on any Mantine component, and which aim to set a single style aspect. The style targets the top-level element.

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

props that are specific to one or more components

Some props are unique to some components. They may determine the style, but also determine the component's inner structure, such as toggling inner-elements. For example, the TextInput's label prop adds a label element in addition to the input element.

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

target an inner-element

We target an inner-element by specifying its Mantine-defined name, and provide a style object (styles pattern) or one or more classes (classNames pattern).

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

class naming pattern

The classes we provide in styles are to customize an instance of a given Mantine component. But we may wrap the instance into a new component, effectively creating a variant of the given Mantine component.

Given that, we may use the variant's name in all classes related to it, as a prefix, such as pinkbutton-* if we are making a Button variant. if we are to target an inner component, we use its name as a suffix.

.pinkbutton-root
.pinkbutton-label

Universal style props

We may set such props on any Mantine component. They target a single style aspect.

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
bgbackgroundtheme.colors
bgszbackgroundSize
bgpbackgroundPosition
bgrbackgroundRepeat
bgabackgroundAttachment
ccolortheme.colors gray.5 blue blue.7 blue.5 blue.0
opacityopacity
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration
wwidththeme.spacing
miwminWidththeme.spacing
mawmaxWidththeme.spacing
hheighttheme.spacing
mihminHeighttheme.spacing
mahmaxHeighttheme.spacing
posposition
toptop
leftleft
bottombottom
rightright
insetinset
displaydisplay
flexflex
bdborder
bdrsborderRadius

responsiveness: provide alternative values.

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

  • the base value provides the mobile-centric, default value. Its exact scope depends on which other breakpoints we define.
  • The xs value activates at 576p. It excludes most smartphones. For reference, iPhones are under 450p width, with standard-size iPhones under 400p. If we want to exclude phablets as well, we use sm instead.
  • The sm value activates at 768p (48em), which excludes phablets, and includes tablets and wider screens. For reference, iPads start at 768px.
<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'sm', 	sm: 'lg' }}
>

Global customization

Global customization, also called theming, aims to override style at the style-primitives level (affecting several components), or at the component level, affecting all instances.

customize style primitives

#todo

implement Mantine-provided, component-scoped empty classes

Mantine sticks a series of empty classes on each of its built-in components. We may implement them to customize a given aspect of a given component.

(Implementing such class affects all instances)

For example, Mantine sticks mantine-Button-root and mantine-Button-label on inner-elements of the Button component.

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

empty classes specificity

Those classes have the same specificity than Mantine's implemented internal classes. One technique to make them win is to import our stylesheet after Mantine's one.

CSS Module pattern

css module pattern overview

A processor parses a given stylesheet to scan the classes and expose them to JS. It generates a globally unique name for each of them. It packs the developer-defined classes and globally unique classes in a dictionary object:

export const classes = {
    supercool: "supercool_5cEkq2n0x1",
    supernice: "supernice_1kmox6oL39",
    superGreat: "superGreat_1kmox6oL39",
    "super-awesome": "super-awesome_1kmox6oL39",
} // conceptual // class dictionary

We import the dictionary.

import classes from "xxx.module.css";

<Button
classNames={{
        root: classes.xxx, 	// resolves to the processed class
        label: classes.xxx,	// resolves to the processed class
    }}
  >

pattern: a module targets a Mantine component custom variant

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

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

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

.root:hover {
}

.label {
}

We may then use the classes to inline-customize a Mantine's component instance, or to create a fully-fledged React component variant.

import PrimaryButtonClassNames from "PrimaryButton.module.css";
{/* provide properties */}
<Button
classNames={{
        root: PrimaryButtonClassNames.root,
        label: PrimaryButtonClassNames.label,
    }}
  >

provide the classes object directly

When we follow the inner-element-as-a-classname naming pattern, we may give the CSS-module object directly to classNames, since classNames expects an object with inner-element named properties.

<Button classNames={primaryButtonClassNames}>

Mantine's internal styling

case study: Mantine's implementation of Button

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

HTML element structure

There are several, nested elements:

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

Both the mantine-implemented internal classes and empty classes are present, on each inner-element:

<button class="m-77c9d27d mantine-Button-root .." ..>
    <span class="m-80f1301b mantine-Button-inner">
        <span class="m-811560b9 mantine-Button-label">
          Save
      </span>
    </span>
</button>

some inner-elements have some data-attributes, which may be shared across inner-elements of distinct components, such as the mantine-active and mantine-focus-auto attributes.

For example, mantine-active allows to receive style that activates when the element is active. The attribute is always there, and the stylesheet discriminates to the :active state.

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

Button's stylesheet

The stylesheet is a CSS module and follows the pattern where classes are named after inner-elements of the Button component. We reproduce the root class.

.root {
    --button-height-xs: 30px;
    --button-height-sm: 36px;
    --button-height-md: 42px;
    --button-height-lg: 50px;
    --button-height-xl: 60px;

    --button-height-compact-xs: 22px;
    --button-height-compact-sm: 26px;
    --button-height-compact-md: 30px;
    --button-height-compact-lg: 34px;
    --button-height-compact-xl: 40px;

    --button-padding-x-xs: 14px;
    --button-padding-x-sm: 18px;
    --button-padding-x-md: 22px;
    --button-padding-x-lg: 26px;
    --button-padding-x-xl: 32px;

    --button-padding-x-compact-xs: 7px;
    --button-padding-x-compact-sm: 8px;
    --button-padding-x-compact-md: 10px;
    --button-padding-x-compact-lg: 12px;
    --button-padding-x-compact-xl: 14px;

    --button-height: var(--button-height-sm);
    --button-padding-x: var(--button-padding-x-sm);
    --button-color: var(--mantine-color-white);

    user-select: none;
    font-weight: 600;
    position: relative;
    line-height: 1;
    text-align: center;
    overflow: hidden;

    width: auto;
    cursor: pointer;
    display: inline-block;
    border-radius: var(--button-radius, var(--mantine-radius-default));
    font-size: var(--button-fz, var(--mantine-font-size-sm));
    background: var(--button-bg, var(--mantine-primary-color-filled));
    border: var(--button-bd, rem(1px) solid transparent);
    color: var(--button-color, var(--mantine-color-white));
    height: var(--button-height, var(--button-height-sm));
    padding-inline: var(--button-padding-x, var(--button-padding-x-sm));
    vertical-align: middle;

    &:where([data-block]) {
        display: block;
        width: 100%;
    }

    &:where([data-with-left-section]) {
        padding-inline-start: calc(var(--button-padding-x) / 1.5);
    }

    &:where([data-with-right-section]) {
        padding-inline-end: calc(var(--button-padding-x) / 1.5);
    }

    &:where(:disabled:not([data-loading]), [data-disabled]:not([data-loading])) {
        cursor: not-allowed;
        border: 1px solid transparent;
        transform: none;

        @mixin where-light {
            color: var(--mantine-color-gray-5);
            background: var(--mantine-color-gray-1);
        }

        @mixin where-dark {
            color: var(--mantine-color-dark-3);
            background: var(--mantine-color-dark-6);
        }
    }

    &::before {
        content: "";
        pointer-events: none;
        position: absolute;
        inset: -1px;
        border-radius: var(--button-radius, var(--mantine-radius-default));
        transform: translateY(-100%);
        opacity: 0;
        filter: blur(12px);
        transition: transform 150ms ease, opacity 100ms ease;

        @mixin where-light {
            background-color: rgba(255, 255, 255, 0.15);
        }

        @mixin where-dark {
            background-color: rgba(0, 0, 0, 0.15);
        }
    }

    &:where([data-loading]) {
        cursor: not-allowed;
        transform: none;

        &::before {
            transform: translateY(0);
            opacity: 1;
        }

        & .inner {
            opacity: 0;
            transform: translateY(100%);
        }
    }

    @mixin hover {
        &:where(:not([data-loading], :disabled, [data-disabled])) {
            background-color: var(--button-hover, var(--mantine-primary-color-filled-hover));
            color: var(--button-hover-color, var(--button-color));
        }
    }
}

For reference, we also reproduce the typescript file. It imports the CSS module.

import {
    Box,
    BoxProps,
    createVarsResolver,
    getFontSize,
    getRadius,
    getSize,
    MantineColor,
    MantineGradient,
    MantineRadius,
    MantineSize,
    polymorphicFactory,
    PolymorphicFactory,
    rem,
    StylesApiProps,
    useProps,
    useStyles,
} from "../../core"
import { Loader, LoaderProps } from "../Loader"
import { MantineTransition, Transition } from "../Transition"
import { UnstyledButton } from "../UnstyledButton"
import { ButtonGroup } from "./ButtonGroup/ButtonGroup"
import { ButtonGroupSection } from "./ButtonGroupSection/ButtonGroupSection"
import classes from "./Button.module.css"

export type ButtonStylesNames = "root" | "inner" | "loader" | "section" | "label"
export type ButtonVariant =
    | "filled"
    | "light"
    | "outline"
    | "transparent"
    | "white"
    | "subtle"
    | "default"
    | "gradient"

export type ButtonCssVariables = {
    root:
        | "--button-justify"
        | "--button-height"
        | "--button-padding-x"
        | "--button-fz"
        | "--button-radius"
        | "--button-bg"
        | "--button-hover"
        | "--button-hover-color"
        | "--button-color"
        | "--button-bd"
}

export interface ButtonProps extends BoxProps, StylesApiProps<ButtonFactory> {
    "data-disabled"?: boolean

    /** Controls button `height`, `font-size` and horizontal `padding` @default `'sm'` */
    size?: MantineSize | `compact-${MantineSize}` | (string & {})

    /** Key of `theme.colors` or any valid CSS color @default `theme.primaryColor` */
    color?: MantineColor

    /** Sets `justify-content` of `inner` element, can be used to change distribution of sections and label @default `'center'` */
    justify?: React.CSSProperties["justifyContent"]

    /** Content displayed on the left side of the button label */
    leftSection?: React.ReactNode

    /** Content displayed on the right side of the button label */
    rightSection?: React.ReactNode

    /** If set, the button takes 100% width of its parent container @default `false` */
    fullWidth?: boolean

    /** Key of `theme.radius` or any valid CSS value to set `border-radius` @default `theme.defaultRadius` */
    radius?: MantineRadius

    /** Gradient configuration used when `variant="gradient"` @default `theme.defaultGradient` */
    gradient?: MantineGradient

    /** Sets `disabled` attribute, applies disabled styles */
    disabled?: boolean

    /** Button content */
    children?: React.ReactNode

    /** If set, the `Loader` component is displayed over the button */
    loading?: boolean

    /** Props added to the `Loader` component (only visible when `loading` prop is set) */
    loaderProps?: LoaderProps

    /** If set, adjusts text color based on background color for `filled` variant */
    autoContrast?: boolean
}

export type ButtonFactory = PolymorphicFactory<{
    props: ButtonProps
    defaultRef: HTMLButtonElement
    defaultComponent: "button"
    stylesNames: ButtonStylesNames
    vars: ButtonCssVariables
    variant: ButtonVariant
    staticComponents: {
        Group: typeof ButtonGroup
        GroupSection: typeof ButtonGroupSection
    }
}>

const loaderTransition: MantineTransition = {
    in: { opacity: 1, transform: `translate(-50%, calc(-50% + ${rem(1)}))` },
    out: { opacity: 0, transform: "translate(-50%, -200%)" },
    common: { transformOrigin: "center" },
    transitionProperty: "transform, opacity",
}

const varsResolver = createVarsResolver<ButtonFactory>(
    (theme, { radius, color, gradient, variant, size, justify, autoContrast }) => {
        const colors = theme.variantColorResolver({
            color: color || theme.primaryColor,
            theme,
            gradient,
            variant: variant || "filled",
            autoContrast,
        })

        return {
            root: {
                "--button-justify": justify,
                "--button-height": getSize(size, "button-height"),
                "--button-padding-x": getSize(size, "button-padding-x"),
                "--button-fz": size?.includes("compact")
                    ? getFontSize(size.replace("compact-", ""))
                    : getFontSize(size),
                "--button-radius": radius === undefined ? undefined : getRadius(radius),
                "--button-bg": color || variant ? colors.background : undefined,
                "--button-hover": color || variant ? colors.hover : undefined,
                "--button-color": colors.color,
                "--button-bd": color || variant ? colors.border : undefined,
                "--button-hover-color": color || variant ? colors.hoverColor : undefined,
            },
        }
    }
)

export const Button = polymorphicFactory<ButtonFactory>((_props, ref) => {
    const props = useProps("Button", null, _props)
    const {
        style,
        vars,
        className,
        color,
        disabled,
        children,
        leftSection,
        rightSection,
        fullWidth,
        variant,
        radius,
        loading,
        loaderProps,
        gradient,
        classNames,
        styles,
        unstyled,
        "data-disabled": dataDisabled,
        autoContrast,
        mod,
        attributes,
        ...others
    } = props

    const getStyles = useStyles<ButtonFactory>({
        name: "Button",
        props,
        classes,
        className,
        style,
        classNames,
        styles,
        unstyled,
        attributes,
        vars,
        varsResolver,
    })

    const hasLeftSection = !!leftSection
    const hasRightSection = !!rightSection

    return (
        <UnstyledButton
            ref={ref}
            {...getStyles("root", { active: !disabled && !loading && !dataDisabled })}
            unstyled={unstyled}
            variant={variant}
            disabled={disabled || loading}
            mod={[
                {
                    disabled: disabled || dataDisabled,
                    loading,
                    block: fullWidth,
                    "with-left-section": hasLeftSection,
                    "with-right-section": hasRightSection,
                },
                mod,
            ]}
            {...others}
        >
            {typeof loading === "boolean" && (
                <Transition mounted={loading} transition={loaderTransition} duration={150}>
                    {(transitionStyles) => (
                        <Box
                            component="span"
                            {...getStyles("loader", { style: transitionStyles })}
                            aria-hidden
                        >
                            <Loader
                                color="var(--button-color)"
                                size="calc(var(--button-height) / 1.8)"
                                {...loaderProps}
                            />
                        </Box>
                    )}
                </Transition>
            )}

            <span {...getStyles("inner")}>
                {leftSection && (
                    <Box component="span" {...getStyles("section")} mod={{ position: "left" }}>
                        {leftSection}
                    </Box>
                )}

                <Box component="span" mod={{ loading }} {...getStyles("label")}>
                    {children}
                </Box>

                {rightSection && (
                    <Box component="span" {...getStyles("section")} mod={{ position: "right" }}>
                        {rightSection}
                    </Box>
                )}
            </span>
        </UnstyledButton>
    )
})

Button.classes = classes
Button.displayName = "@mantine/core/Button"
Button.Group = ButtonGroup
Button.GroupSection = ButtonGroupSection

style shipped on npm

the style is mostly the same. It is probably processed by PostCSS. It does not use CSS nesting.

It is then processed even more and merged to a gigantic 232kb stylesheet, shipped on npm too, the one we import from our app.

Light mode, dark mode

Mantine defaults to light mode: defaultColorScheme defaults to light.

```We may change it to darkorauto. auto is better because it follows the user's color scheme.

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

Default theme

The default theme spreads over several categories.

spacing

space between elements and inside elements.

xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32px

used for or usable by:

  • gap for Group, Flex, Stack.
  • padding props.
  • margin props, notably for Divider
  • width and height props such as w and h, notably for Space.

radius

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

used for:

  • Paper
  • Dialog, Modal, ..
  • Button, Tooltip..

we may set the default with defaultRadius:

"defaultRadius": "sm",

breakpoints

a set of alternative thresholds that we may target to activate style conditionally.

  "breakpoints": {
    "xs": "36em", // 576px
    "sm": "48em", // 768px
    "md": "62em",
    "lg": "75em",
    "xl": "88em"
  },

The sm breakpoint targets tablets and wider devices.

we specify one or more thresholds in the styles-props alternative-values pattern.

font sizes: a set of 5 distinct sizes

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

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

  • the fz style prop, on any component.

line heights

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

headings

family, weight and wrap behavior:

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

size and line height:

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

Gray Palette

description and use

It is a slate gray: it has a blue tint.

Mantine's light-mode theme makes use of this palette: it offers several whites for backgrounds, and a few dark grays for text. It does not include pure white, instead, it starts at #F8F9FA.

The following consideration usually apply for light-mode:

  • The background may be pure white. It may be the app/site global background of the controls background.

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

  • The text is black-ish or gray-ish depending on importance and role:

    • pure black text:
      • some headings
      • body text
      • input label
      • button label
    • gray 9 (readable black for body text)
    • gray 8 (readable black)
    • dark 7 (dimmed black)
    • gray 6 (gray) for secondary text, and input description

values

  • gray.0 white: #F8F9FA
  • gray.1 white: #F1F3F5
  • gray.2 white gray: #E9ECEF
  • ...
  • gray.6 gray: #868e96
  • gray.7 dark gray: #495057
  • gray.8 very dark gray: #343A40
  • gray.9: very dark gray: #212529

Dark Palette

the dark palette

It is a neutral gray palette. It was a slate-gray palette (blue-tint) before Mantine 7.3.

Mantine's dark-mode theme makes use of this palette. It offers several darks for backgrounds, grays for controls, and whites for text.

use in dark mode

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

values

  • dark.0: very light gray close to white: #C9C9C9.
  • ..
  • dark.9: black: #141414

revert to blue-tinted dark palette

const theme = createTheme({
    colors: {
        dark: [
            "#C1C2C5",
            "#A6A7AB",
            "#909296",
            "#5c5f66",
            "#373A40",
            "#2C2E33",
            "#25262b",
            "#1A1B1E",
            "#141517",
            "#101113",
        ],
    },
})

Global Style: Colors

#todo

--mantine-color-scheme: dark;
--mantine-primary-color-contrast: var(--mantine-color-white);
--mantine-color-bright: var(--mantine-color-white);
--mantine-color-text: var(--mantine-color-dark-0);
--mantine-color-body: var(--mantine-color-dark-7);
--mantine-color-error: var(--mantine-color-red-8);
--mantine-color-placeholder: var(--mantine-color-dark-3);
--mantine-color-anchor: var(--mantine-color-blue-4);
--mantine-color-default: var(--mantine-color-dark-6);
--mantine-color-default-hover: var(--mantine-color-dark-5);
--mantine-color-default-color: var(--mantine-color-white);
--mantine-color-default-border: var(--mantine-color-dark-4);
--mantine-color-dimmed: var(--mantine-color-dark-2);
--mantine-color-dark-text: var(--mantine-color-dark-4);
--mantine-color-dark-filled: var(--mantine-color-dark-8);
--mantine-color-dark-filled-hover: var(--mantine-color-dark-7);
--mantine-color-dark-light: rgba(36, 36, 36, 0.15);
--mantine-color-dark-light-hover: rgba(36, 36, 36, 0.2);
--mantine-color-dark-light-color: var(--mantine-color-dark-3);
--mantine-color-dark-outline: var(--mantine-color-dark-4);
--mantine-color-dark-outline-hover: rgba(36, 36, 36, 0.05);
--mantine-color-gray-text: var(--mantine-color-gray-4);
--mantine-color-gray-filled: var(--mantine-color-gray-8);
--mantine-color-gray-filled-hover: var(--mantine-color-gray-9);
--mantine-color-gray-light: rgba(134, 142, 150, 0.15);
--mantine-color-gray-light-hover: rgba(134, 142, 150, 0.2);
--mantine-color-gray-light-color: var(--mantine-color-gray-3);
--mantine-color-gray-outline: var(--mantine-color-gray-4);
--mantine-color-gray-outline-hover: rgba(206, 212, 218, 0.05);
--mantine-color-red-text: var(--mantine-color-red-4);

Global style: Others

line-height defaults to 1.55

--mantine-line-height: 1.55;

Flex

Thin abstraction over a flex div.

Mantine only sets display: flex if nothing else is set.

props:

  • the direction, horizontal (row) by default. We may set it as vertical (column), and switch to horizontal on desktop.
  • the gap, defaults to 0, due to matching a regular flex.
  • justify, aka main axis alignment, default to flex-start, due to matching a regular flex.
  • align, aka cross-axis alignment, defaults to stretch, due to matching regular flex.
<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'flex-start', 	sm: 'space-between' }}
>

Group

Layout elements horizontally, add some space between them. This is a high-level abstraction over a horizontal flex. The container spans the whole line.

  • gap: defaults to some spacing, as opposed to a regular flex. The default spacing is md, aka 16px.
  • justify: defaults to flex-start. (elements sit at the left)
  • align: defaults to center (vertically center), instead of stretch for regular flex.
  • grow: make children same width. It calculates the tentative equal width by dividing the remaining space by the number of items. It uses this tentative width a max-width for each children. Then, it sets flex-grow: 1 on each of them. If there was no limit, the bigger items would still be bigger than the others even after the growing process, because each child would receive an equal share of the remaining space. Defaults to false. Also activates mono-line.
  • wrap: pick between mono-line (nowrap) or multi-line flex (wrap) and set the flex-wrap property with it. Mantine default to multi-line (wrap) whereas a flex-div defaults to nowrap.

The subtle differences with a regular flex may not be worth it. The main point could be the use of grow to make children of equal width.

Stack

Layout elements vertically, add some space between elements.

A thin abstraction over a flex: it adds a gap and makes the flex vertical (by setting flex-direction).

The stack itself occupies the full width because it is a display:flex. Elements stretch to fill the stack by default (align prop).

  • gap: defaults to 16px.
  • justify: defaults to browser's default: flex-start (elements sit at the top),
  • align: defaults to browser's default: stretch (elements fill the cross axis horizontally)

SimpleGrid

A grid with equal-width tracks, and which expands horizontally (fluid).

We set the number of tracks with cols. It defaults to a single track. Wide screens accommodate multiple tracks, but mobile devices may stick with a single one. As such, it's common to set cols with at least two values:

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

Mantine adds space between tracks by default (16px). We may set it with spacing and verticalSpacing.

Container

A wrapper that layouts the inner content, such as the main column of a website, so that it displays nicely, both in desktop and mobile.

limited-width container version

The first version, the default one, comes with a max-width that limits the width in desktop, while bringing minimum padding for mobiles so that the content does not touch the screen edges.

it centers the content with margin: auto.

The max-width is defined by size. It defaults to md (aka 960px, or 60rem). We may pick 720px (sm). If we are to provide an arbitrary numeric value, we may as well use maw directly which is semantically more accurate.

The default padding is 16px, aka md. We may set it with the universal px style prop.

fluid container version

We may opt for a fluid container, which expands horizontally, with the fluid prop. It only comes with some padding, at 16px.

The fluid version is a bit of an overkill: We may as well use a Box with some horizontal padding instead.

Center

A utility component that "CenterCenters" the child or children.

It translates to a div with:

  • display: flex
  • justify-content: center
  • align-items: 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.

As a regular div, it occupies the whole parent's width. As a display:flex, it makes children adopt a max-content width.

Divider

A line that acts as a separator and sections divider, similar in essence to the <hr> element.

It may be used as an horizontal line or a vertical line.

Technically, Mantine uses an element with no content, but whose border acts as the visible line. The size determines the line's size.

The line's default color is?

The line does not have any margin by default, we use my or mx, or set the spacing at the parent level.

  • orientation: set the line orientation, "horizontal" (default), or "vertical".
  • size: set the line thickness, xs by default, aka 1px.
  • variant: set a line variant: solid (default), dashed or dotted

Space

Add some spacing at a location in the layout. It uses an element with no content but with some non-zero size. It defaults to size 0. As such, we must set either the height or the width.

  • h, set the height, to set the vertical space, usually in a stack-like layout.
  • w, set the width, to set the horizontal space, usually in a horizontal flex-like layout.

Paper

Encapsulate some UI to set it apart visually.

Paper sets a fill-color background.

In light mode, it sets a plain white #FFF background. This matches the light mode default background. As such, we either change the light mode default background, or change the Paper background.

In dark mode, it sets a dark.7. Similarly, it matches the dark mode default background. As such, we either change the dark mode default background, or change the Paper background.

In addition to the background, we may add a pre-styled border and a pre-styled shadow.

We usually add some padding, but nothing specific to Paper.

props

  • withBorder, toggles a predefined border. defaults to false
  • shadow, sets the size of a predefined shadow, if any. defaults to none.
  • radius, sets the background area radius. defaults to sm, aka 4px.

Accordion

The Accordion is a container for one or more items that are expandable. Expanding an item will show additional content related to that item.

For each item, show a preview and allow disclosure of a detail panel.

The Accordion may contain one or several items, related or not. An item shows up as a clickable preview (Accordion.Control) and embeds a panel, hidden by default, which we toggle through clicking.

In Mantine, the detail panel mounts immediately, regardless if it's toggled or not.

In Mantine, we set the chevron style at the Accordion's root, so it's shared among all items

The panel may be a form. In that case, we may set the chevron as a plus sign, to indicate we intend to add (submit) data to the server.

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

	Accordion.Item value="item2"
		..

Appshell

Appshell is a mega layout component that aims to manage:

  • The header that appears on-top, also called the banner.
  • The main section, that appears below the header
  • The potential left-side navbar and right-side aside.

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 an <input type=text/> element. It inherits some props from the abstract <Input> component.

  • We set the main label with label. A main label is often necessary to explain the purpose of such input.
  • We set the optional secondary label with description
  • placeholder sets the placeholder text
  • leftSection and rightSection allow to insert an icon, visually inside the text input.
  • The required prop adds the required attribute to the element. In that case, Mantine automatically adds an asterisk. But we may remove it with withAsterisk.

NumberInput

Under the hood, It's a text input. This allows more flexibility, such as displaying formatting commas directly in the input.

The value we get is a number or, when it's not possible to infer a number, a string. For example, we get an empty string when the input is empty.

We may customize the input:

  • allowDecimal: allow or forbid decimal numbers. Defaults to allow.
  • allowNegative: allow or forbid negative numbers. Defaults to allow.
  • min, max: specify minimum and maximum values. The way it influences the input is determined by clampBehavior
  • clampBehavior: determine how the value should clamp within the limits. We may forbid input outside the limits (strict), clamp after input on blur (d), or never clamp (none)

As any text input we may have:

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

FileInput

FileInput comes with props specific to the goal of selecting files:

  • We may allow multi-file selection with multiple. It defaults to false.
  • We may specify a file type in accept, such as image/png
  • We may show a clear button on the right by adding clearable
  • We work with the selected file by providing a handler to onChange. The handler receives a File object (or a File array) whenever the user picks some files or clears the selection.

We may limit the input's width to truncate long file names. We may add a file icon in the leftSection. The unstyled variant is similar to the transparent variant: there is no border or background. We may set it size to xs.

Finally, FileInput comes with the traditional input props such as label and placeholder.

<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 HTML <select> element that embeds some <option> elements. We build an array describing data for each option, and provide it to the data attribute. The data for an option is a record with a label and a value.

If the label is the same as the value (not recommended), we may provide a string instead of a record, which acts as both label and value.

  • data: an array of {label,value} elements.
  • label: The label that describes the select
const options = [
    { label: "Death Knight", value: "deathKnight" },
    { label: "Demon Hunter", value: "demonHunter" },
]

const options = ["deathKnight", "demonHunter"]
<NativeSelect data={options} label="Class" />

SegmentedControl

SegmentedControl allows to pick a value among a list of predefined options. In that it resembles a Select control or a Radio button. All options remain simultaneously on-screen , which is typical of a Radio button.

The picked option has a highlight overlay ontop. As the selected option changes, Mantine moves the overlay toward the selection with a smooth animation.

Each option comes with a label, which is the user-facing description, and a value, which is the unique identifier that represents it, and which we store in the client state. We make the value conform to a type union to prevent typos.

We store the options in data. We set the initial selection in value.

onChange is called on user selection and before browser paint. React prevents any change unless we mutate the state accordingly.

type AIProvider = "GPT" | "IMAGEN"

const [provider, setProvider] = useState<AIProvider>("GPT")

<SegmentedControl
    radius="md"
    size="sm"
    value={provider}
    bg={"transparent"}
    onChange={(v) => setProvider(v as AIProvider)}
    data={[
        { label: openAILabel, value: "GPT" },
        { label: geminiLabel, value: "IMAGEN" },
    ]}
/>

Button

An abstraction over a <button>.

props

  • variant, set the button overall appearance, such as filled, outline, subtle, gradient or default. defaults to filled
  • color, set the button overall color. the coloring depends on the variant. defaults to the theme's primaryColor, aka Mantine's blue.
  • leftSection, rightSection, for example for an icon.
  • size, to specify two things. First, if it's a compact button with less inner space. second, if we want a smaller or bigger font size.
  • fullWidth, to fill the parent's width
  • disabled, sets the appearance and behaviour to disabled. the disabled appearance is the same across all variants.
  • loading, sets the appearance to loading, aka loader overlaid on top, and the behaviour to disabled. We may customize the loader with loaderProps (see Loader component)
  • gradient, sets the gradient's colors and direction. It requires using the gradient variant.
<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>.

The idea is for the developer to explicitely set type=submit on the Button.

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

The modal is a container for some UI that is conditionally overlaid on top of the application. It dims the background while opened. We may dismiss it with escape, the close button (if it's displayed), or with a click outside.

The modal works with an opened prop: it expects a boolean state variable, which effectively controls the display.

reminder: We may get such a boolean state variable with useDisclosure, which also gives semantically correct open and close state-mutation functions.

When the user attempts to dismiss the modal, Mantine calls the onClose handler: We have to provide one. This handler has the responsibility to toggle the opened state back to false if it approves the dismiss. It will usually call the close function.

import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';

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

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

      <Button .. onClick={open}>
        Open modal
      </Button>
    </>
  );
}

The modal has a top section by default. It may have

  • a close button, which we may remove with withCloseButton={false}.
  • a title, which accepts any React node (not only text).

The inner content usually comes with controls: inputs and a submit button.

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 2025 - All rights reserved

Overview

Mantine provides pre-styled React UI components.

benefits

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

mantine style is opinionated and cohesive

Mantine brings a set of cohesive style defaults.

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

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

mantine's packages

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

terminology: components and component variants

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

terminology: component's inner-elements

A component is constructed through several, named, inner-elements. For example, Slider is built upon a mark, a track and a bar. We may style the inner-elements distinctively.

customization patterns

We have three main patterns:

  • (Local) customization of an instance: the scope of the change is local and limited.
  • (Global) override of a Mantine component: it affects all sites that use these components.
  • (Global) customization of style primitives, also called theming, which affects several components.

Customize an instance

overview

We want to customize a single instance of a component.

We do that by adding inline style, classes, or provide props.

inner workings

Technically, this pattern resolves to:

  • style that lives in the element's style attribute
  • markers set on the element, such as a class, an id, an attribute or a custom attribute, along with some CSS targeting those markers.
  • inner DOM elements being added.

the limits of setting style or className attributes directly

We may provide quick inline style in the style attribute or classes to the className attribute, but there is no granularity into which inner-element we target. The style we provide usually applies to the root inner-element.

While this is possible, it's not a pattern recommended by Mantine, because it doesn't let Mantine act as an intermediary.

mantine patterns

Mantine offers patterns that are more powerful, and for them, allow to target inner-elements.

props that we may set on any Mantine component instance.

Mantine's style props are a set of props that we may set on any Mantine component, and which aim to set a single style aspect. The style targets the top-level element.

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

props that are specific to one or more components

Some props are unique to some components. They may determine the style, but also determine the component's inner structure, such as toggling inner-elements. For example, the TextInput's label prop adds a label element in addition to the input element.

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

target an inner-element

We target an inner-element by specifying its Mantine-defined name, and provide a style object (styles pattern) or one or more classes (classNames pattern).

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

class naming pattern

The classes we provide in styles are to customize an instance of a given Mantine component. But we may wrap the instance into a new component, effectively creating a variant of the given Mantine component.

Given that, we may use the variant's name in all classes related to it, as a prefix, such as pinkbutton-* if we are making a Button variant. if we are to target an inner component, we use its name as a suffix.

.pinkbutton-root
.pinkbutton-label

Universal style props

We may set such props on any Mantine component. They target a single style aspect.

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
bgbackgroundtheme.colors
bgszbackgroundSize
bgpbackgroundPosition
bgrbackgroundRepeat
bgabackgroundAttachment
ccolortheme.colors gray.5 blue blue.7 blue.5 blue.0
opacityopacity
fffontFamily
fzfontSizetheme.fontSizes
fwfontWeight
ltsletterSpacing
tatextAlign
lhlineHeighttheme.lineHeights
fsfontStyle
tttextTransform
tdtextDecoration
wwidththeme.spacing
miwminWidththeme.spacing
mawmaxWidththeme.spacing
hheighttheme.spacing
mihminHeighttheme.spacing
mahmaxHeighttheme.spacing
posposition
toptop
leftleft
bottombottom
rightright
insetinset
displaydisplay
flexflex
bdborder
bdrsborderRadius

responsiveness: provide alternative values.

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

  • the base value provides the mobile-centric, default value. Its exact scope depends on which other breakpoints we define.
  • The xs value activates at 576p. It excludes most smartphones. For reference, iPhones are under 450p width, with standard-size iPhones under 400p. If we want to exclude phablets as well, we use sm instead.
  • The sm value activates at 768p (48em), which excludes phablets, and includes tablets and wider screens. For reference, iPads start at 768px.
<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'sm', 	sm: 'lg' }}
>

Global customization

Global customization, also called theming, aims to override style at the style-primitives level (affecting several components), or at the component level, affecting all instances.

customize style primitives

#todo

implement Mantine-provided, component-scoped empty classes

Mantine sticks a series of empty classes on each of its built-in components. We may implement them to customize a given aspect of a given component.

(Implementing such class affects all instances)

For example, Mantine sticks mantine-Button-root and mantine-Button-label on inner-elements of the Button component.

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

empty classes specificity

Those classes have the same specificity than Mantine's implemented internal classes. One technique to make them win is to import our stylesheet after Mantine's one.

CSS Module pattern

css module pattern overview

A processor parses a given stylesheet to scan the classes and expose them to JS. It generates a globally unique name for each of them. It packs the developer-defined classes and globally unique classes in a dictionary object:

export const classes = {
    supercool: "supercool_5cEkq2n0x1",
    supernice: "supernice_1kmox6oL39",
    superGreat: "superGreat_1kmox6oL39",
    "super-awesome": "super-awesome_1kmox6oL39",
} // conceptual // class dictionary

We import the dictionary.

import classes from "xxx.module.css";

<Button
classNames={{
        root: classes.xxx, 	// resolves to the processed class
        label: classes.xxx,	// resolves to the processed class
    }}
  >

pattern: a module targets a Mantine component custom variant

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

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

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

.root:hover {
}

.label {
}

We may then use the classes to inline-customize a Mantine's component instance, or to create a fully-fledged React component variant.

import PrimaryButtonClassNames from "PrimaryButton.module.css";
{/* provide properties */}
<Button
classNames={{
        root: PrimaryButtonClassNames.root,
        label: PrimaryButtonClassNames.label,
    }}
  >

provide the classes object directly

When we follow the inner-element-as-a-classname naming pattern, we may give the CSS-module object directly to classNames, since classNames expects an object with inner-element named properties.

<Button classNames={primaryButtonClassNames}>

Mantine's internal styling

case study: Mantine's implementation of Button

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

HTML element structure

There are several, nested elements:

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

Both the mantine-implemented internal classes and empty classes are present, on each inner-element:

<button class="m-77c9d27d mantine-Button-root .." ..>
    <span class="m-80f1301b mantine-Button-inner">
        <span class="m-811560b9 mantine-Button-label">
          Save
      </span>
    </span>
</button>

some inner-elements have some data-attributes, which may be shared across inner-elements of distinct components, such as the mantine-active and mantine-focus-auto attributes.

For example, mantine-active allows to receive style that activates when the element is active. The attribute is always there, and the stylesheet discriminates to the :active state.

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

Button's stylesheet

The stylesheet is a CSS module and follows the pattern where classes are named after inner-elements of the Button component. We reproduce the root class.

.root {
    --button-height-xs: 30px;
    --button-height-sm: 36px;
    --button-height-md: 42px;
    --button-height-lg: 50px;
    --button-height-xl: 60px;

    --button-height-compact-xs: 22px;
    --button-height-compact-sm: 26px;
    --button-height-compact-md: 30px;
    --button-height-compact-lg: 34px;
    --button-height-compact-xl: 40px;

    --button-padding-x-xs: 14px;
    --button-padding-x-sm: 18px;
    --button-padding-x-md: 22px;
    --button-padding-x-lg: 26px;
    --button-padding-x-xl: 32px;

    --button-padding-x-compact-xs: 7px;
    --button-padding-x-compact-sm: 8px;
    --button-padding-x-compact-md: 10px;
    --button-padding-x-compact-lg: 12px;
    --button-padding-x-compact-xl: 14px;

    --button-height: var(--button-height-sm);
    --button-padding-x: var(--button-padding-x-sm);
    --button-color: var(--mantine-color-white);

    user-select: none;
    font-weight: 600;
    position: relative;
    line-height: 1;
    text-align: center;
    overflow: hidden;

    width: auto;
    cursor: pointer;
    display: inline-block;
    border-radius: var(--button-radius, var(--mantine-radius-default));
    font-size: var(--button-fz, var(--mantine-font-size-sm));
    background: var(--button-bg, var(--mantine-primary-color-filled));
    border: var(--button-bd, rem(1px) solid transparent);
    color: var(--button-color, var(--mantine-color-white));
    height: var(--button-height, var(--button-height-sm));
    padding-inline: var(--button-padding-x, var(--button-padding-x-sm));
    vertical-align: middle;

    &:where([data-block]) {
        display: block;
        width: 100%;
    }

    &:where([data-with-left-section]) {
        padding-inline-start: calc(var(--button-padding-x) / 1.5);
    }

    &:where([data-with-right-section]) {
        padding-inline-end: calc(var(--button-padding-x) / 1.5);
    }

    &:where(:disabled:not([data-loading]), [data-disabled]:not([data-loading])) {
        cursor: not-allowed;
        border: 1px solid transparent;
        transform: none;

        @mixin where-light {
            color: var(--mantine-color-gray-5);
            background: var(--mantine-color-gray-1);
        }

        @mixin where-dark {
            color: var(--mantine-color-dark-3);
            background: var(--mantine-color-dark-6);
        }
    }

    &::before {
        content: "";
        pointer-events: none;
        position: absolute;
        inset: -1px;
        border-radius: var(--button-radius, var(--mantine-radius-default));
        transform: translateY(-100%);
        opacity: 0;
        filter: blur(12px);
        transition: transform 150ms ease, opacity 100ms ease;

        @mixin where-light {
            background-color: rgba(255, 255, 255, 0.15);
        }

        @mixin where-dark {
            background-color: rgba(0, 0, 0, 0.15);
        }
    }

    &:where([data-loading]) {
        cursor: not-allowed;
        transform: none;

        &::before {
            transform: translateY(0);
            opacity: 1;
        }

        & .inner {
            opacity: 0;
            transform: translateY(100%);
        }
    }

    @mixin hover {
        &:where(:not([data-loading], :disabled, [data-disabled])) {
            background-color: var(--button-hover, var(--mantine-primary-color-filled-hover));
            color: var(--button-hover-color, var(--button-color));
        }
    }
}

For reference, we also reproduce the typescript file. It imports the CSS module.

import {
    Box,
    BoxProps,
    createVarsResolver,
    getFontSize,
    getRadius,
    getSize,
    MantineColor,
    MantineGradient,
    MantineRadius,
    MantineSize,
    polymorphicFactory,
    PolymorphicFactory,
    rem,
    StylesApiProps,
    useProps,
    useStyles,
} from "../../core"
import { Loader, LoaderProps } from "../Loader"
import { MantineTransition, Transition } from "../Transition"
import { UnstyledButton } from "../UnstyledButton"
import { ButtonGroup } from "./ButtonGroup/ButtonGroup"
import { ButtonGroupSection } from "./ButtonGroupSection/ButtonGroupSection"
import classes from "./Button.module.css"

export type ButtonStylesNames = "root" | "inner" | "loader" | "section" | "label"
export type ButtonVariant =
    | "filled"
    | "light"
    | "outline"
    | "transparent"
    | "white"
    | "subtle"
    | "default"
    | "gradient"

export type ButtonCssVariables = {
    root:
        | "--button-justify"
        | "--button-height"
        | "--button-padding-x"
        | "--button-fz"
        | "--button-radius"
        | "--button-bg"
        | "--button-hover"
        | "--button-hover-color"
        | "--button-color"
        | "--button-bd"
}

export interface ButtonProps extends BoxProps, StylesApiProps<ButtonFactory> {
    "data-disabled"?: boolean

    /** Controls button `height`, `font-size` and horizontal `padding` @default `'sm'` */
    size?: MantineSize | `compact-${MantineSize}` | (string & {})

    /** Key of `theme.colors` or any valid CSS color @default `theme.primaryColor` */
    color?: MantineColor

    /** Sets `justify-content` of `inner` element, can be used to change distribution of sections and label @default `'center'` */
    justify?: React.CSSProperties["justifyContent"]

    /** Content displayed on the left side of the button label */
    leftSection?: React.ReactNode

    /** Content displayed on the right side of the button label */
    rightSection?: React.ReactNode

    /** If set, the button takes 100% width of its parent container @default `false` */
    fullWidth?: boolean

    /** Key of `theme.radius` or any valid CSS value to set `border-radius` @default `theme.defaultRadius` */
    radius?: MantineRadius

    /** Gradient configuration used when `variant="gradient"` @default `theme.defaultGradient` */
    gradient?: MantineGradient

    /** Sets `disabled` attribute, applies disabled styles */
    disabled?: boolean

    /** Button content */
    children?: React.ReactNode

    /** If set, the `Loader` component is displayed over the button */
    loading?: boolean

    /** Props added to the `Loader` component (only visible when `loading` prop is set) */
    loaderProps?: LoaderProps

    /** If set, adjusts text color based on background color for `filled` variant */
    autoContrast?: boolean
}

export type ButtonFactory = PolymorphicFactory<{
    props: ButtonProps
    defaultRef: HTMLButtonElement
    defaultComponent: "button"
    stylesNames: ButtonStylesNames
    vars: ButtonCssVariables
    variant: ButtonVariant
    staticComponents: {
        Group: typeof ButtonGroup
        GroupSection: typeof ButtonGroupSection
    }
}>

const loaderTransition: MantineTransition = {
    in: { opacity: 1, transform: `translate(-50%, calc(-50% + ${rem(1)}))` },
    out: { opacity: 0, transform: "translate(-50%, -200%)" },
    common: { transformOrigin: "center" },
    transitionProperty: "transform, opacity",
}

const varsResolver = createVarsResolver<ButtonFactory>(
    (theme, { radius, color, gradient, variant, size, justify, autoContrast }) => {
        const colors = theme.variantColorResolver({
            color: color || theme.primaryColor,
            theme,
            gradient,
            variant: variant || "filled",
            autoContrast,
        })

        return {
            root: {
                "--button-justify": justify,
                "--button-height": getSize(size, "button-height"),
                "--button-padding-x": getSize(size, "button-padding-x"),
                "--button-fz": size?.includes("compact")
                    ? getFontSize(size.replace("compact-", ""))
                    : getFontSize(size),
                "--button-radius": radius === undefined ? undefined : getRadius(radius),
                "--button-bg": color || variant ? colors.background : undefined,
                "--button-hover": color || variant ? colors.hover : undefined,
                "--button-color": colors.color,
                "--button-bd": color || variant ? colors.border : undefined,
                "--button-hover-color": color || variant ? colors.hoverColor : undefined,
            },
        }
    }
)

export const Button = polymorphicFactory<ButtonFactory>((_props, ref) => {
    const props = useProps("Button", null, _props)
    const {
        style,
        vars,
        className,
        color,
        disabled,
        children,
        leftSection,
        rightSection,
        fullWidth,
        variant,
        radius,
        loading,
        loaderProps,
        gradient,
        classNames,
        styles,
        unstyled,
        "data-disabled": dataDisabled,
        autoContrast,
        mod,
        attributes,
        ...others
    } = props

    const getStyles = useStyles<ButtonFactory>({
        name: "Button",
        props,
        classes,
        className,
        style,
        classNames,
        styles,
        unstyled,
        attributes,
        vars,
        varsResolver,
    })

    const hasLeftSection = !!leftSection
    const hasRightSection = !!rightSection

    return (
        <UnstyledButton
            ref={ref}
            {...getStyles("root", { active: !disabled && !loading && !dataDisabled })}
            unstyled={unstyled}
            variant={variant}
            disabled={disabled || loading}
            mod={[
                {
                    disabled: disabled || dataDisabled,
                    loading,
                    block: fullWidth,
                    "with-left-section": hasLeftSection,
                    "with-right-section": hasRightSection,
                },
                mod,
            ]}
            {...others}
        >
            {typeof loading === "boolean" && (
                <Transition mounted={loading} transition={loaderTransition} duration={150}>
                    {(transitionStyles) => (
                        <Box
                            component="span"
                            {...getStyles("loader", { style: transitionStyles })}
                            aria-hidden
                        >
                            <Loader
                                color="var(--button-color)"
                                size="calc(var(--button-height) / 1.8)"
                                {...loaderProps}
                            />
                        </Box>
                    )}
                </Transition>
            )}

            <span {...getStyles("inner")}>
                {leftSection && (
                    <Box component="span" {...getStyles("section")} mod={{ position: "left" }}>
                        {leftSection}
                    </Box>
                )}

                <Box component="span" mod={{ loading }} {...getStyles("label")}>
                    {children}
                </Box>

                {rightSection && (
                    <Box component="span" {...getStyles("section")} mod={{ position: "right" }}>
                        {rightSection}
                    </Box>
                )}
            </span>
        </UnstyledButton>
    )
})

Button.classes = classes
Button.displayName = "@mantine/core/Button"
Button.Group = ButtonGroup
Button.GroupSection = ButtonGroupSection

style shipped on npm

the style is mostly the same. It is probably processed by PostCSS. It does not use CSS nesting.

It is then processed even more and merged to a gigantic 232kb stylesheet, shipped on npm too, the one we import from our app.

Light mode, dark mode

Mantine defaults to light mode: defaultColorScheme defaults to light.

```We may change it to darkorauto. auto is better because it follows the user's color scheme.

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

Default theme

The default theme spreads over several categories.

spacing

space between elements and inside elements.

xs: 10px
sm: 12px
md: 16px
lg: 20px
xl: 32px

used for or usable by:

  • gap for Group, Flex, Stack.
  • padding props.
  • margin props, notably for Divider
  • width and height props such as w and h, notably for Space.

radius

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

used for:

  • Paper
  • Dialog, Modal, ..
  • Button, Tooltip..

we may set the default with defaultRadius:

"defaultRadius": "sm",

breakpoints

a set of alternative thresholds that we may target to activate style conditionally.

  "breakpoints": {
    "xs": "36em", // 576px
    "sm": "48em", // 768px
    "md": "62em",
    "lg": "75em",
    "xl": "88em"
  },

The sm breakpoint targets tablets and wider devices.

we specify one or more thresholds in the styles-props alternative-values pattern.

font sizes: a set of 5 distinct sizes

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

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

  • the fz style prop, on any component.

line heights

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

headings

family, weight and wrap behavior:

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

size and line height:

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

Gray Palette

description and use

It is a slate gray: it has a blue tint.

Mantine's light-mode theme makes use of this palette: it offers several whites for backgrounds, and a few dark grays for text. It does not include pure white, instead, it starts at #F8F9FA.

The following consideration usually apply for light-mode:

  • The background may be pure white. It may be the app/site global background of the controls background.

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

  • The text is black-ish or gray-ish depending on importance and role:

    • pure black text:
      • some headings
      • body text
      • input label
      • button label
    • gray 9 (readable black for body text)
    • gray 8 (readable black)
    • dark 7 (dimmed black)
    • gray 6 (gray) for secondary text, and input description

values

  • gray.0 white: #F8F9FA
  • gray.1 white: #F1F3F5
  • gray.2 white gray: #E9ECEF
  • ...
  • gray.6 gray: #868e96
  • gray.7 dark gray: #495057
  • gray.8 very dark gray: #343A40
  • gray.9: very dark gray: #212529

Dark Palette

the dark palette

It is a neutral gray palette. It was a slate-gray palette (blue-tint) before Mantine 7.3.

Mantine's dark-mode theme makes use of this palette. It offers several darks for backgrounds, grays for controls, and whites for text.

use in dark mode

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

values

  • dark.0: very light gray close to white: #C9C9C9.
  • ..
  • dark.9: black: #141414

revert to blue-tinted dark palette

const theme = createTheme({
    colors: {
        dark: [
            "#C1C2C5",
            "#A6A7AB",
            "#909296",
            "#5c5f66",
            "#373A40",
            "#2C2E33",
            "#25262b",
            "#1A1B1E",
            "#141517",
            "#101113",
        ],
    },
})

Global Style: Colors

#todo

--mantine-color-scheme: dark;
--mantine-primary-color-contrast: var(--mantine-color-white);
--mantine-color-bright: var(--mantine-color-white);
--mantine-color-text: var(--mantine-color-dark-0);
--mantine-color-body: var(--mantine-color-dark-7);
--mantine-color-error: var(--mantine-color-red-8);
--mantine-color-placeholder: var(--mantine-color-dark-3);
--mantine-color-anchor: var(--mantine-color-blue-4);
--mantine-color-default: var(--mantine-color-dark-6);
--mantine-color-default-hover: var(--mantine-color-dark-5);
--mantine-color-default-color: var(--mantine-color-white);
--mantine-color-default-border: var(--mantine-color-dark-4);
--mantine-color-dimmed: var(--mantine-color-dark-2);
--mantine-color-dark-text: var(--mantine-color-dark-4);
--mantine-color-dark-filled: var(--mantine-color-dark-8);
--mantine-color-dark-filled-hover: var(--mantine-color-dark-7);
--mantine-color-dark-light: rgba(36, 36, 36, 0.15);
--mantine-color-dark-light-hover: rgba(36, 36, 36, 0.2);
--mantine-color-dark-light-color: var(--mantine-color-dark-3);
--mantine-color-dark-outline: var(--mantine-color-dark-4);
--mantine-color-dark-outline-hover: rgba(36, 36, 36, 0.05);
--mantine-color-gray-text: var(--mantine-color-gray-4);
--mantine-color-gray-filled: var(--mantine-color-gray-8);
--mantine-color-gray-filled-hover: var(--mantine-color-gray-9);
--mantine-color-gray-light: rgba(134, 142, 150, 0.15);
--mantine-color-gray-light-hover: rgba(134, 142, 150, 0.2);
--mantine-color-gray-light-color: var(--mantine-color-gray-3);
--mantine-color-gray-outline: var(--mantine-color-gray-4);
--mantine-color-gray-outline-hover: rgba(206, 212, 218, 0.05);
--mantine-color-red-text: var(--mantine-color-red-4);

Global style: Others

line-height defaults to 1.55

--mantine-line-height: 1.55;

Flex

Thin abstraction over a flex div.

Mantine only sets display: flex if nothing else is set.

props:

  • the direction, horizontal (row) by default. We may set it as vertical (column), and switch to horizontal on desktop.
  • the gap, defaults to 0, due to matching a regular flex.
  • justify, aka main axis alignment, default to flex-start, due to matching a regular flex.
  • align, aka cross-axis alignment, defaults to stretch, due to matching regular flex.
<Flex
 direction=	{{ base: 'column',sm: 'row' }}
 gap=		{{ base: 'sm', 	sm: 'lg' }}
 justify=	{{ base: 'flex-start', 	sm: 'space-between' }}
>

Group

Layout elements horizontally, add some space between them. This is a high-level abstraction over a horizontal flex. The container spans the whole line.

  • gap: defaults to some spacing, as opposed to a regular flex. The default spacing is md, aka 16px.
  • justify: defaults to flex-start. (elements sit at the left)
  • align: defaults to center (vertically center), instead of stretch for regular flex.
  • grow: make children same width. It calculates the tentative equal width by dividing the remaining space by the number of items. It uses this tentative width a max-width for each children. Then, it sets flex-grow: 1 on each of them. If there was no limit, the bigger items would still be bigger than the others even after the growing process, because each child would receive an equal share of the remaining space. Defaults to false. Also activates mono-line.
  • wrap: pick between mono-line (nowrap) or multi-line flex (wrap) and set the flex-wrap property with it. Mantine default to multi-line (wrap) whereas a flex-div defaults to nowrap.

The subtle differences with a regular flex may not be worth it. The main point could be the use of grow to make children of equal width.

Stack

Layout elements vertically, add some space between elements.

A thin abstraction over a flex: it adds a gap and makes the flex vertical (by setting flex-direction).

The stack itself occupies the full width because it is a display:flex. Elements stretch to fill the stack by default (align prop).

  • gap: defaults to 16px.
  • justify: defaults to browser's default: flex-start (elements sit at the top),
  • align: defaults to browser's default: stretch (elements fill the cross axis horizontally)

SimpleGrid

A grid with equal-width tracks, and which expands horizontally (fluid).

We set the number of tracks with cols. It defaults to a single track. Wide screens accommodate multiple tracks, but mobile devices may stick with a single one. As such, it's common to set cols with at least two values:

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

Mantine adds space between tracks by default (16px). We may set it with spacing and verticalSpacing.

Container

A wrapper that layouts the inner content, such as the main column of a website, so that it displays nicely, both in desktop and mobile.

limited-width container version

The first version, the default one, comes with a max-width that limits the width in desktop, while bringing minimum padding for mobiles so that the content does not touch the screen edges.

it centers the content with margin: auto.

The max-width is defined by size. It defaults to md (aka 960px, or 60rem). We may pick 720px (sm). If we are to provide an arbitrary numeric value, we may as well use maw directly which is semantically more accurate.

The default padding is 16px, aka md. We may set it with the universal px style prop.

fluid container version

We may opt for a fluid container, which expands horizontally, with the fluid prop. It only comes with some padding, at 16px.

The fluid version is a bit of an overkill: We may as well use a Box with some horizontal padding instead.

Center

A utility component that "CenterCenters" the child or children.

It translates to a div with:

  • display: flex
  • justify-content: center
  • align-items: 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.

As a regular div, it occupies the whole parent's width. As a display:flex, it makes children adopt a max-content width.

Divider

A line that acts as a separator and sections divider, similar in essence to the <hr> element.

It may be used as an horizontal line or a vertical line.

Technically, Mantine uses an element with no content, but whose border acts as the visible line. The size determines the line's size.

The line's default color is?

The line does not have any margin by default, we use my or mx, or set the spacing at the parent level.

  • orientation: set the line orientation, "horizontal" (default), or "vertical".
  • size: set the line thickness, xs by default, aka 1px.
  • variant: set a line variant: solid (default), dashed or dotted

Space

Add some spacing at a location in the layout. It uses an element with no content but with some non-zero size. It defaults to size 0. As such, we must set either the height or the width.

  • h, set the height, to set the vertical space, usually in a stack-like layout.
  • w, set the width, to set the horizontal space, usually in a horizontal flex-like layout.

Paper

Encapsulate some UI to set it apart visually.

Paper sets a fill-color background.

In light mode, it sets a plain white #FFF background. This matches the light mode default background. As such, we either change the light mode default background, or change the Paper background.

In dark mode, it sets a dark.7. Similarly, it matches the dark mode default background. As such, we either change the dark mode default background, or change the Paper background.

In addition to the background, we may add a pre-styled border and a pre-styled shadow.

We usually add some padding, but nothing specific to Paper.

props

  • withBorder, toggles a predefined border. defaults to false
  • shadow, sets the size of a predefined shadow, if any. defaults to none.
  • radius, sets the background area radius. defaults to sm, aka 4px.

Accordion

The Accordion is a container for one or more items that are expandable. Expanding an item will show additional content related to that item.

For each item, show a preview and allow disclosure of a detail panel.

The Accordion may contain one or several items, related or not. An item shows up as a clickable preview (Accordion.Control) and embeds a panel, hidden by default, which we toggle through clicking.

In Mantine, the detail panel mounts immediately, regardless if it's toggled or not.

In Mantine, we set the chevron style at the Accordion's root, so it's shared among all items

The panel may be a form. In that case, we may set the chevron as a plus sign, to indicate we intend to add (submit) data to the server.

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

	Accordion.Item value="item2"
		..

Appshell

Appshell is a mega layout component that aims to manage:

  • The header that appears on-top, also called the banner.
  • The main section, that appears below the header
  • The potential left-side navbar and right-side aside.

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 an <input type=text/> element. It inherits some props from the abstract <Input> component.

  • We set the main label with label. A main label is often necessary to explain the purpose of such input.
  • We set the optional secondary label with description
  • placeholder sets the placeholder text
  • leftSection and rightSection allow to insert an icon, visually inside the text input.
  • The required prop adds the required attribute to the element. In that case, Mantine automatically adds an asterisk. But we may remove it with withAsterisk.

NumberInput

Under the hood, It's a text input. This allows more flexibility, such as displaying formatting commas directly in the input.

The value we get is a number or, when it's not possible to infer a number, a string. For example, we get an empty string when the input is empty.

We may customize the input:

  • allowDecimal: allow or forbid decimal numbers. Defaults to allow.
  • allowNegative: allow or forbid negative numbers. Defaults to allow.
  • min, max: specify minimum and maximum values. The way it influences the input is determined by clampBehavior
  • clampBehavior: determine how the value should clamp within the limits. We may forbid input outside the limits (strict), clamp after input on blur (d), or never clamp (none)

As any text input we may have:

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

FileInput

FileInput comes with props specific to the goal of selecting files:

  • We may allow multi-file selection with multiple. It defaults to false.
  • We may specify a file type in accept, such as image/png
  • We may show a clear button on the right by adding clearable
  • We work with the selected file by providing a handler to onChange. The handler receives a File object (or a File array) whenever the user picks some files or clears the selection.

We may limit the input's width to truncate long file names. We may add a file icon in the leftSection. The unstyled variant is similar to the transparent variant: there is no border or background. We may set it size to xs.

Finally, FileInput comes with the traditional input props such as label and placeholder.

<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 HTML <select> element that embeds some <option> elements. We build an array describing data for each option, and provide it to the data attribute. The data for an option is a record with a label and a value.

If the label is the same as the value (not recommended), we may provide a string instead of a record, which acts as both label and value.

  • data: an array of {label,value} elements.
  • label: The label that describes the select
const options = [
    { label: "Death Knight", value: "deathKnight" },
    { label: "Demon Hunter", value: "demonHunter" },
]

const options = ["deathKnight", "demonHunter"]
<NativeSelect data={options} label="Class" />

SegmentedControl

SegmentedControl allows to pick a value among a list of predefined options. In that it resembles a Select control or a Radio button. All options remain simultaneously on-screen , which is typical of a Radio button.

The picked option has a highlight overlay ontop. As the selected option changes, Mantine moves the overlay toward the selection with a smooth animation.

Each option comes with a label, which is the user-facing description, and a value, which is the unique identifier that represents it, and which we store in the client state. We make the value conform to a type union to prevent typos.

We store the options in data. We set the initial selection in value.

onChange is called on user selection and before browser paint. React prevents any change unless we mutate the state accordingly.

type AIProvider = "GPT" | "IMAGEN"

const [provider, setProvider] = useState<AIProvider>("GPT")

<SegmentedControl
    radius="md"
    size="sm"
    value={provider}
    bg={"transparent"}
    onChange={(v) => setProvider(v as AIProvider)}
    data={[
        { label: openAILabel, value: "GPT" },
        { label: geminiLabel, value: "IMAGEN" },
    ]}
/>

Button

An abstraction over a <button>.

props

  • variant, set the button overall appearance, such as filled, outline, subtle, gradient or default. defaults to filled
  • color, set the button overall color. the coloring depends on the variant. defaults to the theme's primaryColor, aka Mantine's blue.
  • leftSection, rightSection, for example for an icon.
  • size, to specify two things. First, if it's a compact button with less inner space. second, if we want a smaller or bigger font size.
  • fullWidth, to fill the parent's width
  • disabled, sets the appearance and behaviour to disabled. the disabled appearance is the same across all variants.
  • loading, sets the appearance to loading, aka loader overlaid on top, and the behaviour to disabled. We may customize the loader with loaderProps (see Loader component)
  • gradient, sets the gradient's colors and direction. It requires using the gradient variant.
<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>.

The idea is for the developer to explicitely set type=submit on the Button.

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

The modal is a container for some UI that is conditionally overlaid on top of the application. It dims the background while opened. We may dismiss it with escape, the close button (if it's displayed), or with a click outside.

The modal works with an opened prop: it expects a boolean state variable, which effectively controls the display.

reminder: We may get such a boolean state variable with useDisclosure, which also gives semantically correct open and close state-mutation functions.

When the user attempts to dismiss the modal, Mantine calls the onClose handler: We have to provide one. This handler has the responsibility to toggle the opened state back to false if it approves the dismiss. It will usually call the close function.

import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';

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

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

      <Button .. onClick={open}>
        Open modal
      </Button>
    </>
  );
}

The modal has a top section by default. It may have

  • a close button, which we may remove with withCloseButton={false}.
  • a title, which accepts any React node (not only text).

The inner content usually comes with controls: inputs and a submit button.

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