@refinedev/mantine package offers pre-built components that integrate seamlessly with Refine’s core functionality.
Installation
npm install @refinedev/mantine @mantine/core @mantine/hooks @mantine/form @mantine/notifications @emotion/react @tabler/icons-react dayjs
Setup
Basic Setup
Wrap your application with Mantine’s providers:App.tsx
import { Refine } from "@refinedev/core";
import {
ThemedLayout,
RefineThemes,
useNotificationProvider,
notificationProvider,
} from "@refinedev/mantine";
import { MantineProvider } from "@mantine/core";
import { NotificationsProvider } from "@mantine/notifications";
import dataProvider from "@refinedev/simple-rest";
function App() {
return (
<MantineProvider theme={RefineThemes.Blue} withNormalizeCSS withGlobalStyles>
<NotificationsProvider position="top-right">
<Refine
dataProvider={dataProvider("https://api.example.com")}
notificationProvider={useNotificationProvider}
resources={[
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
},
]}
>
<ThemedLayout>
{/* Your pages here */}
</ThemedLayout>
</Refine>
</NotificationsProvider>
</MantineProvider>
);
}
export default App;
Available Themes
Refine provides pre-configured Mantine themes:import { RefineThemes } from "@refinedev/mantine";
// Available themes:
RefineThemes.Blue
RefineThemes.Purple
RefineThemes.Magenta
RefineThemes.Red
RefineThemes.Orange
RefineThemes.Yellow
Custom Theme with Dark Mode
Mantine has excellent dark mode support:import { MantineProvider, ColorSchemeProvider } from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
function App() {
const [colorScheme, setColorScheme] = useLocalStorage({
key: "mantine-color-scheme",
defaultValue: "light",
});
const toggleColorScheme = (value) =>
setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));
return (
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
theme={{
colorScheme,
primaryColor: "blue",
fontFamily: "Inter, sans-serif",
}}
withNormalizeCSS
withGlobalStyles
>
{/* Your app */}
</MantineProvider>
</ColorSchemeProvider>
);
}
Layout Components
ThemedLayout
The main layout component with sidebar, header, and content:import { ThemedLayout } from "@refinedev/mantine";
function App() {
return (
<Refine
// ... other props
>
<ThemedLayout>
{/* Your routes */}
</ThemedLayout>
</Refine>
);
}
Custom Layout Components
import {
ThemedLayout,
ThemedSider,
ThemedHeader,
ThemedTitle,
} from "@refinedev/mantine";
function App() {
return (
<ThemedLayout
Sider={() => <ThemedSider />}
Header={() => <ThemedHeader sticky />}
Title={({ collapsed }) => (
<ThemedTitle collapsed={collapsed} text="My Admin" />
)}
>
{/* Your routes */}
</ThemedLayout>
);
}
CRUD Components
List
Display list with Mantine Table:pages/products/list.tsx
import { List, EditButton, ShowButton, DeleteButton } from "@refinedev/mantine";
import { useTable } from "@refinedev/react-table";
import { ColumnDef, flexRender } from "@tanstack/react-table";
import { Table, Group, Pagination } from "@mantine/core";
import { useMemo } from "react";
export const ProductList = () => {
const columns = useMemo<ColumnDef<any>[]>(
() => [
{
id: "id",
accessorKey: "id",
header: "ID",
},
{
id: "name",
accessorKey: "name",
header: "Name",
},
{
id: "price",
accessorKey: "price",
header: "Price",
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => (
<Group spacing="xs" noWrap>
<EditButton hideText recordItemId={row.original.id} />
<ShowButton hideText recordItemId={row.original.id} />
<DeleteButton hideText recordItemId={row.original.id} />
</Group>
),
},
],
[],
);
const {
getHeaderGroups,
getRowModel,
setPageIndex,
getState,
getPageCount,
} = useTable({
columns,
});
return (
<List>
<Table>
<thead>
{getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</Table>
<Pagination
position="right"
total={getPageCount()}
page={getState().pagination.pageIndex + 1}
onChange={(page) => setPageIndex(page - 1)}
/>
</List>
);
};
Create
Create form with Mantine components:pages/products/create.tsx
import { Create, useForm } from "@refinedev/mantine";
import { TextInput, NumberInput, Textarea } from "@mantine/core";
export const ProductCreate = () => {
const {
getInputProps,
saveButtonProps,
refineCore: { formLoading },
} = useForm({
initialValues: {
name: "",
price: 0,
description: "",
},
validate: {
name: (value) => (value.length < 2 ? "Name is too short" : null),
price: (value) => (value <= 0 ? "Price must be positive" : null),
},
});
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<TextInput
mt="sm"
label="Name"
placeholder="Product name"
{...getInputProps("name")}
/>
<NumberInput
mt="sm"
label="Price"
placeholder="0.00"
precision={2}
{...getInputProps("price")}
/>
<Textarea
mt="sm"
label="Description"
placeholder="Product description"
minRows={4}
{...getInputProps("description")}
/>
</Create>
);
};
Edit
Edit form with automatic data fetching:pages/products/edit.tsx
import { Edit, useForm } from "@refinedev/mantine";
import { TextInput, NumberInput } from "@mantine/core";
export const ProductEdit = () => {
const {
getInputProps,
saveButtonProps,
refineCore: { queryResult },
} = useForm({
initialValues: {
name: "",
price: 0,
},
validate: {
name: (value) => (value.length < 2 ? "Name is too short" : null),
price: (value) => (value <= 0 ? "Price must be positive" : null),
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<TextInput
mt="sm"
disabled
label="ID"
{...getInputProps("id")}
/>
<TextInput
mt="sm"
label="Name"
placeholder="Product name"
{...getInputProps("name")}
/>
<NumberInput
mt="sm"
label="Price"
placeholder="0.00"
precision={2}
{...getInputProps("price")}
/>
</Edit>
);
};
Show
Display single record:pages/products/show.tsx
import {
Show,
TextField,
NumberField,
DateField,
} from "@refinedev/mantine";
import { useShow } from "@refinedev/core";
import { Title } from "@mantine/core";
export const ProductShow = () => {
const { queryResult } = useShow();
const { data, isLoading } = queryResult;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Title order={5}>ID</Title>
<TextField value={record?.id} />
<Title mt="md" order={5}>
Name
</Title>
<TextField value={record?.name} />
<Title mt="md" order={5}>
Price
</Title>
<NumberField
value={record?.price}
options={{
style: "currency",
currency: "USD",
}}
/>
<Title mt="md" order={5}>
Created At
</Title>
<DateField value={record?.createdAt} />
</Show>
);
};
Field Components
Mantine field components for displaying data:import {
TextField,
NumberField,
DateField,
EmailField,
UrlField,
BooleanField,
TagField,
MarkdownField,
} from "@refinedev/mantine";
import { Stack } from "@mantine/core";
function FieldExamples() {
return (
<Stack spacing="sm">
<TextField value="Simple text" />
<NumberField
value={1234.56}
options={{ style: "currency", currency: "USD" }}
/>
<DateField value="2024-01-15" format="LL" />
<EmailField value="[email protected]" />
<UrlField value="https://example.com" />
<BooleanField value={true} />
<TagField value="Active" color="green" />
<MarkdownField value="# Hello\n\nThis is **markdown**" />
</Stack>
);
}
Button Components
Mantine styled action buttons:import {
CreateButton,
EditButton,
ShowButton,
DeleteButton,
ListButton,
RefreshButton,
SaveButton,
CloneButton,
ExportButton,
ImportButton,
} from "@refinedev/mantine";
import { Group } from "@mantine/core";
function ButtonExamples() {
return (
<Group spacing="xs">
<CreateButton resource="products" />
<EditButton resource="products" recordItemId="1" />
<ShowButton resource="products" recordItemId="1" />
<DeleteButton resource="products" recordItemId="1" />
<ListButton resource="products" />
<RefreshButton resource="products" />
<SaveButton />
<CloneButton resource="products" recordItemId="1" />
<ExportButton />
<ImportButton />
</Group>
);
}
Advanced Features
Modal Forms
import { useModalForm, CreateButton } from "@refinedev/mantine";
import { Modal, TextInput } from "@mantine/core";
export const ProductList = () => {
const {
modal: { visible, close, title },
getInputProps,
saveButtonProps,
} = useModalForm({
refineCoreProps: { action: "create" },
initialValues: {
name: "",
price: 0,
},
validate: {
name: (value) => (value.length < 2 ? "Name is too short" : null),
},
});
return (
<>
<CreateButton onClick={() => modal.show()} />
<Modal opened={visible} onClose={close} title={title}>
<TextInput
mt="sm"
label="Name"
placeholder="Product name"
{...getInputProps("name")}
/>
<Group position="right" mt="md">
<SaveButton {...saveButtonProps} />
</Group>
</Modal>
</>
);
};
Notifications
import { useNotification } from "@refinedev/core";
import { Button } from "@mantine/core";
function CustomNotification() {
const { open } = useNotification();
const handleClick = () => {
open?.({
type: "success",
message: "Success!",
description: "Operation completed successfully",
});
};
return <Button onClick={handleClick}>Show Notification</Button>;
}
Dark Mode Toggle
import { ActionIcon, useMantineColorScheme } from "@mantine/core";
import { IconSun, IconMoon } from "@tabler/icons-react";
function ColorSchemeToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<ActionIcon
variant="outline"
color={dark ? "yellow" : "blue"}
onClick={() => toggleColorScheme()}
title="Toggle color scheme"
>
{dark ? <IconSun size={18} /> : <IconMoon size={18} />}
</ActionIcon>
);
}
Auto-Save Indicator
import { AutoSaveIndicator, useForm } from "@refinedev/mantine";
import { TextInput } from "@mantine/core";
export const ProductEdit = () => {
const {
getInputProps,
refineCore: { autoSaveProps },
} = useForm({
refineCoreProps: {
autoSave: {
enabled: true,
debounce: 2000,
},
},
});
return (
<>
<AutoSaveIndicator {...autoSaveProps} />
<TextInput mt="sm" label="Name" {...getInputProps("name")} />
</>
);
};
Filters with Search
import { List } from "@refinedev/mantine";
import { useTable } from "@refinedev/react-table";
import { TextInput, Select, Group } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useState } from "react";
export const ProductList = () => {
const [search, setSearch] = useState("");
const [status, setStatus] = useState("");
const { refineCore: { setFilters } } = useTable({
columns: [],
});
const handleSearch = () => {
setFilters([
{
field: "name",
operator: "contains",
value: search,
},
{
field: "status",
operator: "eq",
value: status,
},
]);
};
return (
<List>
<Group mb="md">
<TextInput
placeholder="Search products"
icon={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<Select
placeholder="Status"
data={[
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
]}
value={status}
onChange={setStatus}
/>
<Button onClick={handleSearch}>Search</Button>
</Group>
{/* Table here */}
</List>
);
};
Responsive Design
Mantine has excellent responsive utilities:import { Grid, Container, useMantineTheme } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
function ResponsiveLayout() {
const theme = useMantineTheme();
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
return (
<Container size="xl">
<Grid>
<Grid.Col span={mobile ? 12 : 6}>
{/* Content */}
</Grid.Col>
<Grid.Col span={mobile ? 12 : 6}>
{/* Content */}
</Grid.Col>
</Grid>
</Container>
);
}
Next Steps
Mantine Docs
Official Mantine documentation
Examples
See complete Mantine examples
Theming
Learn about Mantine theming
Hooks
Explore Refine hooks