Inner workings

We study Mantine's inner workings by studying the <Button> component's implementation.

The Button source code includes the two main files: Button.module.css and Button.tsx.

html structure

  • the label span contains the button's text.
  • the inner span is an intermediate container for the label and the sections
  • a section is a container for an icon.
  • the root button is the top level container.
<button> // mantine-Button-root
  <span> // mantine-Button-loader // loader
    <span> // mantine-Loader-root
	<span> // mantine-Button-inner // text and icons
    <span> // mantine-Button-section
      <svg> // icon
		<span> // mantine-Button-label
      <> // text

actual HTML, including mantine-implemented classes.

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

classes on the top level element

the top level element is the <button> element. It contains the following classes:

m-87cf2631				// Mantine-implemented class, (UnstyledButton)
m-77c9d27d				// Mantine-implemented class, (Button)

mantine-UnstyledButton-root	// unimplemented class
mantine-Button-root					// unimplemented class

mantine-active // the class is always attached, and contains style for the :active state.
/*
mantine-active:active {
    transform: translateY(calc(.0625rem * var(--mantine-scale)));
}
*/
mantine-focus-auto	// the class is always attached, and contains style for the :focus-visible state.
/*
.mantine-focus-auto:focus-visible {
    outline: 2px solid var(--mantine-primary-color-filled);
    outline-offset: calc(.125rem * var(--mantine-scale));
*/

mantine source style for Button's root:

Mantine's implementation of root, which is the <button> element.

tsx file

.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));
        }
    }
}
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.

earlymorning logo

© 2025 - All rights reserved

Inner workings

We study Mantine's inner workings by studying the <Button> component's implementation.

The Button source code includes the two main files: Button.module.css and Button.tsx.

html structure

  • the label span contains the button's text.
  • the inner span is an intermediate container for the label and the sections
  • a section is a container for an icon.
  • the root button is the top level container.
<button> // mantine-Button-root
  <span> // mantine-Button-loader // loader
    <span> // mantine-Loader-root
	<span> // mantine-Button-inner // text and icons
    <span> // mantine-Button-section
      <svg> // icon
		<span> // mantine-Button-label
      <> // text

actual HTML, including mantine-implemented classes.

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

classes on the top level element

the top level element is the <button> element. It contains the following classes:

m-87cf2631				// Mantine-implemented class, (UnstyledButton)
m-77c9d27d				// Mantine-implemented class, (Button)

mantine-UnstyledButton-root	// unimplemented class
mantine-Button-root					// unimplemented class

mantine-active // the class is always attached, and contains style for the :active state.
/*
mantine-active:active {
    transform: translateY(calc(.0625rem * var(--mantine-scale)));
}
*/
mantine-focus-auto	// the class is always attached, and contains style for the :focus-visible state.
/*
.mantine-focus-auto:focus-visible {
    outline: 2px solid var(--mantine-primary-color-filled);
    outline-offset: calc(.125rem * var(--mantine-scale));
*/

mantine source style for Button's root:

Mantine's implementation of root, which is the <button> element.

tsx file

.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));
        }
    }
}
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.