# Mantine and CSS modules

A Récit site is drawn by [Mantine](https://mantine.dev/) and plain CSS: every component of the
framework has a CSS module next to it, and the site adds its own stylesheet on top. There is no
utility-class framework: a class such as `flex gap-2` in a page styles nothing.

## Where a site's styles go

| What you style                  | Where the CSS lives                                                  |
| ------------------------------- | -------------------------------------------------------------------- |
| Colors and radius of the site   | `theme.light` and `theme.dark` in `recit.config.ts`                  |
| Global rules (pages, selectors) | a stylesheet imported by `recit.config.ts`, for example `styles.css` |
| One of your components          | a CSS module next to it, `Card.tsx` and `Card.module.css`            |
| A few inline rules, no file     | `theme.customCss` (a string or an object; kept for compatibility)    |
| A deck published by the site    | the deck's own `theme.customCss` and the modules of its components   |

The stylesheet of a site is plain CSS, imported once from the configuration:

```ts title="recit.config.ts"
import { defineConfig } from "recit/config";
import "./styles.css";

export default defineConfig({
  theme: {
    light: { primary: "oklch(0.55 0.2 260)" },
    dark: { primary: "oklch(0.75 0.15 260)" },
  },
});
```

```css title="styles.css"
.lorem-hero {
  padding-block: 3rem;
  color: var(--mantine-color-text);
  background: var(--mantine-color-body);
}
```

Mantine, the tokens, the reset and most of the framework's modules sit in cascade layers
(`@layer mantine, theme, base, components, utilities`), declared before any other sheet. A rule you
write outside a layer wins over those, whatever their specificity: reach for a layer-free rule
before `!important`.

Read colors, spacing and radius from Mantine's variables (`--mantine-color-text`,
`--mantine-color-dimmed`, `--mantine-primary-color-filled`, `--mantine-spacing-md`,
`--mantine-radius-md`…) so that your rules follow the theme of the site.

## CSS modules

A component of the site (in `mdx.components`, a slot or a custom page) imports a module next to it.
Vite compiles it with Mantine's PostCSS preset, so `rem()`, `light-dark()` and the breakpoint mixins
work there:

```tsx title="Card.tsx"
import type { ReactNode } from "react";
import classes from "./Card.module.css";

export const Card = ({ children }: { children: ReactNode }) => (
  <div className={classes.root}>{children}</div>
);
```

```css title="Card.module.css"
.root {
  display: flex;
  flex-direction: column;
  gap: rem(8px);
  padding: rem(16px);
  border: rem(1px) solid var(--mantine-color-default-border);
  border-radius: var(--mantine-radius-md);

  @mixin larger-than $mantine-breakpoint-sm {
    flex-direction: row;
  }
}
```

Classes combine inside the module with `composes`, never by concatenating strings in the JSX. A
class that must stay global, because a stylesheet or a deck targets it, is kept with
`composes: lorem-card from global`.

The components of a deck (`components:` in the deck's `recit.config.ts`) are bundled by the CLI,
without PostCSS: their modules are plain CSS (lengths in `rem`, the `@recit/theme` tokens such as
`hsl(var(--primary))`, plain `@media` queries), and `composes` works the same way.

## Color mode

The mode is the `data-mantine-color-scheme` attribute on `<html>`, `light` or `dark`. Write both
values in one declaration with `light-dark()`, or select on the attribute:

```css title="styles.css"
.lorem-panel {
  background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
}

:root[data-mantine-color-scheme="dark"] .lorem-logo {
  filter: invert(1);
}
```

A `.dark` class on `<html>` and Tailwind's `dark:` prefix no longer exist. Decks embedded in a site
follow its mode; see [Dark mode](/theme/dark-mode) for a deck on its own.

## Migrating a site from Tailwind utilities

Récit sites and decks used to compile Tailwind CSS: the utility classes written in a site's MDX
pages, `mdx.components`, slots and components, and `@apply` in a deck's stylesheet. Tailwind is
gone, and these classes now produce nothing. The page still renders, unstyled where the utilities
were.

What stops working:

- utility classes in MDX (`<div className="grid grid-cols-2 gap-4">`);
- utility classes in the components passed to `mdx.components`, to slots or to custom pages;
- utility classes in the components of a deck, and `@apply` in a deck's `theme.customCss`;
- `theme.registryUrl`, removed with shadcn/ui: set `theme.light` and `theme.dark` instead.

What to write instead:

- a class of your own in the site's stylesheet, used from MDX (`className="lorem-grid"`);
- a CSS module next to each component, imported as `classes`;
- `composes` in a module to share declarations, or to keep a global class;
- in a deck, plain declarations in the `theme.customCss` stylesheet instead of `@apply`.

Before, with utilities in a page:

```mdx title="pages/index.mdx"
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
  <div className="rounded-lg border p-4 text-muted-foreground">Lorem ipsum dolor sit amet.</div>
  <div className="rounded-lg border p-4 text-muted-foreground">Consectetur adipiscing elit.</div>
</div>
```

After, with two classes of the site's stylesheet:

```mdx title="pages/index.mdx"
<div className="lorem-grid">
  <div className="lorem-cell">Lorem ipsum dolor sit amet.</div>
  <div className="lorem-cell">Consectetur adipiscing elit.</div>
</div>
```

```css title="styles.css"
.lorem-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}

@media (min-width: 48em) {
  .lorem-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.lorem-cell {
  padding: 1rem;
  border: 1px solid var(--mantine-color-default-border);
  border-radius: var(--mantine-radius-lg);
  color: var(--mantine-color-dimmed);
}
```

In a deck, the same `@apply` becomes its declarations:

```css title="styles.css (deck, before)"
.metric-value {
  @apply text-5xl font-extrabold text-primary;
}
```

```css title="styles.css (deck, after)"
.metric-value {
  font-size: 3rem;
  font-weight: 800;
  color: hsl(var(--primary));
}
```

The utilities met most often, and what to write:

| Tailwind                             | CSS                                                                                                        |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `flex items-center justify-between`  | `display: flex; align-items: center; justify-content: space-between;`                                      |
| `flex-col` / `gap-2` / `gap-4`       | `flex-direction: column;` / `gap: rem(8px);` / `gap: rem(16px);`                                           |
| `grid grid-cols-3`                   | `display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));`                                         |
| `p-4` / `px-3` / `mt-8`              | `padding: rem(16px);` / `padding-inline: rem(12px);` / `margin-top: rem(32px);`                            |
| `w-full` / `max-w-prose` / `min-w-0` | `width: 100%;` / `max-width: 65ch;` / `min-width: 0;`                                                      |
| `text-sm` / `font-semibold`          | `font-size: var(--mantine-font-size-sm);` / `font-weight: 600;`                                            |
| `text-muted-foreground`              | `color: var(--mantine-color-dimmed);`                                                                      |
| `bg-background` / `text-foreground`  | `background: var(--mantine-color-body);` / `color: var(--mantine-color-text);`                             |
| `border` / `rounded-lg`              | `border: rem(1px) solid var(--mantine-color-default-border);` / `border-radius: var(--mantine-radius-lg);` |
| `shadow-sm`                          | `box-shadow: var(--mantine-shadow-sm);`                                                                    |
| `truncate`                           | `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`                                          |
| `hover:underline`                    | `&:hover { text-decoration: underline; }`                                                                  |
| `dark:bg-zinc-900`                   | `background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));`                         |
| `md:grid-cols-3`                     | `@mixin larger-than $mantine-breakpoint-sm { … }` (48em, Tailwind's `md`)                                  |
| `sr-only`                            | Mantine's `VisuallyHidden` component                                                                       |

`rem()` and the mixins exist in the modules and stylesheets of a site (Mantine's PostCSS preset); in
a deck, write `1rem` and plain `@media (min-width: 48em)`.
