| name | frontend-style-guide |
| description | Apply the Lightdash frontend style guide when working on React components, migrating Mantine v6 to v8, or styling frontend code. Use when editing TSX files, fixing styling issues, or when user mentions Mantine, styling, or CSS modules. |
| allowed-tools | Read, Edit, Write, Glob, Grep |
Lightdash Frontend Style Guide
Apply these rules when working on any frontend component in packages/frontend/.
Mantine 8 Migration
CRITICAL: We are migrating from Mantine 6 to 8. Always upgrade v6 components when you encounter them.
Component Checklist
When creating/updating components:
Quick Migration Guide
import { Button, Group } from '@mantine/core';
<Group spacing="xs" noWrap>
<Button sx={{ mt: 20 }}>Click</Button>
</Group>;
import { Button, Group } from '@mantine-8/core';
<Group gap="xs" wrap="nowrap">
<Button mt={20}>Click</Button>
</Group>;
Key Prop Changes
spacing → gap
noWrap → wrap="nowrap"
sx → Component props (e.g., mt, w, c) or CSS modules
leftIcon → leftSection
rightIcon → rightSection
Styling Best Practices
Core Principle: Theme First
The goal is to use theme defaults whenever possible. Style overrides should be the exception, not the rule.
Styling Hierarchy
- Best: No custom styles (use theme defaults)
- Theme extension: For repeated patterns, add to
mantine8Theme.ts
- Component props: Simple overrides (1-3 props like
mt="xl" w={240})
- CSS modules: Complex styling or more than 3 props
NEVER Use
styles prop (always use CSS modules instead)
sx prop (it's a v6 prop)
style prop (inline styles)
Theme Extensions (For Repeated Patterns)
If you find yourself applying the same style override multiple times, add it to the theme in mantine8Theme.ts:
components: {
Button: Button.extend({
styles: {
root: {
minWidth: '120px',
fontWeight: 600,
}
}
}),
}
Context-Specific Overrides
Inline-style Component Props (1-3 simple props)
<Button mt="xl" w={240} c="blue.6">Submit</Button>
<Button mt={20} mb={20} ml={10} mr={10} w={240} c="blue.6" bg="white">Submit</Button>
Common inline-style props:
- Layout:
mt, mb, ml, mr, m, p, pt, pb, pl, pr
- Sizing:
w, h, maw, mah, miw, mih
- Colors:
c (color), bg (background)
- Font:
ff, fs, fw
- Text:
ta, lh
CSS Modules (complex styles or >3 props)
Create a .module.css file in the same folder as the component:
.customCard {
transition: transform 0.2s ease;
cursor: pointer;
}
.customCard:hover {
transform: translateY(-2px);
box-shadow: var(--mantine-shadow-lg);
}
import styles from './Component.module.css';
<Card className={styles.customCard}>{/* content */}</Card>;
Do NOT include .css.d.ts files - Vite handles this automatically.
Color Guidelines
Prefer default component colors - Mantine handles theme switching automatically.
When you need custom colors, use our custom scales for dark mode compatibility:
<Text c="gray.6">Secondary text</Text>
<Text c="ldGray.6">Secondary text</Text>
<Button bg="ldDark.8" c="ldDark.0">Dark button</Button>
<Text c="foreground">Primary text</Text>
<Box bg="background">Main background</Box>
Custom Color Scales
| Token | Purpose |
|---|
ldGray.0-9 | Borders, subtle text, neutral UI elements |
ldDark.0-9 | Buttons/badges with dark backgrounds in light mode |
background | Page/card backgrounds |
foreground | Primary text color |
Dark Mode in CSS Modules
Use @mixin dark for theme-specific overrides:
.clickableRow {
&:hover {
background-color: var(--mantine-color-ldGray-0);
@mixin dark {
background-color: var(--mantine-color-ldDark-5);
}
}
}
Alternative: use CSS light-dark() function for single-line theme switching:
.clickableRow:hover {
background-color: light-dark(
var(--mantine-color-ldGray-0),
var(--mantine-color-ldDark-5)
);
}
Always Use Theme Tokens
<Box p={16} mt={24}>
<Box p="md" mt="lg">
Beware of dependencies
If a component is migrated to use Mantine 8 Menu.Item, ensure its parent also uses Mantine 8 Menu
Remove Dead Styles
Before moving styles to CSS modules, check if they're actually needed:
<Flex justify="flex-end">
<Button style={{display: 'block'}}>Submit</Button>
</Flex>
<Flex justify="flex-end">
<Button>Submit</Button>
</Flex>
Theme-Aware Component Logic
For JavaScript logic that needs to know the current theme:
import { useMantineColorScheme } from '@mantine/core';
const MyComponent = () => {
const { colorScheme } = useMantineColorScheme();
const iconColor = colorScheme === 'dark' ? 'blue.4' : 'blue.6';
};
Keep using mantine/core's clsx utility until we migrate to Mantine 8 fully
import { clsx } from '@mantine/core';
const MyComponent = () => {
return (
<div className={clsx('my-class', 'my-other-class')}>My Component</div>
);
};
Select/MultiSelect grouping has a different structure on Mantine 8
<Select
label="Your favorite library"
placeholder="Pick value"
data={[
{ group: 'Frontend', items: ['React', 'Angular'] },
{ group: 'Backend', items: ['Express', 'Django'] },
]}
/>
Reusable Components
Modals
- Always use
MantineModal from components/common/MantineModal - never use Mantine's Modal directly
- See
stories/Modal.stories.tsx for usage examples
- For forms inside modals: use
id on the form and form="form-id" on the submit button
- For alerts inside modals: use
Callout with variants danger, warning, info
Callouts
- Use
Callout from components/common/Callout
- Variants:
danger, warning, info
Polymorphic Clickable Containers
Use these when you need a layout container that is also clickable — avoids the native <button> background/border reset problem.
PolymorphicGroupButton from components/common/PolymorphicGroupButton — a Group (flex row) that is polymorphic and sets cursor: pointer. Use for horizontal groups of elements that act as a single button.
PolymorphicPaperButton from components/common/PolymorphicPaperButton — a Paper (card surface) that is polymorphic and sets cursor: pointer. Use for card-like clickable surfaces.
Both accept all props of their base component (GroupProps / PaperProps) plus a component prop for the underlying element.
<PolymorphicGroupButton component="div" gap="sm" onClick={handleClick}>
<MantineIcon icon={IconFolder} />
<Text>Label</Text>
</PolymorphicGroupButton>
<PolymorphicPaperButton component="div" p="md" onClick={handleClick}>
Card content
</PolymorphicPaperButton>
<UnstyledButton>
<Group>...</Group>
</UnstyledButton>
EmptyStateLoader
- Use
EmptyStateLoader from components/common/EmptyStateLoader for any centered loading state: page-level guards, panels, tables, empty containers
- Built on
SuboptimalState (Mantine v8) — renders a spinner with an optional title, fully centered in its parent
TruncatedText
- Use
TruncatedText from components/common/TruncatedText whenever text may overflow a constrained width
- Pass
maxWidth (number or string) to control the truncation boundary
- Automatically shows a tooltip with the full text only when the text is actually truncated (no tooltip spam for short names)
- Defaults to
fz="sm"; override via standard Text props
<TruncatedText maxWidth={200}>{item.name}</TruncatedText>
<TruncatedText maxWidth="100%" fw={500}>{space.name}</TruncatedText>
Tables with search, pagination, and sorting
Use the ContentTable component from components/common/ContentTable for tables with search, pagination, and sorting.
If you need filters, use FilterFacet
Mantine Documentation
List of all components and links to their documentation in LLM-friendly format: https://mantine.dev/llms.txt