frontend-style-guide

Lightdash Frontend Style Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "frontend-style-guide" with this command: npx skills add lightdash/lightdash/lightdash-lightdash-frontend-style-guide

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

ld-permissions

No summary provided by upstream source.

Repository SourceNeeds Review
General

investigate-pylon

No summary provided by upstream source.

Repository SourceNeeds Review
General

debug-local

No summary provided by upstream source.

Repository SourceNeeds Review