View changelog with demos on mantine.dev website
Auto convert px to rem in .css files
Start from version 1.14.4
postcss-preset-mantine
supports autoRem
option that can be used to automatically convert all px
values
to rem
units in .css
files.
module.exports = {
plugins: {
'postcss-preset-mantine': {
autoRem: true,
},
},
};
This option works similar to rem
function. The following code:
.demo {
font-size: 16px;
@media (min-width: 320px) {
font-size: 32px;
}
}
Will be transformed to:
.demo {
font-size: calc(1rem * var(--mantine-scale));
@media (min-width: 320px) {
font-size: calc(2rem * var(--mantine-scale));
}
}
Note that autoRem
converts only CSS properties, values in @media
queries are
not converted automatically – you still need to use em
function to convert them.
autoRem
option does not convert values in the following cases:
- Values in
calc()
,var()
,clamp()
andurl()
functions - Values in
content
property - Values that contain
rgb()
,rgba()
,hsl()
,hsla()
colors
If you want to convert above values to rem units, use rem
function manually.
Uncontrolled form mode
useForm hook now supports uncontrolled mode.
Uncontrolled mode provides a significant performance improvement by reducing
the number of re-renders and the amount of state updates almost to 0. Uncontrolled
mode is now the recommended way to use the useForm
hook for almost all use cases.
Example of uncontrolled form (form.values
are not updated):
import { useState } from 'react';
import { Button, Code, Text, TextInput } from '@mantine/core';
import { hasLength, isEmail, useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: '', email: '' },
validate: {
name: hasLength({ min: 3 }, 'Must be at least 3 characters'),
email: isEmail('Invalid email'),
},
});
const [submittedValues, setSubmittedValues] = useState<typeof form.values | null>(null);
return (
<form onSubmit={form.onSubmit(setSubmittedValues)}>
<TextInput {...form.getInputProps('name')} label="Name" placeholder="Name" />
<TextInput {...form.getInputProps('email')} mt="md" label="Email" placeholder="Email" />
<Button type="submit" mt="md">
Submit
</Button>
<Text mt="md">Form values:</Text>
<Code block>{JSON.stringify(form.values, null, 2)}</Code>
<Text mt="md">Submitted values:</Text>
<Code block>{submittedValues ? JSON.stringify(submittedValues, null, 2) : '–'}</Code>
</form>
);
}
form.getValues
With uncontrolled mode, you can not access form.values
as a state variable,
instead, you can use form.getValues()
method to get current form values at any time:
import { useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: 'John Doe' },
});
form.getValues(); // { name: 'John Doe' }
form.setValues({ name: 'John Smith' });
form.getValues(); // { name: 'John Smith' }
form.getValues()
always returns the latest form values, it is safe to use it
after state updates:
import { useForm } from '@mantine/form';
const form = useForm({
mode: 'uncontrolled',
initialValues: { name: 'John Doe' },
});
const handleNameChange = () => {
form.setFieldValue('name', 'Test Name');
// ❌ Do not use form.values to get the current form values
// form.values has stale name value until next rerender in controlled mode
// and is always outdated in uncontrolled mode
console.log(form.values); // { name: 'John Doe' }
// ✅ Use form.getValues to get the current form values
// form.getValues always returns the latest form values
console.log(form.getValues()); // { name: 'Test Name' }
};
form.watch
form.watch
is an effect function that allows subscribing to changes of a
specific form field. It accepts field path and a callback function that is
called with new value, previous value, touched and dirty field states:
import { TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
function Demo() {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
},
});
form.watch('name', ({ previousValue, value, touched, dirty }) => {
console.log({ previousValue, value, touched, dirty });
});
return (
<div>
<TextInput label="Name" placeholder="Name" {...form.getInputProps('name')} />
<TextInput mt="md" label="Email" placeholder="Email" {...form.getInputProps('email')} />
</div>
);
}
Customize Popover middlewares
You can now customize middlewares
options in Popover component and
in other components (Menu, Select, Combobox, etc.)
based on Popover.
To customize Floating UI middlewares options, pass them as
an object to the middlewares
prop. For example, to change shift
middleware padding to 20px
use the following configuration:
import { Popover } from '@mantine/core';
function Demo() {
return (
<Popover middlewares={{ shift: { padding: 20 } }} position="bottom">
{/* Popover content */}
</Popover>
);
}
use-fetch hook
New use-fetch hook:
import { Box, Button, Code, Group, LoadingOverlay, Text } from '@mantine/core';
import { useFetch } from '@mantine/hooks';
interface Item {
userId: number;
id: number;
title: string;
completed: boolean;
}
function Demo() {
const { data, loading, error, refetch, abort } = useFetch<Item[]>(
'https://jsonplaceholder.typicode.com/todos/'
);
return (
<div>
{error && <Text c="red">{error.message}</Text>}
<Group>
<Button onClick={refetch} color="blue">
Refetch
</Button>
<Button onClick={abort} color="red">
Abort
</Button>
</Group>
<Box pos="relative" mt="md">
<Code block>{data ? JSON.stringify(data.slice(0, 3), null, 2) : 'Fetching'}</Code>
<LoadingOverlay visible={loading} />
</Box>
</div>
);
}
use-map hook
New use-map hook:
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { ActionIcon, Group, Table } from '@mantine/core';
import { useMap } from '@mantine/hooks';
function Demo() {
const map = useMap([
['/hooks/use-media-query', 4124],
['/hooks/use-clipboard', 8341],
['/hooks/use-fetch', 9001],
]);
const rows = Array.from(map.entries()).map(([key, value]) => (
<Table.Tr key={key}>
<Table.Td>{key}</Table.Td>
<Table.Td>{value}</Table.Td>
<Table.Td>
<Group>
<ActionIcon variant="default" onClick={() => map.set(key, value + 1)} fw={500}>
<IconPlus stroke={1.5} size={18} />
</ActionIcon>
<ActionIcon variant="default" onClick={() => map.delete(key)} c="red">
<IconTrash stroke={1.5} size={18} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Table layout="fixed">
<Table.Thead>
<Table.Tr>
<Table.Th>Page</Table.Th>
<Table.Th>Views last month</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
);
}
use-set hook
New use-set hook:
import { useState } from 'react';
import { Code, Stack, TextInput } from '@mantine/core';
import { useSet } from '@mantine/hooks';
function Demo() {
const [input, setInput] = useState('');
const scopes = useSet<string>(['@mantine', '@mantine-tests', '@mantinex']);
const isDuplicate = scopes.has(input.trim().toLowerCase());
const items = Array.from(scopes).map((scope) => <Code key={scope}>{scope}</Code>);
return (
<>
<TextInput
label="Add new scope"
placeholder="Enter scope"
description="Duplicate scopes are not allowed"
value={input}
onChange={(event) => setInput(event.currentTarget.value)}
error={isDuplicate && 'Scope already exists'}
onKeyDown={(event) => {
if (event.nativeEvent.code === 'Enter' && !isDuplicate) {
scopes.add(input.trim().toLowerCase());
setInput('');
}
}}
/>
<Stack gap={5} align="flex-start" mt="md">
{items}
</Stack>
</>
);
}
use-debounced-callback hook
New use-debounced-callback hook:
import { useState } from 'react';
import { Loader, Text, TextInput } from '@mantine/core';
import { useDebouncedCallback } from '@mantine/hooks';
function getSearchResults(query: string): Promise<{ id: number; title: string }[]> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(
query.trim() === ''
? []
: Array(5)
.fill(0)
.map((_, index) => ({ id: index, title: `${query} ${index + 1}` }))
);
}, 1000);
});
}
function Demo() {
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<{ id: number; title: string }[]>([]);
const [loading, setLoading] = useState(false);
const handleSearch = useDebouncedCallback(async (query: string) => {
setLoading(true);
setSearchResults(await getSearchResults(query));
setLoading(false);
}, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.currentTarget.value);
handleSearch(event.currentTarget.value);
};
return (
<>
<TextInput
value={search}
onChange={handleChange}
placeholder="Search..."
rightSection={loading && <Loader size={20} />}
/>
{searchResults.map((result) => (
<Text key={result.id} size="sm">
{result.title}
</Text>
))}
</>
);
}
use-throttled-state hook
New use-throttled-state hook:
import { Text, TextInput } from '@mantine/core';
import { useThrottledState } from '@mantine/hooks';
function Demo() {
const [throttledValue, setThrottledValue] = useThrottledState('', 1000);
return (
<>
<TextInput
placeholder="Search"
onChange={(event) => setThrottledValue(event.currentTarget.value)}
/>
<Text>Throttled value: {throttledValue || '–'}</Text>
</>
);
}
use-throttled-value hook
New use-throttled-value hook:
import { Text, TextInput } from '@mantine/core';
import { useThrottledValue } from '@mantine/hooks';
function Demo() {
const [value, setValue] = useState('');
const throttledValue = useThrottledValue(value, 1000);
return (
<>
<TextInput placeholder="Search" onChange={(event) => setValue(event.currentTarget.value)} />
<Text>Throttled value: {throttledValue || '–'}</Text>
</>
);
}
use-throttled-callback hook
New use-throttled-callback hook:
import { Text, TextInput } from '@mantine/core';
import { useThrottledCallback } from '@mantine/hooks';
function Demo() {
const [throttledValue, setValue] = useState('');
const throttledSetValue = useThrottledCallback((value) => setValue(value), 1000);
return (
<>
<TextInput
placeholder="Search"
onChange={(event) => throttledSetValue(event.currentTarget.value)}
/>
<Text>Throttled value: {throttledValue || '–'}</Text>
</>
);
}
use-orientation hook
New use-orientation hook:
import { Code, Text } from '@mantine/core';
import { useOrientation } from '@mantine/hooks';
function Demo() {
const { angle, type } = useOrientation();
return (
<>
<Text>
Angle: <Code>{angle}</Code>
</Text>
<Text>
Type: <Code>{type}</Code>
</Text>
</>
);
}
use-is-first-render hook
New use-is-first-render hook:
import { useState } from 'react';
import { Button, Text } from '@mantine/core';
import { useIsFirstRender } from '@mantine/hooks';
function Demo() {
const [counter, setCounter] = useState(0);
const firstRender = useIsFirstRender();
return (
<div>
<Text>
Is first render:{' '}
<Text span c={firstRender ? 'teal' : 'red'}>
{firstRender ? 'Yes' : 'No!'}
</Text>
</Text>
<Button onClick={() => setCounter((c) => c + 1)} mt="sm">
Rerendered {counter} times, click to rerender
</Button>
</div>
);
}
Documentation updates
- New uncontrolled form guide
- onValuesChange documentation has been added
- A new demo has been added to tiptap that shows how to customize typography styles
- A new guide has been added to customize Popover middlewares
Other changes
- NumberInput now supports
withKeyboardEvents={false}
to disable up/down arrow keys handling - Popover shift middleware now has default padding of 5px to offset dropdown near the edge of the viewport