---
title: 'Mantine exporter'
description: 'A Mantine 9 createTheme object with one virtual colour per role, and a cssVariablesResolver carrying the per-scheme colours.'
order: 16
---

# Mantine exporter

<div class="callout live-demos">
  <span class="callout-title">See it live</span>
  <p><a href="/transtyle/demo/acme/mantine/">Acme</a> · <a href="/transtyle/demo/cathode/mantine/">Cathode</a> · <a href="/transtyle/demo/govuk/mantine/">GOV.UK</a> · <a href="/transtyle/demo/carbon/mantine/">Carbon</a>: one page, four design systems, compiled to Mantine. <a href="/transtyle/demo/">All 44 demos →</a></p>
</div>

[Mantine](https://mantine.dev/) themes through one object passed to `<MantineProvider>`: colours are named ten-shade tuples, `primaryShade` picks the filled shade per scheme, and `virtualColor()` lets one name stand for a different tuple in light and in dark. That is [the role grid](/transtyle/docs/language/#color-roles-the-role-grid) in another shape, so this exporter is mostly a mapping table. It emits one TypeScript module, `theme.transtyle.ts`, with two exports: `theme` and `cssVariablesResolver`.

<!-- measured: acme.mantine.rows = 199 -->
<!-- measured: acme.mantine.native = 116 -->

On [Acme](/transtyle/docs/examples/) that is 199 classified rows in `report.json`, 116 of them native, measured against [Mantine's whole theming surface](#measured-against-mantines-whole-surface).

```json
"targets": { "mantine": { "output": "dist/mantine" } }
```

```tsx
import '@mantine/core/styles.css';
import { MantineProvider } from '@mantine/core';
import { theme, cssVariablesResolver } from './theme.transtyle';

<MantineProvider theme={theme} cssVariablesResolver={cssVariablesResolver}>
  <App />
</MantineProvider>;
```

Every role is then a colour name: `<Button color="danger">`, `<Badge color="success" variant="light">`, `c="primary"`.

## Why a resolver, and no override stylesheet

Mantine writes its CSS variables **at runtime**, in a `<style>` it renders inside the React tree, after anything in `<head>`. A stylesheet overriding `--mantine-*` variables loses that cascade for every variable Mantine writes itself. The provider's `cssVariablesResolver` result is merged over Mantine's own values instead, so the compiled colours always win. That's the whole reason the output is a resolver and not a CSS file.

## Colours

Each role becomes `virtualColor({ name, light: '<role>-light', dark: '<role>-dark' })` over two ten-step tuples, and an index means the same grid cell in both schemes:

| Index | 0–2                           | 3–4               | 5–7                         | 8      | 9             |
| ----- | ----------------------------- | ----------------- | --------------------------- | ------ | ------------- |
| Cell  | `tint` · `-hover` · `-active` | `outline` · hover | `solid` · `-hover` · active | `text` | `text-strong` |

All ten are direct grid cells, and `primaryShade` is 5, the solid. But Mantine's components don't read tuple positions: they read per-colour variables such as `--mantine-color-danger-filled`, `-light` and `-outline`. The resolver sets each of those per scheme straight from the grid, so every variant shows the compiled colour. Three are approximations, because Mantine's variable means something the catalog's same-named cell doesn't:

| Mantine          | Comes from | Why it's approximated                                                                              |
| ---------------- | ---------- | -------------------------------------------------------------------------------------------------- |
| `-outline`       | `text`     | it's the outline variant's border **and** its label; the catalog's `outline` is too light for text |
| `-outline-hover` | `tint`     | it's a hovered **background**; the catalog's `outline-hover` is a hovered border                   |
| `-light-color`   | `on-tint`  | one variable for text on the tint and on the page                                                  |

`autoContrast` is on, so filled buttons take their text from `--mantine-color-<role>-contrast`, the role's `on-solid`. [Cathode](/transtyle/demo/cathode/mantine/) shows it: its phosphor-green buttons carry dark text, where Mantine's default would put white on them.

Mantine's two neutral tuples, `gray` and `dark`, are read directly by its stylesheet hundreds of times (borders, inputs, the default variant). They're filled from the light and the dark neutral ladders, at the positions Mantine's own page variables read.

## What Mantine has no slot for

- **The focus ring.** Mantine draws every focus outline from the primary filled colour, 2px solid at 2px, in its stylesheet; `ring`, `ring.width` and `ring.offset` are reported `dropped`.
- **The z-index ladder.** Mantine's components take z-index from JavaScript defaults, not from the `--mantine-z-index-*` variables.
- **Display type, `2xl` breakpoint, link hover/visited, motion, density.** No matching key; each is a `dropped` row with its reason.

## Measured against Mantine's whole surface

<!-- measured: mantine.surface.total = 203 -->
<!-- measured: acme.mantine.set = 106 -->
<!-- measured: acme.mantine.follow = 66 -->
<!-- measured: acme.mantine.default = 31 -->

The rows above say what the exporter maps. A checked-in inventory says what there is to map: 203 entries extracted from the installed `@mantine/core`, every leaf of `DEFAULT_THEME` and every variable `defaultCssVariablesResolver` writes. Ten of Mantine's fourteen default palettes fold into one `<color>` entry; `gray`, `dark`, `red` and `teal` stay named, because Mantine's own page variables read them. On Acme, this theme sets 106 entries. Mantine computes 66 more from those (`--mantine-font-size-xs` from `fontSizes.xs`, `--mantine-primary-color-filled` from `primaryColor` and the role's filled colour). The other 31 keep Mantine's value, each on its own row in `report.json` with the reason: behaviour switches such as `focusRing` and `cursorType`, the gradient variant, `scale`, the unread z-index variables. The [coverage bar](/transtyle/docs/concepts/#5-provenance-and-coverage) fails the build when an entry is unaccounted for or kept without a reason.

Component geometry goes through Mantine's data-only routes, `defaultProps` and `styles`: button and control radius, the default button's horizontal padding, and control heights mapped default size to default size. Inputs and buttons are height-driven, so vertical padding is dropped. The full mapping is in the [exporter spec](https://github.com/transtyle/transtyle/blob/main/docs/specs/exporters/mantine.md).

See it running on real Mantine components: `npm run dev -w acme-demo-mantine` (or `cathode-demo-mantine`) in the [examples](/transtyle/docs/examples/). The demo's build type-checks the emitted theme against Mantine's own types before bundling it.

## Descriptions and deprecations

A property that maps exactly one slot carries that slot's `description` and `deprecated` as a `/** … */` JSDoc block above it (`Documented` and `jsdocComment()` from `@transtyle/ir`). The description shows on hover, and `@deprecated <reason>` strikes the property through in an editor. That covers the font families, the scales (`fontSizes`, `lineHeights`, `fontWeights`, `radius`, `defaultRadius`, `spacing`, `shadows`, `breakpoints`) and the light block of the `cssVariablesResolver`; the dark block maps the same slots and doesn't repeat them. The colour tuples and the component tier read several slots and get none.

## Contrast, motion and brand

[`brand`](/transtyle/docs/configuration/#contrast-motion-and-brand) gives one theme file per brand, `theme.transtyle.acme.ts` and `theme.transtyle.globex.ts`: hand the brand's theme to `MantineProvider`. `contrast` and `motion` are reported `dropped` with their reasons (Mantine's color-scheme manager knows light and dark only; transitions are component props).
