Agent Skill
2/7/2026

mantine-ui

Use when modifying files in frontend/src/components/ or frontend/src/pages/, creating React components, building forms, modals, or UI features. This project uses Mantine exclusively for all UI components.

G
glandais
0GitHub Stars
1Views
npx skills add glandais/tribly

SKILL.md

Namemantine-ui
DescriptionUse when modifying files in frontend/src/components/ or frontend/src/pages/, creating React components, building forms, modals, or UI features. This project uses Mantine exclusively for all UI components.

name: mantine-ui description: Use when modifying files in frontend/src/components/ or frontend/src/pages/, creating React components, building forms, modals, or UI features. This project uses Mantine exclusively for all UI components.

Mantine UI Reference (Tribly Patterns)

Overview

Mantine v8 component library with TypeScript, dark mode, and form integration. All components require MantineProvider wrapper.

Docs: https://mantine.dev/llms.txt

Theme Configuration

import { MantineProvider, createTheme, virtualColor } from '@mantine/core';
import '@mantine/core/styles.css';

const theme = createTheme({
  primaryColor: 'primary',
  fontFamily: 'Inter, system-ui, sans-serif',
  defaultRadius: 'md',
  autoContrast: true,
  luminanceThreshold: 0.3,
  colors: {
    primary: virtualColor({ name: 'primary', light: 'indigo', dark: 'indigo' }),
    success: virtualColor({ name: 'success', light: 'green', dark: 'green' }),
    warning: virtualColor({ name: 'warning', light: 'yellow', dark: 'yellow' }),
    danger: virtualColor({ name: 'danger', light: 'red', dark: 'red' }),
  },
  headings: {
    sizes: {
      h1: { fontSize: 'clamp(1.5rem, 5vw, 2.125rem)', lineHeight: '1.2' },
      h2: { fontSize: 'clamp(1.25rem, 4vw, 1.625rem)', lineHeight: '1.3' },
    },
  },
  components: {
    Button: { styles: { root: { minHeight: 'var(--button-min-height, 44px)' } } },
    ActionIcon: { defaultProps: { size: 'lg' } },
  },
});

<MantineProvider theme={theme} defaultColorScheme="auto">
  <Notifications position="top-right" />
  {children}
</MantineProvider>

Core Components

Layout

ComponentPurposeKey Props
StackVertical flexgap, align, justify
GroupHorizontal flexgap, wrap, justify
PaperCard containershadow, withBorder, p, radius
BoxGeneric wrapperAll style props
SimpleGridResponsive gridcols={{ base: 1, sm: 2, lg: 3 }}
CenterCenter contentmih for min-height
CollapseCollapsible contentin={isOpen}
// Common page layout
<Stack>
  <Group justify="space-between">
    <Title order={2}>{title}</Title>
    <Button leftSection={<IconPlus />}>{t('actions.add')}</Button>
  </Group>
  <Paper withBorder p="md">
    <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
      {items.map(item => <ItemCard key={item.id} {...item} />)}
    </SimpleGrid>
  </Paper>
</Stack>

Form with Zod Validation

import { useForm } from '@mantine/form';
import { zodFormValidator } from '@/lib/formUtils'; // Project wrapper

const form = useForm<MyFormData>({
  validate: zodFormValidator<MyFormData>(mySchema),
  initialValues,
  validateInputOnChange: true,
});

<form onSubmit={form.onSubmit(handleSubmit)}>
  <Stack>
    <TextInput label={t('field.name')} {...form.getInputProps('name')} />
    <Select
      label={t('field.type')}
      data={typeOptions}
      {...form.getInputProps('type')}
    />
    <Group justify="flex-end" pt="md">
      <Button variant="default" onClick={onCancel}>{t('actions.cancel')}</Button>
      <Button type="submit" loading={isPending}>{t('actions.save')}</Button>
    </Group>
  </Stack>
</form>

Array fields:

form.insertListItem('groups', { name: '' });
form.removeListItem('groups', index);
form.reorderListItem('groups', { from: index, to: newIndex });
form.setFieldValue('groups.0.name', 'New Name');

Inputs

ComponentPurposeKey Props
TextInputText fieldlabel, error, leftSection, rightSection
SelectDropdown (restricted)data, searchable, clearable
NumberInputNumericmin, max, step, decimalScale
TextareaMulti-lineautosize, minRows, maxRows
DateTimePickerDate/time@mantine/dates package
Radio.GroupRadio buttonsFor status/visibility selection
// Controlled input with clear button
<TextInput
  value={search}
  onChange={(e) => setSearch(e.currentTarget.value)}
  leftSection={<IconSearch size={16} />}
  rightSection={search && <CloseButton onClick={() => setSearch('')} />}
/>

Buttons

ComponentPurposeKey Props
ButtonPrimary actionvariant, color, loading, leftSection
ActionIconIcon-onlyvariant, aria-label (required!)

Variants: filled, light, outline, subtle, default, transparent

<Button variant="default" onClick={onCancel}>{t('actions.cancel')}</Button>
<Button color="danger" loading={isDeleting}>{t('actions.delete')}</Button>
<ActionIcon variant="subtle" color="red" aria-label="Delete">
  <IconTrash size={16} />
</ActionIcon>

Modal & Dialog

import { useDisclosure } from '@mantine/hooks';

const [opened, { open, close }] = useDisclosure(false);

<Modal opened={opened} onClose={close} title={title} centered size="lg">
  <Stack>
    <Text c="dimmed">{message}</Text>
    <Group justify="flex-end" mt="md">
      <Button variant="default" onClick={close}>{t('actions.cancel')}</Button>
      <Button color="primary" onClick={handleConfirm}>{t('actions.confirm')}</Button>
    </Group>
  </Stack>
</Modal>

Modal sizes: xs, sm, md, lg, xl, 4xl

ConfirmDialog pattern (project component):

<ConfirmDialog
  isOpen={isOpen}
  onClose={close}
  onConfirm={handleDelete}
  title={t('confirm.delete.title')}
  message={t('confirm.delete.message')}
  variant="danger"
  isLoading={isPending}
/>

Display

ComponentPurposeKey Props
TextBody textsize, c (color), fw (weight), lh (line-height)
TitleHeadingsorder (1-6), mb, mt
BadgeStatus labelscolor, variant
AlertMessagesicon, title, color
SkeletonLoading placeholderheight, width, circle
ProgressProgress barvalue, color, size
// Color conventions
<Text c="dimmed">Subtle text</Text>
<Text c="red">Error text</Text>
<Text fw={500}>Semi-bold label</Text>

// Role/status badges
const roleBadgeColors = { ADMIN: 'grape', ORGANIZER: 'blue', MEMBER: 'gray' };
<Badge color={roleBadgeColors[role]}>{t(`roles.${role}`)}</Badge>

CSS Variables

Use Mantine CSS vars for dark mode support:

<Box bg="var(--mantine-color-body)">
<Paper style={{ borderColor: 'var(--mantine-color-default-border)' }}>
<Text c="var(--mantine-color-dimmed)">
<Box style={{ boxShadow: 'var(--mantine-shadow-sm)' }}>

Common vars: --mantine-color-body, --mantine-color-default, --mantine-color-default-border, --mantine-color-dimmed, --mantine-color-anchor, --mantine-radius-md, --mantine-shadow-sm

Responsive Patterns

// Responsive grid
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">

// Responsive props
<Box p={{ base: 'xs', sm: 'md', lg: 'xl' }} display={{ base: 'none', md: 'block' }}>

// useResponsive hook (project)
const { sizeCompact, isMobile } = useResponsive();
<Button size={sizeCompact}>{text}</Button>

// Responsive typography (in theme)
fontSize: 'clamp(1.5rem, 5vw, 2.125rem)'

Breakpoints: xs (36em), sm (48em), md (62em), lg (75em), xl (88em)

i18n Integration

const { t } = useTranslation();

// Direct keys
<Button>{t('actions.save')}</Button>
<Text>{t('pagination.page', { current: 1, total: 10 })}</Text>

// Templated keys with type safety
t(`status.${status satisfies 'DRAFT' | 'PUBLISHED'}`)
t(`roles.${role satisfies 'ADMIN' | 'ORGANIZER' | 'MEMBER'}`)

Rich Text (Tiptap)

import { RichTextEditor } from '@mantine/tiptap';

<RichTextEditor editor={editor}>
  <RichTextEditor.Toolbar sticky stickyOffset={0}>
    <RichTextEditor.ControlsGroup>
      <RichTextEditor.Bold />
      <RichTextEditor.Italic />
    </RichTextEditor.ControlsGroup>
  </RichTextEditor.Toolbar>
  <RichTextEditor.Content />
</RichTextEditor>

Icons

Always use @tabler/icons-react, never SVG:

import { IconPlus, IconTrash, IconSettings } from '@tabler/icons-react';

<Button leftSection={<IconPlus size={16} />}>{t('actions.add')}</Button>
<ActionIcon aria-label="Delete"><IconTrash size={16} /></ActionIcon>

Common Mistakes

1. Missing MantineProvider

All components need MantineProvider at root.

2. ActionIcon without aria-label

// ❌ BAD
<ActionIcon><IconTrash /></ActionIcon>
// ✅ GOOD
<ActionIcon aria-label="Delete"><IconTrash /></ActionIcon>

3. Nested interactive elements

// ❌ BAD - button inside button
<Button><ActionIcon /></Button>
// ✅ GOOD - use Menu or separate
<Group><Button /><ActionIcon /></Group>

4. Select vs Autocomplete

  • Select: Restricts to provided options
  • Autocomplete: Allows free-form input with suggestions

5. Controlled/Uncontrolled mixing

// ❌ BAD
<TextInput defaultValue="x" value={val} onChange={...} />
// ✅ GOOD - pick one
<TextInput value={val} onChange={...} />

6. Hard-coded text

Always use i18n:

// ❌ BAD
<Button>Save</Button>
// ✅ GOOD
<Button>{t('actions.save')}</Button>

7. Hard-coded routes

Use paths from config:

// ❌ BAD
<Link to={`/teams/${slug}/rides`}>
// ✅ GOOD
<Link to={paths.teamRides(slug)}>

Quick Reference

import {
  MantineProvider, createTheme,
  Button, ActionIcon, TextInput, Select, NumberInput,
  Modal, Menu, Tooltip, Collapse,
  Stack, Group, Paper, Box, SimpleGrid, Center,
  Text, Title, Badge, Alert, Skeleton, Progress,
  Table, Tabs,
} from '@mantine/core';
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { useForm } from '@mantine/form';
import { DateTimePicker } from '@mantine/dates';
import { Notifications } from '@mantine/notifications';
import '@mantine/core/styles.css';
Skills Info
Original Name:mantine-uiAuthor:glandais