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:
-
Use @mantine-8/core imports
-
No style or styles or sx props
-
Check Mantine docs/types for available component props
-
Use inline-style component props for styling when available (and follow <=3 props rule)
-
Use CSS modules when component props aren't available or when more than 3 inline-style props are needed
-
Theme values ('md', 'lg', 'xl', or 'ldGray.1', 'ldGray.2', 'ldDark.1', 'ldDark.2', etc) instead of magic numbers
-
When using mantine colors in css modules, always use the theme awared variables:
-
--mantine-color-${color}-text : for text on filled background
-
--mantine-color-${color}-filled : for filled background (strong color)
-
--mantine-color-${color}-filled-hover : for filled background on hover
-
--mantine-color-${color}-light : for light background
-
--mantine-color-${color}-light-hover : for light background on hover (light color)
-
--mantine-color-${color}-light-color : for text on light background
-
--mantine-color-${color}-outline : for outlines
-
--mantine-color-${color}-outline-hover : for outlines on hover
Quick Migration Guide
// ❌ Mantine 6 import { Button, Group } from '@mantine/core';
<Group spacing="xs" noWrap> <Button sx={{ mt: 20 }}>Click</Button> </Group>;
// ✅ Mantine 8 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 :
// In src/mantine8Theme.ts - inside the components object components: { Button: Button.extend({ styles: { root: { minWidth: '120px', fontWeight: 600, } } }), }
Context-Specific Overrides
Inline-style Component Props (1-3 simple props)
// ✅ Good <Button mt="xl" w={240} c="blue.6">Submit</Button>
// ❌ Bad - Too many props, use CSS modules instead <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:
/* Component.module.css */ .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:
// ❌ Bad - Standard Mantine colors (poor dark mode support) <Text c="gray.6">Secondary text</Text>
// ✅ Good - ldGray for borders and neutral elements <Text c="ldGray.6">Secondary text</Text>
// ✅ Good - ldDark for elements that appear dark in light mode <Button bg="ldDark.8" c="ldDark.0">Dark button</Button>
// ✅ Good - Foreground/background variables <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
// ❌ Bad - Magic numbers <Box p={16} mt={24}>
// ✅ Good - Theme tokens <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:
// ❌ Unnecessary - display: block has no effect on flex children <Flex justify="flex-end"> <Button style={{display: 'block'}}>Submit</Button> </Flex>
// ✅ Better - Remove the style entirely <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.
// ✅ Clickable row without native button style bleed <PolymorphicGroupButton component="div" gap="sm" onClick={handleClick}> <MantineIcon icon={IconFolder} /> <Text>Label</Text> </PolymorphicGroupButton>
// ✅ Clickable card surface <PolymorphicPaperButton component="div" p="md" onClick={handleClick}> Card content </PolymorphicPaperButton>
// ❌ Avoid - native <button> brings unwanted background/border in menus and panels <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
// ✅ Good - truncates long names, tooltip only appears when needed <TruncatedText maxWidth={200}>{item.name}</TruncatedText>
// ✅ Accepts any Text prop <TruncatedText maxWidth="100%" fw={500}>{space.name}</TruncatedText>
Mantine Documentation
List of all components and links to their documentation in LLM-friendly format: https://mantine.dev/llms.txt