Skip to main content

emotion-migrate

Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports from components.

Jump to install

Source facts

Repository
metabase/metabase
Last source activity
September 11, 2026 at 21:30
Detected SKILL.md language
English
Stars
49,344
Forks
6,833

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
emotion-migrate
description
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports from components.
# Emotion → Mantine + CSS Modules Migration Skill Migrate Emotion styled-components (`@emotion/styled`, `@emotion/react`) to Mantine layout components with style props and CSS modules. The goal is zero Emotion imports, zero inline styles, and maximum use of design system tokens. ## Priority Order (Strict) 1. **Mantine components + style props** — `Box`, `Flex`, `Stack`, `Group`, `Text`, `Title`, `Card`. This is the DEFAULT. Every CSS property must be checked against style props FIRST. 2. **CSS modules** (`.module.css`) — ONLY for properties that Mantine style props genuinely cannot express: pseudo-selectors (`:hover`, `:focus`, `::before`), `box-shadow`, `border` shorthand, `animation`/`@keyframes`, complex selectors, `cursor`, `pointer-events`, `overflow`, `text-overflow`, `white-space`, `transition`. 3. **Inline styles ONLY for dynamic values** — `style={{ }}` is allowed only for truly dynamic runtime values (e.g., computed widths, positions, data-driven colors). All static styles must use Mantine props or CSS modules. ### Mantine-First Decision Gate (CRITICAL) For EACH styled component, go through every CSS property and ask: "Can this be a Mantine style prop?" If yes → style prop. If no → CSS module. Do NOT dump an entire component into a CSS module just because one property needs it — split them. **Properties that ARE style props** (use these, not CSS modules): - `display` → `display` prop - `color` → `c` prop (`c="core-brand"`, `c="text-primary"`) - `background-color` → `bg` prop (`bg="background_page-primary"`) - `font-size` → `fz` prop (`fz="md"`) - `font-weight` → `fw` prop (`fw="bold"`) - `line-height` → `lh` prop (`lh="md"`) - `text-align` → `ta` prop (`ta="center"`) - `padding` (all variants) → `p`, `px`, `py`, `pt`, `pb`, `pl`, `pr` - `margin` (all variants) → `m`, `mx`, `my`, `mt`, `mb`, `ml`, `mr` - `width` → `w`, `min-width` → `miw`, `max-width` → `maw` - `height` → `h`, `min-height` → `mih`, `max-height` → `mah` - `flex` → `flex` prop (`flex="0 0 auto"`, `flex={1}`) - `gap` → `gap` prop (on Flex/Stack/Group) - `align-items` → `align` prop (on Flex/Stack/Group) - `justify-content` → `justify` prop (on Flex/Stack/Group) - `flex-direction` → `direction` prop (on Flex) - `flex-wrap` → `wrap` prop (on Flex) - `position` → `pos` prop - `top/right/bottom/left` → `top`, `right`, `bottom`, `left` props - `opacity` → `opacity` prop **Properties that NEED CSS modules** (no style prop equivalent): - `:hover`, `:focus`, `:active`, `::before`, `::after` (pseudo-selectors) - `box-shadow`, `border` (shorthand with color), `outline` - `cursor`, `pointer-events` - `overflow`, `text-overflow`, `white-space` - `animation`, `transition`, `transform` - `@media` queries (UNLESS it's simple responsive spacing/sizing — then use responsive syntax: `p={{ base: "md", lg: "xl" }}`) **Hybrid approach** — when a component needs BOTH, put style props on the Mantine component AND add a CSS module class for the rest: ```tsx <Flex className={S.root} /* for :hover, box-shadow, border */ align="center" /* style prop */ gap="sm" /* style prop */ p="md" /* style prop */ bg="background_page-primary" /* style prop */ > ``` ## CSS Module Conventions (Strict) ### Class Naming: camelCase All CSS module class names MUST use **camelCase**. This is the dominant convention across the codebase (~830 camelCase vs ~620 PascalCase classes), used consistently in Mantine UI components, and matches standard CSS module conventions. ```css /* CORRECT */ .root { } .settingsSection { } .dragHandle { } .closeIcon { } /* WRONG — do not use PascalCase or kebab-case */ .ItemRoot { } .settings-section { } ``` Modifier/state classes also use camelCase: ```css .selected { } .disabled { } .interactive { } .draggable { } ``` ### No Cascading — Direct Class Assignment Cascading selectors are **discouraged**. Instead of styling through parent-child relationships, assign a class directly to the element that needs styling. ```css /* WRONG — cascading/descendant selectors */ .root > input { } .root .label { } .container > div > span { } /* CORRECT — direct class on the target element */ .input { } .label { } .title { } ``` The only acceptable nesting patterns are: - **Pseudo-selectors on the same element**: `.item { &:hover { } }` - **Modifier composition**: `.item { &.selected { } }` - **Hover-reveal patterns** where a parent hover affects a child: `.root:hover .showOnHover { opacity: 1; }` — but only when structurally necessary (the child has no way to know about the parent's hover state) ### Import Alias Always import the CSS module as `S`: ```tsx import S from "./ComponentName.module.css"; ``` ## Step-by-Step Migration Process ### Step 1: Read and Understand Read the `.styled.tsx` file AND every component that imports from it. Understand: - Which styled components are used and where - Which props drive dynamic styles - Which styles are static vs conditional - Which styles can map directly to Mantine style props ### Step 2: Classify Each Styled Component For each styled component, apply the Mantine-First Decision Gate above. Then determine the migration target: | Emotion Pattern | Migration Target | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `styled.div` with only layout/spacing/color | `Box`, `Flex`, `Stack`, or `Group` with style props. **NO CSS module needed.** | | `styled.div` with flexbox column | `Stack` component with style props | | `styled.div` with flexbox row | `Flex` or `Group` component with style props | | `styled.span` / `styled.p` with color/weight/size | **`Text component="span"` with style props** (`c`, `fw`, `fz`). NO CSS module. | | `styled.div` with hover/focus/pseudo-selectors | **Hybrid**: Mantine component with style props for expressible properties + CSS module class for pseudo-selectors only | | `styled.div` with media queries (simple spacing/sizing) | **Responsive style props**: `p={{ base: "md", lg: "xl" }}`. NO CSS module. | | `styled.div` with media queries (complex/non-spacing) | CSS module for the media query parts, style props for the rest | | `styled.div` with animations/keyframes | CSS module for animation, style props for layout | | `styled(SomeComponent)` with only color/spacing/flex | **Wrap in Mantine component** with style props: `<Box c="core-brand" flex="0 0 auto"><Icon /></Box>`, or pass style props if the component accepts them | | `styled(SomeComponent)` with pseudo-selectors/complex styles | CSS module `className` on the component | | Dynamic props `styled.div<{ isActive: boolean }>` | Mantine style props for simple toggles (`c={active ? "core-brand" : "text-primary"}`), `cx()` with CSS module classes for complex state combinations involving pseudo-selectors | ### Step 3: Create CSS Module (if needed) Create `ComponentName.module.css` alongside the component file: ```css /* Use design system tokens — NEVER raw color/spacing values */ .root { border: 1px solid var(--mb-color-border-neutral); border-radius: var(--mantine-radius-md); background-color: var(--mb-color-background_page-primary); } /* Conditional states as separate classes, combined with cx() */ .active { background-color: var(--mb-color-core-brand); color: var(--mb-color-text-primary-inverse); } .disabled { color: var(--mb-color-text-disabled); pointer-events: none; } /* Hover/focus/pseudo-selectors — nest with & */ .interactive { cursor: pointer; &:hover { color: var(--mb-color-core-brand); background-color: var(--mb-color-background_surface-hover); } } /* Hover-reveal: acceptable parent→child nesting */ .root:hover .showOnHover { opacity: 1; } /* Responsive styles */ @media (--breakpoint-min-md) { .root { padding: var(--mantine-spacing-lg); } } ``` ### Step 4: Update the Component TSX ```tsx import cx from "classnames"; import CS from "metabase/css/core/index.css"; import { Box, Flex, Group, Stack, Text } from "metabase/ui"; import S from "./ComponentName.module.css"; // Dynamic props → cx() with conditional classes <Flex className={cx(S.root, { [S.active]: isActive, [S.disabled]: disabled, })} align="center" gap="sm" p="md" w="100%" > ``` ### Step 5: Delete the `.styled.tsx` File Remove the old styled file entirely. Remove all imports of it from other files. ### Step 6: Verify - Confirm zero `@emotion/styled` or `@emotion/react` imports remain in the migrated files - Confirm no static inline styles (dynamic runtime values are fine) - Confirm all colors use tokens, not raw hex/rgb values ## Design System Token Reference ### Mantine Style Props (use directly on components) **Spacing** (`p`, `px`, `py`, `pt`, `pb`, `pl`, `pr`, `m`, `mx`, `my`, `mt`, `mb`, `ml`, `mr`): - `"xs"` = 4px, `"sm"` = 8px, `"md"` = 16px, `"lg"` = 24px, `"xl"` = 32px - Custom: `rem(48)` for non-standard values (import `rem` from `metabase/ui`) **Dimensions** (`w`, `h`, `maw`, `mah`, `miw`, `mih`): - `"100%"`, `"100vh"`, `rem(400)`, etc. **Colors** (`c`, `bg`): - Text: `"text-primary"`, `"text-secondary"`, `"text-disabled"` - Background: `"background_page-primary"`, `"background_page-secondary"`, `"background_surface-hover"`
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub