diff --git a/.demo/social-preview.png b/.demo/social-preview.png
new file mode 100644
index 00000000000..21dc46b7b33
Binary files /dev/null and b/.demo/social-preview.png differ
diff --git a/.eslintignore b/.eslintignore
index 23fbaa61b7b..2c03c06d218 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -6,3 +6,4 @@ public
.cache
.eslintrc.js
*.d.ts
+*.js
diff --git a/.eslintrc.js b/.eslintrc.js
index b54ad07aa26..1310562e000 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,10 +4,17 @@ module.exports = {
rules: {
'jest/no-export': 'off',
'jest/expect-expect': 'off',
+ 'jest/valid-title': 'off',
'react/jsx-pascal-case': 'off',
'newline-per-chained-call': 'off',
+ 'import/extensions': 'off',
+ 'jsx-a11y/label-has-associated-control': 'off',
+ 'react/self-closing-comp': 'off',
+ 'react/jsx-closing-bracket-location': 'off',
+ '@typescript-eslint/no-loop-func': 'off',
+ 'no-restricted-syntax': 'off',
},
parserOptions: {
- project: './tsconfig.json',
+ project: './tsconfig.eslint.json',
},
};
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index f26a778cf35..0fb3ce439a1 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,5 +1,6 @@
name: Report an issue with @mantine/ scoped package
description: Tell us if something is not working as expected
+labels: 'review required'
body:
- type: markdown
attributes:
@@ -17,12 +18,15 @@ body:
- '@mantine/form'
- '@mantine/notifications'
- '@mantine/tiptap'
- - '@mantine/prism'
+ - '@mantine/code-highlight'
- '@mantine/modals'
- '@mantine/dropzone'
- '@mantine/spotlight'
- '@mantine/nprogress'
- '@mantine/carousel'
+ - '@mantine/colors-generator'
+ - '@mantine/store'
+ - '@mantine/vanilla-extract'
validations:
required: true
- type: textarea
@@ -34,7 +38,7 @@ body:
- type: input
id: version
attributes:
- label: What version of @mantine/hooks page do you have in package.json?
+ label: What version of @mantine/* page do you have in package.json? (Note that all @mantine/* packages must have the same version in order to work correctly)
validations:
required: true
- type: input
diff --git a/.github/ISSUE_TEMPLATE/docs_report.yml b/.github/ISSUE_TEMPLATE/docs_report.yml
index 419f85294d7..c4b4fb8a497 100644
--- a/.github/ISSUE_TEMPLATE/docs_report.yml
+++ b/.github/ISSUE_TEMPLATE/docs_report.yml
@@ -1,5 +1,6 @@
name: Report an issue with mantine.dev website
description: Nothing is perfect, especially our docs, help us find and fix mistakes, bad wording, etc.
+labels: 'review required'
body:
- type: markdown
attributes:
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 3a86313fc72..0275c67cde4 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: '16.10.0'
+ node-version: '18.17.0'
cache: 'yarn'
cache-dependency-path: '**/yarn.lock'
- name: Install dependencies
diff --git a/.gitignore b/.gitignore
index 7180eef5c37..bc1afb58030 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,9 +34,8 @@ docs/.docgen/
# project
.eslintcache
docs/.cache
-docs/public
storybook-static
.next
-docs/src/components/Test.tsx
-____*.internal.ts
-____*.internal.tsx
+docs/components/___test.internal.tsx
+src/*/styles.css
+.stylelintcache
diff --git a/.nvmrc b/.nvmrc
index 53a42214a46..1df6fd41c70 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v16.13.2
+v20.5.0
diff --git a/.storybook/main.js b/.storybook/main.js
new file mode 100644
index 00000000000..0b351349648
--- /dev/null
+++ b/.storybook/main.js
@@ -0,0 +1,52 @@
+const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin').default;
+const path = require('path');
+
+module.exports = {
+ stories: ['../src/**/*.story.@(js|jsx|ts|tsx)'],
+ addons: [
+ 'storybook-dark-mode',
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ {
+ name: 'storybook-css-modules',
+ options: {
+ cssModulesLoaderOptions: {
+ importLoaders: 1,
+ modules: {
+ localIdentName: 'mantine-[hash:base64:7]',
+ },
+ },
+ },
+ },
+ {
+ name: '@storybook/addon-postcss',
+ options: {
+ postcssLoaderOptions: {
+ implementation: require('postcss'),
+ },
+ },
+ },
+ ],
+ framework: '@storybook/react',
+ core: {
+ builder: '@storybook/builder-webpack5',
+ },
+ webpackFinal: async (config) => {
+ config.resolve = {
+ ...config.resolve,
+ plugins: [
+ ...(config.resolve.plugins || []),
+ new TsconfigPathsPlugin({
+ extensions: ['.ts', '.tsx', '.js'],
+ configFile: path.join(__dirname, '../tsconfig.json'),
+ }),
+ ],
+ };
+
+ // Turn off docgen plugin as it breaks bundle with displayName
+ config.plugins.pop();
+
+ return config;
+ },
+};
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
new file mode 100644
index 00000000000..78d2eb368fc
--- /dev/null
+++ b/.storybook/preview.tsx
@@ -0,0 +1,58 @@
+import React, { useEffect } from 'react';
+import addons from '@storybook/addons';
+import { IconTextDirectionLtr, IconTextDirectionRtl } from '@tabler/icons-react';
+import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
+import {
+ MantineProvider,
+ useMantineColorScheme,
+ ActionIcon,
+ DirectionProvider,
+ useDirection,
+} from '@mantine/core';
+import { Notifications } from '@mantine/notifications';
+import { theme } from '../docs/theme';
+
+export const parameters = { layout: 'fullscreen' };
+
+const channel = addons.getChannel();
+
+function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
+ const { setColorScheme } = useMantineColorScheme();
+ const handleColorScheme = (value: boolean) => setColorScheme(value ? 'dark' : 'light');
+
+ useEffect(() => {
+ channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
+ return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
+ }, [channel]);
+
+ return {children} ;
+}
+
+function DirectionWrapper({ children }: { children: React.ReactNode }) {
+ const { dir, toggleDirection } = useDirection();
+ return (
+ <>
+
+ {dir === 'ltr' ? : }
+
+
+ {children}
+ >
+ );
+}
+
+export const decorators = [
+ (renderStory: any) => {renderStory()} ,
+ (renderStory: any) => {renderStory()} ,
+ (renderStory: any) => {renderStory()} ,
+];
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 00000000000..f49d0a4b522
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,4 @@
+src/*/esm/**/*.css
+src/*/cjs/**/*.css
+docs/.next
+docs/out
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 00000000000..4ea6506d911
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,28 @@
+{
+ "extends": ["stylelint-config-standard-scss"],
+ "rules": {
+ "custom-property-pattern": null,
+ "selector-class-pattern": null,
+ "scss/no-duplicate-mixins": null,
+ "declaration-empty-line-before": null,
+ "declaration-block-no-redundant-longhand-properties": null,
+ "alpha-value-notation": null,
+ "custom-property-empty-line-before": null,
+ "property-no-vendor-prefix": null,
+ "color-function-notation": null,
+ "length-zero-no-unit": null,
+ "selector-not-notation": null,
+ "no-descending-specificity": null,
+ "comment-empty-line-before": null,
+ "scss/at-mixin-pattern": null,
+ "scss/at-rule-no-unknown": null,
+ "value-keyword-case": null,
+ "media-feature-range-notation": null,
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ "ignorePseudoClasses": ["global"]
+ }
+ ]
+ }
+}
diff --git a/.syncpackrc.json b/.syncpackrc.json
index 4df8d337c94..fc66d432148 100644
--- a/.syncpackrc.json
+++ b/.syncpackrc.json
@@ -1,6 +1,27 @@
{
- "dev": true,
- "peer": false,
- "prod": true,
- "source": ["package.json", "src/*/package.json", "docs/package.json"]
+ "dependencyTypes": ["dev", "prod", "peer"],
+ "source": ["package.json", "src/*/package.json", "docs/package.json"],
+ "versionGroups": [
+ {
+ "packages": ["docs"],
+ "dependencies": [
+ "@mantine/store",
+ "@mantine/styles-api",
+ "@mantine/code-highlight",
+ "@mantine/core",
+ "@mantine/ds",
+ "@mantine/hooks",
+ "@mantine/notifications",
+ "@mantine/spotlight",
+ "@mantine/carousel",
+ "@mantine/dropzone",
+ "@mantine/form",
+ "@mantine/nprogress",
+ "@mantine/dates",
+ "@mantine/modals",
+ "@mantine/tiptap"
+ ],
+ "isIgnored": true
+ }
+ ]
}
diff --git a/.tool-versions b/.tool-versions
index 1237b21d3fa..f62fc07b5d2 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1 +1 @@
-nodejs 16.13.2
+nodejs 14.17.0
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index 664a520c1bc..00000000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "recommendations": [
- "formulahendry.auto-rename-tag",
- "streetsidesoftware.code-spell-checker",
- "dbaeumer.vscode-eslint",
- "esbenp.prettier-vscode",
- "firsttris.vscode-jest-runner",
- "meganrogge.template-string-converter",
- "silvenon.mdx"
- ]
-}
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 231838c4500..00000000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Debug Jest Tests",
- "type": "node",
- "request": "launch",
- "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"],
- "console": "integratedTerminal",
- "internalConsoleOptions": "neverOpen",
- "port": 9229
- }
- ]
-}
diff --git a/changelog/7.0.0.md b/changelog/7.0.0.md
new file mode 100644
index 00000000000..379839fd7db
--- /dev/null
+++ b/changelog/7.0.0.md
@@ -0,0 +1,1099 @@
+## Migration to native CSS
+
+Mantine no longer depends on [Emotion](https://emotion.sh/) for styles generation. All `@mantine/*`
+packages are now shipped with native CSS files which can be imported from `@mantine/{package}/styles.css`,
+for example:
+
+```tsx
+import '@mantine/core/styles.css';
+```
+
+This change improves performance, reduces bundle size of the library and allows using Mantine
+in environments where CSS-in-JS is not supported (or supported with limitations), for example,
+Next.js app directory.
+
+Important breaking changes:
+
+- `createStyles` function is no longer available, use [CSS modules](https://mantine.dev/styles/css-modules) or any other styling solution of your choice instead
+- Components no longer support `sx` prop. You can use `className` or `style` props instead.
+- `styles` prop no longer supports nested selectors
+
+It is now recommended to use [CSS modules](https://mantine.dev/styles/css-modules) to style Mantine components.
+To update your project to [CSS modules](https://mantine.dev/styles/css-modules), follow the [6.x → 7.x migration guide](https://mantine.dev/guides/6x-to-7x).
+
+## Vanilla extract integration
+
+If you prefer CSS-in-JS syntax for styling, you can use [Vanilla extract](https://mantine.dev/styles/vanilla-extract)
+as a TypeScript CSS preprocessor. You will be able to use most of Mantine styling features
+with [Vanilla extract](https://mantine.dev/styles/vanilla-extract).
+
+## System color scheme support
+
+All components now support system color scheme – when `colorScheme` value is `auto`,
+components will use `prefers-color-scheme` media query to determine if the user prefers light or dark color scheme.
+
+Note that `auto` is not the default value. You need to set it manually to enable system color scheme support
+both on [MantineProvider](https://mantine.dev/theming/mantine-provider) and in [ColorSchemeScript](https://mantine.dev/theming/color-schemes#colorschemescript):
+
+```tsx
+import { MantineProvider, ColorSchemeScript } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+```
+
+## Built-in color scheme manager
+
+[MantineProvider](https://mantine.dev/theming/mantine-provider) now comes with a built-in color scheme manager.
+It is no longer required to use any other components to set up color scheme logic.
+Color scheme can be changed with [useMantineColorScheme hook](https://mantine.dev/theming/color-schemes#use-mantine-color-scheme-hook):
+
+```tsx
+import { useMantineColorScheme, Button, Group } from '@mantine/core';
+
+function Demo() {
+ const { setColorScheme, clearColorScheme } = useMantineColorScheme();
+
+ return (
+
+ setColorScheme('light')}>Light
+ setColorScheme('dark')}>Dark
+ setColorScheme('auto')}>Auto
+ Clear
+
+ );
+}
+```
+
+## CSS modules and PostCSS preset
+
+[CSS modules](https://mantine.dev/styles/css-modules) is now the recommended way to style Mantine components,
+although it is not required – you can choose any other styling solution of your choice.
+
+It is also recommended to use [postcss-preset-mantine](https://mantine.dev/styles/postcss-preset). It includes
+mixins and functions to simplify styling of Mantine components. [postcss-preset-mantine](https://mantine.dev/styles/postcss-preset)
+is included in all templates.
+
+## Global styles
+
+Mantine no longer includes normalize.css. Instead, it uses a bare minimum set of [global styles](https://mantine.dev/styles/global-styles).
+These styles are part of the `@mantine/core` package and are applied automatically when you import
+`@mantine/core/styles.css` in your application. Note that these styles cannot be decoupled from the
+rest of the library.
+
+## Mantine as a headless UI library
+
+You can now use Mantine as a [headless](https://mantine.dev/styles/unstyled) library. To achieve that, just do not import
+`@mantine/*/styles.css` in your application. Then you will be able to apply styles with
+[Styles API](https://mantine.dev/styles/styles-api).
+
+## createTheme function
+
+`createTheme` function is a replacement for `MantineThemeOverride` type. Use it to create a theme
+override, it will give you autocomplete for all theme properties:
+
+```tsx
+import { createTheme, MantineProvider } from '@mantine/core';
+
+const theme = createTheme({
+ fontFamily: 'sans-serif',
+ primaryColor: 'orange',
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## Components extend functions
+
+All components that support [default props](https://mantine.dev/theming/default-props) or [Styles API](https://mantine.dev/styles/styles-api) now have a static `extend` function which allows getting autocomplete when customizing
+[defaultProps](https://mantine.dev/theming/default-props), [classNames and styles](https://mantine.dev/styles/styles-api) of the component
+on [theme](https://mantine.dev/theming/theme-object):
+
+```tsx
+import { useState } from 'react';
+import { TextInput, MantineProvider, createTheme } from '@mantine/core';
+import classes from './Demo.module.css';
+
+const theme = createTheme({
+ components: {
+ TextInput: TextInput.extends({
+ styles: (theme, props) => ({
+ input: {
+ fontSize: props.size === 'compact' ? theme.fontSizes.sm : undefined,
+ }
+ })
+ classNames: {
+ root: classes.root,
+ input: classes.input,
+ label: classes.label,
+ },
+
+ defaultProps: {
+ size: 'compact',
+ },
+ }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## classNames based on component props
+
+You can now get component props in [classNames and styles](https://mantine.dev/styles/styles-api) to conditionally apply styles.
+This feature is a more powerful replacement for styles params.
+
+```tsx
+import cx from 'clsx';
+import { MantineProvider, createTheme, TextInput } from '@mantine/core';
+import classes from './Demo.module.css';
+
+const theme = createTheme({
+ components: {
+ TextInput: TextInput.extend({
+ classNames: (_theme, props) => ({
+ label: cx({ [classes.labelRequired]: props.required }),
+ input: cx({ [classes.inputError]: props.error }),
+ }),
+ }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+
+ );
+}
+```
+
+```scss
+.labelRequired {
+ color: var(--mantine-color-red-filled);
+}
+
+.inputError {
+ background-color: var(--mantine-color-red-light);
+}
+```
+
+## Components CSS variables
+
+You can now customize components [CSS variables](https://mantine.dev/styles/styles-api) to change component styles based on its props.
+For example, it can be used to add new [sizes](https://mantine.dev/styles/variants-sizes):
+
+```tsx
+import { Button, rem, Group, MantineProvider, createTheme } from '@mantine/core';
+
+const theme = createTheme({
+ components: {
+ Button: Button.extend({
+ vars: (theme, props) => {
+ if (props.size === 'xxl') {
+ return {
+ root: {
+ '--button-height': rem(60),
+ '--button-padding-x': rem(30),
+ '--button-fz': rem(24),
+ },
+ };
+ }
+
+ if (props.size === 'xxs') {
+ return {
+ root: {
+ '--button-height': rem(24),
+ '--button-padding-x': rem(10),
+ '--button-fz': rem(10),
+ },
+ };
+ }
+
+ return { root: {} };
+ },
+ }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+ XXL Button
+ XXS Button
+
+
+ );
+}
+```
+
+## New variants system
+
+All components now have `data-variant` attribute on the root element, even if the component does not have any predefined variants.
+You can use it to apply styles to all components of the same type on [theme](https://mantine.dev/theming/theme-object):
+
+```tsx
+import { Input, MantineProvider, createTheme } from '@mantine/core';
+import classes from './Demo.module.css';
+
+// It is better to add new variants in theme.components
+// This way you will be able to use them in anywhere in the app
+const theme = createTheme({
+ components: {
+ Input: Input.extend({ classNames: classes }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+
+ );
+}
+```
+
+```scss
+.input {
+ &[data-variant='underline'] {
+ border-bottom: rem(2px) solid;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-3);
+ }
+
+ &:focus {
+ border-color: var(--mantine-color-blue-filled);
+ }
+ }
+}
+```
+
+## New sizes system
+
+There are multiple ways to customize component sizes:
+
+- With `data-size` attribute
+- With component [CSS variables](https://mantine.dev/styles/styles-api)
+- With [static CSS variables](https://mantine.dev/styles/variants-sizes#sizes-with-static-css-variables)
+
+Example of customizing [Button](https://mantine.dev/core/button) size with `data-size` attribute:
+
+```tsx
+import { Input, createTheme, MantineProvider } from '@mantine/core';
+import classes from './Demo.module.css';
+
+const theme = createTheme({
+ components: {
+ Input: Input.extend({ classNames: classes }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+
+ );
+}
+```
+
+```scss
+.wrapper {
+ &[data-size='xxl'] {
+ & .input {
+ padding-left: rem(28px);
+ padding-right: rem(28px);
+ height: rem(68px);
+ font-size: rem(28px);
+ }
+ }
+
+ &[data-size='xxs'] {
+ & .input {
+ padding-left: rem(10px);
+ padding-right: rem(10px);
+ height: rem(28px);
+ font-size: rem(10px);
+ }
+ }
+}
+```
+
+## theme.variantColorResolver
+
+[Button](https://mantine.dev/core/button), [Badge](https://mantine.dev/core/badge), [ActionIcon](https://mantine.dev/core/action-icon), [ThemeIcon](https://mantine.dev/core/theme-icon) and other
+components now support custom variants with [variantColorResolver](https://mantine.dev/theming/colors#colors-variant-resolver)
+– it supports both changing colors of existing variants and adding new variants.
+
+```tsx
+import {
+ Button,
+ Group,
+ MantineProvider,
+ defaultVariantColorsResolver,
+ VariantColorsResolver,
+ parseThemeColor,
+ rem,
+ rgba,
+ darken,
+} from '@mantine/core';
+
+const variantColorResolver: VariantColorsResolver = (input) => {
+ const defaultResolvedColors = defaultVariantColorsResolver(input);
+ const parsedColor = parseThemeColor({
+ color: input.color || input.theme.primaryColor,
+ theme: input.theme,
+ });
+
+ // Override some properties for variant
+ if (parsedColor.isThemeColor && parsedColor.color === 'lime' && input.variant === 'filled') {
+ return { ...defaultResolvedColors, color: 'var(--mantine-color-black)' };
+ }
+
+ // Completely override variant
+ if (input.variant === 'light') {
+ return {
+ background: rgba(parsedColor.value, 0.1),
+ hover: rgba(parsedColor.value, 0.15),
+ border: `${rem(1)} solid ${parsedColor.value}`,
+ color: darken(parsedColor.value, 0.1),
+ };
+ }
+
+ // Add new variants support
+ if (input.variant === 'danger') {
+ return {
+ background: 'var(--mantine-color-red-9)',
+ hover: 'var(--mantine-color-red-8)',
+ color: 'var(--mantine-color-white)',
+ border: 'none',
+ };
+ }
+
+ return defaultResolvedColors;
+};
+
+function Demo() {
+ return (
+
+
+
+ Lime filled button
+
+
+
+ Orange light button
+
+
+ Danger button
+
+
+ );
+}
+```
+
+## rem units scaling
+
+It is now possible to scale [rem](https://mantine.dev/styles/rem#rem-units-scaling) units. It is useful when you want to change
+font-size of `html`/`:root` element and preserve Mantine components sizes. For example, if you would like to set `html` font-size to `10px` and scale Mantine components accordingly, you need
+to set `scale` to `1 / (10 / 16)` (16 – default font-size) = `1 / 0.625` = `1.6`:
+
+```css
+:root {
+ font-size: 10px;
+}
+```
+
+```tsx
+import { MantineProvider, createTheme } from '@mantine/core';
+
+const theme = createTheme({
+ scale: 1.6,
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## color prop improvements
+
+All components that support `color` prop now support the following color values:
+
+- Key of `theme.colors`, for example, `blue`
+- `theme.colors` index reference, for example, `blue.5`
+- Any valid CSS color value, for example, `#fff`, `rgba(0, 0, 0, 0.5)`, `hsl(0, 0%, 100%)`
+
+```tsx
+import { Group, Button, Text } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+ Filled variant
+
+
+ Theme color
+ Hex color
+
+
+
+ Light variant
+
+
+
+ Theme color
+
+
+ Hex color
+
+
+
+
+ Outline variant
+
+
+
+ Theme color
+
+
+ Hex color
+
+
+ >
+ );
+}
+```
+
+## Components classes
+
+Classes of each component are now available in `Component.classes` object. For example, you can
+find [Button](https://mantine.dev/core/button) classes in `Button.classes`.
+
+You can use these classes to create components with the same styles as Mantine components:
+
+```tsx
+import { Button } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+```
+
+## Theme object changes
+
+- `theme.lineHeight` is now `theme.lineHeights` – it is now possible to specify multiple line heights. `theme.lineHeights` values are used in the [Text](https://mantine.dev/core/text) component.
+- `theme.colorScheme` is no longer available, use [useMantineColorScheme](https://mantine.dev/theming/color-schemes#use-mantine-color-scheme-hook) to get color scheme value
+- `theme.dir` is no longer needed, direction is now managed by [DirectionProvider](https://mantine.dev/styles/rtl)
+- `theme.loader` was removed, you can now configure default loader with [default props](https://mantine.dev/theming/default-props) of [Loader](https://mantine.dev/core/loader) component
+- `theme.transitionTimingFunction` was removed
+- `theme.focusRingStyles` was replaced with `theme.focusClassName`
+- `theme.activeStyles` was replaced with `theme.activeClassName`
+- `theme.globalStyles` was removed
+- `theme.fn` functions were removed, some of the functions are available as [standalone utilities](https://mantine.dev/styles/color-functions)
+- New [theme.scale](https://mantine.dev/styles/rem#rem-units-scaling) property controls rem units scaling
+- New `theme.fontSmoothing` property determines whether font smoothing styles should be applied to the body element
+- New [theme.variantColorResolver](https://mantine.dev/theming/colors#colors-variant-resolver) property allows customizing component colors per variant
+
+## Colors generator
+
+New [@mantine/colors-generator](https://mantine.dev/theming/colors#colors-generation) package is now available to generate
+color palettes based on single color value. It is also available as [online tool](https://mantine.dev/colors-generator/).
+Note that it is usually better to generate colors in advance to avoid contrast issues.
+
+```tsx
+import { MantineProvider } from '@mantine/core';
+import { generateColors } from '@mantine/colors-generator';
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## New setup for RTL
+
+`@mantine/core` package now exports [DirectionProvider](https://mantine.dev/styles/rtl) component, which should be used to configure the direction of the application.
+`useDirection` hook can be used to toggle direction. All components now include RTL styles by default, it is no
+longer required to set up additional plugins. See [RTL documentation](https://mantine.dev/styles/rtl) to learn more.
+
+## React 18+ only
+
+Starting from version 7.0 Mantine no longer supports older React versions. The minimum supported version is now React 18.
+It is required because Mantine components now use [useId](https://react.dev/reference/react/useId) and [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
+hooks, which are available only in React 18.
+
+## left and right section
+
+Components that previously had `rightSection` and `icon` props, now use `leftSection` instead of `icon`.
+Example of [Button](https://mantine.dev/core/button) sections:
+
+```tsx
+import { Button } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ Label
+
+ );
+}
+```
+
+## NumberInput changes
+
+[NumberInput](https://mantine.dev/core/number-input) was migrated to [react-number-format](https://s-yadav.github.io/react-number-format/).
+It now supports more features and has improvements in cursor position management.
+Due to migration, some of the props were renamed – follow the [documentation](https://mantine.dev/core/number-input) to learn about the changes.
+
+## Loader changes
+
+[Loader](https://mantine.dev/core/loader) component is now built with CSS only. This change improves performance and reduces
+HTML output of the component.
+
+[Theme](https://mantine.dev/theming/theme-object) object no longer supports `theme.loader` property –
+it is also now possible to add custom loaders on [theme](https://mantine.dev/theming/theme-object) with [default props](https://mantine.dev/theming/default-props).
+Specified [Loader](https://mantine.dev/core/loader) will be used in all components that use it under the hood ([LoadingOverlay](https://mantine.dev/core/loading-overlay), [Button](https://mantine.dev/core/button), [ActionIcon](https://mantine.dev/core/action-icon), [Stepper](https://mantine.dev/core/stepper), etc.).
+
+## Progress changes
+
+[Progress](https://mantine.dev/core/progress) component now supports compound components pattern.
+Advanced features that were previously implemented in [Progress](https://mantine.dev/core/progress) are now supposed to be implemented with
+compound components instead.
+
+```tsx
+import { Progress } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ Documents
+
+
+ Photos
+
+
+ Other
+
+
+ );
+}
+```
+
+## Table changes
+
+[Table](https://mantine.dev/core/table) component changes:
+
+- [Styles API](https://mantine.dev/styles/styles-api) support
+- It is now required to use compound components instead of elements: `Table.Tr`, `Table.Td`, etc.
+- It is now easier to override styles – all styles are added with classes instead of deeply nested selectors on root element
+- New props: `borderColor`, `withRowBorders`, `stripedColor`, `highlightOnHoverColor`
+- `withBorder` prop was renamed to `withTableBorder`
+- `fontSize` prop was removed, use `fz` [style prop](https://mantine.dev/styles/style-props) instead
+- New `Table.ScrollContainer` component to create scrollable table
+
+```tsx
+import { Table } from '@mantine/core';
+
+function Demo() {
+ const rows = elements.map((element) => (
+
+ {element.position}
+ {element.name}
+ {element.symbol}
+ {element.mass}
+
+ ));
+
+ return (
+
+
+
+ Element position
+ Element name
+ Symbol
+ Atomic mass
+
+
+ {rows}
+
+ );
+}
+```
+
+## Group changes
+
+[Group](https://mantine.dev/core/group) component changes:
+
+- `position` prop was renamed to `justify` – it now supports all `justify-content` values
+- `noWrap` prop was replaced with `wrap="nowrap"`, `wrap` prop now supports all `flex-wrap` values
+- `spacing` prop was replaced with `gap`
+- `Group` now supports new `preventGrowOverflow` prop which allows customizing how group items will behave when they grow larger than their dedicated space
+
+## Tabs changes
+
+- Styles API selector `tabsList` renamed to `list`
+- `TabProps` type was renamed to `TabsTabProps`
+- `onTabChange` prop was renamed to `onChange`
+- `Tabs.List` `position` prop was renamed to `justify`, it now supports all `justify-content` values
+
+## Button changes
+
+- `compact` prop was removed, use `size="compact-XXX"` instead
+- `leftIcon` and `rightIcon` props were renamed to `leftSection` and `rightSection`
+- `uppercase` prop was removed, use `tt` [style prop](https://mantine.dev/styles/style-props) instead
+- `loaderPosition` prop was removed, [Loader](https://mantine.dev/core/loader) is now always rendered in the center to prevent layout shifts
+- Styles API selectors were changed, see [Button Styles API](https://mantine.dev/core/button?t=styles-api) documentation for more details
+
+## AppShell changes
+
+[AppShell](https://mantine.dev/core/app-shell) component was completely rewritten, it now supports more features:
+
+- `AppShell` now uses compound components pattern: `Navbar`, `Aside`, `Header` and `Footer` are no longer exported from `@mantine/core` package. Instead, use `AppShell.Navbar`, `AppShell.Aside`, etc.
+- `AppShell` now supports animations when navbar/aside are opened/closed
+- Navbar/aside can now be collapsed on desktop – state is handled separately for mobile and desktop
+- Header/footer can now be collapsed the same way as navbar/aside. For example, the header can be collapsed based on scroll position or direction.
+- `AppShell` no longer supports `fixed` prop – all components have `position: fixed` styles, static positioning is no longer supported
+- The documentation was updated, it now includes [10+ examples on a separate page](https://mantine.dev/app-shell?e=BasicAppShell)
+
+## SimpleGrid changes
+
+[SimpleGrid](https://mantine.dev/core/simple-grid) now uses object format to define grid breakpoints and spacing,
+it works the same way as [style props](https://mantine.dev/styles/style-props).
+
+```tsx
+import { SimpleGrid } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ 1
+ 2
+ 3
+ 4
+ 5
+
+ );
+}
+```
+
+## Grid changes
+
+[Grid](https://mantine.dev/core/grid) now uses object format in `gutter`, `offset`, `span` and order props,
+all props now work the same way as [style props](https://mantine.dev/styles/style-props).
+
+```tsx
+import { Grid } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ 1
+ 2
+ 3
+ 4
+
+ );
+}
+```
+
+## Image changes
+
+[Image](https://mantine.dev/core/image) component changes:
+
+- `Image` component no longer includes `figure` and other associated elements
+- `caption` prop is no longer available
+- `width` and `height` props are replaced with `w` and `h` [style props](https://mantine.dev/styles/style-props)
+- Placeholder functionality was replaced with fallback image
+
+```tsx
+import { Image } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+## Spotlight changes
+
+[Spotlight](https://mantine.dev/others/spotlight) changes:
+
+- The logic is no longer based on React context
+- `SpotlightProvider` was renamed to `Spotlight`
+- `useSpotlight` hook was removed, use `spotlight` object instead
+- `actions` prop now uses a different data format
+- It is now possible to have multiple spotlights in the same app
+- `Spotlight` component now uses compound components pattern for advanced customization
+
+```tsx
+import { useState } from 'react';
+import { Spotlight, spotlight } from '@mantine/spotlight';
+import { Button } from '@mantine/core';
+import { IconSearch } from '@tabler/icons-react';
+
+const data = ['Home', 'About us', 'Contacts', 'Blog', 'Careers', 'Terms of service'];
+
+function Demo() {
+ const [query, setQuery] = useState('');
+
+ const items = data
+ .filter((item) => item.toLowerCase().includes(query.toLowerCase().trim()))
+ .map((item) => );
+
+ return (
+ <>
+ Open spotlight
+
+
+ } />
+
+ {items.length > 0 ? items : Nothing found... }
+
+
+ >
+ );
+}
+```
+
+## Carousel changes
+
+[Carousel](https://mantine.dev/others/carousel) now uses object format for responsive values in
+`slideSize` and `slideGap` props instead of `breakpoints` prop:
+
+```tsx
+import { Carousel } from '@mantine/carousel';
+
+function Demo() {
+ return (
+
+ 1
+ 2
+ 3
+ {/* ...other slides */}
+
+ );
+}
+```
+
+## @mantine/prism deprecation
+
+`@mantine/prism` package was deprecated in favor of `@mantine/code-highlight` package. [The new package](https://mantine.dev/others/code-highlight) uses [highlight.js](https://highlightjs.org/) instead of [Prism](https://prismjs.com/).
+
+```tsx
+import { CodeHighlightTabs } from '@mantine/code-highlight';
+import { TypeScriptIcon, CssIcon } from '@mantine/ds';
+
+const tsxCode = `
+function Button() {
+ return Click me ;
+}
+`;
+
+const cssCode = `
+.button {
+ background-color: transparent;
+ color: var(--mantine-color-blue-9);
+}
+`;
+
+function getFileIcon(fileName: string) {
+ if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
+ return ;
+ }
+
+ if (fileName.endsWith('.css')) {
+ return ;
+ }
+
+ return null;
+}
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+## Fieldset component
+
+New [Fieldset](https://mantine.dev/core/fieldset) component:
+
+```tsx
+import { Fieldset } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+
+
+ );
+}
+```
+
+## Combobox component
+
+The new [Combobox](https://mantine.dev/core/combobox) component allows building custom select, autocomplete, tags input, multiselect and other
+similar components. It is used as a base for updated [Autocomplete](https://mantine.dev/core/autocomplete), [Select](https://mantine.dev/core/select),
+[TagsInput](https://mantine.dev/core/tags-input) and [MultiSelect](https://mantine.dev/core/multi-select) components.
+
+[Combobox](https://mantine.dev/core/combobox) is very flexible and allows you to have full control over the component rendering and logic.
+Advanced features that were previously implemented in [Autocomplete](https://mantine.dev/core/autocomplete), [Select](https://mantine.dev/core/select)
+and [MultiSelect](https://mantine.dev/core/multi-select) are now supposed to be implemented with [Combobox](https://mantine.dev/core/combobox) instead.
+
+You can find 50+ `Combobox` examples on [this page](https://mantine.dev/combobox).
+
+```tsx
+import { useState } from 'react';
+import { Input, InputBase, Combobox, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+function Demo() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
+```
+
+## TagsInput component
+
+New [TagsInput](https://mantine.dev/core/tags-input) component based on [Combobox](https://mantine.dev/core/combobox) component.
+The component is similar to [MultiSelect](https://mantine.dev/core/multi-select) but allows entering custom values.
+
+```tsx
+import { TagsInput } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+## withErrorStyles prop
+
+All inputs now support `withErrorStyles` prop, which allows removing error styles from the input.
+It can be used to apply custom error styles without override, or use other techniques to
+indicate error state. For example, it can be used to render an icon in the right section:
+
+```tsx
+import { TextInput, rem } from '@mantine/core';
+import { IconExclamationCircle } from '@tabler/icons-react';
+
+function Demo() {
+ return (
+ <>
+
+
+
+
+ }
+ />
+ >
+ );
+}
+```
+
+## hiddenFrom and visibleFrom props
+
+All Mantine components now support `hiddenFrom` and `visibleFrom` props.
+These props accept breakpoint (`xs`, `sm`, `md`, `lg`, `xl`) and hide the component when
+viewport width is less than or greater than the specified breakpoint:
+
+```tsx
+import { Button, Group } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ Hidden from sm
+
+
+ Visible from sm
+
+
+ Visible from md
+
+
+ );
+}
+```
+
+## Other changes
+
+- New [VisuallyHidden](https://mantine.dev/core/visually-hidden) component
+- New [ActionIcon.Group](https://mantine.dev/core/action-icon#actionicongroup) component
+- [DatesProvider](https://mantine.dev/dates/dates-provider) now supports setting timezone
+- All transitions are now disabled during color scheme change
+- `theme.respectReducedMotion` is now set to `false` by default – it caused a lot of confusion for users who did not know about it
+- [Notifications system](https://mantine.dev/others/notifications) now exports `notifications.updateState` function to update notifications state with a given callback
+- [Blockquote](https://mantine.dev/core/blockquote) component has new design
+- [Breadcrumbs](https://mantine.dev/core/breadcrumbs) component now supports `separatorMargin` prop
+- [Tooltip](https://mantine.dev/core/tooltip) component now supports `mainAxis` and `crossAxis` offset configuration
+- [Slider and RangeSlider](https://mantine.dev/core/slider) components `radius` prop now affects thumb as well as track
+- [NativeSelect](https://mantine.dev/core/native-select/) component now supports setting options as `children` and options groups
+- [Anchor](https://mantine.dev/core/anchor) component now supports `underline` prop which lets you configure how link will be underlined: `hover` (default), `always` or `never`
+- [Affix](https://mantine.dev/core/affix) component no longer supports `target` prop, use `portalProps` instead
+- [Container](https://mantine.dev/core/container) component no longer supports `sizes` prop, use [CSS variables](https://mantine.dev/styles/styles-api) to customize sizes on [theme](https://mantine.dev/theming/theme-object) instead
+- `useComponentDefaultProps` hook was renamed to [useProps](https://mantine.dev/theming/default-props#useprops-hook)
+- `withinPortal` prop is now true by default in all overlay components ([Popover](https://mantine.dev/core/popover), [HoverCard](https://mantine.dev/core/hover-card), [Tooltip](https://mantine.dev/core/tooltip), etc.)
+- `AlphaSlider` and `HueSlider` components are no longer available, they can be used only as a part of [ColorPicker](https://mantine.dev/core/color-picker)
+- [Text](https://mantine.dev/core/text) default root element is now `
`
+- [Title](https://mantine.dev/core/title) no longer supports all [Text](https://mantine.dev/core/text) props, only [style props](https://mantine.dev/styles/style-props) are available
+- `MediaQuery` component was removed – use [CSS modules](https://mantine.dev/styles/css-modules) to apply responsive styles
+- [Highlight](https://mantine.dev/core/highlight) component prop `highlightColor` was renamed to `color`
+- [Tooltip and Tooltip.Floating](https://mantine.dev/core/tooltip) components no longer support `width` prop, use `w` [style prop](https://mantine.dev/styles/style-props) instead
+- [Stack](https://mantine.dev/core/stack) component `spacing` prop was renamed to `gap`
+- [Input](https://mantine.dev/core/input) and other `Input` based components `icon` prop was renamed to `leftSection`
+- [Loader](https://mantine.dev/core/loader) component `variant` prop was renamed to `type`
+- `@mantine/core` package no longer depends on [Radix UI ScrollArea](https://www.radix-ui.com/docs/primitives/components/scroll-area#scroll-area), [ScrollArea](https://mantine.dev/core/scroll-area) component is now a native Mantine component – it reduces bundle size, allows setting CSP for styles and improves performance (all styles are now applied with classes instead of inline `` tags)
+- [Overlay](https://mantine.dev/core/overlay) `opacity` prop was renamed to `backgroundOpacity` to avoid collision with `opacity` [style prop](https://mantine.dev/styles/style-props)
+- [Badge](https://mantine.dev/core/badge) Styles API selectors were changed, see [Badge Styles API](https://mantine.dev/core/badge?t=styles-api) documentation for more details
+- [Slider](https://mantine.dev/core/slider) Styles API selectors were changed, see [Slider Styles API](https://mantine.dev/core/slider?t=styles-api) documentation for more details
+- [Text](https://mantine.dev/core/text) component no longer supports `underline`, `color`, `strikethrough`, `italic`, `transform`, `align` and `weight` prop – use [style props](https://mantine.dev/styles/style-props) instead
+- [Portal](https://mantine.dev/core/portal) component `innerRef` prop was renamed to `ref`
+- [ScrollArea](https://mantine.dev/core/scroll-area) now supports `x` and `y` values in `offsetScrollbars` prop
+- `TransferList` component is no longer available as a part of `@mantine/core` package, instead you can implement it with [Combobox](https://mantine.dev/core/combobox) component ([example](https://mantine.dev/combobox?e=TransferList))
+- [Chip](https://mantine.dev/core/chip) component now supports custom check icon
+- [PasswordInput](https://mantine.dev/core/password-input) no longer supports `visibilityToggleLabel` and `toggleTabIndex` props, use `visibilityToggleButtonProps` prop instead
+- [Stepper](https://mantine.dev/core/stepper) now longer supports `breakpoint` prop, you can apply responsive styles with Styles API
+- [ColorInput](https://mantine.dev/core/color-input) no longer supports `dropdownZIndex`, `transitionProps`, `withinPortal`, `portalProps` and `shadow` props, you can now pass these props with `popoverProps` prop
+- [LoadingOverlay](https://mantine.dev/core/loading-overlay) props are now grouped by the component they are passed down to: `overlayProps`, `loaderProps` and `transitionProps` now replace props that were previously passed to `LoadingOverlay` directly
+- [Dropzone](https://mantine.dev/others/dropzone) component no longer supports `padding` prop, use `p` style prop instead
+- [Dialog](https://mantine.dev/core/dialog) component now supports all [Paper](https://mantine.dev/core/paper) and [Affix](https://mantine.dev/core/affix) props, `transitionDuration`, `transition` and other transition related props were replaced with `transitionProps`
+- [Checkbox](https://mantine.dev/core/checkbox), [Radio](https://mantine.dev/core/radio), [Chip](https://mantine.dev/core/chip) and [Switch](https://mantine.dev/core/switch) components now support `rootRef` prop which allows using them with [Tooltip](https://mantine.dev/core/tooltip) and other similar components
diff --git a/changelog/README.md b/changelog/README.md
new file mode 100644
index 00000000000..1f9335bbb3b
--- /dev/null
+++ b/changelog/README.md
@@ -0,0 +1,4 @@
+# Changelogs
+
+This folder contains changelog files that are published in the [releases](https://github.com/mantinedev/mantine/releases). If you want to see changelog with demos
+visit [mantine.dev website](https://mantine.dev/).
diff --git a/configuration/jest/global-setup.js b/configuration/jest/global-setup.js
new file mode 100644
index 00000000000..c8714edf7cb
--- /dev/null
+++ b/configuration/jest/global-setup.js
@@ -0,0 +1,3 @@
+module.exports = async () => {
+ process.env.TZ = "America/New_York"; // -05:00 (STD) or -04:00 (DST)
+};
diff --git a/configuration/rollup/create-package-config.ts b/configuration/rollup/create-package-config.ts
index 51713447d68..a54eee00619 100644
--- a/configuration/rollup/create-package-config.ts
+++ b/configuration/rollup/create-package-config.ts
@@ -4,12 +4,15 @@ import { RollupOptions, OutputOptions, ModuleFormat } from 'rollup';
import commonjs from '@rollup/plugin-commonjs';
import nodeExternals from 'rollup-plugin-node-externals';
import { nodeResolve } from '@rollup/plugin-node-resolve';
-import esbuild from 'rollup-plugin-esbuild';
import json from '@rollup/plugin-json';
import alias, { Alias } from '@rollup/plugin-alias';
import replace from '@rollup/plugin-replace';
import visualizer from 'rollup-plugin-visualizer';
+import postcss from 'rollup-plugin-postcss';
+import banner from 'rollup-plugin-banner2';
+import esbuild from 'rollup-plugin-esbuild';
import { getPackagesList } from '../../scripts/utils/get-packages-list';
+import { generateScopedName } from './hash-css-name';
interface PkgConfigInput {
basePath: string;
@@ -38,13 +41,23 @@ export default async function createPackageConfig(config: PkgConfigInput): Promi
nodeExternals(),
nodeResolve({ extensions: ['.ts', '.tsx', '.js', '.jsx'] }),
esbuild({
- minify: config.format === 'umd',
sourceMap: false,
tsconfig: path.resolve(process.cwd(), 'tsconfig.json'),
}),
json(),
alias({ entries: aliasEntries }),
replace({ preventAssignment: true }),
+ postcss({
+ extract: true,
+ modules: { generateScopedName },
+ }),
+ banner((chunk) => {
+ if (chunk.fileName === 'index.js') {
+ return "'use client';\n";
+ }
+
+ return undefined;
+ }),
];
let externals;
@@ -58,13 +71,12 @@ export default async function createPackageConfig(config: PkgConfigInput): Promi
];
} else {
externals = [
- '@emotion/server/create-instance',
'dayjs/locale/ru',
+ 'dayjs/plugin/customParseFormat',
+ 'dayjs/plugin/utc',
+ 'dayjs/plugin/timezone',
'klona/full',
'highlight.js/lib/languages/typescript',
- '@emotion/cache',
- '@emotion/utils',
- '@emotion/serialize',
'prism-react-renderer/themes/duotoneDark',
'prism-react-renderer/themes/duotoneLight',
...(config?.externals || []),
diff --git a/configuration/rollup/hash-css-name.ts b/configuration/rollup/hash-css-name.ts
new file mode 100644
index 00000000000..291006a545c
--- /dev/null
+++ b/configuration/rollup/hash-css-name.ts
@@ -0,0 +1,17 @@
+import path from 'path';
+
+/* eslint-disable no-bitwise */
+export function hashCode(input: string) {
+ let hash = 0;
+ for (let i = 0; i < input.length; i += 1) {
+ const chr = input.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0;
+ }
+ return (hash + 2147483648).toString(16);
+}
+
+export function generateScopedName(selector: string, fileName: string) {
+ const componentName = path.basename(fileName).replace('.module', '').replace('.css', '');
+ return `m-${hashCode(`${componentName}-${selector}`)}`;
+}
diff --git a/configuration/storybook/main.js b/configuration/storybook/main.js
deleted file mode 100644
index 54d58db157d..00000000000
--- a/configuration/storybook/main.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable no-param-reassign */
-const path = require('path');
-const { argv } = require('yargs');
-const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin').default;
-
-const getPath = (storyPath) => path.resolve(__dirname, storyPath).replace(/\\/g, '/');
-
-const storiesPath = !argv._[0]
- ? [getPath('../../src/**/*.story.@(ts|tsx)')]
- : [
- getPath(`../../src/mantine-*/**/${argv._[0]}.story.@(ts|tsx)`),
- getPath(`../../src/mantine-*/**/${argv._[0]}.demos.story.@(ts|tsx)`),
- ];
-
-module.exports = {
- core: {
- builder: 'webpack5',
- },
- stories: [...storiesPath],
- addons: ['storybook-addon-turbo-build', 'storybook-dark-mode'],
- webpackFinal: async (config) => {
- config.resolve = {
- ...config.resolve,
- plugins: [
- ...(config.resolve.plugins || []),
- new TsconfigPathsPlugin({
- extensions: ['.ts', '.tsx', '.js'],
- configFile: path.join(__dirname, '../../tsconfig.json'),
- }),
- ],
- };
-
- // Turn off docgen plugin as it breaks bundle with displayName
- config.plugins.pop();
-
- return config;
- },
-};
diff --git a/configuration/storybook/preview.tsx b/configuration/storybook/preview.tsx
deleted file mode 100644
index dd69aff6ac5..00000000000
--- a/configuration/storybook/preview.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useState } from 'react';
-import { useDarkMode } from 'storybook-dark-mode';
-import {
- MantineProvider,
- ColorSchemeProvider,
- Affix,
- ActionIcon,
- createEmotionCache,
- rem,
-} from '@mantine/core';
-import { useHotkeys } from '@mantine/hooks';
-import rtlPlugin from 'stylis-plugin-rtl';
-
-export const parameters = { layout: 'fullscreen' };
-
-const rtlCache = createEmotionCache({
- key: 'mantine-rtl',
- prepend: true,
- stylisPlugins: [rtlPlugin],
-});
-
-function ThemeWrapper(props: any) {
- const [rtl, setRtl] = useState(false);
- const toggleRtl = () => setRtl((r) => !r);
- useHotkeys([['mod + L', toggleRtl]]);
-
- return (
- {}}>
-
-
-
- {rtl ? 'RTL' : 'LTR'}
-
-
- {props.children}
-
-
- );
-}
-
-export const decorators = [(renderStory: any) => {renderStory()} ];
diff --git a/configuration/types/emotion-theme.d.ts b/configuration/types/emotion-theme.d.ts
deleted file mode 100644
index 7a03f8f3aa7..00000000000
--- a/configuration/types/emotion-theme.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import '@emotion/react';
-
-declare module '@emotion/react' {
- export interface Theme extends Record {}
-}
diff --git a/configuration/types/webpack-overrides.d.ts b/configuration/types/webpack-overrides.d.ts
index 199b7a2b5ba..4e02d2957ac 100644
--- a/configuration/types/webpack-overrides.d.ts
+++ b/configuration/types/webpack-overrides.d.ts
@@ -10,3 +10,5 @@ declare module '*.woff';
declare module '*.woff2';
declare module '*.mdx';
declare module '*.webp';
+declare module '*.css';
+declare module 'is-directory';
diff --git a/docs/.env b/docs/.env
new file mode 100644
index 00000000000..c5d369bc993
--- /dev/null
+++ b/docs/.env
@@ -0,0 +1 @@
+SITE_URL="https://mantine.dev/"
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..c87c9b392c0
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 00000000000..cc601b01feb
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+mantine.dev
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000000..2fffbf2311a
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1 @@
+# Mantine docs
diff --git a/docs/app-shell-examples/AppShellPage/AppShellPage.tsx b/docs/app-shell-examples/AppShellPage/AppShellPage.tsx
new file mode 100644
index 00000000000..91f4826b515
--- /dev/null
+++ b/docs/app-shell-examples/AppShellPage/AppShellPage.tsx
@@ -0,0 +1,43 @@
+import React, { useEffect } from 'react';
+import { useRouter } from 'next/router';
+import { CodeHighlightTabs } from '@mantine/code-highlight';
+import { getCodeFileIcon } from '@mantine/ds';
+import { PageHead } from '@/components/PageHead';
+import { ExamplesDrawer } from './ExamplesDrawer/ExamplesDrawer';
+import { APP_SHELL_EXAMPLES_COMPONENTS } from '../examples';
+
+export function AppShellPage() {
+ const router = useRouter();
+ const exampleId = router.query.e as string;
+ const state = router.query.s as string;
+
+ useEffect(() => {
+ if (exampleId && !(exampleId in APP_SHELL_EXAMPLES_COMPONENTS)) {
+ router.replace('/app-shell?e=BasicAppShell');
+ }
+ }, [exampleId]);
+
+ if (!(exampleId in APP_SHELL_EXAMPLES_COMPONENTS)) {
+ return ;
+ }
+ const data =
+ APP_SHELL_EXAMPLES_COMPONENTS[exampleId as keyof typeof APP_SHELL_EXAMPLES_COMPONENTS];
+
+ return (
+ <>
+
+
+
+ {state === 'code' ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.module.css b/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.module.css
new file mode 100644
index 00000000000..c3c7b1b398b
--- /dev/null
+++ b/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.module.css
@@ -0,0 +1,47 @@
+.control {
+ display: block;
+ width: 100%;
+ padding: var(--mantine-spacing-sm) var(--mantine-spacing-lg);
+ border-radius: var(--mantine-radius-md);
+
+ @mixin hover {
+ &:not([data-active]) {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ }
+ }
+
+ &[data-active] {
+ @mixin light {
+ color: var(--mantine-color-blue-8);
+ background-color: var(--mantine-color-blue-0);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-blue-1);
+ background-color: rgba(24, 100, 171, 0.45);
+ }
+ }
+}
+
+.name {
+ display: block;
+ font-weight: 500;
+ color: inherit;
+}
+
+.description {
+ display: block;
+ opacity: 0.6;
+ font-size: var(--mantine-font-size-sm);
+ color: inherit;
+}
+
+.menuButton {
+ @mixin not-rtl {
+ padding-right: rem(5px);
+ }
+
+ @mixin rtl {
+ padding-left: rem(5px);
+ }
+}
diff --git a/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.tsx b/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.tsx
new file mode 100644
index 00000000000..90c2eb37b64
--- /dev/null
+++ b/docs/app-shell-examples/AppShellPage/ExamplesDrawer/ExamplesDrawer.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useDisclosure } from '@mantine/hooks';
+import { Button, Affix, Drawer, Group, rem, UnstyledButton, Text, ScrollArea } from '@mantine/core';
+import { IconMenu2, IconArrowLeft, IconCode, IconLayoutBoard } from '@tabler/icons-react';
+import { APP_SHELL_EXAMPLES_DATA } from '../../app-shell-examples-data';
+import classes from './ExamplesDrawer.module.css';
+
+export function ExamplesDrawer() {
+ const [opened, { open, close }] = useDisclosure();
+ const router = useRouter();
+ const currentExample = router.query.e as string;
+ const currentState = router.query.s === 'code' ? 'code' : 'demo';
+ const StateIcon = currentState === 'code' ? IconLayoutBoard : IconCode;
+
+ const examples = APP_SHELL_EXAMPLES_DATA.map((example) => (
+
+
+ {example.name}
+
+
+ {example.description}
+
+
+ ));
+
+ return (
+ <>
+
+
+
+ }
+ style={{ boxShadow: 'var(--mantine-shadow-sm)' }}
+ radius="xl"
+ >
+ Back to documentation
+
+
+ }
+ style={{ boxShadow: 'var(--mantine-shadow-sm)' }}
+ radius="xl"
+ >
+ View {currentState === 'code' ? 'demo' : 'code'}
+
+
+ }
+ w="var(--button-height)"
+ style={{ boxShadow: 'var(--mantine-shadow-sm)' }}
+ aria-label="Other examples"
+ className={classes.menuButton}
+ />
+
+
+
+
+
+ {examples}
+
+
+ >
+ );
+}
diff --git a/docs/app-shell-examples/app-shell-examples-data.ts b/docs/app-shell-examples/app-shell-examples-data.ts
new file mode 100644
index 00000000000..2a23a67d23e
--- /dev/null
+++ b/docs/app-shell-examples/app-shell-examples-data.ts
@@ -0,0 +1,68 @@
+import { APP_SHELL_EXAMPLES_COMPONENTS } from './examples';
+
+export interface AppShellExample {
+ /** Demo id, based on it component will render component on the page */
+ id: keyof typeof APP_SHELL_EXAMPLES_COMPONENTS;
+
+ /** Name used in search */
+ name: string;
+
+ /** Short component description, used in search */
+ description: string;
+
+ /** Full component description, displayed on the page */
+ fullDescription?: string;
+}
+
+export const APP_SHELL_EXAMPLES_DATA: AppShellExample[] = [
+ {
+ id: 'BasicAppShell',
+ name: 'Basic app shell',
+ description: 'App shell with Header and Navbar',
+ },
+ {
+ id: 'ResponsiveSizes',
+ name: 'Responsive width and height',
+ description: 'App shell with responsive navbar width and header height',
+ },
+ {
+ id: 'MobileNavbar',
+ name: 'Mobile only navbar',
+ description: 'Navbar visible only on mobile',
+ },
+ {
+ id: 'CollapseDesktop',
+ name: 'Collapsible navbar',
+ description: 'Collapsible navbar both on desktop and mobile',
+ },
+ {
+ id: 'FullLayout',
+ name: 'AppShell with all elements',
+ description: 'Navbar, header, aside and footer used together',
+ },
+ {
+ id: 'AltLayout',
+ name: 'Alt layout',
+ description: 'Navbar and Aside are rendered on top on Header and Footer',
+ },
+ {
+ id: 'NoTransitions',
+ name: 'Without transitions',
+ description: 'Disable all collapse/expand animations',
+ },
+ {
+ id: 'Disabled',
+ name: 'Disabled app shell',
+ description: 'Hide all app shell elements with disabled prop',
+ },
+ {
+ id: 'Headroom',
+ name: 'Usage with use-headroom',
+ description: 'Hide header on scroll down and show on scroll up',
+ },
+ {
+ id: 'NavbarSection',
+ name: 'Navbar with sections',
+ description: 'AppShell.Section component usage',
+ },
+];
diff --git a/docs/app-shell-examples/examples/AltLayout/AltLayout.tsx b/docs/app-shell-examples/examples/AltLayout/AltLayout.tsx
new file mode 100644
index 00000000000..94777a26b4e
--- /dev/null
+++ b/docs/app-shell-examples/examples/AltLayout/AltLayout.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton, Text } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function AltLayout() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Navbar
+
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+ Alt layout – Navbar and Aside are rendered on top on Header and Footer
+
+ Aside
+ Footer
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/AltLayout/code.json b/docs/app-shell-examples/examples/AltLayout/code.json
new file mode 100644
index 00000000000..9cf689596c2
--- /dev/null
+++ b/docs/app-shell-examples/examples/AltLayout/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AltLayout.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton, Text } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function AltLayout() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n \n \n Navbar \n \n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n \n Alt layout – Navbar and Aside are rendered on top on Header and Footer\n \n Aside \n Footer \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/BasicAppShell/BasicAppShell.tsx b/docs/app-shell-examples/examples/BasicAppShell/BasicAppShell.tsx
new file mode 100644
index 00000000000..cabff8c06dd
--- /dev/null
+++ b/docs/app-shell-examples/examples/BasicAppShell/BasicAppShell.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function BasicAppShell() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+ Main
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/BasicAppShell/code.json b/docs/app-shell-examples/examples/BasicAppShell/code.json
new file mode 100644
index 00000000000..77e6a1aebd7
--- /dev/null
+++ b/docs/app-shell-examples/examples/BasicAppShell/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "BasicAppShell.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function BasicAppShell() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n Main \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/CollapseDesktop/CollapseDesktop.tsx b/docs/app-shell-examples/examples/CollapseDesktop/CollapseDesktop.tsx
new file mode 100644
index 00000000000..d8acd788063
--- /dev/null
+++ b/docs/app-shell-examples/examples/CollapseDesktop/CollapseDesktop.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function CollapseDesktop() {
+ const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
+ const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+ Main
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/CollapseDesktop/code.json b/docs/app-shell-examples/examples/CollapseDesktop/code.json
new file mode 100644
index 00000000000..b126573afaa
--- /dev/null
+++ b/docs/app-shell-examples/examples/CollapseDesktop/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "CollapseDesktop.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function CollapseDesktop() {\n const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();\n const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);\n\n return (\n \n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n Main \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/Disabled/Disabled.tsx b/docs/app-shell-examples/examples/Disabled/Disabled.tsx
new file mode 100644
index 00000000000..2e2f3f5a95f
--- /dev/null
+++ b/docs/app-shell-examples/examples/Disabled/Disabled.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton, Button } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function Disabled() {
+ const [opened, { toggle }] = useDisclosure();
+ const [disabled, { toggle: toggleDisabled }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+ Toggle disabled
+
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/Disabled/code.json b/docs/app-shell-examples/examples/Disabled/code.json
new file mode 100644
index 00000000000..b2da32cad6a
--- /dev/null
+++ b/docs/app-shell-examples/examples/Disabled/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "Disabled.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton, Button } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function Disabled() {\n const [opened, { toggle }] = useDisclosure();\n const [disabled, { toggle: toggleDisabled }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n \n Toggle disabled \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/FullLayout/FullLayout.tsx b/docs/app-shell-examples/examples/FullLayout/FullLayout.tsx
new file mode 100644
index 00000000000..45b7e7c0814
--- /dev/null
+++ b/docs/app-shell-examples/examples/FullLayout/FullLayout.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function FullLayout() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+ Aside is hidden on on md breakpoint and cannot be opened when it is collapsed
+
+ Aside
+ Footer
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/FullLayout/code.json b/docs/app-shell-examples/examples/FullLayout/code.json
new file mode 100644
index 00000000000..544f04c484d
--- /dev/null
+++ b/docs/app-shell-examples/examples/FullLayout/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "FullLayout.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function FullLayout() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n \n Aside is hidden on on md breakpoint and cannot be opened when it is collapsed\n \n Aside \n Footer \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/Headroom/Headroom.tsx b/docs/app-shell-examples/examples/Headroom/Headroom.tsx
new file mode 100644
index 00000000000..e6ecebfb5c4
--- /dev/null
+++ b/docs/app-shell-examples/examples/Headroom/Headroom.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { useHeadroom } from '@mantine/hooks';
+import { AppShell, Group, Text, rem } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+const lorem =
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos ullam, ex cum repellat alias ea nemo. Ducimus ex nesciunt hic ad saepe molestiae nobis necessitatibus laboriosam officia, reprehenderit, earum fugiat?';
+
+export function Headroom() {
+ const pinned = useHeadroom({ fixedAt: 120 });
+
+ return (
+
+
+
+
+
+
+
+
+ {Array(40)
+ .fill(0)
+ .map((_, index) => (
+
+ {lorem}
+
+ ))}
+
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/Headroom/code.json b/docs/app-shell-examples/examples/Headroom/code.json
new file mode 100644
index 00000000000..f1375a94bdb
--- /dev/null
+++ b/docs/app-shell-examples/examples/Headroom/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "Headroom.tsx",
+ "language": "tsx",
+ "code": "import { useHeadroom } from '@mantine/hooks';\nimport { AppShell, Group, Text, rem } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nconst lorem =\n 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos ullam, ex cum repellat alias ea nemo. Ducimus ex nesciunt hic ad saepe molestiae nobis necessitatibus laboriosam officia, reprehenderit, earum fugiat?';\n\nexport function Headroom() {\n const pinned = useHeadroom({ fixedAt: 120 });\n\n return (\n \n \n \n \n \n \n\n \n {Array(40)\n .fill(0)\n .map((_, index) => (\n \n {lorem}\n \n ))}\n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.module.css b/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.module.css
new file mode 100644
index 00000000000..63e565780b7
--- /dev/null
+++ b/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.module.css
@@ -0,0 +1,10 @@
+.control {
+ display: block;
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
+ border-radius: var(--mantine-radius-md);
+ font-weight: 500;
+
+ @mixin hover {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ }
+}
diff --git a/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.tsx b/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.tsx
new file mode 100644
index 00000000000..96e7c7af3ce
--- /dev/null
+++ b/docs/app-shell-examples/examples/MobileNavbar/MobileNavbar.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, UnstyledButton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+import classes from './MobileNavbar.module.css';
+
+export function MobileNavbar() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+ Home
+ Blog
+ Contacts
+ Support
+
+
+
+
+
+
+ Home
+ Blog
+ Contacts
+ Support
+
+
+
+ Navbar is only visible on mobile, links that are rendered in the header on desktop are
+ hidden on mobile in header and rendered in navbar instead.
+
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/MobileNavbar/code.json b/docs/app-shell-examples/examples/MobileNavbar/code.json
new file mode 100644
index 00000000000..276189d1c1b
--- /dev/null
+++ b/docs/app-shell-examples/examples/MobileNavbar/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "MobileNavbar.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, UnstyledButton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\nimport classes from './MobileNavbar.module.css';\n\nexport function MobileNavbar() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n Home \n Blog \n Contacts \n Support \n \n \n \n \n\n \n Home \n Blog \n Contacts \n Support \n \n\n \n Navbar is only visible on mobile, links that are rendered in the header on desktop are\n hidden on mobile in header and rendered in navbar instead.\n \n \n );\n}\n"
+ },
+ {
+ "fileName": "MobileNavbar.module.css",
+ "language": "css",
+ "code": ".control {\n display: block;\n padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);\n border-radius: var(--mantine-radius-md);\n font-weight: 500;\n\n @mixin hover {\n background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/NavbarSection/NavbarSection.tsx b/docs/app-shell-examples/examples/NavbarSection/NavbarSection.tsx
new file mode 100644
index 00000000000..6a066bab3a4
--- /dev/null
+++ b/docs/app-shell-examples/examples/NavbarSection/NavbarSection.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton, ScrollArea } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function NavbarSection() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar header
+
+ 60 links in a scrollable section
+ {Array(60)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+ Navbar footer – always at the bottom
+
+ Main
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/NavbarSection/code.json b/docs/app-shell-examples/examples/NavbarSection/code.json
new file mode 100644
index 00000000000..f0bbd779782
--- /dev/null
+++ b/docs/app-shell-examples/examples/NavbarSection/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "NavbarSection.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton, ScrollArea } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function NavbarSection() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar header \n \n 60 links in a scrollable section\n {Array(60)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n Navbar footer – always at the bottom \n \n Main \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/NoTransitions/NoTransitions.tsx b/docs/app-shell-examples/examples/NoTransitions/NoTransitions.tsx
new file mode 100644
index 00000000000..826493e93c8
--- /dev/null
+++ b/docs/app-shell-examples/examples/NoTransitions/NoTransitions.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function NoTransitions() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+ Main
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/NoTransitions/code.json b/docs/app-shell-examples/examples/NoTransitions/code.json
new file mode 100644
index 00000000000..c5ecab8237f
--- /dev/null
+++ b/docs/app-shell-examples/examples/NoTransitions/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "NoTransitions.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function NoTransitions() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n Main \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/ResponsiveSizes/ResponsiveSizes.tsx b/docs/app-shell-examples/examples/ResponsiveSizes/ResponsiveSizes.tsx
new file mode 100644
index 00000000000..eedf4c40788
--- /dev/null
+++ b/docs/app-shell-examples/examples/ResponsiveSizes/ResponsiveSizes.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger, Group, Skeleton } from '@mantine/core';
+import { MantineLogo } from '@mantine/ds';
+
+export function ResponsiveSizes() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+
+
+
+
+
+ Navbar
+ {Array(15)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+ Main
+
+ );
+}
diff --git a/docs/app-shell-examples/examples/ResponsiveSizes/code.json b/docs/app-shell-examples/examples/ResponsiveSizes/code.json
new file mode 100644
index 00000000000..fee49ad6886
--- /dev/null
+++ b/docs/app-shell-examples/examples/ResponsiveSizes/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "ResponsiveSizes.tsx",
+ "language": "tsx",
+ "code": "import { useDisclosure } from '@mantine/hooks';\nimport { AppShell, Burger, Group, Skeleton } from '@mantine/core';\nimport { MantineLogo } from '@mantine/ds';\n\nexport function ResponsiveSizes() {\n const [opened, { toggle }] = useDisclosure();\n\n return (\n \n \n \n \n \n \n \n \n Navbar\n {Array(15)\n .fill(0)\n .map((_, index) => (\n \n ))}\n \n Main \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/app-shell-examples/examples/index.ts b/docs/app-shell-examples/examples/index.ts
new file mode 100644
index 00000000000..7f4b9c1db6c
--- /dev/null
+++ b/docs/app-shell-examples/examples/index.ts
@@ -0,0 +1,75 @@
+import { BasicAppShell } from './BasicAppShell/BasicAppShell';
+import { ResponsiveSizes } from './ResponsiveSizes/ResponsiveSizes';
+import { MobileNavbar } from './MobileNavbar/MobileNavbar';
+import { FullLayout } from './FullLayout/FullLayout';
+import { AltLayout } from './AltLayout/AltLayout';
+import { CollapseDesktop } from './CollapseDesktop/CollapseDesktop';
+import { NoTransitions } from './NoTransitions/NoTransitions';
+import { Disabled } from './Disabled/Disabled';
+import { Headroom } from './Headroom/Headroom';
+import { NavbarSection } from './NavbarSection/NavbarSection';
+
+import BasicAppShellCode from './BasicAppShell/code.json';
+import ResponsiveSizesCode from './ResponsiveSizes/code.json';
+import MobileNavbarCode from './MobileNavbar/code.json';
+import FullLayoutCode from './FullLayout/code.json';
+import AltLayoutCode from './AltLayout/code.json';
+import CollapseDesktopCode from './CollapseDesktop/code.json';
+import NoTransitionsCode from './NoTransitions/code.json';
+import DisabledCode from './Disabled/code.json';
+import HeadroomCode from './Headroom/code.json';
+import NavbarSectionCode from './NavbarSection/code.json';
+
+interface AppShellExampleComponent {
+ component: () => JSX.Element;
+ code: {
+ fileName: string;
+ language: string;
+ code: string;
+ }[];
+}
+
+export const APP_SHELL_EXAMPLES_COMPONENTS = {
+ BasicAppShell: {
+ component: BasicAppShell,
+ code: BasicAppShellCode,
+ },
+ ResponsiveSizes: {
+ component: ResponsiveSizes,
+ code: ResponsiveSizesCode,
+ },
+ MobileNavbar: {
+ component: MobileNavbar,
+ code: MobileNavbarCode,
+ },
+ FullLayout: {
+ component: FullLayout,
+ code: FullLayoutCode,
+ },
+ AltLayout: {
+ component: AltLayout,
+ code: AltLayoutCode,
+ },
+ CollapseDesktop: {
+ component: CollapseDesktop,
+ code: CollapseDesktopCode,
+ },
+ NoTransitions: {
+ component: NoTransitions,
+ code: NoTransitionsCode,
+ },
+ Disabled: {
+ component: Disabled,
+ code: DisabledCode,
+ },
+ Headroom: {
+ component: Headroom,
+ code: HeadroomCode,
+ },
+ NavbarSection: {
+ component: NavbarSection,
+ code: NavbarSectionCode,
+ },
+} satisfies Record;
+
+export type AppShellExampleId = keyof typeof APP_SHELL_EXAMPLES_COMPONENTS;
diff --git a/docs/app-shell-examples/index.ts b/docs/app-shell-examples/index.ts
new file mode 100644
index 00000000000..e067da8f5af
--- /dev/null
+++ b/docs/app-shell-examples/index.ts
@@ -0,0 +1 @@
+export { AppShellPage } from './AppShellPage/AppShellPage';
diff --git a/docs/combobox-examples/ComboboxDemo/ComboboxDemo.module.css b/docs/combobox-examples/ComboboxDemo/ComboboxDemo.module.css
new file mode 100644
index 00000000000..2f377c4b327
--- /dev/null
+++ b/docs/combobox-examples/ComboboxDemo/ComboboxDemo.module.css
@@ -0,0 +1,71 @@
+.root {
+ --preview-size: rem(600px);
+ padding-top: var(--preview-size);
+}
+
+.preview {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ height: var(--preview-size);
+ position: fixed;
+ top: var(--docs-header-height);
+ left: var(--combobox-shell-navbar-width);
+ right: 0;
+ z-index: 1;
+
+ @mixin rtl {
+ left: 0;
+ right: var(--combobox-shell-navbar-width);
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--mantine-spacing-md) var(--mantine-spacing-lg);
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
+ border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ flex-direction: column-reverse;
+ align-items: flex-start;
+ gap: rem(7px);
+ }
+}
+
+.headerBody {
+ flex: 1;
+ margin-right: var(--mantine-spacing-md);
+}
+
+.title {
+ font-size: var(--mantine-font-size-sm);
+ font-weight: 500;
+}
+
+.description {
+ font-size: var(--mantine-font-size-xs);
+ color: var(--mantine-color-gray-6);
+ margin-top: rem(3px);
+}
+
+.inner {
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ padding-top: rem(180px);
+}
+
+.wrapper {
+ flex: 1;
+ max-width: rem(420px);
+ padding-left: rem(20px);
+ padding-right: rem(20px);
+}
+
+.code {
+ border-top: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
+ position: relative;
+ z-index: 2;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
+}
diff --git a/docs/combobox-examples/ComboboxDemo/ComboboxDemo.tsx b/docs/combobox-examples/ComboboxDemo/ComboboxDemo.tsx
new file mode 100644
index 00000000000..0474b982317
--- /dev/null
+++ b/docs/combobox-examples/ComboboxDemo/ComboboxDemo.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import cx from 'clsx';
+import { RemoveScroll, Text, Badge } from '@mantine/core';
+import { useRouter } from 'next/router';
+import { getCodeFileIcon } from '@mantine/ds';
+import { CodeHighlightTabs } from '@mantine/code-highlight';
+import { COMBOBOX_EXAMPLES_COMPONENTS, ComboboxExampleId } from '../examples';
+import { COMBOBOX_EXAMPLES_DATA } from '../combobox-examples-data';
+import classes from './ComboboxDemo.module.css';
+
+export function ComboboxDemo() {
+ const router = useRouter();
+ const id = router.query.e as ComboboxExampleId;
+ const codeData = COMBOBOX_EXAMPLES_COMPONENTS[id];
+ const metaData = COMBOBOX_EXAMPLES_DATA.find((item) => item.id === id);
+
+ if (!codeData || !metaData) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {metaData.name}
+
+ {metaData.fullDescription || metaData.description}
+
+
+
+
{metaData.type}
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/ComboboxPage/ComboboxPage.tsx b/docs/combobox-examples/ComboboxPage/ComboboxPage.tsx
new file mode 100644
index 00000000000..cd3a114c5f6
--- /dev/null
+++ b/docs/combobox-examples/ComboboxPage/ComboboxPage.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { ComboboxShell } from '../ComboboxShell/ComboboxShell';
+import { ComboboxDemo } from '../ComboboxDemo/ComboboxDemo';
+import { PageHead } from '@/components/PageHead';
+
+export function ComboboxPage() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.module.css b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.module.css
new file mode 100644
index 00000000000..095abe61a4c
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.module.css
@@ -0,0 +1,119 @@
+.group {
+ margin-bottom: 0;
+ padding-left: var(--mantine-spacing-xs);
+ padding-right: var(--mantine-spacing-xs);
+
+ &[data-opened] {
+ margin-bottom: calc(var(--mantine-spacing-xl) * 1.2);
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: rem(32px);
+ cursor: pointer;
+
+ @mixin light {
+ color: var(--mantine-color-gray-7);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
+
+.chevron {
+ width: rem(15px);
+ height: rem(15px);
+ margin-right: var(--mantine-spacing-md);
+ transition: transform 150ms ease;
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: var(--mantine-spacing-md);
+ }
+
+ &[data-collapsed] {
+ transform: rotate(-90deg);
+ }
+}
+
+.title {
+ user-select: none;
+ font-weight: 700;
+ font-family: var(--docs-font-primary);
+ line-height: 1;
+ padding-top: rem(4px);
+ letter-spacing: rem(0.5px);
+ word-spacing: rem(1px);
+ text-transform: uppercase;
+ font-size: var(--mantine-font-size-xs);
+}
+
+.link {
+ --border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-6));
+
+ display: block;
+ border-left: rem(1px) solid var(--border-color);
+ padding-left: rem(23px);
+ padding-right: var(--mantine-spacing-md);
+ padding-top: rem(8px);
+ padding-bottom: rem(8px);
+ margin-left: rem(7px);
+ border-top-right-radius: var(--mantine-radius-sm);
+ border-bottom-right-radius: var(--mantine-radius-sm);
+ font-size: var(--mantine-font-size-sm);
+ user-select: none;
+ text-decoration: none;
+
+ @mixin light {
+ color: var(--mantine-color-gray-7);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-1);
+ }
+
+ @mixin rtl {
+ padding-left: var(--mantine-spacing-md);
+ padding-right: rem(23px);
+ border-left: 0;
+ border-right: rem(1px) solid var(--border-color);
+ margin-left: 0;
+ margin-right: rem(7px);
+ border-radius: 0;
+ border-top-left-radius: var(--mantine-radius-sm);
+ border-bottom-left-radius: var(--mantine-radius-sm);
+ }
+
+ &[data-navbar-link-active] {
+ border-color: var(--mantine-color-blue-5) !important;
+
+ @mixin light {
+ color: var(--mantine-color-blue-8);
+ background-color: var(--mantine-color-blue-0);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-blue-1);
+ background-color: rgba(24, 100, 171, 0.45);
+ }
+
+ & .linkTitle {
+ font-weight: 500;
+ }
+ }
+}
+
+.linkTitle {
+ font-size: var(--mantine-font-size-sm);
+}
+
+.linkDescription {
+ font-size: var(--mantine-font-size-xs);
+ opacity: 0.6;
+ margin-top: rem(4px);
+ line-height: 1.4;
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.tsx b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.tsx
new file mode 100644
index 00000000000..7f8fb22ec2f
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxLinksGroup/ComboboxLinksGroup.tsx
@@ -0,0 +1,52 @@
+import React, { useEffect } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useDisclosure } from '@mantine/hooks';
+import { UnstyledButton, Highlight, Box, Text } from '@mantine/core';
+import { IconChevronDown } from '@tabler/icons-react';
+import type { ComboboxExamplesGroup } from '../get-grouped-data';
+import classes from './ComboboxLinksGroup.module.css';
+
+interface ComboboxLinksGroupProps {
+ data: ComboboxExamplesGroup;
+ searchQuery: string | string[];
+ onClose(): void;
+}
+
+export function ComboboxLinksGroup({ data, searchQuery, onClose }: ComboboxLinksGroupProps) {
+ const router = useRouter();
+ const [opened, { toggle, open }] = useDisclosure(true);
+
+ useEffect(open, [searchQuery]);
+
+ if (data.items.length === 0) {
+ return null;
+ }
+
+ const items = data.items.map((item) => (
+
+
+ {item.name}
+
+
+ {item.description}
+
+
+ ));
+
+ return (
+
+
+
+ {data.group.replace('-', ' ')}
+
+ {opened && items}
+
+ );
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.module.css b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.module.css
new file mode 100644
index 00000000000..327af4a2aef
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.module.css
@@ -0,0 +1,101 @@
+.navbar {
+ --border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
+
+ position: fixed;
+ top: var(--docs-header-height);
+ bottom: 0;
+ width: var(--combobox-shell-navbar-width);
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ border-right: rem(1px) solid var(--border-color);
+
+ @mixin rtl {
+ border-right: none;
+ border-left: rem(1px) solid var(--border-color);
+ }
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ width: unset;
+ left: 0;
+ right: 0;
+ z-index: 3;
+ }
+
+ &[data-hidden] {
+ @media (max-width: $docs-navbar-breakpoint) {
+ display: none;
+ }
+ }
+}
+
+.scroll {
+ height: calc(100vh - rem(134px));
+}
+
+.search {
+ margin-left: var(--mantine-spacing-xs);
+ margin-right: var(--mantine-spacing-xs);
+ width: auto;
+ margin-bottom: var(--mantine-spacing-md);
+ margin-top: var(--mantine-spacing-md);
+}
+
+.searchInput {
+ font-size: var(--mantine-font-size-sm);
+}
+
+.searchIcon {
+ width: rem(18px);
+ height: rem(18px);
+}
+
+.item {
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
+
+ @mixin hover {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ }
+
+ & + & {
+ border-top: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8));
+ }
+
+ &[data-active] {
+ &,
+ &:hover {
+ background-color: var(--mantine-color-blue-light);
+ border-top-color: transparent !important;
+ }
+
+ & .itemName {
+ color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-2));
+ }
+
+ & .itemDescription {
+ color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4));
+ opacity: 0.7;
+ }
+ }
+}
+
+.itemName {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ font-weight: 500;
+ font-size: var(--mantine-font-size-sm);
+}
+
+.itemDescription {
+ color: var(--mantine-color-dimmed);
+ font-size: var(--mantine-font-size-xs);
+ margin-top: rem(2px);
+}
+
+.empty {
+ padding: var(--mantine-spacing-md);
+ text-align: center;
+ color: var(--mantine-color-dimmed);
+ font-size: var(--mantine-font-size-sm);
+
+ &:not(:only-child) {
+ display: none;
+ }
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.tsx b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.tsx
new file mode 100644
index 00000000000..36bfac280b4
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/ComboboxNavbar.tsx
@@ -0,0 +1,81 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useHotkeys } from '@mantine/hooks';
+import { Text, TextInput, ScrollArea } from '@mantine/core';
+import { IconSearch } from '@tabler/icons-react';
+import { COMBOBOX_EXAMPLES_DATA } from '../../combobox-examples-data';
+import { ComboboxLinksGroup } from './ComboboxLinksGroup/ComboboxLinksGroup';
+import { getGroupedData } from './get-grouped-data';
+import classes from './ComboboxNavbar.module.css';
+
+interface ComboboxNavbarProps {
+ opened: boolean;
+ onClose(): void;
+}
+
+export function ComboboxNavbar({ opened, onClose }: ComboboxNavbarProps) {
+ const [search, setSearch] = useState('');
+ const searchInputRef = useRef(null);
+ const splittedSearch = search
+ .toLowerCase()
+ .split(' ')
+ .filter((item) => item.trim().length > 0);
+
+ const filteredData = COMBOBOX_EXAMPLES_DATA.filter((item) => {
+ const splittedName = item.name
+ .toLowerCase()
+ .split(' ')
+ .filter((part) => part.trim().length > 0);
+ const splittedDescription = item.description
+ .toLowerCase()
+ .split(' ')
+ .filter((part) => part.trim().length > 0);
+
+ return splittedSearch.every(
+ (part) =>
+ splittedName.some((name) => name.includes(part)) ||
+ splittedDescription.some((name) => name.includes(part)) ||
+ item.type.includes(part)
+ );
+ });
+
+ const groupedData = getGroupedData(filteredData);
+
+ const groups = groupedData.map((item) => (
+
+ ));
+
+ useHotkeys([['mod + shift + k', () => searchInputRef.current?.focus()]], []);
+
+ useEffect(() => {
+ setTimeout(() => {
+ document
+ .querySelector('[data-navbar-link-active]')
+ ?.scrollIntoView({ block: 'center', behavior: 'instant' });
+ }, 500);
+ }, []);
+
+ return (
+
+ }
+ radius="md"
+ size="md"
+ value={search}
+ onChange={(event) => setSearch(event.currentTarget.value)}
+ ref={searchInputRef}
+ />
+
+
+ {groups}
+ Nothing found...
+
+
+ );
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxNavbar/get-grouped-data.ts b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/get-grouped-data.ts
new file mode 100644
index 00000000000..1143637a88a
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxNavbar/get-grouped-data.ts
@@ -0,0 +1,32 @@
+import { ComboboxExample } from '../../combobox-examples-data';
+
+export interface ComboboxExamplesGroup {
+ group: string;
+ items: ComboboxExample[];
+}
+
+export function getGroupedData(data: ComboboxExample[]): ComboboxExamplesGroup[] {
+ const items: Record = {
+ select: [],
+ autocomplete: [],
+ multiselect: [],
+ dropdown: [],
+ button: [],
+ animations: [],
+ other: [],
+ };
+
+ data.forEach((item) => {
+ items[item.type].push(item);
+ });
+
+ return [
+ { group: 'Select', items: items.select },
+ { group: 'Autocomplete', items: items.autocomplete },
+ { group: 'Multiselect', items: items.multiselect },
+ { group: 'Button', items: items.button },
+ { group: 'Dropdown', items: items.dropdown },
+ { group: 'Animations', items: items.animations },
+ { group: 'Other', items: items.other },
+ ];
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxShell.module.css b/docs/combobox-examples/ComboboxShell/ComboboxShell.module.css
new file mode 100644
index 00000000000..59d8caa5213
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxShell.module.css
@@ -0,0 +1,16 @@
+.root {
+ --combobox-shell-navbar-width: rem(300px);
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ --combobox-shell-navbar-width: 0;
+ }
+}
+
+.main {
+ padding-left: var(--combobox-shell-navbar-width);
+
+ @mixin rtl {
+ padding-left: 0;
+ padding-right: var(--combobox-shell-navbar-width);
+ }
+}
diff --git a/docs/combobox-examples/ComboboxShell/ComboboxShell.tsx b/docs/combobox-examples/ComboboxShell/ComboboxShell.tsx
new file mode 100644
index 00000000000..bc632805bd6
--- /dev/null
+++ b/docs/combobox-examples/ComboboxShell/ComboboxShell.tsx
@@ -0,0 +1,21 @@
+import React, { useState } from 'react';
+import { Shell } from '@/components/Shell';
+import { ComboboxNavbar } from './ComboboxNavbar/ComboboxNavbar';
+import classes from './ComboboxShell.module.css';
+
+interface ComboboxShellProps {
+ children: React.ReactNode;
+}
+
+export function ComboboxShell({ children }: ComboboxShellProps) {
+ const [opened, setNavbarOpened] = useState(false);
+
+ return (
+
+
+ setNavbarOpened(false)} />
+ {children}
+
+
+ );
+}
diff --git a/docs/combobox-examples/combobox-examples-data.ts b/docs/combobox-examples/combobox-examples-data.ts
new file mode 100644
index 00000000000..aa72e2679d0
--- /dev/null
+++ b/docs/combobox-examples/combobox-examples-data.ts
@@ -0,0 +1,285 @@
+import { COMBOBOX_EXAMPLES_COMPONENTS } from './examples';
+
+export interface ComboboxExample {
+ /** Demo id, based on it component will render component on the page */
+ id: keyof typeof COMBOBOX_EXAMPLES_COMPONENTS;
+
+ /** Name used in search */
+ name: string;
+
+ /** Short component description, used in search */
+ description: string;
+
+ /** Full component description, displayed on the page */
+ fullDescription?: string;
+
+ /** Type based on which components are ordered in the navbar, also used for filtering */
+ type: 'select' | 'autocomplete' | 'multiselect' | 'dropdown' | 'button' | 'animations' | 'other';
+}
+
+export const COMBOBOX_EXAMPLES_DATA: ComboboxExample[] = [
+ {
+ id: 'BasicSelect',
+ name: 'Basic select',
+ description: 'Primitive select component',
+ type: 'select',
+ },
+ {
+ id: 'SelectActive',
+ name: 'Select with active option',
+ description: 'Active option is marked in the dropdown',
+ type: 'select',
+ },
+ {
+ id: 'SearchableSelect',
+ name: 'Searchable select',
+ description: 'Select with search',
+ type: 'select',
+ },
+ {
+ id: 'SelectLimit',
+ name: 'Select with options limit',
+ description: '100 000 options searchable select',
+ fullDescription:
+ 'Limiting the number of options rendered at a time is the most efficient way to deal with large data sets. The example below shows how to use the limit prop to only render 7 options at a time while performing a search in a data set of 100,000 options.',
+ type: 'select',
+ },
+ {
+ id: 'SelectOptionComponent',
+ name: 'Select with custom option',
+ description: 'Select with custom option and value component',
+ type: 'select',
+ },
+ {
+ id: 'SelectAsync',
+ name: 'Select with async data',
+ description: 'Data is requested only when the dropdown is opened',
+ type: 'select',
+ },
+ {
+ id: 'SelectClearable',
+ name: 'Select with clear button',
+ description: 'Clearable select',
+ type: 'select',
+ },
+ {
+ id: 'SelectDropdownSearch',
+ name: 'Select with search in dropdown',
+ description: 'Select with search input in the dropdown',
+ type: 'select',
+ },
+ {
+ id: 'SelectOptionOnHover',
+ name: 'Select option on hover',
+ description: 'Move selection to hovered option',
+ fullDescription:
+ 'Moving selection to hovered option can be useful when you want to combine mouse and keyboard interactions.',
+ type: 'select',
+ },
+ {
+ id: 'SelectGroups',
+ name: 'Options groups',
+ description: 'Select with options groups',
+ type: 'select',
+ },
+ {
+ id: 'SelectGroupsSearchable',
+ name: 'Searchable select with groups',
+ description: 'Options filtering with grouped data',
+ type: 'select',
+ },
+ {
+ id: 'SelectGroupsStyles',
+ name: 'Custom group styles',
+ description: 'Customize groups styles with Styles API',
+ type: 'select',
+ },
+ {
+ id: 'SelectCreatable',
+ name: 'Creatable select',
+ description: 'Select with option to create new options',
+ type: 'select',
+ },
+ {
+ id: 'BasicAutocomplete',
+ name: 'Basic autocomplete',
+ description: 'Primitive autocomplete component',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AutocompleteLimit',
+ name: 'Autocomplete with options limit',
+ description: '100 000 options search',
+ fullDescription:
+ 'Limiting the number of options rendered at a time is the most efficient way to deal with large data sets. The example below shows how to use the limit prop to only render 7 options at a time while performing a search in a data set of 100,000 options.',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AsyncAutocomplete',
+ name: 'Async autocomplete',
+ description: 'Autocomplete with async data',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AutocompleteHighlight',
+ name: 'Autocomplete with highlight',
+ description: 'Autocomplete with highlighted search query in options',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AutocompleteSelectFirstOption',
+ name: 'Select first option on type',
+ description: 'Autocomplete with first option selected when user types',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AutocompleteDynamic',
+ name: 'Dynamic options',
+ description: 'Autocomplete with options that depend on the user input',
+ type: 'autocomplete',
+ },
+ {
+ id: 'AutocompleteClearable',
+ name: 'Clearable autocomplete',
+ description: 'Autocomplete with clear button',
+ type: 'autocomplete',
+ },
+ {
+ id: 'BasicButton',
+ name: 'Basic button',
+ description: 'Primitive combobox with button target',
+ type: 'button',
+ },
+ {
+ id: 'ButtonSearch',
+ name: 'Button with search in dropdown',
+ description: 'Button with search input in the dropdown',
+ type: 'button',
+ },
+ {
+ id: 'ButtonMultiSelect',
+ name: 'Button multi select',
+ description: 'Button with multiple items selection',
+ type: 'button',
+ },
+ {
+ id: 'DropdownScroll',
+ name: 'Dropdown scroll',
+ description: 'Dropdown with native scrollbars',
+ type: 'dropdown',
+ },
+ {
+ id: 'DropdownScrollArea',
+ name: 'Dropdown with ScrollArea',
+ description: 'Dropdown with ScrollArea.Autosize scrollbars',
+ type: 'dropdown',
+ },
+ {
+ id: 'DropdownHeader',
+ name: 'Dropdown with header',
+ description: 'Dropdown with ScrollArea and header',
+ type: 'dropdown',
+ },
+ {
+ id: 'DropdownFooter',
+ name: 'Dropdown with footer',
+ description: 'Dropdown with ScrollArea and footer',
+ type: 'dropdown',
+ },
+ {
+ id: 'DropdownPositionStyles',
+ name: 'Dropdown with custom styles',
+ description: 'Change dropdown styles with Styles API',
+ type: 'dropdown',
+ },
+ {
+ id: 'SelectedStyles',
+ name: 'Custom selected styles',
+ description: 'Customize selected option styles',
+ type: 'dropdown',
+ },
+ {
+ id: 'DropdownAnimation',
+ name: 'Dropdown animation',
+ description: 'Dropdown with open/close animation',
+ type: 'animations',
+ },
+ {
+ id: 'DropdownOptionsAnimation',
+ name: 'Dropdown with option animation',
+ description: 'Play options animation on dropdown open',
+ type: 'animations',
+ },
+ {
+ id: 'SelectedAnimation',
+ name: 'Animate selected option',
+ description: 'Play options animation on when option is selected',
+ type: 'animations',
+ },
+ {
+ id: 'DropdownSmoothScroll',
+ name: 'Smooth scroll',
+ description: 'Smooth scroll of the options with keyboard navigation',
+ type: 'animations',
+ },
+ {
+ id: 'BasicMultiSelect',
+ name: 'Basic multiselect',
+ description: 'Primitive multiselect component',
+ type: 'multiselect',
+ },
+ {
+ id: 'SearchableMultiSelect',
+ name: 'Searchable multiselect',
+ description: 'Multiselect with search',
+ type: 'multiselect',
+ },
+ {
+ id: 'MaxSelectedItems',
+ name: 'Max selected options',
+ description: 'Limit max number of options that can be selected',
+ type: 'multiselect',
+ },
+ {
+ id: 'MultiSelectCheckbox',
+ name: 'Multiselect with checkboxes',
+ description: 'Multiselect options with checkboxes',
+ type: 'multiselect',
+ },
+ {
+ id: 'ActiveOptionsFilter',
+ name: 'Hide active options',
+ description: 'Remove active options from the dropdown',
+ type: 'multiselect',
+ },
+ {
+ id: 'MaxDisplayedItems',
+ name: 'Max displayed values',
+ description: 'Limit max number of values that can be displayed',
+ type: 'multiselect',
+ },
+ {
+ id: 'MultiSelectValueRenderer',
+ name: 'Custom value pills',
+ description: 'Use a custom component to render selected values',
+ type: 'multiselect',
+ },
+ {
+ id: 'MultiSelectCreatable',
+ name: 'Creatable multiselect',
+ description: 'Multiselect with option to create new options',
+ type: 'multiselect',
+ },
+ {
+ id: 'SelectList',
+ name: 'Inline options list',
+ description: 'Inline list with checkboxes',
+ type: 'other',
+ },
+ {
+ id: 'TransferList',
+ name: 'Transfer list',
+ description: 'Transfer list with search',
+ type: 'other',
+ },
+];
diff --git a/docs/combobox-examples/examples/ActiveOptionsFilter/ActiveOptionsFilter.tsx b/docs/combobox-examples/examples/ActiveOptionsFilter/ActiveOptionsFilter.tsx
new file mode 100644
index 00000000000..e2624472489
--- /dev/null
+++ b/docs/combobox-examples/examples/ActiveOptionsFilter/ActiveOptionsFilter.tsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function ActiveOptionsFilter() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries
+ .filter((item) => !value.includes(item))
+ .map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {values.length > 0 ? (
+ values
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ {options.length === 0 ? All options selected : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/ActiveOptionsFilter/code.json b/docs/combobox-examples/examples/ActiveOptionsFilter/code.json
new file mode 100644
index 00000000000..12ad6725576
--- /dev/null
+++ b/docs/combobox-examples/examples/ActiveOptionsFilter/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "ActiveOptionsFilter.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function ActiveOptionsFilter() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries\n .filter((item) => !value.includes(item))\n .map((item) => (\n \n {item}\n \n ));\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {values.length > 0 ? (\n values\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n \n {options.length === 0 ? All options selected : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AsyncAutocomplete/AsyncAutocomplete.tsx b/docs/combobox-examples/examples/AsyncAutocomplete/AsyncAutocomplete.tsx
new file mode 100644
index 00000000000..fdc59dd4c21
--- /dev/null
+++ b/docs/combobox-examples/examples/AsyncAutocomplete/AsyncAutocomplete.tsx
@@ -0,0 +1,125 @@
+import React, { useState, useRef } from 'react';
+import { Combobox, TextInput, Loader, useCombobox } from '@mantine/core';
+
+const MOCKDATA = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+function getAsyncData(searchQuery: string, signal: AbortSignal) {
+ return new Promise((resolve, reject) => {
+ signal.addEventListener('abort', () => {
+ reject(new Error('Request aborted'));
+ });
+
+ setTimeout(() => {
+ resolve(
+ MOCKDATA.filter((item) => item.toLowerCase().includes(searchQuery.toLowerCase())).slice(
+ 0,
+ 5
+ )
+ );
+ }, Math.random() * 1000);
+ });
+}
+
+export function AsyncAutocomplete() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [data, setData] = useState(null);
+ const [value, setValue] = useState('');
+ const [empty, setEmpty] = useState(false);
+ const abortController = useRef();
+
+ const fetchOptions = (query: string) => {
+ abortController.current?.abort();
+ abortController.current = new AbortController();
+ setLoading(true);
+
+ getAsyncData(query, abortController.current.signal)
+ .then((result) => {
+ setData(result);
+ setLoading(false);
+ setEmpty(result.length === 0);
+ abortController.current = undefined;
+ })
+ .catch(() => {});
+ };
+
+ const options = (data || []).map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ withinPortal={false}
+ store={combobox}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ fetchOptions(event.currentTarget.value);
+ combobox.resetSelectedOption();
+ combobox.openDropdown();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => {
+ combobox.openDropdown();
+ if (data === null) {
+ fetchOptions(value);
+ }
+ }}
+ onBlur={() => combobox.closeDropdown()}
+ rightSection={loading && }
+ />
+
+
+
+
+ {options}
+ {empty && No results found }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AsyncAutocomplete/code.json b/docs/combobox-examples/examples/AsyncAutocomplete/code.json
new file mode 100644
index 00000000000..0685170e3a6
--- /dev/null
+++ b/docs/combobox-examples/examples/AsyncAutocomplete/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AsyncAutocomplete.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, Loader, useCombobox } from '@mantine/core';\n\nconst MOCKDATA = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nfunction getAsyncData(searchQuery: string, signal: AbortSignal) {\n return new Promise((resolve, reject) => {\n signal.addEventListener('abort', () => {\n reject(new Error('Request aborted'));\n });\n\n setTimeout(() => {\n resolve(\n MOCKDATA.filter((item) => item.toLowerCase().includes(searchQuery.toLowerCase())).slice(\n 0,\n 5\n )\n );\n }, Math.random() * 1000);\n });\n}\n\nexport function AsyncAutocomplete() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [loading, setLoading] = useState(false);\n const [data, setData] = useState(null);\n const [value, setValue] = useState('');\n const [empty, setEmpty] = useState(false);\n const abortController = useRef();\n\n const fetchOptions = (query: string) => {\n abortController.current?.abort();\n abortController.current = new AbortController();\n setLoading(true);\n\n getAsyncData(query, abortController.current.signal)\n .then((result) => {\n setData(result);\n setLoading(false);\n setEmpty(result.length === 0);\n abortController.current = undefined;\n })\n .catch(() => {});\n };\n\n const options = (data || []).map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n withinPortal={false}\n store={combobox}\n >\n \n {\n setValue(event.currentTarget.value);\n fetchOptions(event.currentTarget.value);\n combobox.resetSelectedOption();\n combobox.openDropdown();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => {\n combobox.openDropdown();\n if (data === null) {\n fetchOptions(value);\n }\n }}\n onBlur={() => combobox.closeDropdown()}\n rightSection={loading && }\n />\n \n\n \n \n {options}\n {empty && No results found }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AutocompleteClearable/AutocompleteClearable.tsx b/docs/combobox-examples/examples/AutocompleteClearable/AutocompleteClearable.tsx
new file mode 100644
index 00000000000..9b09f035bc0
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteClearable/AutocompleteClearable.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, CloseButton, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function AutocompleteClearable() {
+ const combobox = useCombobox();
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ rightSection={
+ value !== '' && (
+ event.preventDefault()}
+ onClick={() => setValue('')}
+ aria-label="Clear value"
+ />
+ )
+ }
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AutocompleteClearable/code.json b/docs/combobox-examples/examples/AutocompleteClearable/code.json
new file mode 100644
index 00000000000..76a37e67f1e
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteClearable/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AutocompleteClearable.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, CloseButton, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function AutocompleteClearable() {\n const combobox = useCombobox();\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n rightSection={\n value !== '' && (\n event.preventDefault()}\n onClick={() => setValue('')}\n aria-label=\"Clear value\"\n />\n )\n }\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AutocompleteDynamic/AutocompleteDynamic.tsx b/docs/combobox-examples/examples/AutocompleteDynamic/AutocompleteDynamic.tsx
new file mode 100644
index 00000000000..193da62fa6d
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteDynamic/AutocompleteDynamic.tsx
@@ -0,0 +1,46 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, useCombobox } from '@mantine/core';
+
+export function AutocompleteDynamic() {
+ const combobox = useCombobox();
+ const [value, setValue] = useState('');
+
+ const options = ['gmail.com', 'outlook.com', 'mantine.dev'].map((item) => (
+
+ {`${value}@${item}`}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AutocompleteDynamic/code.json b/docs/combobox-examples/examples/AutocompleteDynamic/code.json
new file mode 100644
index 00000000000..b8e133fa295
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteDynamic/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AutocompleteDynamic.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, useCombobox } from '@mantine/core';\n\nexport function AutocompleteDynamic() {\n const combobox = useCombobox();\n const [value, setValue] = useState('');\n\n const options = ['gmail.com', 'outlook.com', 'mantine.dev'].map((item) => (\n \n {`${value}@${item}`}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AutocompleteHighlight/AutocompleteHighlight.tsx b/docs/combobox-examples/examples/AutocompleteHighlight/AutocompleteHighlight.tsx
new file mode 100644
index 00000000000..90a5701cb7d
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteHighlight/AutocompleteHighlight.tsx
@@ -0,0 +1,63 @@
+import React, { useState } from 'react';
+import { Combobox, Highlight, TextInput, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function AutocompleteHighlight() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+
+ {item}
+
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ withinPortal={false}
+ store={combobox}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.updateSelectedOptionIndex();
+ combobox.openDropdown();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AutocompleteHighlight/code.json b/docs/combobox-examples/examples/AutocompleteHighlight/code.json
new file mode 100644
index 00000000000..25b841d6d6a
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteHighlight/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AutocompleteHighlight.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, Highlight, TextInput, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function AutocompleteHighlight() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n \n {item}\n \n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n withinPortal={false}\n store={combobox}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.updateSelectedOptionIndex();\n combobox.openDropdown();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AutocompleteLimit/AutocompleteLimit.tsx b/docs/combobox-examples/examples/AutocompleteLimit/AutocompleteLimit.tsx
new file mode 100644
index 00000000000..ada7cb982f1
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteLimit/AutocompleteLimit.tsx
@@ -0,0 +1,66 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, useCombobox } from '@mantine/core';
+
+const MOCKDATA = Array(100000)
+ .fill(0)
+ .map((_, index) => `Item ${index}`);
+
+function getFilteredOptions(data: string[], searchQuery: string, limit: number) {
+ const result: string[] = [];
+
+ for (let i = 0; i < data.length; i += 1) {
+ if (result.length === limit) {
+ break;
+ }
+
+ if (data[i].toLowerCase().includes(searchQuery.trim().toLowerCase())) {
+ result.push(data[i]);
+ }
+ }
+
+ return result;
+}
+
+export function AutocompleteLimit() {
+ const combobox = useCombobox();
+ const [value, setValue] = useState('');
+ const filteredOptions = getFilteredOptions(MOCKDATA, value, 7);
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ withinPortal={false}
+ store={combobox}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AutocompleteLimit/code.json b/docs/combobox-examples/examples/AutocompleteLimit/code.json
new file mode 100644
index 00000000000..0860e20fefd
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteLimit/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AutocompleteLimit.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, useCombobox } from '@mantine/core';\n\nconst MOCKDATA = Array(100000)\n .fill(0)\n .map((_, index) => `Item ${index}`);\n\nfunction getFilteredOptions(data: string[], searchQuery: string, limit: number) {\n const result: string[] = [];\n\n for (let i = 0; i < data.length; i += 1) {\n if (result.length === limit) {\n break;\n }\n\n if (data[i].toLowerCase().includes(searchQuery.trim().toLowerCase())) {\n result.push(data[i]);\n }\n }\n\n return result;\n}\n\nexport function AutocompleteLimit() {\n const combobox = useCombobox();\n const [value, setValue] = useState('');\n const filteredOptions = getFilteredOptions(MOCKDATA, value, 7);\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n withinPortal={false}\n store={combobox}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/AutocompleteSelectFirstOption/AutocompleteSelectFirstOption.tsx b/docs/combobox-examples/examples/AutocompleteSelectFirstOption/AutocompleteSelectFirstOption.tsx
new file mode 100644
index 00000000000..00cfc44ef1d
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteSelectFirstOption/AutocompleteSelectFirstOption.tsx
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from 'react';
+import { Combobox, TextInput, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function AutocompleteSelectFirstOption() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ useEffect(() => {
+ // we need to wait for options to render before we can select first one
+ combobox.selectFirstOption();
+ }, [value]);
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/AutocompleteSelectFirstOption/code.json b/docs/combobox-examples/examples/AutocompleteSelectFirstOption/code.json
new file mode 100644
index 00000000000..9130fb36408
--- /dev/null
+++ b/docs/combobox-examples/examples/AutocompleteSelectFirstOption/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "AutocompleteSelectFirstOption.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function AutocompleteSelectFirstOption() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n useEffect(() => {\n // we need to wait for options to render before we can select first one\n combobox.selectFirstOption();\n }, [value]);\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/BasicAutocomplete/BasicAutocomplete.tsx b/docs/combobox-examples/examples/BasicAutocomplete/BasicAutocomplete.tsx
new file mode 100644
index 00000000000..d3ab425589e
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicAutocomplete/BasicAutocomplete.tsx
@@ -0,0 +1,59 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function BasicAutocomplete() {
+ const combobox = useCombobox();
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/BasicAutocomplete/code.json b/docs/combobox-examples/examples/BasicAutocomplete/code.json
new file mode 100644
index 00000000000..dcb46bdba06
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicAutocomplete/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "BasicAutocomplete.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function BasicAutocomplete() {\n const combobox = useCombobox();\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/BasicButton/BasicButton.tsx b/docs/combobox-examples/examples/BasicButton/BasicButton.tsx
new file mode 100644
index 00000000000..a284639c0e9
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicButton/BasicButton.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react';
+import { useCombobox, Combobox, Text, Button, Box } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function BasicButton() {
+ const [selectedItem, setSelectedItem] = useState(null);
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ <>
+ {
+ setSelectedItem(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ combobox.toggleDropdown()}>Pick item
+
+
+
+ {options}
+
+
+
+
+
+ Selected item:{' '}
+
+
+
+ {selectedItem || 'Nothing selected'}
+
+
+ >
+ );
+}
diff --git a/docs/combobox-examples/examples/BasicButton/code.json b/docs/combobox-examples/examples/BasicButton/code.json
new file mode 100644
index 00000000000..8bc3404640d
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicButton/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "BasicButton.tsx",
+ "language": "tsx",
+ "code": "\nimport { useCombobox, Combobox, Text, Button, Box } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function BasicButton() {\n const [selectedItem, setSelectedItem] = useState(null);\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n <>\n {\n setSelectedItem(val);\n combobox.closeDropdown();\n }}\n >\n \n combobox.toggleDropdown()}>Pick item \n \n\n \n {options} \n \n \n\n \n \n Selected item:{' '}\n \n\n \n {selectedItem || 'Nothing selected'}\n \n \n >\n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/BasicMultiSelect/BasicMultiSelect.tsx b/docs/combobox-examples/examples/BasicMultiSelect/BasicMultiSelect.tsx
new file mode 100644
index 00000000000..8b4fc8c25d1
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicMultiSelect/BasicMultiSelect.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function BasicMultiSelect() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries.map((item) => (
+
+
+ {value.includes(item) ? : null}
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {values.length > 0 ? (
+ values
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/BasicMultiSelect/code.json b/docs/combobox-examples/examples/BasicMultiSelect/code.json
new file mode 100644
index 00000000000..3daf303994f
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicMultiSelect/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "BasicMultiSelect.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function BasicMultiSelect() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries.map((item) => (\n \n \n {value.includes(item) ? : null}\n {item} \n \n \n ));\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {values.length > 0 ? (\n values\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/BasicSelect/BasicSelect.tsx b/docs/combobox-examples/examples/BasicSelect/BasicSelect.tsx
new file mode 100644
index 00000000000..b2a41894a46
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicSelect/BasicSelect.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function BasicSelect() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/BasicSelect/code.json b/docs/combobox-examples/examples/BasicSelect/code.json
new file mode 100644
index 00000000000..93652552ddb
--- /dev/null
+++ b/docs/combobox-examples/examples/BasicSelect/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "BasicSelect.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function BasicSelect() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/ButtonMultiSelect/ButtonMultiSelect.tsx b/docs/combobox-examples/examples/ButtonMultiSelect/ButtonMultiSelect.tsx
new file mode 100644
index 00000000000..87b5e6127d8
--- /dev/null
+++ b/docs/combobox-examples/examples/ButtonMultiSelect/ButtonMultiSelect.tsx
@@ -0,0 +1,64 @@
+import React, { useState } from 'react';
+import { useCombobox, Combobox, Text, Button, Box, Group, CheckIcon } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function ButtonMultiSelect() {
+ const [selectedItems, setSelectedItems] = useState([]);
+
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const options = groceries.map((item) => (
+
+
+ {selectedItems.includes(item) && }
+ {item}
+
+
+ ));
+
+ return (
+ <>
+
+
+ Selected item:{' '}
+
+
+
+ {selectedItems.length > 0 ? selectedItems.join(', ') : 'Nothing selected'}
+
+
+
+ {
+ setSelectedItems((current) =>
+ current.includes(val) ? current.filter((item) => item !== val) : [...current, val]
+ );
+ }}
+ >
+
+ combobox.toggleDropdown()}>Pick item
+
+
+
+ {options}
+
+
+ >
+ );
+}
diff --git a/docs/combobox-examples/examples/ButtonMultiSelect/code.json b/docs/combobox-examples/examples/ButtonMultiSelect/code.json
new file mode 100644
index 00000000000..c5a1df7e60a
--- /dev/null
+++ b/docs/combobox-examples/examples/ButtonMultiSelect/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "ButtonMultiSelect.tsx",
+ "language": "tsx",
+ "code": "\nimport { useCombobox, Combobox, Text, Button, Box, Group, CheckIcon } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function ButtonMultiSelect() {\n const [selectedItems, setSelectedItems] = useState([]);\n\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const options = groceries.map((item) => (\n \n \n {selectedItems.includes(item) && }\n {item} \n \n \n ));\n\n return (\n <>\n \n \n Selected item:{' '}\n \n\n \n {selectedItems.length > 0 ? selectedItems.join(', ') : 'Nothing selected'}\n \n \n\n {\n setSelectedItems((current) =>\n current.includes(val) ? current.filter((item) => item !== val) : [...current, val]\n );\n }}\n >\n \n combobox.toggleDropdown()}>Pick item \n \n\n \n {options} \n \n \n >\n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/ButtonSearch/ButtonSearch.tsx b/docs/combobox-examples/examples/ButtonSearch/ButtonSearch.tsx
new file mode 100644
index 00000000000..86070628303
--- /dev/null
+++ b/docs/combobox-examples/examples/ButtonSearch/ButtonSearch.tsx
@@ -0,0 +1,76 @@
+import React, { useState } from 'react';
+import { useCombobox, Combobox, Text, Button, Box } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function ButtonSearch() {
+ const [search, setSearch] = useState('');
+ const [selectedItem, setSelectedItem] = useState(null);
+ const combobox = useCombobox({
+ onDropdownClose: () => {
+ combobox.resetSelectedOption();
+ combobox.focusTarget();
+ setSearch('');
+ },
+
+ onDropdownOpen: () => {
+ combobox.focusSearchInput();
+ },
+ });
+
+ const options = groceries
+ .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ .map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ <>
+ {
+ setSelectedItem(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ combobox.toggleDropdown()}>Pick item
+
+
+
+ setSearch(event.currentTarget.value)}
+ placeholder="Search groceries"
+ />
+
+ {options.length > 0 ? options : Nothing found }
+
+
+
+
+
+
+ Selected item:{' '}
+
+
+
+ {selectedItem || 'Nothing selected'}
+
+
+ >
+ );
+}
diff --git a/docs/combobox-examples/examples/ButtonSearch/code.json b/docs/combobox-examples/examples/ButtonSearch/code.json
new file mode 100644
index 00000000000..63e1c089d19
--- /dev/null
+++ b/docs/combobox-examples/examples/ButtonSearch/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "ButtonSearch.tsx",
+ "language": "tsx",
+ "code": "\nimport { useCombobox, Combobox, Text, Button, Box } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function ButtonSearch() {\n const [search, setSearch] = useState('');\n const [selectedItem, setSelectedItem] = useState(null);\n const combobox = useCombobox({\n onDropdownClose: () => {\n combobox.resetSelectedOption();\n combobox.focusTarget();\n setSearch('');\n },\n\n onDropdownOpen: () => {\n combobox.focusSearchInput();\n },\n });\n\n const options = groceries\n .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n .map((item) => (\n \n {item}\n \n ));\n\n return (\n <>\n {\n setSelectedItem(val);\n combobox.closeDropdown();\n }}\n >\n \n combobox.toggleDropdown()}>Pick item \n \n\n \n setSearch(event.currentTarget.value)}\n placeholder=\"Search groceries\"\n />\n \n {options.length > 0 ? options : Nothing found }\n \n \n \n\n \n \n Selected item:{' '}\n \n\n \n {selectedItem || 'Nothing selected'}\n \n \n >\n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownAnimation/DropdownAnimation.tsx b/docs/combobox-examples/examples/DropdownAnimation/DropdownAnimation.tsx
new file mode 100644
index 00000000000..164efca6de1
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownAnimation/DropdownAnimation.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function DropdownAnimation() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownAnimation/code.json b/docs/combobox-examples/examples/DropdownAnimation/code.json
new file mode 100644
index 00000000000..86b9f2275b1
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownAnimation/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownAnimation.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function DropdownAnimation() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownFooter/DropdownFooter.tsx b/docs/combobox-examples/examples/DropdownFooter/DropdownFooter.tsx
new file mode 100644
index 00000000000..6bbdb284563
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownFooter/DropdownFooter.tsx
@@ -0,0 +1,95 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, ScrollArea, Anchor, useCombobox, Text } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+export function DropdownFooter() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+
+ Search powered by{' '}
+
+ Mantine
+
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownFooter/code.json b/docs/combobox-examples/examples/DropdownFooter/code.json
new file mode 100644
index 00000000000..976ce837eed
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownFooter/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownFooter.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, ScrollArea, Anchor, useCombobox, Text } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nexport function DropdownFooter() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n \n Search powered by{' '}\n \n Mantine\n \n \n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownHeader/DropdownHeader.tsx b/docs/combobox-examples/examples/DropdownHeader/DropdownHeader.tsx
new file mode 100644
index 00000000000..6577da8561d
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownHeader/DropdownHeader.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, ScrollArea, useCombobox, Text } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+export function DropdownHeader() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+
+ Note: Carrots are not currently available
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownHeader/code.json b/docs/combobox-examples/examples/DropdownHeader/code.json
new file mode 100644
index 00000000000..fede010a8c4
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownHeader/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownHeader.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, ScrollArea, useCombobox, Text } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nexport function DropdownHeader() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n \n Note: Carrots are not currently available \n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.module.css b/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.module.css
new file mode 100644
index 00000000000..886b5a3a23c
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.module.css
@@ -0,0 +1,17 @@
+@keyframes option-animation {
+ from {
+ opacity: 0;
+ transform: translateX(rem(-20px));
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.animateOption {
+ opacity: 0;
+ animation: option-animation 200ms ease;
+ animation-fill-mode: forwards;
+}
diff --git a/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.tsx b/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.tsx
new file mode 100644
index 00000000000..d78f66bb755
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownOptionsAnimation/DropdownOptionsAnimation.tsx
@@ -0,0 +1,66 @@
+import React, { useState } from 'react';
+import cx from 'clsx';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+import classes from './DropdownOptionsAnimation.module.css';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function DropdownOptionsAnimation() {
+ const [animating, setAnimating] = useState(false);
+
+ const combobox = useCombobox({
+ onDropdownClose: () => {
+ combobox.resetSelectedOption();
+ setAnimating(false);
+ },
+
+ onDropdownOpen: () => setAnimating(true),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item, index) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownOptionsAnimation/code.json b/docs/combobox-examples/examples/DropdownOptionsAnimation/code.json
new file mode 100644
index 00000000000..aa764386d1c
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownOptionsAnimation/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "DropdownOptionsAnimation.tsx",
+ "language": "tsx",
+ "code": "\nimport cx from 'clsx';\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\nimport classes from './DropdownOptionsAnimation.module.css';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function DropdownOptionsAnimation() {\n const [animating, setAnimating] = useState(false);\n\n const combobox = useCombobox({\n onDropdownClose: () => {\n combobox.resetSelectedOption();\n setAnimating(false);\n },\n\n onDropdownOpen: () => setAnimating(true),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item, index) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "DropdownOptionsAnimation.module.css",
+ "language": "css",
+ "code": "@keyframes option-animation {\n from {\n opacity: 0;\n transform: translateX(rem(-20px));\n }\n\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.animateOption {\n opacity: 0;\n animation: option-animation 200ms ease;\n animation-fill-mode: forwards;\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.module.css b/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.module.css
new file mode 100644
index 00000000000..4b5a0ab9748
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.module.css
@@ -0,0 +1,13 @@
+.dropdown {
+ border-top: 0;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
+}
+
+.input {
+ &[data-expanded] {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
diff --git a/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.tsx b/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.tsx
new file mode 100644
index 00000000000..09401ac56ec
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownPositionStyles/DropdownPositionStyles.tsx
@@ -0,0 +1,55 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+import classes from './DropdownPositionStyles.module.css';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function DropdownPositionStyles() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ classNames={{ input: classes.input }}
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownPositionStyles/code.json b/docs/combobox-examples/examples/DropdownPositionStyles/code.json
new file mode 100644
index 00000000000..77a0e52d206
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownPositionStyles/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "DropdownPositionStyles.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\nimport classes from './DropdownPositionStyles.module.css';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function DropdownPositionStyles() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n classNames={{ input: classes.input }}\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "DropdownPositionStyles.module.css",
+ "language": "css",
+ "code": ".dropdown {\n border-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));\n}\n\n.input {\n &[data-expanded] {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownScroll/DropdownScroll.tsx b/docs/combobox-examples/examples/DropdownScroll/DropdownScroll.tsx
new file mode 100644
index 00000000000..fe874e5fcc8
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownScroll/DropdownScroll.tsx
@@ -0,0 +1,85 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+export function DropdownScroll() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownScroll/code.json b/docs/combobox-examples/examples/DropdownScroll/code.json
new file mode 100644
index 00000000000..e437c12da2e
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownScroll/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownScroll.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nexport function DropdownScroll() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownScrollArea/DropdownScrollArea.tsx b/docs/combobox-examples/examples/DropdownScrollArea/DropdownScrollArea.tsx
new file mode 100644
index 00000000000..a2d06c6a93b
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownScrollArea/DropdownScrollArea.tsx
@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, ScrollArea, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+export function DropdownScrollArea() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownScrollArea/code.json b/docs/combobox-examples/examples/DropdownScrollArea/code.json
new file mode 100644
index 00000000000..74adb35c06c
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownScrollArea/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownScrollArea.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, ScrollArea, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nexport function DropdownScrollArea() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/DropdownSmoothScroll/DropdownSmoothScroll.tsx b/docs/combobox-examples/examples/DropdownSmoothScroll/DropdownSmoothScroll.tsx
new file mode 100644
index 00000000000..d6489c86c14
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownSmoothScroll/DropdownSmoothScroll.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from 'react';
+import { Combobox, TextInput, ScrollArea, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+ '🍋 Lemon',
+ '🥬 Lettuce',
+ '🍄 Mushrooms',
+ '🍊 Oranges',
+ '🥔 Potatoes',
+ '🍅 Tomatoes',
+ '🥚 Eggs',
+ '🥛 Milk',
+ '🍞 Bread',
+ '🍗 Chicken',
+ '🍔 Hamburger',
+ '🧀 Cheese',
+ '🥩 Steak',
+ '🍟 French Fries',
+ '🍕 Pizza',
+ '🥦 Cauliflower',
+ '🥜 Peanuts',
+ '🍦 Ice Cream',
+ '🍯 Honey',
+ '🥖 Baguette',
+ '🍣 Sushi',
+ '🥝 Kiwi',
+ '🍓 Strawberries',
+];
+
+export function DropdownSmoothScroll() {
+ const combobox = useCombobox({
+ scrollBehavior: 'smooth',
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState('');
+ const shouldFilterOptions = !groceries.some((item) => item === value);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(optionValue);
+ combobox.closeDropdown();
+ }}
+ store={combobox}
+ withinPortal={false}
+ >
+
+ {
+ setValue(event.currentTarget.value);
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ />
+
+
+
+
+
+ {options.length === 0 ? Nothing found : options}
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/DropdownSmoothScroll/code.json b/docs/combobox-examples/examples/DropdownSmoothScroll/code.json
new file mode 100644
index 00000000000..c5f8983c203
--- /dev/null
+++ b/docs/combobox-examples/examples/DropdownSmoothScroll/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "DropdownSmoothScroll.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, TextInput, ScrollArea, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n '🍋 Lemon',\n '🥬 Lettuce',\n '🍄 Mushrooms',\n '🍊 Oranges',\n '🥔 Potatoes',\n '🍅 Tomatoes',\n '🥚 Eggs',\n '🥛 Milk',\n '🍞 Bread',\n '🍗 Chicken',\n '🍔 Hamburger',\n '🧀 Cheese',\n '🥩 Steak',\n '🍟 French Fries',\n '🍕 Pizza',\n '🥦 Cauliflower',\n '🥜 Peanuts',\n '🍦 Ice Cream',\n '🍯 Honey',\n '🥖 Baguette',\n '🍣 Sushi',\n '🥝 Kiwi',\n '🍓 Strawberries',\n];\n\nexport function DropdownSmoothScroll() {\n const combobox = useCombobox({\n scrollBehavior: 'smooth',\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState('');\n const shouldFilterOptions = !groceries.some((item) => item === value);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(value.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(optionValue);\n combobox.closeDropdown();\n }}\n store={combobox}\n withinPortal={false}\n >\n \n {\n setValue(event.currentTarget.value);\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n />\n \n\n \n \n \n {options.length === 0 ? Nothing found : options}\n \n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MaxDisplayedItems/MaxDisplayedItems.tsx b/docs/combobox-examples/examples/MaxDisplayedItems/MaxDisplayedItems.tsx
new file mode 100644
index 00000000000..70c775bdeef
--- /dev/null
+++ b/docs/combobox-examples/examples/MaxDisplayedItems/MaxDisplayedItems.tsx
@@ -0,0 +1,81 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+const MAX_DISPLAYED_VALUES = 2;
+
+export function MaxDisplayedItems() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value
+ .slice(
+ 0,
+ MAX_DISPLAYED_VALUES === value.length ? MAX_DISPLAYED_VALUES : MAX_DISPLAYED_VALUES - 1
+ )
+ .map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries.map((item) => (
+
+
+ {value.includes(item) ? : null}
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {value.length > 0 ? (
+ <>
+ {values}
+ {value.length > MAX_DISPLAYED_VALUES && (
+ +{value.length - (MAX_DISPLAYED_VALUES - 1)} more
+ )}
+ >
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MaxDisplayedItems/code.json b/docs/combobox-examples/examples/MaxDisplayedItems/code.json
new file mode 100644
index 00000000000..451f9f11457
--- /dev/null
+++ b/docs/combobox-examples/examples/MaxDisplayedItems/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "MaxDisplayedItems.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nconst MAX_DISPLAYED_VALUES = 2;\n\nexport function MaxDisplayedItems() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value\n .slice(\n 0,\n MAX_DISPLAYED_VALUES === value.length ? MAX_DISPLAYED_VALUES : MAX_DISPLAYED_VALUES - 1\n )\n .map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries.map((item) => (\n \n \n {value.includes(item) ? : null}\n {item} \n \n \n ));\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {value.length > 0 ? (\n <>\n {values}\n {value.length > MAX_DISPLAYED_VALUES && (\n +{value.length - (MAX_DISPLAYED_VALUES - 1)} more \n )}\n >\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MaxSelectedItems/MaxSelectedItems.tsx b/docs/combobox-examples/examples/MaxSelectedItems/MaxSelectedItems.tsx
new file mode 100644
index 00000000000..e6fc039ad1e
--- /dev/null
+++ b/docs/combobox-examples/examples/MaxSelectedItems/MaxSelectedItems.tsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+const ITEMS_LIMIT = 2;
+
+export function MaxSelectedItems() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries.map((item) => (
+ = ITEMS_LIMIT && !value.includes(item)}
+ >
+
+ {value.includes(item) ? : null}
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {values.length > 0 ? (
+ values
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ You can select up to 2 items, currently selected: {value.length}
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MaxSelectedItems/code.json b/docs/combobox-examples/examples/MaxSelectedItems/code.json
new file mode 100644
index 00000000000..2f50dd33886
--- /dev/null
+++ b/docs/combobox-examples/examples/MaxSelectedItems/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "MaxSelectedItems.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nconst ITEMS_LIMIT = 2;\n\nexport function MaxSelectedItems() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries.map((item) => (\n = ITEMS_LIMIT && !value.includes(item)}\n >\n \n {value.includes(item) ? : null}\n {item} \n \n \n ));\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {values.length > 0 ? (\n values\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n \n You can select up to 2 items, currently selected: {value.length}\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MultiSelectCheckbox/MultiSelectCheckbox.tsx b/docs/combobox-examples/examples/MultiSelectCheckbox/MultiSelectCheckbox.tsx
new file mode 100644
index 00000000000..a9e2625a3f2
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectCheckbox/MultiSelectCheckbox.tsx
@@ -0,0 +1,75 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, Group, useCombobox, Checkbox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function MultiSelectCheckbox() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries.map((item) => (
+
+
+ {}}
+ aria-hidden
+ tabIndex={-1}
+ style={{ pointerEvents: 'none' }}
+ />
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {values.length > 0 ? (
+ values
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MultiSelectCheckbox/code.json b/docs/combobox-examples/examples/MultiSelectCheckbox/code.json
new file mode 100644
index 00000000000..4796d65a7b3
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectCheckbox/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "MultiSelectCheckbox.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, Group, useCombobox, Checkbox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function MultiSelectCheckbox() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries.map((item) => (\n \n \n {}}\n aria-hidden\n tabIndex={-1}\n style={{ pointerEvents: 'none' }}\n />\n {item} \n \n \n ));\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {values.length > 0 ? (\n values\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MultiSelectCreatable/MultiSelectCreatable.tsx b/docs/combobox-examples/examples/MultiSelectCreatable/MultiSelectCreatable.tsx
new file mode 100644
index 00000000000..36d3b4d4118
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectCreatable/MultiSelectCreatable.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function MultiSelectCreatable() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [search, setSearch] = useState('');
+ const [data, setData] = useState(groceries);
+ const [value, setValue] = useState([]);
+
+ const exactOptionMatch = data.some((item) => item === search);
+
+ const handleValueSelect = (val: string) => {
+ if (val === '$create') {
+ setData((current) => [...current, search]);
+ setValue((current) => [...current, search]);
+ setSearch('');
+ } else {
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+ }
+ };
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries
+ .filter((item) => item.toLowerCase().includes(search.trim().toLowerCase()))
+ .map((item) => (
+
+
+ {value.includes(item) ? : null}
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.openDropdown()}>
+
+ {values}
+
+
+ combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ value={search}
+ placeholder="Search values"
+ onChange={(event) => {
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace' && search.length === 0) {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ {options}
+
+ {!exactOptionMatch && search.trim().length > 0 && (
+ + Create {search}
+ )}
+
+ {exactOptionMatch && search.trim().length > 0 && options.length === 0 && (
+ Nothing found
+ )}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MultiSelectCreatable/code.json b/docs/combobox-examples/examples/MultiSelectCreatable/code.json
new file mode 100644
index 00000000000..5f633dfb588
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectCreatable/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "MultiSelectCreatable.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function MultiSelectCreatable() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [search, setSearch] = useState('');\n const [data, setData] = useState(groceries);\n const [value, setValue] = useState([]);\n\n const exactOptionMatch = data.some((item) => item === search);\n\n const handleValueSelect = (val: string) => {\n if (val === '$create') {\n setData((current) => [...current, search]);\n setValue((current) => [...current, search]);\n setSearch('');\n } else {\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n }\n };\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries\n .filter((item) => item.toLowerCase().includes(search.trim().toLowerCase()))\n .map((item) => (\n \n \n {value.includes(item) ? : null}\n {item} \n \n \n ));\n\n return (\n \n \n combobox.openDropdown()}>\n \n {values}\n\n \n combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n value={search}\n placeholder=\"Search values\"\n onChange={(event) => {\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onKeyDown={(event) => {\n if (event.key === 'Backspace' && search.length === 0) {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n \n {options}\n\n {!exactOptionMatch && search.trim().length > 0 && (\n + Create {search} \n )}\n\n {exactOptionMatch && search.trim().length > 0 && options.length === 0 && (\n Nothing found \n )}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.module.css b/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.module.css
new file mode 100644
index 00000000000..f2c9ca1c0ce
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.module.css
@@ -0,0 +1,18 @@
+.pill {
+ display: flex;
+ align-items: center;
+ cursor: default;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-7));
+ padding-left: var(--mantine-spacing-xs);
+ border-radius: var(--mantine-radius-xl);
+}
+
+.label {
+ line-height: 1;
+ font-size: var(--mantine-font-size-xs);
+}
+
+.flag {
+ margin-right: var(--mantine-spacing-xs);
+}
diff --git a/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.tsx b/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.tsx
new file mode 100644
index 00000000000..da00ee5d869
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectValueRenderer/CountryPill.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { CloseButton } from '@mantine/core';
+import { countriesData, flags } from './countries-data';
+import classes from './CountryPill.module.css';
+
+interface CountryPillProps extends React.ComponentPropsWithoutRef<'div'> {
+ value: string;
+ onRemove?(): void;
+}
+
+export function CountryPill({ value, onRemove, ...others }: CountryPillProps) {
+ const OptionFlag = flags[value];
+ const country = countriesData.find((item) => item.value === value);
+
+ return (
+
+
+
+
+
{country?.label}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MultiSelectValueRenderer/MultiSelectValueRenderer.tsx b/docs/combobox-examples/examples/MultiSelectValueRenderer/MultiSelectValueRenderer.tsx
new file mode 100644
index 00000000000..3137ae1cd9f
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectValueRenderer/MultiSelectValueRenderer.tsx
@@ -0,0 +1,75 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+import { countriesData, flags } from './countries-data';
+import { CountryPill } from './CountryPill';
+
+export function MultiSelectValueRenderer() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = countriesData.map((item) => {
+ const OptionFlag = flags[item.value];
+ return (
+
+
+ {value.includes(item.value) ? : null}
+
+
+ {item.label}
+
+
+
+ );
+ });
+
+ return (
+
+
+ combobox.toggleDropdown()}>
+
+ {values.length > 0 ? (
+ values
+ ) : (
+ Pick one or more values
+ )}
+
+
+ combobox.closeDropdown()}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace') {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/MultiSelectValueRenderer/code.json b/docs/combobox-examples/examples/MultiSelectValueRenderer/code.json
new file mode 100644
index 00000000000..07e6b26a180
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectValueRenderer/code.json
@@ -0,0 +1,22 @@
+[
+ {
+ "fileName": "MultiSelectValueRenderer.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Input, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\nimport { countriesData, flags } from './countries-data';\nimport { CountryPill } from './CountryPill';\n\nexport function MultiSelectValueRenderer() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = countriesData.map((item) => {\n const OptionFlag = flags[item.value];\n return (\n \n \n {value.includes(item.value) ? : null}\n \n \n {item.label} \n \n \n \n );\n });\n\n return (\n \n \n combobox.toggleDropdown()}>\n \n {values.length > 0 ? (\n values\n ) : (\n Pick one or more values \n )}\n\n \n combobox.closeDropdown()}\n onKeyDown={(event) => {\n if (event.key === 'Backspace') {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n {options} \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "CountryPill.module.css",
+ "language": "css",
+ "code": ".pill {\n display: flex;\n align-items: center;\n cursor: default;\n background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));\n border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-7));\n padding-left: var(--mantine-spacing-xs);\n border-radius: var(--mantine-radius-xl);\n}\n\n.label {\n line-height: 1;\n font-size: var(--mantine-font-size-xs);\n}\n\n.flag {\n margin-right: var(--mantine-spacing-xs);\n}\n"
+ },
+ {
+ "fileName": "CountryPill.tsx",
+ "language": "tsx",
+ "code": "import { CloseButton } from '@mantine/core';\nimport { countriesData, flags } from './countries-data';\nimport classes from './CountryPill.module.css';\n\ninterface CountryPillProps extends React.ComponentPropsWithoutRef<'div'> {\n value: string;\n onRemove?(): void;\n}\n\nexport function CountryPill({ value, onRemove, ...others }: CountryPillProps) {\n const OptionFlag = flags[value];\n const country = countriesData.find((item) => item.value === value);\n\n return (\n \n
\n \n
\n
{country?.label}
\n
\n
\n );\n}\n"
+ },
+ {
+ "fileName": "countries-data.tsx",
+ "language": "tsx",
+ "code": "import { rem } from '@mantine/core';\n\nconst flagProps = {\n xmlns: 'http://www.w3.org/2000/svg',\n viewBox: '0 0 640 480',\n style: { display: 'block', width: rem(16) },\n};\n\nfunction UsFlag() {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\nfunction GbFlag() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction FiFlag() {\n return (\n \n \n \n \n \n );\n}\n\nfunction FrFlag() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction RuFlag() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nexport const countriesData = [\n { label: 'United States', value: 'US' },\n { label: 'Great Britain', value: 'GB' },\n { label: 'Finland', value: 'FI' },\n { label: 'France', value: 'FR' },\n { label: 'Russia', value: 'RU' },\n];\n\nexport const flags: Record = {\n US: UsFlag,\n GB: GbFlag,\n FI: FiFlag,\n FR: FrFlag,\n RU: RuFlag,\n};\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/MultiSelectValueRenderer/countries-data.tsx b/docs/combobox-examples/examples/MultiSelectValueRenderer/countries-data.tsx
new file mode 100644
index 00000000000..fafb8aeb7ba
--- /dev/null
+++ b/docs/combobox-examples/examples/MultiSelectValueRenderer/countries-data.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import { rem } from '@mantine/core';
+
+const flagProps = {
+ xmlns: 'http://www.w3.org/2000/svg',
+ viewBox: '0 0 640 480',
+ style: { display: 'block', width: rem(16) },
+};
+
+function UsFlag() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function GbFlag() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function FiFlag() {
+ return (
+
+
+
+
+
+ );
+}
+
+function FrFlag() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function RuFlag() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export const countriesData = [
+ { label: 'United States', value: 'US' },
+ { label: 'Great Britain', value: 'GB' },
+ { label: 'Finland', value: 'FI' },
+ { label: 'France', value: 'FR' },
+ { label: 'Russia', value: 'RU' },
+];
+
+export const flags: Record = {
+ US: UsFlag,
+ GB: GbFlag,
+ FI: FiFlag,
+ FR: FrFlag,
+ RU: RuFlag,
+};
diff --git a/docs/combobox-examples/examples/SearchableMultiSelect/SearchableMultiSelect.tsx b/docs/combobox-examples/examples/SearchableMultiSelect/SearchableMultiSelect.tsx
new file mode 100644
index 00000000000..f54b7ab24a4
--- /dev/null
+++ b/docs/combobox-examples/examples/SearchableMultiSelect/SearchableMultiSelect.tsx
@@ -0,0 +1,76 @@
+import React, { useState } from 'react';
+import { PillsInput, Pill, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function SearchableMultiSelect() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
+ });
+
+ const [search, setSearch] = useState('');
+ const [value, setValue] = useState([]);
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const handleValueRemove = (val: string) =>
+ setValue((current) => current.filter((v) => v !== val));
+
+ const values = value.map((item) => (
+ handleValueRemove(item)}>
+ {item}
+
+ ));
+
+ const options = groceries
+ .filter((item) => item.toLowerCase().includes(search.trim().toLowerCase()))
+ .map((item) => (
+
+
+ {value.includes(item) ? : null}
+ {item}
+
+
+ ));
+
+ return (
+
+
+ combobox.openDropdown()}>
+
+ {values}
+
+
+ combobox.openDropdown()}
+ onBlur={() => combobox.closeDropdown()}
+ value={search}
+ placeholder="Search values"
+ onChange={(event) => {
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === 'Backspace' && search.length === 0) {
+ event.preventDefault();
+ handleValueRemove(value[value.length - 1]);
+ }
+ }}
+ />
+
+
+
+
+
+
+
+ {options.length > 0 ? options : Nothing found... }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SearchableMultiSelect/code.json b/docs/combobox-examples/examples/SearchableMultiSelect/code.json
new file mode 100644
index 00000000000..cf4773892a7
--- /dev/null
+++ b/docs/combobox-examples/examples/SearchableMultiSelect/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SearchableMultiSelect.tsx",
+ "language": "tsx",
+ "code": "\nimport { PillsInput, Pill, Combobox, CheckIcon, Group, useCombobox } from '@mantine/core';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function SearchableMultiSelect() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),\n });\n\n const [search, setSearch] = useState('');\n const [value, setValue] = useState([]);\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const handleValueRemove = (val: string) =>\n setValue((current) => current.filter((v) => v !== val));\n\n const values = value.map((item) => (\n handleValueRemove(item)}>\n {item}\n \n ));\n\n const options = groceries\n .filter((item) => item.toLowerCase().includes(search.trim().toLowerCase()))\n .map((item) => (\n \n \n {value.includes(item) ? : null}\n {item} \n \n \n ));\n\n return (\n \n \n combobox.openDropdown()}>\n \n {values}\n\n \n combobox.openDropdown()}\n onBlur={() => combobox.closeDropdown()}\n value={search}\n placeholder=\"Search values\"\n onChange={(event) => {\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onKeyDown={(event) => {\n if (event.key === 'Backspace' && search.length === 0) {\n event.preventDefault();\n handleValueRemove(value[value.length - 1]);\n }\n }}\n />\n \n \n \n \n\n \n \n {options.length > 0 ? options : Nothing found... }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SearchableSelect/SearchableSelect.tsx b/docs/combobox-examples/examples/SearchableSelect/SearchableSelect.tsx
new file mode 100644
index 00000000000..6ab020520cd
--- /dev/null
+++ b/docs/combobox-examples/examples/SearchableSelect/SearchableSelect.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SearchableSelect() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+ const [search, setSearch] = useState('');
+
+ const shouldFilterOptions = groceries.every((item) => item !== search);
+ const filteredOptions = shouldFilterOptions
+ ? groceries.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ : groceries;
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ setSearch(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ value={search}
+ onChange={(event) => {
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => {
+ combobox.closeDropdown();
+ setSearch(value || '');
+ }}
+ placeholder="Search value"
+ rightSectionPointerEvents="none"
+ />
+
+
+
+
+ {options.length > 0 ? options : Nothing found }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SearchableSelect/code.json b/docs/combobox-examples/examples/SearchableSelect/code.json
new file mode 100644
index 00000000000..662aa275e04
--- /dev/null
+++ b/docs/combobox-examples/examples/SearchableSelect/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SearchableSelect.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SearchableSelect() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n const [search, setSearch] = useState('');\n\n const shouldFilterOptions = groceries.every((item) => item !== search);\n const filteredOptions = shouldFilterOptions\n ? groceries.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n : groceries;\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n setSearch(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n value={search}\n onChange={(event) => {\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => {\n combobox.closeDropdown();\n setSearch(value || '');\n }}\n placeholder=\"Search value\"\n rightSectionPointerEvents=\"none\"\n />\n \n\n \n \n {options.length > 0 ? options : Nothing found }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectActive/SelectActive.tsx b/docs/combobox-examples/examples/SelectActive/SelectActive.tsx
new file mode 100644
index 00000000000..2906fd0e524
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectActive/SelectActive.tsx
@@ -0,0 +1,62 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, Group, CheckIcon, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectActive() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: (eventSource) => {
+ if (eventSource === 'keyboard') {
+ combobox.selectActiveOption();
+ } else {
+ combobox.updateSelectedOptionIndex('active');
+ }
+ },
+ });
+
+ const [value, setValue] = useState('🥦 Broccoli');
+
+ const options = groceries.map((item) => (
+
+
+ {item === value && }
+ {item}
+
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.updateSelectedOptionIndex('active');
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectActive/code.json b/docs/combobox-examples/examples/SelectActive/code.json
new file mode 100644
index 00000000000..5ad409133b0
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectActive/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectActive.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, Group, CheckIcon, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectActive() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: (eventSource) => {\n if (eventSource === 'keyboard') {\n combobox.selectActiveOption();\n } else {\n combobox.updateSelectedOptionIndex('active');\n }\n },\n });\n\n const [value, setValue] = useState('🥦 Broccoli');\n\n const options = groceries.map((item) => (\n \n \n {item === value && }\n {item} \n \n \n ));\n\n return (\n {\n setValue(val);\n combobox.updateSelectedOptionIndex('active');\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectAsync/SelectAsync.tsx b/docs/combobox-examples/examples/SelectAsync/SelectAsync.tsx
new file mode 100644
index 00000000000..86b485e4e28
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectAsync/SelectAsync.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, Loader, useCombobox } from '@mantine/core';
+
+const MOCKDATA = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+function getAsyncData() {
+ return new Promise((resolve) => {
+ setTimeout(() => resolve(MOCKDATA), 2000);
+ });
+}
+
+export function SelectAsync() {
+ const [value, setValue] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [data, setData] = useState([]);
+
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ onDropdownOpen: () => {
+ if (data.length === 0 && !loading) {
+ setLoading(true);
+ getAsyncData().then((response) => {
+ setData(response);
+ setLoading(false);
+ combobox.resetSelectedOption();
+ });
+ }
+ },
+ });
+
+ const options = data.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ : }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+
+ {loading ? Loading.... : options}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectAsync/code.json b/docs/combobox-examples/examples/SelectAsync/code.json
new file mode 100644
index 00000000000..843c74d7097
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectAsync/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectAsync.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, Loader, useCombobox } from '@mantine/core';\n\nconst MOCKDATA = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nfunction getAsyncData() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(MOCKDATA), 2000);\n });\n}\n\nexport function SelectAsync() {\n const [value, setValue] = useState(null);\n const [loading, setLoading] = useState(false);\n const [data, setData] = useState([]);\n\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n onDropdownOpen: () => {\n if (data.length === 0 && !loading) {\n setLoading(true);\n getAsyncData().then((response) => {\n setData(response);\n setLoading(false);\n combobox.resetSelectedOption();\n });\n }\n },\n });\n\n const options = data.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n : }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n \n {loading ? Loading.... : options}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectClearable/SelectClearable.tsx b/docs/combobox-examples/examples/SelectClearable/SelectClearable.tsx
new file mode 100644
index 00000000000..8b989c177b6
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectClearable/SelectClearable.tsx
@@ -0,0 +1,63 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, CloseButton, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectClearable() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ event.preventDefault()}
+ onClick={() => setValue(null)}
+ aria-label="Clear value"
+ />
+ ) : (
+
+ )
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents={value === null ? 'none' : 'all'}
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectClearable/code.json b/docs/combobox-examples/examples/SelectClearable/code.json
new file mode 100644
index 00000000000..49ff1c0c39a
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectClearable/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectClearable.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, CloseButton, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectClearable() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n event.preventDefault()}\n onClick={() => setValue(null)}\n aria-label=\"Clear value\"\n />\n ) : (\n \n )\n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents={value === null ? 'none' : 'all'}\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectCreatable/SelectCreatable.tsx b/docs/combobox-examples/examples/SelectCreatable/SelectCreatable.tsx
new file mode 100644
index 00000000000..84c055f8ffd
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectCreatable/SelectCreatable.tsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectCreatable() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [data, setData] = useState(groceries);
+ const [value, setValue] = useState(null);
+ const [search, setSearch] = useState('');
+
+ const exactOptionMatch = data.some((item) => item === search);
+ const filteredOptions = exactOptionMatch
+ ? data
+ : data.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()));
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ if (val === '$create') {
+ setData((current) => [...current, search]);
+ setValue(search);
+ } else {
+ setValue(val);
+ setSearch(val);
+ }
+
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ value={search}
+ onChange={(event) => {
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => {
+ combobox.closeDropdown();
+ setSearch(value || '');
+ }}
+ placeholder="Search value"
+ rightSectionPointerEvents="none"
+ />
+
+
+
+
+ {options}
+ {!exactOptionMatch && search.trim().length > 0 && (
+ + Create {search}
+ )}
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectCreatable/code.json b/docs/combobox-examples/examples/SelectCreatable/code.json
new file mode 100644
index 00000000000..1aeba85e236
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectCreatable/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectCreatable.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectCreatable() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [data, setData] = useState(groceries);\n const [value, setValue] = useState(null);\n const [search, setSearch] = useState('');\n\n const exactOptionMatch = data.some((item) => item === search);\n const filteredOptions = exactOptionMatch\n ? data\n : data.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()));\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n if (val === '$create') {\n setData((current) => [...current, search]);\n setValue(search);\n } else {\n setValue(val);\n setSearch(val);\n }\n\n combobox.closeDropdown();\n }}\n >\n \n }\n value={search}\n onChange={(event) => {\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => {\n combobox.closeDropdown();\n setSearch(value || '');\n }}\n placeholder=\"Search value\"\n rightSectionPointerEvents=\"none\"\n />\n \n\n \n \n {options}\n {!exactOptionMatch && search.trim().length > 0 && (\n + Create {search} \n )}\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectDropdownSearch/SelectDropdownSearch.tsx b/docs/combobox-examples/examples/SelectDropdownSearch/SelectDropdownSearch.tsx
new file mode 100644
index 00000000000..0461a38a4d9
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectDropdownSearch/SelectDropdownSearch.tsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectDropdownSearch() {
+ const [search, setSearch] = useState('');
+ const combobox = useCombobox({
+ onDropdownClose: () => {
+ combobox.resetSelectedOption();
+ combobox.focusTarget();
+ setSearch('');
+ },
+
+ onDropdownOpen: () => {
+ combobox.focusSearchInput();
+ },
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries
+ .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ .map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+ setSearch(event.currentTarget.value)}
+ placeholder="Search groceries"
+ />
+
+ {options.length > 0 ? options : Nothing found }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectDropdownSearch/code.json b/docs/combobox-examples/examples/SelectDropdownSearch/code.json
new file mode 100644
index 00000000000..fbea3443369
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectDropdownSearch/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectDropdownSearch.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectDropdownSearch() {\n const [search, setSearch] = useState('');\n const combobox = useCombobox({\n onDropdownClose: () => {\n combobox.resetSelectedOption();\n combobox.focusTarget();\n setSearch('');\n },\n\n onDropdownOpen: () => {\n combobox.focusSearchInput();\n },\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries\n .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n .map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n setSearch(event.currentTarget.value)}\n placeholder=\"Search groceries\"\n />\n \n {options.length > 0 ? options : Nothing found }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectGroups/SelectGroups.tsx b/docs/combobox-examples/examples/SelectGroups/SelectGroups.tsx
new file mode 100644
index 00000000000..77f4bf8a69f
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroups/SelectGroups.tsx
@@ -0,0 +1,48 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+
+export function SelectGroups() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+
+
+ 🍎 Apples
+ 🍌 Bananas
+ 🍇 Grapes
+
+
+
+ 🥦 Broccoli
+ 🥕 Carrots
+
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectGroups/code.json b/docs/combobox-examples/examples/SelectGroups/code.json
new file mode 100644
index 00000000000..b4f4ce301fe
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroups/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectGroups.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\n\nexport function SelectGroups() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n \n \n 🍎 Apples \n 🍌 Bananas \n 🍇 Grapes \n \n\n \n 🥦 Broccoli \n 🥕 Carrots \n \n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectGroupsSearchable/SelectGroupsSearchable.tsx b/docs/combobox-examples/examples/SelectGroupsSearchable/SelectGroupsSearchable.tsx
new file mode 100644
index 00000000000..a08d1092203
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroupsSearchable/SelectGroupsSearchable.tsx
@@ -0,0 +1,81 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, useCombobox } from '@mantine/core';
+
+const groceries = [
+ { label: 'Fruits', options: ['🍎 Apples', '🍌 Bananas', '🍇 Grapes'] },
+ { label: 'Vegetables', options: ['🥦 Broccoli', '🥕 Carrots'] },
+];
+
+const allGroceries = groceries.reduce((acc, group) => [...acc, ...group.options], []);
+
+export function SelectGroupsSearchable() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+ const [search, setSearch] = useState('');
+
+ const shouldFilterOptions = allGroceries.every((item) => item !== search);
+ const filteredGroups = groceries.map((group) => {
+ const filteredOptions = shouldFilterOptions
+ ? group.options.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ : group.options;
+
+ return { ...group, options: filteredOptions };
+ });
+
+ const totalOptions = filteredGroups.reduce((acc, group) => acc + group.options.length, 0);
+
+ const groups = filteredGroups.map((group) => {
+ const options = group.options.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+
+ {options}
+
+ );
+ });
+
+ return (
+ {
+ setValue(val);
+ setSearch(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ value={search}
+ onChange={(event) => {
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => {
+ combobox.closeDropdown();
+ setSearch(value || '');
+ }}
+ placeholder="Search value"
+ rightSectionPointerEvents="none"
+ />
+
+
+
+
+ {totalOptions > 0 ? groups : Nothing found }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectGroupsSearchable/code.json b/docs/combobox-examples/examples/SelectGroupsSearchable/code.json
new file mode 100644
index 00000000000..3c469c625f4
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroupsSearchable/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectGroupsSearchable.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, useCombobox } from '@mantine/core';\n\nconst groceries = [\n { label: 'Fruits', options: ['🍎 Apples', '🍌 Bananas', '🍇 Grapes'] },\n { label: 'Vegetables', options: ['🥦 Broccoli', '🥕 Carrots'] },\n];\n\nconst allGroceries = groceries.reduce((acc, group) => [...acc, ...group.options], []);\n\nexport function SelectGroupsSearchable() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n const [search, setSearch] = useState('');\n\n const shouldFilterOptions = allGroceries.every((item) => item !== search);\n const filteredGroups = groceries.map((group) => {\n const filteredOptions = shouldFilterOptions\n ? group.options.filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n : group.options;\n\n return { ...group, options: filteredOptions };\n });\n\n const totalOptions = filteredGroups.reduce((acc, group) => acc + group.options.length, 0);\n\n const groups = filteredGroups.map((group) => {\n const options = group.options.map((item) => (\n \n {item}\n \n ));\n\n return (\n \n {options}\n \n );\n });\n\n return (\n {\n setValue(val);\n setSearch(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n value={search}\n onChange={(event) => {\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => {\n combobox.closeDropdown();\n setSearch(value || '');\n }}\n placeholder=\"Search value\"\n rightSectionPointerEvents=\"none\"\n />\n \n\n \n \n {totalOptions > 0 ? groups : Nothing found }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.module.css b/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.module.css
new file mode 100644
index 00000000000..7bd997281d0
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.module.css
@@ -0,0 +1,13 @@
+.group {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ border-radius: var(--mantine-radius-sm);
+ margin-bottom: var(--mantine-spacing-xs);
+}
+
+.groupLabel {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+
+ &::after {
+ display: none;
+ }
+}
diff --git a/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.tsx b/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.tsx
new file mode 100644
index 00000000000..030dd6f11be
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroupsStyles/SelectGroupsStyles.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+import classes from './SelectGroupsStyles.module.css';
+
+export function SelectGroupsStyles() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+
+
+ 🍎 Apples
+ 🍌 Bananas
+
+
+
+ 🥦 Broccoli
+ 🥕 Carrots
+
+
+ 🥩 Steak
+ 🍗 Chicken
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectGroupsStyles/code.json b/docs/combobox-examples/examples/SelectGroupsStyles/code.json
new file mode 100644
index 00000000000..cbd50cbff48
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectGroupsStyles/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "SelectGroupsStyles.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\nimport classes from './SelectGroupsStyles.module.css';\n\nexport function SelectGroupsStyles() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n \n \n 🍎 Apples \n 🍌 Bananas \n \n\n \n 🥦 Broccoli \n 🥕 Carrots \n \n\n 🥩 Steak \n 🍗 Chicken \n \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "SelectGroupsStyles.module.css",
+ "language": "css",
+ "code": ".group {\n background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));\n border-radius: var(--mantine-radius-sm);\n margin-bottom: var(--mantine-spacing-xs);\n}\n\n.groupLabel {\n color: light-dark(var(--mantine-color-black), var(--mantine-color-white));\n\n &::after {\n display: none;\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectLimit/SelectLimit.tsx b/docs/combobox-examples/examples/SelectLimit/SelectLimit.tsx
new file mode 100644
index 00000000000..bd316e14028
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectLimit/SelectLimit.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, useCombobox } from '@mantine/core';
+
+const MOCKDATA = Array(100000)
+ .fill(0)
+ .map((_, index) => `Item ${index}`);
+
+function getFilteredOptions(data: string[], searchQuery: string, limit: number) {
+ const result: string[] = [];
+
+ for (let i = 0; i < data.length; i += 1) {
+ if (result.length === limit) {
+ break;
+ }
+
+ if (data[i].toLowerCase().includes(searchQuery.trim().toLowerCase())) {
+ result.push(data[i]);
+ }
+ }
+
+ return result;
+}
+
+export function SelectLimit() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+ const [search, setSearch] = useState('');
+
+ const filteredOptions = getFilteredOptions(MOCKDATA, search, 7);
+
+ const options = filteredOptions.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ setSearch(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ value={search}
+ onChange={(event) => {
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={() => combobox.openDropdown()}
+ onBlur={() => {
+ combobox.closeDropdown();
+ setSearch(value || '');
+ }}
+ placeholder="Search value"
+ rightSectionPointerEvents="none"
+ />
+
+
+
+
+ {options.length > 0 ? options : Nothing found }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectLimit/code.json b/docs/combobox-examples/examples/SelectLimit/code.json
new file mode 100644
index 00000000000..626945a8db3
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectLimit/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectLimit.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, useCombobox } from '@mantine/core';\n\nconst MOCKDATA = Array(100000)\n .fill(0)\n .map((_, index) => `Item ${index}`);\n\nfunction getFilteredOptions(data: string[], searchQuery: string, limit: number) {\n const result: string[] = [];\n\n for (let i = 0; i < data.length; i += 1) {\n if (result.length === limit) {\n break;\n }\n\n if (data[i].toLowerCase().includes(searchQuery.trim().toLowerCase())) {\n result.push(data[i]);\n }\n }\n\n return result;\n}\n\nexport function SelectLimit() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n const [search, setSearch] = useState('');\n\n const filteredOptions = getFilteredOptions(MOCKDATA, search, 7);\n\n const options = filteredOptions.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n setSearch(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n value={search}\n onChange={(event) => {\n combobox.openDropdown();\n combobox.updateSelectedOptionIndex();\n setSearch(event.currentTarget.value);\n }}\n onClick={() => combobox.openDropdown()}\n onFocus={() => combobox.openDropdown()}\n onBlur={() => {\n combobox.closeDropdown();\n setSearch(value || '');\n }}\n placeholder=\"Search value\"\n rightSectionPointerEvents=\"none\"\n />\n \n\n \n \n {options.length > 0 ? options : Nothing found }\n \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectList/SelectList.module.css b/docs/combobox-examples/examples/SelectList/SelectList.module.css
new file mode 100644
index 00000000000..2301c870aea
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectList/SelectList.module.css
@@ -0,0 +1,14 @@
+.list {
+ background-color: var(--mantine-color-body);
+ padding: var(--mantine-spacing-xs) rem(6px);
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
+ border-top: 0;
+ border-bottom-left-radius: var(--mantine-radius-md);
+ border-bottom-right-radius: var(--mantine-radius-md);
+ min-height: rem(190px);
+}
+
+.input {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
diff --git a/docs/combobox-examples/examples/SelectList/SelectList.tsx b/docs/combobox-examples/examples/SelectList/SelectList.tsx
new file mode 100644
index 00000000000..e22ff58d019
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectList/SelectList.tsx
@@ -0,0 +1,61 @@
+import React, { useState } from 'react';
+import { TextInput, Combobox, useCombobox, Checkbox, Group } from '@mantine/core';
+import classes from './SelectList.module.css';
+
+const groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];
+
+export function SelectList() {
+ const combobox = useCombobox();
+
+ const [value, setValue] = useState([]);
+ const [search, setSearch] = useState('');
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const options = groceries
+ .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ .map((item) => (
+ combobox.resetSelectedOption()}
+ >
+
+ {}}
+ aria-hidden
+ tabIndex={-1}
+ style={{ pointerEvents: 'none' }}
+ />
+ {item}
+
+
+ ));
+
+ return (
+
+
+ {
+ setSearch(event.currentTarget.value);
+ combobox.updateSelectedOptionIndex();
+ }}
+ />
+
+
+
+
+ {options.length > 0 ? options : Nothing found.... }
+
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectList/code.json b/docs/combobox-examples/examples/SelectList/code.json
new file mode 100644
index 00000000000..6d2c2e95535
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectList/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "SelectList.tsx",
+ "language": "tsx",
+ "code": "\nimport { TextInput, Combobox, useCombobox, Checkbox, Group } from '@mantine/core';\nimport classes from './SelectList.module.css';\n\nconst groceries = ['🍎 Apples', '🍌 Bananas', '🥦 Broccoli', '🥕 Carrots', '🍫 Chocolate'];\n\nexport function SelectList() {\n const combobox = useCombobox();\n\n const [value, setValue] = useState([]);\n const [search, setSearch] = useState('');\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const options = groceries\n .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n .map((item) => (\n combobox.resetSelectedOption()}\n >\n \n {}}\n aria-hidden\n tabIndex={-1}\n style={{ pointerEvents: 'none' }}\n />\n {item} \n \n \n ));\n\n return (\n \n \n {\n setSearch(event.currentTarget.value);\n combobox.updateSelectedOptionIndex();\n }}\n />\n \n\n \n \n {options.length > 0 ? options : Nothing found.... }\n \n
\n \n );\n}\n"
+ },
+ {
+ "fileName": "SelectList.module.css",
+ "language": "css",
+ "code": ".list {\n background-color: var(--mantine-color-body);\n padding: var(--mantine-spacing-xs) rem(6px);\n border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));\n border-top: 0;\n border-bottom-left-radius: var(--mantine-radius-md);\n border-bottom-right-radius: var(--mantine-radius-md);\n min-height: rem(190px);\n}\n\n.input {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectOptionComponent/SelectOptionComponent.tsx b/docs/combobox-examples/examples/SelectOptionComponent/SelectOptionComponent.tsx
new file mode 100644
index 00000000000..720cf87733e
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectOptionComponent/SelectOptionComponent.tsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox, Group, Text } from '@mantine/core';
+
+interface Item {
+ emoji: string;
+ value: string;
+ description: string;
+}
+
+const groceries: Item[] = [
+ { emoji: '🍎', value: 'Apples', description: 'Crisp and refreshing fruit' },
+ { emoji: '🍌', value: 'Bananas', description: 'Naturally sweet and potassium-rich fruit' },
+ { emoji: '🥦', value: 'Broccoli', description: 'Nutrient-packed green vegetable' },
+ { emoji: '🥕', value: 'Carrots', description: 'Crunchy and vitamin-rich root vegetable' },
+ { emoji: '🍫', value: 'Chocolate', description: 'Indulgent and decadent treat' },
+];
+
+function SelectOption({ emoji, value, description }: Item) {
+ return (
+
+ {emoji}
+
+
+ {value}
+
+
+ {description}
+
+
+
+ );
+}
+
+export function SelectOptionComponent() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+ const selectedOption = groceries.find((item) => item.value === value);
+
+ const options = groceries.map((item) => (
+
+
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ multiline
+ >
+ {selectedOption ? (
+
+ ) : (
+ Pick value
+ )}
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectOptionComponent/code.json b/docs/combobox-examples/examples/SelectOptionComponent/code.json
new file mode 100644
index 00000000000..e0b9c60cdef
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectOptionComponent/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectOptionComponent.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox, Group, Text } from '@mantine/core';\n\ninterface Item {\n emoji: string;\n value: string;\n description: string;\n}\n\nconst groceries: Item[] = [\n { emoji: '🍎', value: 'Apples', description: 'Crisp and refreshing fruit' },\n { emoji: '🍌', value: 'Bananas', description: 'Naturally sweet and potassium-rich fruit' },\n { emoji: '🥦', value: 'Broccoli', description: 'Nutrient-packed green vegetable' },\n { emoji: '🥕', value: 'Carrots', description: 'Crunchy and vitamin-rich root vegetable' },\n { emoji: '🍫', value: 'Chocolate', description: 'Indulgent and decadent treat' },\n];\n\nfunction SelectOption({ emoji, value, description }: Item) {\n return (\n \n {emoji} \n \n \n {value}\n \n \n {description}\n \n
\n \n );\n}\n\nexport function SelectOptionComponent() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n const selectedOption = groceries.find((item) => item.value === value);\n\n const options = groceries.map((item) => (\n \n \n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n multiline\n >\n {selectedOption ? (\n \n ) : (\n Pick value \n )}\n \n \n\n \n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectOptionOnHover/SelectOptionOnHover.tsx b/docs/combobox-examples/examples/SelectOptionOnHover/SelectOptionOnHover.tsx
new file mode 100644
index 00000000000..04dab608ceb
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectOptionOnHover/SelectOptionOnHover.tsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectOptionOnHover() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item, index) => (
+ combobox.selectOption(index)}>
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+ combobox.resetSelectedOption()}>
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectOptionOnHover/code.json b/docs/combobox-examples/examples/SelectOptionOnHover/code.json
new file mode 100644
index 00000000000..ab091ed8d08
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectOptionOnHover/code.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fileName": "SelectOptionOnHover.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectOptionOnHover() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item, index) => (\n combobox.selectOption(index)}>\n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n combobox.resetSelectedOption()}>\n {options} \n \n \n );\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.module.css b/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.module.css
new file mode 100644
index 00000000000..6cbf001effe
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.module.css
@@ -0,0 +1,9 @@
+.option {
+ transition: transform 100ms ease, box-shadow 100ms ease;
+
+ &[data-combobox-selected] {
+ font-weight: 500;
+ transform: scale(1.1);
+ box-shadow: var(--mantine-shadow-lg);
+ }
+}
diff --git a/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.tsx b/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.tsx
new file mode 100644
index 00000000000..019b343ad94
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedAnimation/SelectedAnimation.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+import classes from './SelectedAnimation.module.css';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectedAnimation() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item) => (
+
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectedAnimation/code.json b/docs/combobox-examples/examples/SelectedAnimation/code.json
new file mode 100644
index 00000000000..0a32e23b605
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedAnimation/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "SelectedAnimation.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\nimport classes from './SelectedAnimation.module.css';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectedAnimation() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item) => (\n \n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n \n {options} \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "SelectedAnimation.module.css",
+ "language": "css",
+ "code": ".option {\n transition: transform 100ms ease, box-shadow 100ms ease;\n\n &[data-combobox-selected] {\n font-weight: 500;\n transform: scale(1.1);\n box-shadow: var(--mantine-shadow-lg);\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.module.css b/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.module.css
new file mode 100644
index 00000000000..06571ac623e
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.module.css
@@ -0,0 +1,7 @@
+.option {
+ &[data-combobox-selected] {
+ background-color: var(--mantine-color-red-9);
+ color: var(--mantine-color-red-0);
+ font-weight: 500;
+ }
+}
diff --git a/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.tsx b/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.tsx
new file mode 100644
index 00000000000..9be959141ff
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedStyles/SelectedStyles.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react';
+import { Combobox, InputBase, Input, useCombobox } from '@mantine/core';
+import classes from './SelectedStyles.module.css';
+
+const groceries = [
+ '🍎 Apples',
+ '🍌 Bananas',
+ '🥦 Broccoli',
+ '🥕 Carrots',
+ '🍫 Chocolate',
+ '🍇 Grapes',
+];
+
+export function SelectedStyles() {
+ const combobox = useCombobox({
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ const [value, setValue] = useState(null);
+
+ const options = groceries.map((item, index) => (
+ combobox.selectOption(index)}
+ >
+ {item}
+
+ ));
+
+ return (
+ {
+ setValue(val);
+ combobox.closeDropdown();
+ }}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ rightSectionPointerEvents="none"
+ >
+ {value || Pick value }
+
+
+
+ combobox.resetSelectedOption()}>
+ {options}
+
+
+ );
+}
diff --git a/docs/combobox-examples/examples/SelectedStyles/code.json b/docs/combobox-examples/examples/SelectedStyles/code.json
new file mode 100644
index 00000000000..41a804812c5
--- /dev/null
+++ b/docs/combobox-examples/examples/SelectedStyles/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "SelectedStyles.tsx",
+ "language": "tsx",
+ "code": "\nimport { Combobox, InputBase, Input, useCombobox } from '@mantine/core';\nimport classes from './SelectedStyles.module.css';\n\nconst groceries = [\n '🍎 Apples',\n '🍌 Bananas',\n '🥦 Broccoli',\n '🥕 Carrots',\n '🍫 Chocolate',\n '🍇 Grapes',\n];\n\nexport function SelectedStyles() {\n const combobox = useCombobox({\n onDropdownClose: () => combobox.resetSelectedOption(),\n });\n\n const [value, setValue] = useState(null);\n\n const options = groceries.map((item, index) => (\n combobox.selectOption(index)}\n >\n {item}\n \n ));\n\n return (\n {\n setValue(val);\n combobox.closeDropdown();\n }}\n >\n \n }\n onClick={() => combobox.toggleDropdown()}\n rightSectionPointerEvents=\"none\"\n >\n {value || Pick value }\n \n \n\n combobox.resetSelectedOption()}>\n {options} \n \n \n );\n}\n"
+ },
+ {
+ "fileName": "SelectedStyles.module.css",
+ "language": "css",
+ "code": ".option {\n &[data-combobox-selected] {\n background-color: var(--mantine-color-red-9);\n color: var(--mantine-color-red-0);\n font-weight: 500;\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/TransferList/TransferList.module.css b/docs/combobox-examples/examples/TransferList/TransferList.module.css
new file mode 100644
index 00000000000..7dd6f3ad19e
--- /dev/null
+++ b/docs/combobox-examples/examples/TransferList/TransferList.module.css
@@ -0,0 +1,58 @@
+.root {
+ display: flex;
+ gap: var(--mantine-spacing-md);
+
+ @media (max-width: em(755px)) {
+ flex-direction: column;
+ }
+}
+
+.controls {
+ [data-type='backward'] & {
+ flex-direction: row-reverse;
+ }
+}
+
+.list {
+ background-color: var(--mantine-color-body);
+ padding: var(--mantine-spacing-xs) rem(6px);
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
+ border-top: 0;
+ border-bottom-left-radius: var(--mantine-radius-md);
+ border-bottom-right-radius: var(--mantine-radius-md);
+ min-height: rem(224px);
+}
+
+.input {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+
+ [data-type='backward'] & {
+ border-left: 0;
+ border-top-left-radius: 0;
+ }
+
+ [data-type='forward'] & {
+ border-right: 0;
+ border-top-right-radius: 0;
+ }
+}
+
+.control {
+ [data-type='backward'] & {
+ border-top-left-radius: var(--mantine-radius-sm);
+ }
+
+ [data-type='forward'] & {
+ border-top-right-radius: var(--mantine-radius-sm);
+ }
+}
+
+.icon {
+ width: rem(18px);
+ height: rem(18px);
+
+ [data-type='backward'] & {
+ transform: rotate(180deg);
+ }
+}
diff --git a/docs/combobox-examples/examples/TransferList/TransferList.tsx b/docs/combobox-examples/examples/TransferList/TransferList.tsx
new file mode 100644
index 00000000000..0fde1d6cf8a
--- /dev/null
+++ b/docs/combobox-examples/examples/TransferList/TransferList.tsx
@@ -0,0 +1,116 @@
+import React, { useState } from 'react';
+import { IconChevronRight } from '@tabler/icons-react';
+import { Combobox, TextInput, useCombobox, Checkbox, ActionIcon, Group } from '@mantine/core';
+import classes from './TransferList.module.css';
+
+const fruits = ['🍎 Apples', '🍌 Bananas', '🍓 Strawberries'];
+
+const vegetables = ['🥦 Broccoli', '🥕 Carrots', '🥬 Lettuce'];
+
+interface RenderListProps {
+ options: string[];
+ onTransfer(options: string[]): void;
+ type: 'forward' | 'backward';
+}
+
+function RenderList({ options, onTransfer, type }: RenderListProps) {
+ const combobox = useCombobox();
+ const [value, setValue] = useState([]);
+ const [search, setSearch] = useState('');
+
+ const handleValueSelect = (val: string) =>
+ setValue((current) =>
+ current.includes(val) ? current.filter((v) => v !== val) : [...current, val]
+ );
+
+ const items = options
+ .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))
+ .map((item) => (
+ combobox.resetSelectedOption()}
+ >
+
+ {}}
+ aria-hidden
+ tabIndex={-1}
+ style={{ pointerEvents: 'none' }}
+ />
+ {item}
+
+
+ ));
+
+ return (
+
+
+
+
+ {
+ setSearch(event.currentTarget.value);
+ combobox.updateSelectedOptionIndex();
+ }}
+ />
+ {
+ onTransfer(value);
+ setValue([]);
+ }}
+ >
+
+
+
+
+
+
+
+ {items.length > 0 ? items : Nothing found.... }
+
+
+
+
+ );
+}
+
+export function TransferList() {
+ const [data, setData] = useState<[string[], string[]]>([fruits, vegetables]);
+
+ const handleTransfer = (transferFrom: number, options: string[]) =>
+ setData((current) => {
+ const transferTo = transferFrom === 0 ? 1 : 0;
+ const transferFromData = current[transferFrom].filter((item) => !options.includes(item));
+ const transferToData = [...current[transferTo], ...options];
+
+ const result = [];
+ result[transferFrom] = transferFromData;
+ result[transferTo] = transferToData;
+ return result as [string[], string[]];
+ });
+
+ return (
+
+ handleTransfer(0, options)}
+ />
+ handleTransfer(1, options)}
+ />
+
+ );
+}
diff --git a/docs/combobox-examples/examples/TransferList/code.json b/docs/combobox-examples/examples/TransferList/code.json
new file mode 100644
index 00000000000..a310bac5942
--- /dev/null
+++ b/docs/combobox-examples/examples/TransferList/code.json
@@ -0,0 +1,12 @@
+[
+ {
+ "fileName": "TransferList.tsx",
+ "language": "tsx",
+ "code": "\nimport { IconChevronRight } from '@tabler/icons-react';\nimport { Combobox, TextInput, useCombobox, Checkbox, ActionIcon, Group } from '@mantine/core';\nimport classes from './TransferList.module.css';\n\nconst fruits = ['🍎 Apples', '🍌 Bananas', '🍓 Strawberries'];\n\nconst vegetables = ['🥦 Broccoli', '🥕 Carrots', '🥬 Lettuce'];\n\ninterface RenderListProps {\n options: string[];\n onTransfer(options: string[]): void;\n type: 'forward' | 'backward';\n}\n\nfunction RenderList({ options, onTransfer, type }: RenderListProps) {\n const combobox = useCombobox();\n const [value, setValue] = useState([]);\n const [search, setSearch] = useState('');\n\n const handleValueSelect = (val: string) =>\n setValue((current) =>\n current.includes(val) ? current.filter((v) => v !== val) : [...current, val]\n );\n\n const items = options\n .filter((item) => item.toLowerCase().includes(search.toLowerCase().trim()))\n .map((item) => (\n combobox.resetSelectedOption()}\n >\n \n {}}\n aria-hidden\n tabIndex={-1}\n style={{ pointerEvents: 'none' }}\n />\n {item} \n \n \n ));\n\n return (\n \n
\n \n \n {\n setSearch(event.currentTarget.value);\n combobox.updateSelectedOptionIndex();\n }}\n />\n {\n onTransfer(value);\n setValue([]);\n }}\n >\n \n \n \n \n\n \n \n {items.length > 0 ? items : Nothing found.... }\n \n
\n \n
\n );\n}\n\nexport function TransferList() {\n const [data, setData] = useState<[string[], string[]]>([fruits, vegetables]);\n\n const handleTransfer = (transferFrom: number, options: string[]) =>\n setData((current) => {\n const transferTo = transferFrom === 0 ? 1 : 0;\n const transferFromData = current[transferFrom].filter((item) => !options.includes(item));\n const transferToData = [...current[transferTo], ...options];\n\n const result = [];\n result[transferFrom] = transferFromData;\n result[transferTo] = transferToData;\n return result as [string[], string[]];\n });\n\n return (\n \n handleTransfer(0, options)}\n />\n handleTransfer(1, options)}\n />\n
\n );\n}\n"
+ },
+ {
+ "fileName": "TransferList.module.css",
+ "language": "css",
+ "code": ".root {\n display: flex;\n gap: var(--mantine-spacing-md);\n\n @media (max-width: em(755px)) {\n flex-direction: column;\n }\n}\n\n.controls {\n [data-type='backward'] & {\n flex-direction: row-reverse;\n }\n}\n\n.list {\n background-color: var(--mantine-color-body);\n padding: var(--mantine-spacing-xs) rem(6px);\n border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));\n border-top: 0;\n border-bottom-left-radius: var(--mantine-radius-md);\n border-bottom-right-radius: var(--mantine-radius-md);\n min-height: rem(224px);\n}\n\n.input {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n\n [data-type='backward'] & {\n border-left: 0;\n border-top-left-radius: 0;\n }\n\n [data-type='forward'] & {\n border-right: 0;\n border-top-right-radius: 0;\n }\n}\n\n.control {\n [data-type='backward'] & {\n border-top-left-radius: var(--mantine-radius-sm);\n }\n\n [data-type='forward'] & {\n border-top-right-radius: var(--mantine-radius-sm);\n }\n}\n\n.icon {\n width: rem(18px);\n height: rem(18px);\n\n [data-type='backward'] & {\n transform: rotate(180deg);\n }\n}\n"
+ }
+]
\ No newline at end of file
diff --git a/docs/combobox-examples/examples/index.ts b/docs/combobox-examples/examples/index.ts
new file mode 100644
index 00000000000..f61452b434f
--- /dev/null
+++ b/docs/combobox-examples/examples/index.ts
@@ -0,0 +1,315 @@
+import { BasicSelect } from './BasicSelect/BasicSelect';
+import { BasicAutocomplete } from './BasicAutocomplete/BasicAutocomplete';
+import { SearchableSelect } from './SearchableSelect/SearchableSelect';
+import { AutocompleteLimit } from './AutocompleteLimit/AutocompleteLimit';
+import { AsyncAutocomplete } from './AsyncAutocomplete/AsyncAutocomplete';
+import { AutocompleteHighlight } from './AutocompleteHighlight/AutocompleteHighlight';
+import { SelectActive } from './SelectActive/SelectActive';
+import { SelectOptionComponent } from './SelectOptionComponent/SelectOptionComponent';
+import { SelectLimit } from './SelectLimit/SelectLimit';
+import { SelectAsync } from './SelectAsync/SelectAsync';
+import { AutocompleteSelectFirstOption } from './AutocompleteSelectFirstOption/AutocompleteSelectFirstOption';
+import { SelectClearable } from './SelectClearable/SelectClearable';
+import { DropdownScroll } from './DropdownScroll/DropdownScroll';
+import { DropdownScrollArea } from './DropdownScrollArea/DropdownScrollArea';
+import { AutocompleteDynamic } from './AutocompleteDynamic/AutocompleteDynamic';
+import { DropdownAnimation } from './DropdownAnimation/DropdownAnimation';
+import { DropdownPositionStyles } from './DropdownPositionStyles/DropdownPositionStyles';
+import { SelectDropdownSearch } from './SelectDropdownSearch/SelectDropdownSearch';
+import { BasicButton } from './BasicButton/BasicButton';
+import { ButtonSearch } from './ButtonSearch/ButtonSearch';
+import { DropdownOptionsAnimation } from './DropdownOptionsAnimation/DropdownOptionsAnimation';
+import { SelectOptionOnHover } from './SelectOptionOnHover/SelectOptionOnHover';
+import { SelectedStyles } from './SelectedStyles/SelectedStyles';
+import { DropdownFooter } from './DropdownFooter/DropdownFooter';
+import { DropdownHeader } from './DropdownHeader/DropdownHeader';
+import { SelectedAnimation } from './SelectedAnimation/SelectedAnimation';
+import { AutocompleteClearable } from './AutocompleteClearable/AutocompleteClearable';
+import { BasicMultiSelect } from './BasicMultiSelect/BasicMultiSelect';
+import { SearchableMultiSelect } from './SearchableMultiSelect/SearchableMultiSelect';
+import { DropdownSmoothScroll } from './DropdownSmoothScroll/DropdownSmoothScroll';
+import { ButtonMultiSelect } from './ButtonMultiSelect/ButtonMultiSelect';
+import { MaxSelectedItems } from './MaxSelectedItems/MaxSelectedItems';
+import { MultiSelectCheckbox } from './MultiSelectCheckbox/MultiSelectCheckbox';
+import { ActiveOptionsFilter } from './ActiveOptionsFilter/ActiveOptionsFilter';
+import { MaxDisplayedItems } from './MaxDisplayedItems/MaxDisplayedItems';
+import { MultiSelectValueRenderer } from './MultiSelectValueRenderer/MultiSelectValueRenderer';
+import { SelectGroups } from './SelectGroups/SelectGroups';
+import { SelectGroupsSearchable } from './SelectGroupsSearchable/SelectGroupsSearchable';
+import { SelectGroupsStyles } from './SelectGroupsStyles/SelectGroupsStyles';
+import { SelectCreatable } from './SelectCreatable/SelectCreatable';
+import { MultiSelectCreatable } from './MultiSelectCreatable/MultiSelectCreatable';
+import { SelectList } from './SelectList/SelectList';
+import { TransferList } from './TransferList/TransferList';
+
+import BasicSelectCode from './BasicSelect/code.json';
+import BasicAutocompleteCode from './BasicAutocomplete/code.json';
+import SearchableSelectCode from './SearchableSelect/code.json';
+import AutocompleteLimitCode from './AutocompleteLimit/code.json';
+import AsyncAutocompleteCode from './AsyncAutocomplete/code.json';
+import AutocompleteHighlightCode from './AutocompleteHighlight/code.json';
+import SelectActiveCode from './SelectActive/code.json';
+import SelectOptionComponentCode from './SelectOptionComponent/code.json';
+import SelectLimitCode from './SelectLimit/code.json';
+import SelectAsyncCode from './SelectAsync/code.json';
+import AutocompleteSelectFirstOptionCode from './AutocompleteSelectFirstOption/code.json';
+import SelectClearableCode from './SelectClearable/code.json';
+import DropdownScrollCode from './DropdownScroll/code.json';
+import DropdownScrollAreaCode from './DropdownScrollArea/code.json';
+import AutocompleteDynamicCode from './AutocompleteDynamic/code.json';
+import DropdownAnimationCode from './DropdownAnimation/code.json';
+import DropdownPositionStylesCode from './DropdownPositionStyles/code.json';
+import SelectDropdownSearchCode from './SelectDropdownSearch/code.json';
+import BasicButtonCode from './BasicButton/code.json';
+import ButtonSearchCode from './ButtonSearch/code.json';
+import DropdownOptionsAnimationCode from './DropdownOptionsAnimation/code.json';
+import SelectOptionOnHoverCode from './SelectOptionOnHover/code.json';
+import SelectedStylesCode from './SelectedStyles/code.json';
+import DropdownFooterCode from './DropdownFooter/code.json';
+import DropdownHeaderCode from './DropdownHeader/code.json';
+import SelectedAnimationCode from './SelectedAnimation/code.json';
+import AutocompleteClearableCode from './AutocompleteClearable/code.json';
+import BasicMultiSelectCode from './BasicMultiSelect/code.json';
+import SearchableMultiSelectCode from './SearchableMultiSelect/code.json';
+import DropdownSmoothScrollCode from './DropdownSmoothScroll/code.json';
+import ButtonMultiSelectCode from './ButtonMultiSelect/code.json';
+import MaxSelectedItemsCode from './MaxSelectedItems/code.json';
+import MultiSelectCheckboxCode from './MultiSelectCheckbox/code.json';
+import ActiveOptionsFilterCode from './ActiveOptionsFilter/code.json';
+import MaxDisplayedItemsCode from './MaxDisplayedItems/code.json';
+import MultiSelectValueRendererCode from './MultiSelectValueRenderer/code.json';
+import SelectGroupsCode from './SelectGroups/code.json';
+import SelectGroupsSearchableCode from './SelectGroupsSearchable/code.json';
+import SelectGroupsStylesCode from './SelectGroupsStyles/code.json';
+import SelectCreatableCode from './SelectCreatable/code.json';
+import MultiSelectCreatableCode from './MultiSelectCreatable/code.json';
+import SelectListCode from './SelectList/code.json';
+import TransferListCode from './TransferList/code.json';
+
+interface ComboboxExampleComponent {
+ component: () => JSX.Element;
+ code: {
+ fileName: string;
+ language: string;
+ code: string;
+ }[];
+}
+
+export const COMBOBOX_EXAMPLES_COMPONENTS = {
+ BasicSelect: {
+ component: BasicSelect,
+ code: BasicSelectCode,
+ },
+
+ SearchableSelect: {
+ component: SearchableSelect,
+ code: SearchableSelectCode,
+ },
+
+ BasicAutocomplete: {
+ component: BasicAutocomplete,
+ code: BasicAutocompleteCode,
+ },
+
+ AutocompleteLimit: {
+ component: AutocompleteLimit,
+ code: AutocompleteLimitCode,
+ },
+
+ AsyncAutocomplete: {
+ component: AsyncAutocomplete,
+ code: AsyncAutocompleteCode,
+ },
+
+ AutocompleteHighlight: {
+ component: AutocompleteHighlight,
+ code: AutocompleteHighlightCode,
+ },
+
+ SelectActive: {
+ component: SelectActive,
+ code: SelectActiveCode,
+ },
+
+ SelectOptionComponent: {
+ component: SelectOptionComponent,
+ code: SelectOptionComponentCode,
+ },
+
+ SelectLimit: {
+ component: SelectLimit,
+ code: SelectLimitCode,
+ },
+
+ SelectAsync: {
+ component: SelectAsync,
+ code: SelectAsyncCode,
+ },
+
+ AutocompleteSelectFirstOption: {
+ component: AutocompleteSelectFirstOption,
+ code: AutocompleteSelectFirstOptionCode,
+ },
+
+ SelectClearable: {
+ component: SelectClearable,
+ code: SelectClearableCode,
+ },
+
+ DropdownScroll: {
+ component: DropdownScroll,
+ code: DropdownScrollCode,
+ },
+
+ DropdownScrollArea: {
+ component: DropdownScrollArea,
+ code: DropdownScrollAreaCode,
+ },
+
+ AutocompleteDynamic: {
+ component: AutocompleteDynamic,
+ code: AutocompleteDynamicCode,
+ },
+
+ DropdownAnimation: {
+ component: DropdownAnimation,
+ code: DropdownAnimationCode,
+ },
+
+ DropdownPositionStyles: {
+ component: DropdownPositionStyles,
+ code: DropdownPositionStylesCode,
+ },
+
+ SelectDropdownSearch: {
+ component: SelectDropdownSearch,
+ code: SelectDropdownSearchCode,
+ },
+
+ BasicButton: {
+ component: BasicButton,
+ code: BasicButtonCode,
+ },
+
+ ButtonSearch: {
+ component: ButtonSearch,
+ code: ButtonSearchCode,
+ },
+
+ DropdownOptionsAnimation: {
+ component: DropdownOptionsAnimation,
+ code: DropdownOptionsAnimationCode,
+ },
+
+ SelectOptionOnHover: {
+ component: SelectOptionOnHover,
+ code: SelectOptionOnHoverCode,
+ },
+
+ SelectedStyles: {
+ component: SelectedStyles,
+ code: SelectedStylesCode,
+ },
+
+ DropdownFooter: {
+ component: DropdownFooter,
+ code: DropdownFooterCode,
+ },
+
+ DropdownHeader: {
+ component: DropdownHeader,
+ code: DropdownHeaderCode,
+ },
+
+ SelectedAnimation: {
+ component: SelectedAnimation,
+ code: SelectedAnimationCode,
+ },
+
+ AutocompleteClearable: {
+ component: AutocompleteClearable,
+ code: AutocompleteClearableCode,
+ },
+
+ BasicMultiSelect: {
+ component: BasicMultiSelect,
+ code: BasicMultiSelectCode,
+ },
+
+ SearchableMultiSelect: {
+ component: SearchableMultiSelect,
+ code: SearchableMultiSelectCode,
+ },
+
+ DropdownSmoothScroll: {
+ component: DropdownSmoothScroll,
+ code: DropdownSmoothScrollCode,
+ },
+
+ ButtonMultiSelect: {
+ component: ButtonMultiSelect,
+ code: ButtonMultiSelectCode,
+ },
+
+ MaxSelectedItems: {
+ component: MaxSelectedItems,
+ code: MaxSelectedItemsCode,
+ },
+
+ MultiSelectCheckbox: {
+ component: MultiSelectCheckbox,
+ code: MultiSelectCheckboxCode,
+ },
+
+ ActiveOptionsFilter: {
+ component: ActiveOptionsFilter,
+ code: ActiveOptionsFilterCode,
+ },
+
+ MaxDisplayedItems: {
+ component: MaxDisplayedItems,
+ code: MaxDisplayedItemsCode,
+ },
+
+ MultiSelectValueRenderer: {
+ component: MultiSelectValueRenderer,
+ code: MultiSelectValueRendererCode,
+ },
+
+ SelectGroups: {
+ component: SelectGroups,
+ code: SelectGroupsCode,
+ },
+
+ SelectGroupsSearchable: {
+ component: SelectGroupsSearchable,
+ code: SelectGroupsSearchableCode,
+ },
+
+ SelectGroupsStyles: {
+ component: SelectGroupsStyles,
+ code: SelectGroupsStylesCode,
+ },
+
+ SelectCreatable: {
+ component: SelectCreatable,
+ code: SelectCreatableCode,
+ },
+
+ MultiSelectCreatable: {
+ component: MultiSelectCreatable,
+ code: MultiSelectCreatableCode,
+ },
+
+ SelectList: {
+ component: SelectList,
+ code: SelectListCode,
+ },
+
+ TransferList: {
+ component: TransferList,
+ code: TransferListCode,
+ },
+} satisfies Record;
+
+export type ComboboxExampleId = keyof typeof COMBOBOX_EXAMPLES_COMPONENTS;
diff --git a/docs/combobox-examples/index.ts b/docs/combobox-examples/index.ts
new file mode 100644
index 00000000000..0d69a3de095
--- /dev/null
+++ b/docs/combobox-examples/index.ts
@@ -0,0 +1,4 @@
+export { COMBOBOX_EXAMPLES_DATA } from './combobox-examples-data';
+export { COMBOBOX_EXAMPLES_COMPONENTS } from './examples';
+export { ComboboxPage } from './ComboboxPage/ComboboxPage';
+export type { ComboboxExampleId } from './examples';
diff --git a/docs/components/ColorsGenerator/ColorsGenerator.tsx b/docs/components/ColorsGenerator/ColorsGenerator.tsx
new file mode 100644
index 00000000000..8744e57db84
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsGenerator.tsx
@@ -0,0 +1,47 @@
+import React, { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import { useLocalStorage } from '@mantine/hooks';
+import { generateColorsMap } from '@mantine/colors-generator';
+import { MdxTitle } from '@/components/MdxProvider';
+import { ColorsList } from './ColorsList/ColorsList';
+import { ColorsInput } from './ColorsInput/ColorsInput';
+import { ComponentsPreview } from './ComponentsPreview/ComponentsPreview';
+import { ColorsOutput } from './ColorsOutput/ColorsOutput';
+
+export function ColorsGenerator() {
+ const router = useRouter();
+ const urlColor = `#${router.query.color}`;
+ const [color, setColor] = useState('#5474B4');
+ const { colors, baseColorIndex } = generateColorsMap(color);
+ const [displayColorsInfo, setDisplayColorsInfo] = useLocalStorage({
+ key: 'display-colors-info',
+ defaultValue: true,
+ });
+
+ useEffect(() => {
+ if (/^#[0-9A-F]{6}$/i.test(urlColor)) {
+ setColor(urlColor);
+ }
+ }, [router.query.color]);
+
+ return (
+
+ Mantine colors generator
+
+
+
+ c.hex()) as any} />
+
+ c.hex())} />
+
+ );
+}
diff --git a/docs/components/ColorsGenerator/ColorsInput/ColorsInput.module.css b/docs/components/ColorsGenerator/ColorsInput/ColorsInput.module.css
new file mode 100644
index 00000000000..6540358002f
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsInput/ColorsInput.module.css
@@ -0,0 +1,36 @@
+.root {
+ display: flex;
+ gap: rem(50px);
+
+ @media (max-width: em(950px)) {
+ flex-direction: column;
+ }
+}
+
+.colorPicker {
+ @media (max-width: em(950px)) {
+ width: 100%;
+ }
+}
+
+.saturation {
+ & :global(.mantine-ColorPicker-saturationOverlay) {
+ border-radius: var(--mantine-radius-md);
+ }
+}
+
+.input {
+ max-width: rem(280px);
+ margin-bottom: var(--mantine-spacing-sm);
+
+ @media (max-width: em(950px)) {
+ width: 100%;
+ max-width: 100%;
+ }
+}
+
+.switch {
+ @media (max-width: em(600px)) {
+ display: none;
+ }
+}
diff --git a/docs/components/ColorsGenerator/ColorsInput/ColorsInput.tsx b/docs/components/ColorsGenerator/ColorsInput/ColorsInput.tsx
new file mode 100644
index 00000000000..50bd19573b3
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsInput/ColorsInput.tsx
@@ -0,0 +1,126 @@
+import React, { useState } from 'react';
+import { useRouter } from 'next/router';
+import chroma from 'chroma-js';
+import { IconCopy, IconCheck } from '@tabler/icons-react';
+import { useClipboard } from '@mantine/hooks';
+import {
+ ColorPicker,
+ TextInput,
+ Input,
+ Button,
+ ColorSwatch,
+ Group,
+ Switch,
+ rem,
+} from '@mantine/core';
+import { COLORS_PRESET } from './colors-preset';
+import classes from './ColorsInput.module.css';
+
+interface ColorsInputProps {
+ value: string;
+ onChange(value: string): void;
+ displayColorsInfo: boolean | undefined;
+ setDisplayColorsInfo(value: boolean): void;
+}
+
+export function ColorsInput({
+ value,
+ onChange,
+ displayColorsInfo,
+ setDisplayColorsInfo,
+}: ColorsInputProps) {
+ const [inputState, setInputState] = useState(value);
+ const [error, setError] = useState(false);
+ const router = useRouter();
+ const clipboard = useClipboard();
+
+ const updateQuery = (color: string) => {
+ router.replace({ query: { ...router.query, color: color.replace('#', '') } }, undefined, {
+ scroll: false,
+ });
+ };
+
+ const handleChange = (val: string) => {
+ setInputState(val);
+ onChange(val);
+ };
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ const val = event.currentTarget.value;
+ const hasError = !chroma.valid(val);
+ setInputState(val);
+ setError(hasError);
+ !hasError && onChange(val);
+ !hasError && updateQuery(val);
+ };
+
+ const presetControls = COLORS_PRESET.map((color) => (
+ }
+ radius="md"
+ key={color.color}
+ onClick={() => {
+ handleChange(color.color);
+ updateQuery(color.color);
+ }}
+ >
+ {color.name}
+
+ ));
+
+ return (
+
+
+
+
+
+ setDisplayColorsInfo(event.currentTarget.checked)}
+ mt="xl"
+ />
+
+
+ ) : (
+
+ )
+ }
+ rightSection={ }
+ justify="space-between"
+ size="md"
+ mt="xl"
+ radius="md"
+ onClick={() => clipboard.copy(window.location.href)}
+ >
+ {clipboard.copied ? 'Copied' : 'Copy URL'}
+
+
+
+ Preset
+ {presetControls}
+
+
+ );
+}
diff --git a/docs/components/ColorsGenerator/ColorsInput/colors-preset.ts b/docs/components/ColorsGenerator/ColorsInput/colors-preset.ts
new file mode 100644
index 00000000000..96bc554424f
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsInput/colors-preset.ts
@@ -0,0 +1,28 @@
+export const COLORS_PRESET = [
+ { name: 'Blue gray', color: '#63687C' },
+ { name: 'Brown', color: '#5D4037' },
+ { name: 'Tomato', color: '#F06418' },
+ { name: 'Deep orange', color: '#fc8a08' },
+ { name: 'Bright orange', color: '#FFA903' },
+ { name: 'Yellow', color: '#D9D02F' },
+ { name: 'Bright green', color: '#6BD731' },
+ { name: 'Green', color: '#2BDD66' },
+ { name: 'Light blue', color: '#00B5FF' },
+ { name: 'Sky blue', color: '#099CFF' },
+ { name: 'Pale blue', color: '#5474B4' },
+ { name: 'Bright blue', color: '#0063FF' },
+ { name: 'Deep blue', color: '#1F32C4' },
+ { name: 'Pale indigo', color: '#4C5897' },
+ { name: 'Purple', color: '#4F23C0' },
+ { name: 'Pale purple', color: '#504C97' },
+ { name: 'Violet', color: '#7B2EDA' },
+ { name: 'Pale violet', color: '#6B31B2' },
+ { name: 'Dark pink', color: '#8931B2' },
+ { name: 'Bright pink', color: '#C02ADF' },
+ { name: 'Pink', color: '#F018E8' },
+ { name: 'Magenta', color: '#F01879' },
+ { name: 'Bright red', color: '#F0185C' },
+ { name: 'Pale red', color: '#C91A52' },
+ { name: 'Deep red', color: '#C91A25' },
+ { name: 'Red', color: '#F21616' },
+];
diff --git a/docs/components/ColorsGenerator/ColorsList/ColorsList.module.css b/docs/components/ColorsGenerator/ColorsList/ColorsList.module.css
new file mode 100644
index 00000000000..fa0a7f231f7
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsList/ColorsList.module.css
@@ -0,0 +1,62 @@
+.root {
+ margin-top: rem(50px);
+}
+
+.item {
+ flex: 1;
+
+ &:first-of-type > .swatch {
+ border-top-left-radius: var(--mantine-radius-md) !important;
+ border-bottom-left-radius: var(--mantine-radius-md) !important;
+ }
+
+ &:last-of-type > .swatch {
+ border-top-right-radius: var(--mantine-radius-md) !important;
+ border-bottom-right-radius: var(--mantine-radius-md) !important;
+ }
+}
+
+.swatch {
+ width: 100%;
+ height: 0;
+ padding-bottom: 100%;
+ overflow: hidden;
+ transition: transform 100ms ease, box-shadow 100ms ease, border-radius 100ms ease;
+
+ &[data-base] {
+ transform: scale(1.2) translateY(rem(-5px));
+ z-index: 1;
+ box-shadow: var(--mantine-shadow-md);
+ border-radius: var(--mantine-radius-md);
+
+ @media (max-width: em(600px)) {
+ transform: none;
+ box-shadow: none;
+ border-radius: 0;
+ }
+ }
+}
+
+.label {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ gap: rem(5px);
+
+ @media (max-width: em(600px)) {
+ display: none;
+ }
+}
+
+.hex {
+ font-weight: 700;
+ text-transform: uppercase;
+ font-family: var(--mantine-font-family-monospace);
+ font-size: rem(10px);
+}
+
+.index {
+ font-size: var(--mantine-font-size-lg);
+ font-weight: 700;
+ font-family: var(--docs-font-primary);
+}
diff --git a/docs/components/ColorsGenerator/ColorsList/ColorsList.tsx b/docs/components/ColorsGenerator/ColorsList/ColorsList.tsx
new file mode 100644
index 00000000000..049ec733a29
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsList/ColorsList.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import chroma from 'chroma-js';
+import { ColorSwatch, Group, isLightColor } from '@mantine/core';
+import classes from './ColorsList.module.css';
+
+interface ColorsListProps {
+ colors: chroma.Color[];
+ baseColorIndex: number;
+ displayColorsInfo: boolean | undefined;
+}
+
+export function ColorsList({ colors, baseColorIndex, displayColorsInfo }: ColorsListProps) {
+ const items = colors.map((color, index) => (
+
+
+ {displayColorsInfo && (
+
+ {index}
+ {color.hex()}
+
+ )}
+
+
+ ));
+
+ return (
+
+ {items}
+
+ );
+}
diff --git a/docs/components/ColorsGenerator/ColorsOutput/ColorsOutput.tsx b/docs/components/ColorsGenerator/ColorsOutput/ColorsOutput.tsx
new file mode 100644
index 00000000000..c7f88acb396
--- /dev/null
+++ b/docs/components/ColorsGenerator/ColorsOutput/ColorsOutput.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Input } from '@mantine/core';
+import { MdxCodeHighlight } from '@/components/MdxProvider';
+
+interface ColorsOutputProps {
+ colors: string[];
+}
+
+function getProviderCode(colors: string[]) {
+ return `import { MantineProvider, createTheme, MantineColorsTuple } from '@mantine/core';
+
+const myColor: MantineColorsTuple = ${JSON.stringify(colors, null, 2).replace(/"/g, "'")};
+
+const theme = createTheme({
+ colors: {
+ myColor,
+ }
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}`;
+}
+
+export function ColorsOutput({ colors }: ColorsOutputProps) {
+ // For some reason code highlight does not properly update when colors change without key
+ const keyBase = JSON.stringify(colors);
+ return (
+ <>
+
+ Colors array
+
+
+
+
+
+ Usage with MantineProvider
+
+
+
+ >
+ );
+}
diff --git a/docs/components/ColorsGenerator/ComponentsPreview/ComponentsPreview.tsx b/docs/components/ColorsGenerator/ComponentsPreview/ComponentsPreview.tsx
new file mode 100644
index 00000000000..7c2437ee1b2
--- /dev/null
+++ b/docs/components/ColorsGenerator/ComponentsPreview/ComponentsPreview.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { MantineColorsTuple, MantineProvider, Button, Input, Table } from '@mantine/core';
+import { useDebouncedValue } from '@mantine/hooks';
+
+interface ComponentsPreviewProps {
+ colors: MantineColorsTuple;
+}
+
+export function ComponentsPreview({ colors }: ComponentsPreviewProps) {
+ const [debouncedColors] = useDebouncedValue(colors, 100);
+
+ return (
+ <>
+
+ Variants preview
+
+
+
+
+
+
+
+ Filled
+ Light
+ Outline
+ Subtle
+
+
+
+
+
+
+
+ Button
+
+
+
+
+ Button
+
+
+
+
+ Button
+
+
+
+
+ Button
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/docs/components/ColorsGenerator/index.ts b/docs/components/ColorsGenerator/index.ts
new file mode 100644
index 00000000000..75e09a6d5ab
--- /dev/null
+++ b/docs/components/ColorsGenerator/index.ts
@@ -0,0 +1 @@
+export { ColorsGenerator } from './ColorsGenerator';
diff --git a/docs/components/ContentPageBase/ContentPageBase.tsx b/docs/components/ContentPageBase/ContentPageBase.tsx
new file mode 100644
index 00000000000..71ed2ca529b
--- /dev/null
+++ b/docs/components/ContentPageBase/ContentPageBase.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import Head from 'next/head';
+import { PageBase } from '@/components/PageBase';
+import { PageContentContainer } from '../PageContentContainer';
+
+interface ContentPageBaseProps extends React.ComponentPropsWithoutRef<'div'> {
+ title: string;
+}
+
+export function ContentPageBase({ title, ...others }: ContentPageBaseProps) {
+ return (
+ <>
+
+ {`${title} | Mantine`}
+
+
+
+
+ >
+ );
+}
diff --git a/docs/components/ContentPageBase/index.ts b/docs/components/ContentPageBase/index.ts
new file mode 100644
index 00000000000..017c089cfd6
--- /dev/null
+++ b/docs/components/ContentPageBase/index.ts
@@ -0,0 +1 @@
+export { ContentPageBase } from './ContentPageBase';
diff --git a/docs/components/CssFilesList/CssFilesList.tsx b/docs/components/CssFilesList/CssFilesList.tsx
new file mode 100644
index 00000000000..4ddeb7d577d
--- /dev/null
+++ b/docs/components/CssFilesList/CssFilesList.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { Code } from '@mantine/core';
+import CSS_FILES_LIST from '@/.docgen/css-exports.json';
+import { MdxDataTable } from '../MdxProvider';
+
+export function CssFilesList() {
+ const files = ['global.css', ...CSS_FILES_LIST.modules].map((file) => [
+ file.replace('.css', ''),
+ {`import '@mantine/core/styles/${file}';`}
,
+ ]);
+
+ return ;
+}
diff --git a/docs/components/CssFilesList/index.ts b/docs/components/CssFilesList/index.ts
new file mode 100644
index 00000000000..505d6a024a7
--- /dev/null
+++ b/docs/components/CssFilesList/index.ts
@@ -0,0 +1 @@
+export { CssFilesList } from './CssFilesList';
diff --git a/docs/components/CssVariablesList/CssVariablesList.tsx b/docs/components/CssVariablesList/CssVariablesList.tsx
new file mode 100644
index 00000000000..61fc4e194d1
--- /dev/null
+++ b/docs/components/CssVariablesList/CssVariablesList.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Code, DEFAULT_THEME, defaultCssVariablesResolver, keys } from '@mantine/core';
+import { MdxTitle, MdxDataTable } from '../MdxProvider';
+
+export function CssVariablesList() {
+ const resolvedVariables = defaultCssVariablesResolver(DEFAULT_THEME);
+ const variables = keys(resolvedVariables.variables).map((key) => [
+ {key}
,
+ resolvedVariables.variables[key],
+ ]);
+
+ const lightVariables = keys(resolvedVariables.light).map((key) => [
+ {key}
,
+ resolvedVariables.light[key],
+ ]);
+
+ const darkVariables = keys(resolvedVariables.dark).map((key) => [
+ {key}
,
+ resolvedVariables.dark[key],
+ ]);
+
+ return (
+ <>
+
+ CSS variables not depending on color scheme
+
+
+
+
+ Light color scheme only variables
+
+
+
+
+ Dark color scheme only variables
+
+
+ >
+ );
+}
diff --git a/docs/components/CssVariablesList/index.ts b/docs/components/CssVariablesList/index.ts
new file mode 100644
index 00000000000..c644374b57a
--- /dev/null
+++ b/docs/components/CssVariablesList/index.ts
@@ -0,0 +1 @@
+export { CssVariablesList } from './CssVariablesList';
diff --git a/docs/components/CssVariablesTable/CssVariablesTable.tsx b/docs/components/CssVariablesTable/CssVariablesTable.tsx
new file mode 100644
index 00000000000..9b3cf4f8371
--- /dev/null
+++ b/docs/components/CssVariablesTable/CssVariablesTable.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { DEFAULT_THEME, defaultCssVariablesResolver, Code } from '@mantine/core';
+import { MdxDataTable } from '../MdxProvider';
+
+interface CssVariablesTableProps {
+ variables: string[];
+}
+
+export function CssVariablesTable({ variables }: CssVariablesTableProps) {
+ const resolvedVariables = defaultCssVariablesResolver(DEFAULT_THEME);
+
+ const data = variables.map((variable) => [
+ {variable}
,
+ resolvedVariables.variables[variable as keyof typeof resolvedVariables.variables],
+ ]);
+
+ return ;
+}
diff --git a/docs/components/CssVariablesTable/index.ts b/docs/components/CssVariablesTable/index.ts
new file mode 100644
index 00000000000..c3c98d2a1c6
--- /dev/null
+++ b/docs/components/CssVariablesTable/index.ts
@@ -0,0 +1 @@
+export { CssVariablesTable } from './CssVariablesTable';
diff --git a/docs/components/DefaultThemeData/DefaultThemeData.tsx b/docs/components/DefaultThemeData/DefaultThemeData.tsx
new file mode 100644
index 00000000000..235c44e74de
--- /dev/null
+++ b/docs/components/DefaultThemeData/DefaultThemeData.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { DEFAULT_THEME } from '@mantine/core';
+import { MdxCodeHighlight } from '../MdxProvider';
+
+export function DefaultThemeData() {
+ return ;
+}
diff --git a/docs/components/DefaultThemeData/index.ts b/docs/components/DefaultThemeData/index.ts
new file mode 100644
index 00000000000..b679b9ca854
--- /dev/null
+++ b/docs/components/DefaultThemeData/index.ts
@@ -0,0 +1 @@
+export { DefaultThemeData } from './DefaultThemeData';
diff --git a/docs/components/DocsSection/DocsSection.module.css b/docs/components/DocsSection/DocsSection.module.css
new file mode 100644
index 00000000000..143720e8571
--- /dev/null
+++ b/docs/components/DocsSection/DocsSection.module.css
@@ -0,0 +1,4 @@
+.section {
+ margin-bottom: calc(var(--mantine-spacing-xl) * 1.5);
+ margin-top: var(--mantine-spacing-sm);
+}
diff --git a/docs/components/DocsSection/DocsSection.tsx b/docs/components/DocsSection/DocsSection.tsx
new file mode 100644
index 00000000000..d8c26d62bac
--- /dev/null
+++ b/docs/components/DocsSection/DocsSection.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import cx from 'clsx';
+import classes from './DocsSection.module.css';
+
+export function DocsSection({ className, ...others }: React.ComponentPropsWithoutRef<'div'>) {
+ return
;
+}
diff --git a/docs/components/DocsSection/index.ts b/docs/components/DocsSection/index.ts
new file mode 100644
index 00000000000..3662545356e
--- /dev/null
+++ b/docs/components/DocsSection/index.ts
@@ -0,0 +1 @@
+export { DocsSection } from './DocsSection';
diff --git a/docs/components/Footer/Footer.module.css b/docs/components/Footer/Footer.module.css
new file mode 100644
index 00000000000..b65e941ef3c
--- /dev/null
+++ b/docs/components/Footer/Footer.module.css
@@ -0,0 +1,170 @@
+$footer-breakpoint-tablet: 62.5em;
+$footer-breakpoint-mobile: 40em;
+
+.root {
+ --docs-footer-height: rem(400px);
+
+ @media (max-width: 50em) {
+ --docs-footer-height: rem(460px);
+ }
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ --docs-footer-height: rem(320px);
+ }
+}
+
+.spacer {
+ height: var(--docs-footer-height);
+}
+
+.wrapper {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: var(--docs-footer-height);
+
+ @mixin light {
+ background-color: var(--mantine-color-gray-0);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-8);
+ }
+
+ &[data-with-navbar] {
+ padding-left: calc(var(--docs-navbar-width) + var(--mantine-spacing-md) * 2);
+ padding-right: calc(var(--mantine-spacing-md) * 2);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: calc(var(--docs-navbar-width) + var(--mantine-spacing-md));
+ padding-right: calc(var(--mantine-spacing-md));
+ }
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ }
+
+ @mixin rtl {
+ padding-left: calc(var(--mantine-spacing-md) * 2);
+ padding-right: calc(var(--docs-navbar-width) + var(--mantine-spacing-md) * 2);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: calc(var(--mantine-spacing-md));
+ padding-right: calc(var(--docs-navbar-width) + var(--mantine-spacing-md));
+ }
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ }
+ }
+ }
+}
+
+.inner {
+ padding-top: calc(var(--mantine-spacing-xl) * 2);
+ padding-bottom: calc(var(--mantine-spacing-xl) * 2);
+ display: flex;
+ justify-content: space-between;
+
+ @media (max-width: $footer-breakpoint-tablet) {
+ flex-direction: column;
+ }
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ padding-bottom: var(--mantine-spacing-md);
+ }
+}
+
+.logoSection {
+ max-width: rem(300px);
+
+ @media (max-width: $footer-breakpoint-tablet) {
+ margin-bottom: var(--mantine-spacing-xl);
+ }
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ margin-bottom: 0;
+ }
+}
+
+.description {
+ margin-top: var(--mantine-spacing-xs);
+
+ @mixin light {
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-2);
+ }
+}
+
+.title {
+ margin-bottom: var(--mantine-spacing-sm);
+ line-height: 1;
+}
+
+.afterFooter {
+ border-top: rem(1px) solid;
+ padding-top: var(--mantine-spacing-md);
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-4);
+ }
+}
+
+.afterFooterNote {
+ &,
+ & a {
+ @mixin light {
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-2);
+ }
+ }
+}
+
+.groups {
+ display: flex;
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ display: none;
+ }
+}
+
+.social {
+ display: flex;
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ display: block;
+ }
+}
+
+.socialButton {
+ &:last-of-type {
+ margin-left: var(--mantine-spacing-md);
+ }
+
+ @mixin rtl {
+ &:last-of-type {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-md);
+ }
+ }
+
+ @media (max-width: $footer-breakpoint-mobile) {
+ width: 100%;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ margin-top: var(--mantine-spacing-sm);
+ }
+}
diff --git a/docs/components/Footer/Footer.tsx b/docs/components/Footer/Footer.tsx
new file mode 100644
index 00000000000..d0f68372b23
--- /dev/null
+++ b/docs/components/Footer/Footer.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import cx from 'clsx';
+import { Container, Text, Group, Box, RemoveScroll } from '@mantine/core';
+import { TwitterButton, DiscordButton } from '@mantine/ds';
+import { Logo } from '../Logo/Logo';
+import { LinksGroup } from './LinksGroup/LinksGroup';
+import { FOOTER_LINKS_DATA } from './data';
+import classes from './Footer.module.css';
+
+interface FooterProps {
+ withNavbar?: boolean;
+}
+
+export function Footer({ withNavbar }: FooterProps) {
+ const groups = FOOTER_LINKS_DATA.map((group) => (
+
+ ));
+
+ return (
+
+
+
+
+
+
+
+
+ Build fully functional accessible web applications faster than ever
+
+
+
+
{groups}
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/Footer/LinksGroup/LinksGroup.module.css b/docs/components/Footer/LinksGroup/LinksGroup.module.css
new file mode 100644
index 00000000000..605c7fa8df6
--- /dev/null
+++ b/docs/components/Footer/LinksGroup/LinksGroup.module.css
@@ -0,0 +1,58 @@
+.wrapper {
+ margin-right: rem(60px);
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ @media (max-width: 62.5em) {
+ margin-right: rem(40px);
+ }
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: rem(60px);
+
+ &:last-of-type {
+ margin-left: 0;
+ }
+
+ @media (max-width: 62.5em) {
+ margin-left: rem(40px);
+ }
+ }
+}
+
+.link {
+ display: block;
+ font-size: var(--mantine-font-size-sm);
+ padding-top: rem(3px);
+ padding-bottom: rem(3px);
+
+ @mixin hover {
+ text-decoration: underline;
+ }
+
+ @mixin light {
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-1);
+ }
+}
+
+.title {
+ font-size: var(--mantine-font-size-lg);
+ font-weight: 700;
+ font-family: var(--docs-font-primary);
+ margin-bottom: calc(var(--mantine-spacing-sm) / 2);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
diff --git a/docs/components/Footer/LinksGroup/LinksGroup.tsx b/docs/components/Footer/LinksGroup/LinksGroup.tsx
new file mode 100644
index 00000000000..802339e8cff
--- /dev/null
+++ b/docs/components/Footer/LinksGroup/LinksGroup.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import Link from 'next/link';
+import { Text } from '@mantine/core';
+import classes from './LinksGroup.module.css';
+
+export interface LinksGroupProps {
+ title: string;
+ data: {
+ type: 'link' | 'next';
+ link: string;
+ label: string;
+ }[];
+}
+
+export function LinksGroup({ data, title }: LinksGroupProps) {
+ const links = data.map((link, index) => (
+
+ {link.label}
+
+ ));
+
+ return (
+
+ {title}
+ {links}
+
+ );
+}
diff --git a/docs/components/Footer/data.ts b/docs/components/Footer/data.ts
new file mode 100644
index 00000000000..a78d95d5d76
--- /dev/null
+++ b/docs/components/Footer/data.ts
@@ -0,0 +1,37 @@
+import { meta } from '@mantine/ds';
+import { LinksGroupProps } from './LinksGroup/LinksGroup';
+
+export const FOOTER_LINKS_DATA: LinksGroupProps[] = [
+ {
+ title: 'About',
+ data: [
+ { type: 'next', label: 'Contribute', link: '/pages/contributing/' },
+ { type: 'next', label: 'About Mantine', link: '/pages/about/' },
+ { type: 'next', label: 'Changelog', link: '/pages/changelog/' },
+ { type: 'link', label: 'Releases', link: meta.gitHubLinks.releases },
+ ],
+ },
+
+ {
+ title: 'Community',
+ data: [
+ { type: 'link', label: 'Chat on Discord', link: meta.discordLink },
+ { type: 'link', label: 'Follow on Twitter', link: meta.twitterLink },
+ { type: 'link', label: 'Follow on Github', link: 'https://github.com/rtivital' },
+ {
+ type: 'link',
+ label: 'GitHub discussions',
+ link: meta.gitHubLinks.discussions,
+ },
+ ],
+ },
+ {
+ title: 'Project',
+ data: [
+ { type: 'link', label: 'Mantine UI', link: meta.uiLink },
+ { type: 'link', label: 'Documentation', link: meta.docsLink },
+ { type: 'link', label: 'Github organization', link: meta.gitHubLinks.organization },
+ { type: 'link', label: 'npm organization', link: meta.npmLink },
+ ],
+ },
+];
diff --git a/docs/components/Footer/index.ts b/docs/components/Footer/index.ts
new file mode 100644
index 00000000000..65e2506faf5
--- /dev/null
+++ b/docs/components/Footer/index.ts
@@ -0,0 +1 @@
+export { Footer } from './Footer';
diff --git a/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.module.css b/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.module.css
new file mode 100644
index 00000000000..b7f508d55a8
--- /dev/null
+++ b/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.module.css
@@ -0,0 +1,38 @@
+.link {
+ flex: 1 1 rem(120px);
+ min-width: rem(120px);
+ text-decoration: none;
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+ border-radius: var(--mantine-radius-md);
+ padding: var(--mantine-spacing-md);
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
+
+ @mixin hover {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
+ }
+}
+
+.icon {
+ width: 100%;
+ max-width: rem(60px);
+
+ &[data-large] {
+ max-width: rem(100px);
+ }
+}
+
+.iconWrapper {
+ display: flex;
+ align-items: center;
+ flex: 1;
+}
+
+.name {
+ margin-top: var(--mantine-spacing-xs);
+ font-size: var(--mantine-font-size-sm);
+}
diff --git a/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.tsx b/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.tsx
new file mode 100644
index 00000000000..c3b4bd33293
--- /dev/null
+++ b/docs/components/FrameworksGuides/FrameworkLink/FrameworkLink.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import Link from 'next/link';
+import { frameworkIcons } from '@/components/icons';
+import type { FrameworkData } from '../data';
+import classes from './FrameworkLink.module.css';
+
+interface FrameworkLinkProps {
+ data: FrameworkData;
+}
+
+export function FrameworkLink({ data }: FrameworkLinkProps) {
+ const Icon = frameworkIcons[data.type];
+ return (
+
+
+
+
+ {data.name}
+
+ );
+}
diff --git a/docs/components/FrameworksGuides/FrameworksGuides.module.css b/docs/components/FrameworksGuides/FrameworksGuides.module.css
new file mode 100644
index 00000000000..e7132be7fbc
--- /dev/null
+++ b/docs/components/FrameworksGuides/FrameworksGuides.module.css
@@ -0,0 +1,5 @@
+.root {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--mantine-spacing-md);
+}
diff --git a/docs/components/FrameworksGuides/FrameworksGuides.tsx b/docs/components/FrameworksGuides/FrameworksGuides.tsx
new file mode 100644
index 00000000000..aa2bd5e07e0
--- /dev/null
+++ b/docs/components/FrameworksGuides/FrameworksGuides.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { FrameworkLink } from './FrameworkLink/FrameworkLink';
+import { FRAMEWORKS_GUIDES_DATA } from './data';
+import classes from './FrameworksGuides.module.css';
+
+export function FrameworksGuides() {
+ const frameworks = FRAMEWORKS_GUIDES_DATA.map((framework) => (
+
+ ));
+
+ return {frameworks}
;
+}
diff --git a/docs/components/FrameworksGuides/data.ts b/docs/components/FrameworksGuides/data.ts
new file mode 100644
index 00000000000..c9c34ac97b9
--- /dev/null
+++ b/docs/components/FrameworksGuides/data.ts
@@ -0,0 +1,15 @@
+import { Template } from '../MdxProvider/MdxTemplatesList/data';
+
+export interface FrameworkData {
+ name: string;
+ link: string;
+ type: Template['type'];
+}
+
+export const FRAMEWORKS_GUIDES_DATA: FrameworkData[] = [
+ { name: 'Next.js', link: '/guides/next', type: 'next' },
+ { name: 'Vite', link: '/guides/vite', type: 'vite' },
+ { name: 'Remix', link: '/guides/remix', type: 'remix' },
+ { name: 'RedwoodJS', link: '/guides/redwood', type: 'redwood' },
+ { name: 'Gatsby', link: '/guides/gatsby', type: 'gatsby' },
+];
diff --git a/docs/components/FrameworksGuides/index.ts b/docs/components/FrameworksGuides/index.ts
new file mode 100644
index 00000000000..b3978c6caba
--- /dev/null
+++ b/docs/components/FrameworksGuides/index.ts
@@ -0,0 +1 @@
+export { FrameworksGuides } from './FrameworksGuides';
diff --git a/docs/components/GaScript/GaScript.tsx b/docs/components/GaScript/GaScript.tsx
new file mode 100644
index 00000000000..ab48991a677
--- /dev/null
+++ b/docs/components/GaScript/GaScript.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Script from 'next/script';
+
+const gaScript = `
+window.dataLayer = window.dataLayer || [];
+function gtag(){dataLayer.push(arguments);}
+gtag('js', new Date());
+
+gtag('config', 'G-4Z4NNVLRH5');
+`;
+
+export function GaScript() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/docs/components/GaScript/index.ts b/docs/components/GaScript/index.ts
new file mode 100644
index 00000000000..10a9232440f
--- /dev/null
+++ b/docs/components/GaScript/index.ts
@@ -0,0 +1 @@
+export { GaScript } from './GaScript';
diff --git a/docs/components/HomePage/Banner/Banner.module.css b/docs/components/HomePage/Banner/Banner.module.css
new file mode 100644
index 00000000000..89a38dc74bb
--- /dev/null
+++ b/docs/components/HomePage/Banner/Banner.module.css
@@ -0,0 +1,109 @@
+.wrapper {
+ position: relative;
+ min-height: rem(580px);
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ min-height: auto;
+ padding-bottom: var(--mantine-spacing-xl);
+ }
+}
+
+.overlay {
+ background-color: transparent;
+ background-image: linear-gradient(
+ 45deg,
+ light-dark(var(--mantine-color-white), var(--mantine-color-dark-8)) 25%,
+ rgba(0, 0, 0, 0) 95%
+ );
+}
+
+.supTitle {
+ font-size: var(--mantine-font-size-sm);
+ text-transform: uppercase;
+ font-weight: bold;
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ opacity: 0.8;
+ margin-bottom: var(--mantine-spacing-sm);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: var(--mantine-font-size-xs);
+ }
+}
+
+.title {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ font-family: var(--docs-font-primary);
+ font-size: rem(44px);
+ line-height: 1.2;
+ font-weight: 900;
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: rem(26px);
+ }
+}
+
+.highlight {
+ color: light-dark(var(--mantine-color-cyan-6), var(--mantine-color-yellow-4));
+ background: var(--docs-home-page-gradient-text);
+ -webkit-text-fill-color: transparent;
+ -webkit-background-clip: text;
+ background-clip: text;
+}
+
+.description {
+ color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
+ line-height: 1.5;
+ max-width: rem(580px);
+ margin-top: var(--mantine-spacing-md);
+}
+
+.body {
+ flex: 0 0 rem(300px);
+ padding-top: rem(140px);
+ position: relative;
+ z-index: 1;
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ padding-top: calc(var(--mantine-spacing-xl) * 2);
+ }
+}
+
+.image {
+ min-height: rem(500px);
+ width: rem(800px);
+ flex: 1;
+ background-size: auto 100%;
+ background-repeat: no-repeat;
+ background-position: right;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ z-index: 0;
+
+ @mixin rtl {
+ display: none;
+ }
+
+ @media (max-width: $mantine-breakpoint-md) {
+ display: none;
+ }
+}
+
+.controls {
+ margin-top: var(--mantine-spacing-md);
+ display: flex;
+ gap: var(--mantine-spacing-md);
+ flex-wrap: wrap;
+
+ & > * {
+ @media (max-width: $mantine-breakpoint-sm) {
+ flex: 1;
+ }
+ }
+}
+
+.control {
+ background: var(--docs-home-page-gradient);
+}
diff --git a/docs/src/components/HomePage/Banner/Banner.tsx b/docs/components/HomePage/Banner/Banner.tsx
similarity index 78%
rename from docs/src/components/HomePage/Banner/Banner.tsx
rename to docs/components/HomePage/Banner/Banner.tsx
index ff29a932e5b..7654f1da835 100644
--- a/docs/src/components/HomePage/Banner/Banner.tsx
+++ b/docs/components/HomePage/Banner/Banner.tsx
@@ -1,22 +1,15 @@
import React from 'react';
import { Title, Overlay, Text, Button, Container } from '@mantine/core';
import { GithubIcon } from '@mantine/ds';
-import useStyles from './Banner.styles';
+import banner from './banner.webp';
+import classes from './Banner.module.css';
export function Banner() {
- const { classes, theme } = useStyles();
-
return (
-
-
+
+
Build even faster with Mantine UI
@@ -43,7 +36,7 @@ export function Banner() {
Explore components
}
+ leftSection={
}
component="a"
radius="md"
href="https://github.com/mantinedev/ui.mantine.dev"
diff --git a/docs/src/components/HomePage/Banner/banner.webp b/docs/components/HomePage/Banner/banner.webp
similarity index 100%
rename from docs/src/components/HomePage/Banner/banner.webp
rename to docs/components/HomePage/Banner/banner.webp
diff --git a/docs/src/components/HomePage/Components/Components.tsx b/docs/components/HomePage/Components/Components.tsx
similarity index 100%
rename from docs/src/components/HomePage/Components/Components.tsx
rename to docs/components/HomePage/Components/Components.tsx
diff --git a/docs/components/HomePage/Components/demos/Carousel.tsx b/docs/components/HomePage/Components/demos/Carousel.tsx
new file mode 100644
index 00000000000..70f84388a96
--- /dev/null
+++ b/docs/components/HomePage/Components/demos/Carousel.tsx
@@ -0,0 +1,15 @@
+import React, { useState } from 'react';
+import { CarouselCardsDemos } from '@mantine/demos';
+import { Embla, useAnimationOffsetEffect } from '@mantine/carousel';
+
+export function Carousel() {
+ const [embla, setEmbla] = useState
(null);
+
+ useAnimationOffsetEffect(embla, 300);
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Components/demos/Content.tsx b/docs/components/HomePage/Components/demos/Content.tsx
new file mode 100644
index 00000000000..fce93f58338
--- /dev/null
+++ b/docs/components/HomePage/Components/demos/Content.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Text, Box } from '@mantine/core';
+import { AccordionDemo, TimelineBase } from '@mantine/demos';
+
+export function Content() {
+ return (
+
+
+ Accordion component
+
+
+
+
+
+
+
+ Timeline component
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Components/demos/Dates.tsx b/docs/components/HomePage/Components/demos/Dates.tsx
new file mode 100644
index 00000000000..a0e086f646c
--- /dev/null
+++ b/docs/components/HomePage/Components/demos/Dates.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Paper, SimpleGrid, Input } from '@mantine/core';
+import { DatePickerInput, TimeInput, DatePicker, DateTimePicker } from '@mantine/dates';
+import dayjs from 'dayjs';
+import classes from './Demos.module.css';
+
+function CalendarWrapper() {
+ return (
+
+
+
+
+
+ );
+}
+
+function RangeCalendarWrapper() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function Dates() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Components/demos/Demos.module.css b/docs/components/HomePage/Components/demos/Demos.module.css
new file mode 100644
index 00000000000..930440cc303
--- /dev/null
+++ b/docs/components/HomePage/Components/demos/Demos.module.css
@@ -0,0 +1,9 @@
+.calendarWrapper {
+ display: flex;
+ justify-content: center;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
+}
+
+.richTextWrapper :global(.mantine-RichTextEditor-toolbar) {
+ position: static;
+}
diff --git a/docs/src/components/HomePage/Components/demos/Inputs.tsx b/docs/components/HomePage/Components/demos/Inputs.tsx
similarity index 89%
rename from docs/src/components/HomePage/Components/demos/Inputs.tsx
rename to docs/components/HomePage/Components/demos/Inputs.tsx
index cdb3d8d3db0..69b794c2548 100644
--- a/docs/src/components/HomePage/Components/demos/Inputs.tsx
+++ b/docs/components/HomePage/Components/demos/Inputs.tsx
@@ -13,9 +13,8 @@ import {
export function Inputs() {
return (
({ rowGap: theme.spacing.md })}
+ cols={{ base: 1, sm: 2 }}
+ style={{ rowGap: 'var(--mantine-spacing-md' }}
spacing="xl"
>
@@ -25,6 +24,7 @@ export function Inputs() {
defaultValue="rgba(34, 138, 230, 0.8)"
format="rgba"
placeholder="What other library has that?"
+ eyeDropperButtonProps={{ 'aria-label': 'Pick color' }}
/>
+
+
+
+
+ Completed, click back button to get to previous step
+
+
+
+
+
+
+ Completed, click back button to get to previous step
+
+ >
+ );
+}
+
+function TabsDemo() {
+ return (
+
+
+ }>
+ Gallery
+
+ }>
+ Messages
+
+ }>
+ Settings
+
+
+
+ );
+}
+
+function PopoverDemo() {
+ return (
+
+
+ Toggle popover
+
+
+ This is popover, it will show a dropdown relative to target element
+
+
+ );
+}
+
+export function Overlays() {
+ const [modalOpened, setModalOpened] = useState(false);
+ const [drawerOpened, setDrawerOpened] = useState(false);
+
+ return (
+
+
setModalOpened(false)}>
+
+
+
+
setDrawerOpened(false)}
+ padding="md"
+ size={440}
+ withCloseButton={false}
+ >
+
+ Register
+ setDrawerOpened(false)} />
+
+
+
+
+
+
+ Overlays
+
+
+
+ setModalOpened(true)}>
+ Open Modal
+
+ setDrawerOpened(true)}>
+ Open Drawer
+
+
+
+
+
+
+ Reveal on hover
+
+
+
+
+
+ Tabs component
+
+
+
+
+
+ Stepper component
+
+
+
+
+
+
+ Pagination component
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Components/demos/RichText.tsx b/docs/components/HomePage/Components/demos/RichText.tsx
new file mode 100644
index 00000000000..56d0b225d50
--- /dev/null
+++ b/docs/components/HomePage/Components/demos/RichText.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { TipTapDemos } from '@mantine/demos';
+import classes from './Demos.module.css';
+
+export function RichText() {
+ const Component = TipTapDemos.usage.component as any;
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.module.css b/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.module.css
new file mode 100644
index 00000000000..c91bf1b1f1b
--- /dev/null
+++ b/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.module.css
@@ -0,0 +1,31 @@
+.track {
+ &::before {
+ background-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-3));
+ }
+}
+
+.mark {
+ width: rem(6px);
+ height: rem(6px);
+ border-radius: rem(6px);
+ transform: translateX(rem(-3px)) translateY(rem(-2px));
+ border-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-3));
+
+ &[data-filled] {
+ border-color: var(--mantine-color-blue-6);
+ }
+}
+
+.markLabel {
+ font-size: var(--mantine-font-size-xs);
+ margin-bottom: rem(5px);
+ margin-top: 0;
+}
+
+.thumb {
+ height: rem(16px);
+ width: rem(16px);
+ background-color: var(--mantine-color-white);
+ border-width: rem(1px);
+ box-shadow: var(--mantine-shadow-sm);
+}
diff --git a/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.tsx b/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.tsx
new file mode 100644
index 00000000000..25226b3f465
--- /dev/null
+++ b/docs/components/HomePage/CustomizeStyles/CustomizeSlider.demo.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { Slider } from '@mantine/core';
+import classes from './CustomizeSlider.demo.module.css';
+
+const marks = [
+ { value: 20, label: '20%' },
+ { value: 50, label: '50%' },
+ { value: 80, label: '80%' },
+];
+
+export function CustomizeSliderDemo() {
+ return ;
+}
+
+const tsxCode = `
+import { Slider } from '@mantine/core';
+import classes from './Demo.module.css';
+
+const marks = [
+ { value: 20, label: '20%' },
+ { value: 50, label: '50%' },
+ { value: 80, label: '80%' },
+];
+
+export function CustomizeSliderDemo() {
+ return ;
+}`;
+
+const cssCode = `
+.track {
+ &::before {
+ background-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-3));
+ }
+}
+
+.mark {
+ width: rem(6px);
+ height: rem(6px);
+ border-radius: rem(6px);
+ transform: translateX(rem(-3px)) translateY(rem(-2px));
+ border-color: light-dark(var(--mantine-color-blue-1), var(--mantine-color-dark-3));
+
+ &[data-filled] {
+ border-color: var(--mantine-color-blue-6);
+ }
+}
+
+.markLabel {
+ font-size: var(--mantine-font-size-xs);
+ margin-bottom: rem(5px);
+ margin-top: 0;
+}
+
+.thumb {
+ height: rem(16px);
+ width: rem(16px);
+ background-color: var(--mantine-color-white);
+ border-width: rem(1px);
+ box-shadow: var(--mantine-shadow-sm);
+}
+
+`;
+
+export const code = [
+ { fileName: 'Demo.module.css', code: cssCode, language: 'scss' },
+ { fileName: 'Demo.tsx', code: tsxCode, language: 'tsx' },
+];
diff --git a/docs/components/HomePage/CustomizeStyles/CustomizeStyles.module.css b/docs/components/HomePage/CustomizeStyles/CustomizeStyles.module.css
new file mode 100644
index 00000000000..3b27dd250b3
--- /dev/null
+++ b/docs/components/HomePage/CustomizeStyles/CustomizeStyles.module.css
@@ -0,0 +1,9 @@
+.codeHighlight {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ border-radius: var(--mantine-radius-md);
+ overflow: hidden;
+}
+
+.control {
+ background: var(--docs-home-page-gradient);
+}
diff --git a/docs/components/HomePage/CustomizeStyles/CustomizeStyles.tsx b/docs/components/HomePage/CustomizeStyles/CustomizeStyles.tsx
new file mode 100644
index 00000000000..024fc47d1e0
--- /dev/null
+++ b/docs/components/HomePage/CustomizeStyles/CustomizeStyles.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import Link from 'next/link';
+import { IconArrowRight } from '@tabler/icons-react';
+import { Text, SimpleGrid, Slider, Button, rem } from '@mantine/core';
+import { CodeHighlightTabs, CodeHighlight } from '@mantine/code-highlight';
+import { getCodeFileIcon } from '@mantine/ds';
+import { SliderStylesApi } from '@mantine/styles-api';
+import { MdxDataTable } from '../../MdxProvider/MdxDataTable/MdxDataTable';
+import { PageSection } from '../PageSection/PageSection';
+import { CustomizeSliderDemo, code } from './CustomizeSlider.demo';
+import classes from './CustomizeStyles.module.css';
+
+const marks = [
+ { value: 20, label: '20%' },
+ { value: 50, label: '50%' },
+ { value: 80, label: '80%' },
+];
+
+const defaultCode = `
+import { Slider } from '@mantine/core';
+
+const marks = [
+ { value: 20, label: '20%' },
+ { value: 50, label: '50%' },
+ { value: 80, label: '80%' },
+];
+
+function Demo() {
+ return ;
+}
+`;
+
+export function CustomizeStyles() {
+ return (
+
+
+
+
+ Default slider styles
+
+
+
+
+
+
+ Find elements that you need to change in styles API table
+
+
+ [
+ name,
+ SliderStylesApi.selectors[name as keyof typeof SliderStylesApi.selectors],
+ ])}
+ />
+
+
+
+
+ Then apply styles and add other props:
+
+
+
+
+
+ }
+ variant="gradient"
+ className={classes.control}
+ >
+ View more examples
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.module.css b/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.module.css
new file mode 100644
index 00000000000..b360a3e25ba
--- /dev/null
+++ b/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.module.css
@@ -0,0 +1,7 @@
+.wrapper {
+ box-shadow: var(--mantine-shadow-md);
+ border-radius: var(--mantine-radius-md);
+ background-color: var(--mantine-color-body);
+ margin-top: var(--mantine-spacing-xl);
+ margin-bottom: rem(60px);
+}
diff --git a/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.tsx b/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.tsx
new file mode 100644
index 00000000000..26dcfc99924
--- /dev/null
+++ b/docs/components/HomePage/CustomizeWithProps/CustomizeWithProps.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Box } from '@mantine/core';
+import { TimelineDemos } from '@mantine/demos';
+import { Demo } from '@mantine/ds';
+import { PageSection } from '../PageSection/PageSection';
+import classes from './CustomizeWithProps.module.css';
+
+export function CustomizeWithProps() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/DarkTheme/DarkTheme.module.css b/docs/components/HomePage/DarkTheme/DarkTheme.module.css
new file mode 100644
index 00000000000..664ee1002bc
--- /dev/null
+++ b/docs/components/HomePage/DarkTheme/DarkTheme.module.css
@@ -0,0 +1,32 @@
+.image {
+ box-shadow: var(--mantine-shadow-md);
+ border-radius: var(--mantine-radius-md);
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
+}
+
+.darkIcon,
+.lightIcon {
+ width: rem(16px);
+ height: rem(16px);
+}
+
+.darkIcon {
+ color: var(--mantine-color-yellow-4);
+
+ @mixin light {
+ display: none;
+ }
+}
+
+.lightIcon {
+ color: var(--mantine-color-blue-7);
+
+ @mixin dark {
+ display: none;
+ }
+}
+
+.codeHighlight {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ border-radius: var(--mantine-radius-md);
+}
diff --git a/docs/components/HomePage/DarkTheme/DarkTheme.tsx b/docs/components/HomePage/DarkTheme/DarkTheme.tsx
new file mode 100644
index 00000000000..b58ac4bc7cb
--- /dev/null
+++ b/docs/components/HomePage/DarkTheme/DarkTheme.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { Button, Image, SimpleGrid, Text, useMantineColorScheme, Group } from '@mantine/core';
+import { CodeHighlight } from '@mantine/code-highlight';
+import { IconMoonStars, IconSun } from '@tabler/icons-react';
+import { PageSection } from '../PageSection/PageSection';
+import classes from './DarkTheme.module.css';
+import image from './dark-theme-image.png';
+
+export function DarkTheme() {
+ const { toggleColorScheme } = useMantineColorScheme();
+
+ const code = `
+import { MantineProvider } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+`;
+
+ return (
+
+
+
+
+
+
+
+ Add dark theme to your application with just a few lines of code – Mantine exports
+ global styles both for light and dark theme, all components support dark theme out of
+ the box.
+
+
+
+
+
+ toggleColorScheme()}
+ leftSection={
+ <>
+
+
+ >
+ }
+ >
+ Toggle color scheme
+
+
+
+
+
+ );
+}
diff --git a/docs/src/components/HomePage/DarkTheme/dark-theme-image.png b/docs/components/HomePage/DarkTheme/dark-theme-image.png
similarity index 100%
rename from docs/src/components/HomePage/DarkTheme/dark-theme-image.png
rename to docs/components/HomePage/DarkTheme/dark-theme-image.png
diff --git a/docs/components/HomePage/DemoTabs/DemoTabs.module.css b/docs/components/HomePage/DemoTabs/DemoTabs.module.css
new file mode 100644
index 00000000000..4e1e0ed29b6
--- /dev/null
+++ b/docs/components/HomePage/DemoTabs/DemoTabs.module.css
@@ -0,0 +1,112 @@
+@keyframes fade-in-demo-tabs {
+ 0% {
+ opacity: 0;
+ transform: translateY(rem(-20px)) scale(0.95) skewX(-2deg);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1) rotate(0);
+ }
+}
+
+.root {
+ padding-top: rem(100px);
+ padding-bottom: rem(100px);
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ padding-top: rem(60px);
+ padding-bottom: rem(60px);
+ }
+}
+
+.controls {
+ position: relative;
+}
+
+.controlsIndicator {
+ pointer-events: none;
+ position: absolute;
+ left: 0;
+ right: 0;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
+ box-shadow: var(--mantine-shadow-md);
+ border-radius: var(--mantine-radius-md);
+ transition: transform 250ms ease;
+ z-index: 2;
+}
+
+.control {
+ width: 100%;
+ padding: var(--mantine-spacing-md) var(--mantine-spacing-lg);
+ border-radius: var(--mantine-radius-md);
+ position: relative;
+ height: rem(80px);
+
+ @mixin hover {
+ background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-8));
+ }
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ height: rem(60px);
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
+ }
+}
+
+.controlInner {
+ display: flex;
+ align-items: center;
+ position: relative;
+ z-index: 3;
+}
+
+.controlTitle {
+ font-weight: 700;
+ font-family: var(--docs-font-primary);
+ position: relative;
+ z-index: 5;
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: var(--mantine-font-size-sm);
+ }
+}
+
+.controlDescription {
+ @media (max-width: $mantine-breakpoint-sm) {
+ font-size: var(--mantine-font-size-xs);
+ }
+}
+
+.controlIcon {
+ color: var(--mantine-color-blue-6);
+ margin-right: var(--mantine-spacing-md);
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: var(--mantine-spacing-md);
+ }
+}
+
+.demo {
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+
+ &[data-should-animate] {
+ animation: fade-in-demo-tabs 300ms ease;
+ }
+
+ @mixin rtl {
+ padding-right: calc(var(--mantine-spacing-xl) * 2);
+ padding-left: 0;
+ }
+
+ @media (max-width: $mantine-breakpoint-md) {
+ padding-left: 0;
+ padding-top: var(--mantine-spacing-xl);
+
+ @mixin rtl {
+ padding-right: 0;
+ }
+ }
+}
diff --git a/docs/src/components/HomePage/DemoTabs/DemoTabs.tsx b/docs/components/HomePage/DemoTabs/DemoTabs.tsx
similarity index 79%
rename from docs/src/components/HomePage/DemoTabs/DemoTabs.tsx
rename to docs/components/HomePage/DemoTabs/DemoTabs.tsx
index f2916891fdc..75f8a152450 100644
--- a/docs/src/components/HomePage/DemoTabs/DemoTabs.tsx
+++ b/docs/components/HomePage/DemoTabs/DemoTabs.tsx
@@ -1,9 +1,9 @@
import React, { useState, useRef } from 'react';
-import { Container, Grid, UnstyledButton, Text, Box, rem } from '@mantine/core';
+import { Container, Grid, UnstyledButton, Text, Box, rem, useMantineTheme } from '@mantine/core';
import { IconForms } from '@tabler/icons-react';
import { useMediaQuery } from '@mantine/hooks';
import { SectionTitle } from '../SectionTitle/SectionTitle';
-import useStyles from './DemoTabs.styles';
+import classes from './DemoTabs.module.css';
interface DemoTabsProps {
title: string;
@@ -17,9 +17,9 @@ interface DemoTabsProps {
export function DemoTabs({ data, title }: DemoTabsProps) {
const [shouldAnimate, setShouldAnimate] = useState(false);
- const { classes, theme } = useStyles({ shouldAnimate });
const animationTimeout = useRef();
const [active, setActive] = useState(0);
+ const theme = useMantineTheme();
const controlSize = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`) ? 60 : 80;
const handleActiveChange = (index: number) => {
@@ -39,10 +39,10 @@ export function DemoTabs({ data, title }: DemoTabsProps) {
className={classes.control}
>
-
+
{item.name}
-
+
{item.description}
@@ -57,11 +57,11 @@ export function DemoTabs({ data, title }: DemoTabsProps) {
{title}
-
+
-
-
+
+
diff --git a/docs/components/HomePage/HomePage.module.css b/docs/components/HomePage/HomePage.module.css
new file mode 100644
index 00000000000..d11a5a9e179
--- /dev/null
+++ b/docs/components/HomePage/HomePage.module.css
@@ -0,0 +1,35 @@
+.root {
+ position: relative;
+ z-index: 1;
+ box-shadow: var(--mantine-shadow-md);
+ background-color: var(--mantine-color-body);
+ min-height: 100vh;
+
+ @mixin light {
+ --docs-home-page-gradient-text: linear-gradient(
+ 52deg,
+ var(--mantine-color-blue-7) 3%,
+ var(--mantine-color-cyan-5) 97%
+ );
+
+ --docs-home-page-gradient: linear-gradient(
+ 52deg,
+ var(--mantine-color-blue-7) 3%,
+ var(--mantine-color-cyan-5) 97%
+ );
+ }
+
+ @mixin dark {
+ --docs-home-page-gradient-text: linear-gradient(
+ 52deg,
+ var(--mantine-color-blue-5) 3%,
+ var(--mantine-color-cyan-4) 97%
+ );
+
+ --docs-home-page-gradient: linear-gradient(
+ 52deg,
+ var(--mantine-color-blue-7) 3%,
+ var(--mantine-color-cyan-5) 97%
+ );
+ }
+}
diff --git a/docs/components/HomePage/HomePage.tsx b/docs/components/HomePage/HomePage.tsx
new file mode 100644
index 00000000000..7691d98e564
--- /dev/null
+++ b/docs/components/HomePage/HomePage.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import Head from 'next/head';
+import { Shell } from '../Shell';
+import { Footer } from '../Footer';
+import { Jumbotron } from './Jumbotron/Jumbotron';
+import { Waves } from './Waves/Waves';
+import { Components } from './Components/Components';
+import { DarkTheme } from './DarkTheme/DarkTheme';
+import { CustomizeWithProps } from './CustomizeWithProps/CustomizeWithProps';
+import { CustomizeStyles } from './CustomizeStyles/CustomizeStyles';
+import { Theming } from './Theming/Theming';
+import { Banner } from './Banner/Banner';
+import { Hooks } from './Hooks/Hooks';
+import { Usage } from './Usage/Usage';
+import { JoinCommunity } from './JoinCommunity/JoinCommunity';
+import classes from './HomePage.module.css';
+
+export function HomePage() {
+ return (
+ <>
+
+
Mantine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/docs/components/HomePage/Hooks/Hooks.module.css b/docs/components/HomePage/Hooks/Hooks.module.css
new file mode 100644
index 00000000000..27bcb30735a
--- /dev/null
+++ b/docs/components/HomePage/Hooks/Hooks.module.css
@@ -0,0 +1,25 @@
+.demo {
+ & [data-with-padding] {
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ }
+
+ & :global(textarea) {
+ @mixin light {
+ background-color: var(--mantine-color-gray-1) !important;
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-5) !important;
+ }
+ }
+
+ & :global(.mantine-CodeHighlightTabs-root) {
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ }
+}
+
+.demoWrapper {
+ box-shadow: var(--mantine-shadow-md);
+ margin-bottom: var(--mantine-spacing-xl);
+ border-radius: var(--mantine-radius-md);
+}
diff --git a/docs/components/HomePage/Hooks/Hooks.tsx b/docs/components/HomePage/Hooks/Hooks.tsx
new file mode 100644
index 00000000000..7ca7c7eba73
--- /dev/null
+++ b/docs/components/HomePage/Hooks/Hooks.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { Box } from '@mantine/core';
+import {
+ IconArrowsMaximize,
+ IconClock,
+ IconForms,
+ IconHandOff,
+ IconMaximize,
+ IconResize,
+} from '@tabler/icons-react';
+import { Demo } from '@mantine/ds';
+import { FormDemos, HooksDemos } from '@mantine/demos';
+import { DemoTabs } from '../DemoTabs/DemoTabs';
+import classes from './Hooks.module.css';
+
+const data = [
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconResize,
+ name: 'use-element-size',
+ description: 'Subscribe to element size changes',
+ },
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconClock,
+ name: 'use-debounced-value',
+ description: 'Debounce value changes',
+ },
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconHandOff,
+ name: 'use-idle',
+ description: 'Detect if user does nothing',
+ },
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconMaximize,
+ name: 'use-fullscreen',
+ description: 'Enter/exit fullscreen',
+ },
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconArrowsMaximize,
+ name: 'use-move',
+ description: 'Slide behavior over any element',
+ },
+ {
+ demo: () => (
+
+
+
+ ),
+ icon: IconForms,
+ name: 'use-form',
+ description: 'Forms management library',
+ },
+];
+
+export function Hooks() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/JoinCommunity/JoinCommunity.tsx b/docs/components/HomePage/JoinCommunity/JoinCommunity.tsx
new file mode 100644
index 00000000000..0c3f4f5ef84
--- /dev/null
+++ b/docs/components/HomePage/JoinCommunity/JoinCommunity.tsx
@@ -0,0 +1,14 @@
+import { Space } from '@mantine/core';
+import React from 'react';
+import { SocialCards } from '../../SocialCards';
+import { PageSection } from '../PageSection/PageSection';
+
+export function JoinCommunity() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/src/components/HomePage/Jumbotron/Hero.tsx b/docs/components/HomePage/Jumbotron/Hero.tsx
similarity index 89%
rename from docs/src/components/HomePage/Jumbotron/Hero.tsx
rename to docs/components/HomePage/Jumbotron/Hero.tsx
index 842f04e34eb..cf176f61dd9 100644
--- a/docs/src/components/HomePage/Jumbotron/Hero.tsx
+++ b/docs/components/HomePage/Jumbotron/Hero.tsx
@@ -1,12 +1,18 @@
import React from 'react';
import { useMantineTheme } from '@mantine/core';
+import classes from './Jumbotron.module.css';
export function Hero() {
const theme = useMantineTheme();
return (
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
diff --git a/docs/components/HomePage/Jumbotron/Jumbotron.module.css b/docs/components/HomePage/Jumbotron/Jumbotron.module.css
new file mode 100644
index 00000000000..850b7e15d7e
--- /dev/null
+++ b/docs/components/HomePage/Jumbotron/Jumbotron.module.css
@@ -0,0 +1,122 @@
+$jumbotron-breakpoint: em(960px);
+
+.jumbotron {
+ position: relative;
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+}
+
+.inner {
+ position: relative;
+ padding-top: rem(220px);
+ padding-bottom: rem(180px);
+
+ @media (max-width: $jumbotron-breakpoint) {
+ padding-top: rem(60px);
+ padding-bottom: rem(90px);
+ }
+}
+
+.description {
+ margin-top: calc(var(--mantine-spacing-xl) * 1.5);
+ font-size: rem(24px);
+ max-width: rem(800px);
+ color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
+
+ @media (max-width: $jumbotron-breakpoint) {
+ margin-top: var(--mantine-spacing-lg);
+ font-size: rem(18px);
+ }
+}
+
+.controls {
+ margin-top: calc(var(--mantine-spacing-xl) * 1.5);
+
+ @media (max-width: $jumbotron-breakpoint) {
+ margin-top: var(--mantine-spacing-xl);
+ }
+}
+
+.control {
+ height: rem(64px);
+ padding-left: rem(46px);
+ padding-right: rem(46px);
+ font-size: rem(20px);
+ font-weight: 700;
+ border: 0;
+
+ @media (max-width: $jumbotron-breakpoint) {
+ height: rem(54px);
+ padding-left: rem(18px);
+ padding-right: rem(18px);
+ flex: 1;
+ }
+
+ &[data-primary] {
+ background-image: var(--docs-home-page-gradient);
+ }
+
+ &[data-github] {
+ background-color: var(--mantine-color-dark-6);
+ color: var(--mantine-color-white);
+
+ & :global(.mantine-Button-section) {
+ margin-right: rem(16px);
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: rem(16px);
+ }
+ }
+
+ @mixin hover {
+ background-color: var(--mantine-color-dark-5);
+ }
+ }
+}
+
+.feature {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ @media (max-width: $mantine-breakpoint-md) {
+ flex-direction: row;
+ }
+}
+
+.featureBody {
+ margin-top: var(--mantine-spacing-xs);
+
+ @media (max-width: $mantine-breakpoint-md) {
+ margin-top: 0;
+ margin-left: var(--mantine-spacing-lg);
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-lg);
+ }
+ }
+}
+
+.featureTitle {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+}
+
+.featureIcon {
+ color: var(--mantine-color-white);
+ border-radius: var(--mantine-radius-md);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: rem(50px);
+ height: rem(50px);
+ background-image: var(--docs-home-page-gradient);
+
+ & svg {
+ display: block;
+ }
+}
+
+.heroSvg {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+}
diff --git a/docs/components/HomePage/Jumbotron/Jumbotron.tsx b/docs/components/HomePage/Jumbotron/Jumbotron.tsx
new file mode 100644
index 00000000000..6f3331a85f8
--- /dev/null
+++ b/docs/components/HomePage/Jumbotron/Jumbotron.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import Link from 'next/link';
+import { Container, Group, Button, Text, SimpleGrid, rem } from '@mantine/core';
+import { GithubIcon } from '@mantine/ds';
+import { Hero } from './Hero';
+import { FEATURES_DATA } from './features';
+import classes from './Jumbotron.module.css';
+
+export function Jumbotron() {
+ const features = FEATURES_DATA.map((feature) => (
+
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+
+ ));
+
+ return (
+
+
+
+
+
+ Build fully functional accessible web applications faster than ever – Mantine includes
+ more than 100 customizable components and 50 hooks to cover you in any situation
+
+
+
+ {features}
+
+
+
+
+ Get started
+
+
+ }
+ >
+ GitHub
+
+
+
+
+ );
+}
diff --git a/docs/src/components/HomePage/Jumbotron/features.ts b/docs/components/HomePage/Jumbotron/features.ts
similarity index 100%
rename from docs/src/components/HomePage/Jumbotron/features.ts
rename to docs/components/HomePage/Jumbotron/features.ts
diff --git a/docs/components/HomePage/PageSection/PageSection.module.css b/docs/components/HomePage/PageSection/PageSection.module.css
new file mode 100644
index 00000000000..109024d4c69
--- /dev/null
+++ b/docs/components/HomePage/PageSection/PageSection.module.css
@@ -0,0 +1,19 @@
+.wrapper {
+ padding-top: rem(120px);
+ padding-bottom: rem(60px);
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+
+ &[data-alt] {
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ }
+}
+
+.description {
+ line-height: 1.6;
+ opacity: 0.85;
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+
+ @media (max-width: em(960px)) {
+ font-size: var(--mantine-font-size-md);
+ }
+}
diff --git a/docs/components/HomePage/PageSection/PageSection.tsx b/docs/components/HomePage/PageSection/PageSection.tsx
new file mode 100644
index 00000000000..dcc57d904a6
--- /dev/null
+++ b/docs/components/HomePage/PageSection/PageSection.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Container, Text, BoxProps, ElementProps, Box } from '@mantine/core';
+import { SectionTitle } from '../SectionTitle/SectionTitle';
+import classes from './PageSection.module.css';
+
+interface PageSectionProps extends BoxProps, ElementProps<'div'> {
+ title: string;
+ description?: string;
+ children: React.ReactNode;
+ alt?: boolean;
+}
+
+export function PageSection({ title, description, children, alt, ...others }: PageSectionProps) {
+ return (
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+
+ );
+}
diff --git a/docs/components/HomePage/SectionTitle/SectionTitle.module.css b/docs/components/HomePage/SectionTitle/SectionTitle.module.css
new file mode 100644
index 00000000000..18c32eed3ee
--- /dev/null
+++ b/docs/components/HomePage/SectionTitle/SectionTitle.module.css
@@ -0,0 +1,96 @@
+.title {
+ --title-color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+
+ display: inline-block;
+ position: relative;
+ font-family: var(--docs-font-primary);
+ font-size: rem(42px);
+ font-weight: 900;
+ line-height: 1.1;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ margin-bottom: calc(var(--mantine-spacing-xl) * 2);
+ letter-spacing: rem(1px);
+ margin-left: rem(22px);
+ text-transform: uppercase;
+ color: var(--title-color);
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: rem(22px);
+ text-align: right;
+ }
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ margin-bottom: var(--mantine-spacing-md);
+ }
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ width: rem(10px);
+ height: rem(10px);
+ }
+
+ &::before {
+ top: rem(-14px);
+ left: rem(-22px);
+ border-left: rem(5px) solid var(--title-color);
+ border-top: rem(5px) solid var(--title-color);
+
+ @mixin rtl {
+ left: auto;
+ right: rem(-22px);
+ border-left: none;
+ border-right: rem(5px) solid var(--title-color);
+ }
+ }
+
+ &::after {
+ bottom: rem(-14px);
+ right: rem(-22px);
+ border-right: rem(5px) solid var(--title-color);
+ border-bottom: rem(5px) solid var(--title-color);
+
+ @mixin rtl {
+ right: auto;
+ left: rem(-22px);
+ border-right: none;
+ border-left: rem(5px) solid var(--title-color);
+ }
+ }
+
+ @media (max-width: em(600px)) {
+ font-size: rem(28px);
+ margin-left: 0;
+
+ @mixin rtl {
+ margin-right: 0;
+ }
+
+ &::before,
+ &::after {
+ display: none;
+ }
+ }
+
+ &[data-white] {
+ color: var(--mantine-color-white);
+ text-shadow: rem(1px) rem(2px) rem(2px) rgba(0, 0, 0, 0.3);
+
+ &::before,
+ &::after {
+ filter: drop-shadow(rem(1px) rem(2px) rem(2px) rgba(0, 0, 0, 0.3));
+ border-color: var(--mantine-color-white);
+ }
+ }
+
+ &[data-default] {
+ &::before,
+ &::after {
+ border-color: var(--title-color);
+ }
+ }
+}
diff --git a/docs/components/HomePage/SectionTitle/SectionTitle.tsx b/docs/components/HomePage/SectionTitle/SectionTitle.tsx
new file mode 100644
index 00000000000..0cd3164d62b
--- /dev/null
+++ b/docs/components/HomePage/SectionTitle/SectionTitle.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import cx from 'clsx';
+import classes from './SectionTitle.module.css';
+
+interface SectionTitleProps extends React.ComponentProps<'h1'> {
+ type?: 'white' | 'default';
+}
+
+export function SectionTitle({
+ children,
+ className,
+ type = 'default',
+ ...others
+}: SectionTitleProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/docs/components/HomePage/Theming/Theming.module.css b/docs/components/HomePage/Theming/Theming.module.css
new file mode 100644
index 00000000000..96d119118c8
--- /dev/null
+++ b/docs/components/HomePage/Theming/Theming.module.css
@@ -0,0 +1,6 @@
+.codeHighlight {
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ box-shadow: var(--mantine-shadow-md);
+ border-radius: var(--mantine-radius-md);
+ margin-top: var(--mantine-spacing-xl);
+}
diff --git a/docs/components/HomePage/Theming/Theming.tsx b/docs/components/HomePage/Theming/Theming.tsx
new file mode 100644
index 00000000000..1da8461bcb5
--- /dev/null
+++ b/docs/components/HomePage/Theming/Theming.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Group, Button, Badge } from '@mantine/core';
+import { CodeHighlight } from '@mantine/code-highlight';
+import { PageSection } from '../PageSection/PageSection';
+import classes from './Theming.module.css';
+
+const code = `
+import { Badge, Button, MantineProvider, createTheme } from '@mantine/core';
+
+const theme = createTheme({
+ fontFamily: 'Greycliff CF, sans-serif',
+ colors: {
+ 'ocean-blue': ['#7AD1DD', '#5FCCDB', '#44CADC', '#2AC9DE', '#1AC2D9', '#11B7CD', '#09ADC3', '#0E99AC', '#128797', '#147885'],
+ 'bright-pink': ['#F0BBDD', '#ED9BCF', '#EC7CC3', '#ED5DB8', '#F13EAF', '#F71FA7', '#FF00A1', '#E00890', '#C50E82', '#AD1374'],
+ },
+});
+
+function Demo() {
+ return (
+
+ Ocean blue button
+ Bright pink badge
+
+ );
+}
+`;
+
+export function Theming() {
+ return (
+
+
+
+ Ocean blue button
+
+
+ Bright pink badge
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Usage/Usage.tsx b/docs/components/HomePage/Usage/Usage.tsx
new file mode 100644
index 00000000000..e70e620c6ee
--- /dev/null
+++ b/docs/components/HomePage/Usage/Usage.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Space } from '@mantine/core';
+import { FrameworksGuides } from '../../FrameworksGuides';
+import { PageSection } from '../PageSection/PageSection';
+
+export function Usage() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/Waves/Waves.module.css b/docs/components/HomePage/Waves/Waves.module.css
new file mode 100644
index 00000000000..9cdaf963a25
--- /dev/null
+++ b/docs/components/HomePage/Waves/Waves.module.css
@@ -0,0 +1,34 @@
+.root {
+ overflow: hidden;
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+
+ &[data-alt] {
+ transform: rotate(180deg);
+ background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ }
+}
+
+.waves {
+ fill: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
+ width: var(--waves-width);
+ height: var(--waves-height);
+ filter: drop-shadow(rem(10px) rem(5px) rem(5px) rgba(0, 0, 0, 0.03));
+ display: block;
+
+ &[data-flip] {
+ transform: scaleY(-1);
+
+ &[data-rotate] {
+ transform: scaleY(1) rotate(180deg);
+ }
+ }
+
+ &[data-alt] {
+ fill: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
+ filter: none;
+ }
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ height: rem(18px);
+ }
+}
diff --git a/docs/components/HomePage/Waves/Waves.tsx b/docs/components/HomePage/Waves/Waves.tsx
new file mode 100644
index 00000000000..4ed8df7878a
--- /dev/null
+++ b/docs/components/HomePage/Waves/Waves.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Box, rem } from '@mantine/core';
+import classes from './Waves.module.css';
+
+interface WavesProps {
+ height: number;
+ width: number;
+ flip?: boolean;
+ alt?: boolean;
+ rotate?: boolean;
+}
+
+export function Waves({ height, width, flip, alt, rotate }: WavesProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/components/HomePage/index.ts b/docs/components/HomePage/index.ts
new file mode 100644
index 00000000000..0799f479a25
--- /dev/null
+++ b/docs/components/HomePage/index.ts
@@ -0,0 +1 @@
+export { HomePage } from './HomePage';
diff --git a/docs/components/HotKeysHandler/HotKeysHandler.tsx b/docs/components/HotKeysHandler/HotKeysHandler.tsx
new file mode 100644
index 00000000000..07d07a4ad9e
--- /dev/null
+++ b/docs/components/HotKeysHandler/HotKeysHandler.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { useHotkeys } from '@mantine/hooks';
+import { useMantineColorScheme, useComputedColorScheme, useDirection } from '@mantine/core';
+
+export function HotKeysHandler() {
+ const { setColorScheme } = useMantineColorScheme();
+ const { toggleDirection } = useDirection();
+ const computedColorScheme = useComputedColorScheme('light');
+ useHotkeys(
+ [
+ ['mod + J', () => setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')],
+ ['mod + shift + L', () => toggleDirection()],
+ ],
+ []
+ );
+ return <>{null}>;
+}
diff --git a/docs/components/HotKeysHandler/index.ts b/docs/components/HotKeysHandler/index.ts
new file mode 100644
index 00000000000..72cf65929be
--- /dev/null
+++ b/docs/components/HotKeysHandler/index.ts
@@ -0,0 +1 @@
+export { HotKeysHandler } from './HotKeysHandler';
diff --git a/docs/components/HtmlText/HtmlText.tsx b/docs/components/HtmlText/HtmlText.tsx
new file mode 100644
index 00000000000..201b5ba8386
--- /dev/null
+++ b/docs/components/HtmlText/HtmlText.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Text, TextProps, ElementProps } from '@mantine/core';
+
+interface HtmlTextProps extends TextProps, ElementProps<'span', 'color'> {
+ children: string;
+}
+
+function replaceBackticks(str: string): string {
+ return str.replace(/`([^`]+)`/g, '$1
');
+}
+
+export function HtmlText({ children, ...others }: HtmlTextProps) {
+ return (
+
+ );
+}
diff --git a/docs/components/HtmlText/index.ts b/docs/components/HtmlText/index.ts
new file mode 100644
index 00000000000..d3ebf2c9f9b
--- /dev/null
+++ b/docs/components/HtmlText/index.ts
@@ -0,0 +1 @@
+export { HtmlText } from './HtmlText';
diff --git a/docs/components/Logo/Logo.module.css b/docs/components/Logo/Logo.module.css
new file mode 100644
index 00000000000..7aa33df656a
--- /dev/null
+++ b/docs/components/Logo/Logo.module.css
@@ -0,0 +1,14 @@
+.logo {
+ text-decoration: none;
+ user-select: none;
+ display: flex;
+ align-items: center;
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
diff --git a/docs/components/Logo/Logo.tsx b/docs/components/Logo/Logo.tsx
new file mode 100644
index 00000000000..9395dacde6d
--- /dev/null
+++ b/docs/components/Logo/Logo.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import Link from 'next/link';
+import cx from 'clsx';
+import { VisuallyHidden, FOCUS_CLASS_NAMES } from '@mantine/core';
+import { MantineLogo, MantineLogoProps } from '@mantine/ds';
+import classes from './Logo.module.css';
+
+export function Logo(props: MantineLogoProps) {
+ return (
+
+
+ Welcome to Mantine, React components library that you always wished for
+
+
+
+ );
+}
diff --git a/docs/components/Logo/index.ts b/docs/components/Logo/index.ts
new file mode 100644
index 00000000000..33af5053383
--- /dev/null
+++ b/docs/components/Logo/index.ts
@@ -0,0 +1 @@
+export { Logo } from './Logo';
diff --git a/docs/components/LogoAssets/LogoAsset/LogoAsset.module.css b/docs/components/LogoAssets/LogoAsset/LogoAsset.module.css
new file mode 100644
index 00000000000..fb55fe4c6d9
--- /dev/null
+++ b/docs/components/LogoAssets/LogoAsset/LogoAsset.module.css
@@ -0,0 +1,11 @@
+.image {
+ max-width: 100%;
+}
+
+.imageWrapper {
+ padding: var(--mantine-spacing-xl);
+ border-radius: var(--mantine-radius-md);
+ display: flex;
+ justify-content: center;
+ border: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+}
diff --git a/docs/components/LogoAssets/LogoAsset/LogoAsset.tsx b/docs/components/LogoAssets/LogoAsset/LogoAsset.tsx
new file mode 100644
index 00000000000..e30beb45a5f
--- /dev/null
+++ b/docs/components/LogoAssets/LogoAsset/LogoAsset.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Group, Button } from '@mantine/core';
+import { useClipboard } from '@mantine/hooks';
+import classes from './LogoAsset.module.css';
+
+export interface LogoAssetProps extends React.ComponentPropsWithoutRef<'div'> {
+ image: string;
+ imageWidth: number;
+ code: string;
+ fileName: string;
+ background: string;
+}
+
+export function LogoAsset({
+ image,
+ code,
+ fileName,
+ imageWidth,
+ background,
+ ...others
+}: LogoAssetProps) {
+ const clipboard = useClipboard();
+
+ return (
+
+
+
+
+
+
+
+ Download svg
+
+ clipboard.copy(code)}
+ >
+ {clipboard.copied ? 'Copied' : 'Copy svg code'}
+
+
+
+ );
+}
diff --git a/docs/components/LogoAssets/LogoAssets.module.css b/docs/components/LogoAssets/LogoAssets.module.css
new file mode 100644
index 00000000000..089596fa7f5
--- /dev/null
+++ b/docs/components/LogoAssets/LogoAssets.module.css
@@ -0,0 +1,15 @@
+.root {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--mantine-spacing-xl);
+ margin-bottom: var(--mantine-spacing-xl);
+}
+
+.asset {
+ flex: 1;
+ max-width: calc(50% - var(--mantine-spacing-xl) / 2);
+
+ @media (max-width: em(500px)) {
+ max-width: 100%;
+ }
+}
diff --git a/docs/components/LogoAssets/LogoAssets.tsx b/docs/components/LogoAssets/LogoAssets.tsx
new file mode 100644
index 00000000000..abc477aa20d
--- /dev/null
+++ b/docs/components/LogoAssets/LogoAssets.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { LogoAsset } from './LogoAsset/LogoAsset';
+import { logos } from './assets';
+import classes from './LogoAssets.module.css';
+
+export function LogoAssets() {
+ const items = logos.map((logo) => (
+
+ ));
+ return {items}
;
+}
diff --git a/docs/components/LogoAssets/assets/index.ts b/docs/components/LogoAssets/assets/index.ts
new file mode 100644
index 00000000000..5e59dc35ead
--- /dev/null
+++ b/docs/components/LogoAssets/assets/index.ts
@@ -0,0 +1,28 @@
+import logoFull from './mantine-logo-full.svg';
+import logoWhite from './mantine-logo-white.svg';
+import logo from './mantine-logo.svg';
+import { LogoAssetProps } from '../LogoAsset/LogoAsset';
+
+export const logos: LogoAssetProps[] = [
+ {
+ image: logo.src,
+ code: ' ',
+ background: 'var(--mantine-color-white)',
+ fileName: 'mantine-logo.svg',
+ imageWidth: 47,
+ },
+ {
+ image: logoFull.src,
+ code: ' ',
+ background: 'var(--mantine-color-white)',
+ fileName: 'mantine-logo-full.svg',
+ imageWidth: 180,
+ },
+ {
+ image: logoWhite.src,
+ code: ' ',
+ background: 'var(--mantine-color-dark-8)',
+ fileName: 'mantine-logo-white.svg',
+ imageWidth: 180,
+ },
+];
diff --git a/docs/src/components/LogoAssets/assets/mantine-logo-full.svg b/docs/components/LogoAssets/assets/mantine-logo-full.svg
similarity index 100%
rename from docs/src/components/LogoAssets/assets/mantine-logo-full.svg
rename to docs/components/LogoAssets/assets/mantine-logo-full.svg
diff --git a/docs/src/components/LogoAssets/assets/mantine-logo-white.svg b/docs/components/LogoAssets/assets/mantine-logo-white.svg
similarity index 100%
rename from docs/src/components/LogoAssets/assets/mantine-logo-white.svg
rename to docs/components/LogoAssets/assets/mantine-logo-white.svg
diff --git a/docs/src/components/LogoAssets/assets/mantine-logo.svg b/docs/components/LogoAssets/assets/mantine-logo.svg
similarity index 100%
rename from docs/src/components/LogoAssets/assets/mantine-logo.svg
rename to docs/components/LogoAssets/assets/mantine-logo.svg
diff --git a/docs/components/LogoAssets/index.ts b/docs/components/LogoAssets/index.ts
new file mode 100644
index 00000000000..6c660defacc
--- /dev/null
+++ b/docs/components/LogoAssets/index.ts
@@ -0,0 +1 @@
+export { LogoAssets } from './LogoAssets';
diff --git a/docs/components/MdxLayout/MdxLayout.tsx b/docs/components/MdxLayout/MdxLayout.tsx
new file mode 100644
index 00000000000..d38f376bbd5
--- /dev/null
+++ b/docs/components/MdxLayout/MdxLayout.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Frontmatter } from '@/types';
+import { MdxPage } from '@/components/MdxPage';
+import { PageHead } from '../PageHead';
+
+interface MdxLayoutProps {
+ meta: Frontmatter;
+ children: React.ReactNode;
+}
+
+export function MdxLayout({ meta, children }: MdxLayoutProps) {
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
+
+export function Layout(meta: Frontmatter) {
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+}
diff --git a/docs/components/MdxLayout/index.ts b/docs/components/MdxLayout/index.ts
new file mode 100644
index 00000000000..91860e4e9bd
--- /dev/null
+++ b/docs/components/MdxLayout/index.ts
@@ -0,0 +1 @@
+export { MdxLayout, Layout } from './MdxLayout';
diff --git a/docs/components/MdxPage/MdxPage.tsx b/docs/components/MdxPage/MdxPage.tsx
new file mode 100644
index 00000000000..53da57083b1
--- /dev/null
+++ b/docs/components/MdxPage/MdxPage.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { MdxPageHeader } from '@/components/MdxPageHeader';
+import { MdxTabs } from '@/components/MdxTabs';
+import { MdxRawContent } from '@/components/MdxRawContent';
+import { Frontmatter } from '@/types';
+
+interface MdxPageProps {
+ meta: Frontmatter;
+ children: React.ReactNode;
+}
+
+export function MdxPage(props: MdxPageProps) {
+ return (
+ <>
+
+ {Array.isArray(props.meta.props) ? : }
+ >
+ );
+}
diff --git a/docs/components/MdxPage/index.ts b/docs/components/MdxPage/index.ts
new file mode 100644
index 00000000000..b091e7165c2
--- /dev/null
+++ b/docs/components/MdxPage/index.ts
@@ -0,0 +1 @@
+export { MdxPage } from './MdxPage';
diff --git a/docs/components/MdxPageHeader/HeaderItem/HeaderItem.module.css b/docs/components/MdxPageHeader/HeaderItem/HeaderItem.module.css
new file mode 100644
index 00000000000..46e4ffdd3cc
--- /dev/null
+++ b/docs/components/MdxPageHeader/HeaderItem/HeaderItem.module.css
@@ -0,0 +1,29 @@
+.item {
+ display: flex;
+ align-items: center;
+ min-height: rem(20px);
+ overflow-x: hidden;
+
+ & + & {
+ margin-top: var(--mantine-spacing-xs);
+ }
+}
+
+.label {
+ width: rem(100px);
+ min-width: rem(100px);
+ height: rem(20px);
+ line-height: rem(20px);
+
+ @media (max-width: 31em) {
+ display: none;
+ }
+
+ @mixin light {
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-2);
+ }
+}
diff --git a/docs/components/MdxPageHeader/HeaderItem/HeaderItem.tsx b/docs/components/MdxPageHeader/HeaderItem/HeaderItem.tsx
new file mode 100644
index 00000000000..55bda0df17b
--- /dev/null
+++ b/docs/components/MdxPageHeader/HeaderItem/HeaderItem.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import cx from 'clsx';
+import { Text } from '@mantine/core';
+import classes from './HeaderItem.module.css';
+
+interface HeaderItemProps {
+ label: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function HeaderItem({ label, children, className }: HeaderItemProps) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
diff --git a/docs/components/MdxPageHeader/ImportStatement/ImportStatement.module.css b/docs/components/MdxPageHeader/ImportStatement/ImportStatement.module.css
new file mode 100644
index 00000000000..9c2906647d6
--- /dev/null
+++ b/docs/components/MdxPageHeader/ImportStatement/ImportStatement.module.css
@@ -0,0 +1,7 @@
+.wrapper {
+ white-space: nowrap;
+
+ @media (max-width: 31em) {
+ display: none;
+ }
+}
diff --git a/docs/components/MdxPageHeader/ImportStatement/ImportStatement.tsx b/docs/components/MdxPageHeader/ImportStatement/ImportStatement.tsx
new file mode 100644
index 00000000000..47729fc4a3d
--- /dev/null
+++ b/docs/components/MdxPageHeader/ImportStatement/ImportStatement.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { em, Tooltip, UnstyledButton } from '@mantine/core';
+import { InlineCodeHighlight } from '@mantine/code-highlight';
+import { useClipboard, useMediaQuery } from '@mantine/hooks';
+import { HeaderItem } from '../HeaderItem/HeaderItem';
+import classes from './ImportStatement.module.css';
+
+interface ImportStatementProps {
+ code: string;
+}
+
+export function ImportStatement({ code }: ImportStatementProps) {
+ const clipboard = useClipboard();
+ const mobile = useMediaQuery(`(max-width: ${em(500)})`);
+ return (
+
+
+ clipboard.copy(code)}>
+
+
+
+
+ );
+}
diff --git a/docs/components/MdxPageHeader/LinkItem/LinkItem.module.css b/docs/components/MdxPageHeader/LinkItem/LinkItem.module.css
new file mode 100644
index 00000000000..960b58de38f
--- /dev/null
+++ b/docs/components/MdxPageHeader/LinkItem/LinkItem.module.css
@@ -0,0 +1,29 @@
+.wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ line-height: 1;
+
+ @mixin light {
+ color: var(--mantine-colors-gray-9);
+ }
+
+ @mixin dark {
+ color: var(--mantine-colors-dark-0);
+ }
+}
+
+.icon {
+ margin-right: rem(12px);
+ display: flex;
+ align-items: center;
+
+ @mixin rtl {
+ margin-left: rem(12px);
+ margin-right: 0;
+ }
+}
diff --git a/docs/components/MdxPageHeader/LinkItem/LinkItem.tsx b/docs/components/MdxPageHeader/LinkItem/LinkItem.tsx
new file mode 100644
index 00000000000..b58a421643a
--- /dev/null
+++ b/docs/components/MdxPageHeader/LinkItem/LinkItem.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import Link from 'next/link';
+import { Anchor } from '@mantine/core';
+import { HeaderItem } from '../HeaderItem/HeaderItem';
+import classes from './LinkItem.module.css';
+
+interface LinkItemProps {
+ label: React.ReactNode;
+ icon: React.ReactNode;
+ children: React.ReactNode;
+ link: string;
+}
+
+export function LinkItem({ label, icon, children, link }: LinkItemProps) {
+ return (
+
+
+
+ component={link.startsWith('/') ? Link : 'a'}
+ className={classes.link}
+ href={link}
+ fz="sm"
+ >
+ {icon}
+ {children}
+
+
+
+ );
+}
diff --git a/docs/components/MdxPageHeader/MdxPageHeader.module.css b/docs/components/MdxPageHeader/MdxPageHeader.module.css
new file mode 100644
index 00000000000..9a9400e5bf7
--- /dev/null
+++ b/docs/components/MdxPageHeader/MdxPageHeader.module.css
@@ -0,0 +1,73 @@
+.wrapper {
+ position: relative;
+ z-index: 4;
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+ padding-right: calc(var(--mantine-spacing-xl) * 2);
+
+ @mixin light {
+ background-color: var(--mantine-color-gray-0);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-8);
+ }
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: var(--mantine-spacing-xl);
+ padding-right: var(--mantine-spacing-xl);
+ }
+}
+
+.header {
+ padding-top: var(--mantine-spacing-xl);
+ max-width: rem(1082px);
+ margin-left: auto;
+ margin-right: auto;
+ padding-bottom: calc(var(--mantine-spacing-xl) * 1.5);
+
+ &[data-with-tabs] {
+ padding-bottom: calc(var(--mantine-spacing-xl) * 1.5);
+ }
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ max-width: 100%;
+ padding-bottom: var(--mantine-spacing-xl);
+ }
+}
+
+.title {
+ font-size: rem(44px);
+ margin-bottom: rem(5px);
+ font-weight: 900;
+ font-family: var(--docs-font-primary);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ font-size: rem(32px);
+ }
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
+
+.description {
+ max-width: rem(450px);
+ margin-bottom: var(--mantine-spacing-xl);
+ font-size: var(--mantine-font-size-lg);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ font-size: var(--mantine-font-size-md);
+ }
+
+ @mixin light {
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-1);
+ }
+}
diff --git a/docs/components/MdxPageHeader/MdxPageHeader.tsx b/docs/components/MdxPageHeader/MdxPageHeader.tsx
new file mode 100644
index 00000000000..9630911be2f
--- /dev/null
+++ b/docs/components/MdxPageHeader/MdxPageHeader.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { Box, Title, Text, rem } from '@mantine/core';
+import { GithubIcon, NpmIcon } from '@mantine/ds';
+import { IconCalendar, IconLicense, IconPencil, IconSwitch2 } from '@tabler/icons-react';
+import { Frontmatter } from '@/types';
+import { LinkItem } from './LinkItem/LinkItem';
+import classes from './MdxPageHeader.module.css';
+import { ImportStatement } from './ImportStatement/ImportStatement';
+
+const REPO_BASE = 'https://github.com/rtivital/mantine/blob/master';
+const DOCS_BASE = `${REPO_BASE}/docs/pages`;
+const SOURCE_BASE = `${REPO_BASE}/src`;
+
+interface MdxPageHeaderProps {
+ meta: Frontmatter;
+}
+
+export function MdxPageHeader({ meta }: MdxPageHeaderProps) {
+ const withTabs = Array.isArray(meta.props);
+ const hasLinks = !!(meta.import || meta.source);
+ const withTitle = !!meta.title;
+
+ if (meta.hideHeader) {
+ return null;
+ }
+
+ if (!hasLinks && !withTabs && !meta.release && !withTitle) {
+ return null;
+ }
+
+ return (
+
+
+ {meta.title}
+ {meta.description}
+
+ {meta.import && }
+
+ {meta.polymorphic && (
+ }
+ link="/guides/polymorphic"
+ >
+ Polymorphic component
+
+ )}
+
+ {meta.source && (
+ }
+ link={`${SOURCE_BASE}/${meta.source}`}
+ >
+ View source code
+
+ )}
+
+ {meta.date && meta.release && (
+ }
+ link={meta.release}
+ >
+ {meta.date}
+
+ )}
+
+ {meta.release && (
+ } link={meta.release}>
+ Release on GitHub
+
+ )}
+
+ {meta.docs && (
+ }
+ link={`${DOCS_BASE}/${meta.docs}`}
+ >
+ Edit this page
+
+ )}
+
+ {meta.package && (
+ }
+ link={`https://www.npmjs.com/package/${meta.package}`}
+ >
+ {meta.package}
+
+ )}
+
+ {meta.license && (
+ }
+ link="https://github.com/mantinedev/mantine/blob/master/LICENSE"
+ >
+ MIT
+
+ )}
+
+
+ );
+}
diff --git a/docs/components/MdxPageHeader/index.ts b/docs/components/MdxPageHeader/index.ts
new file mode 100644
index 00000000000..8a5290d50f9
--- /dev/null
+++ b/docs/components/MdxPageHeader/index.ts
@@ -0,0 +1 @@
+export { MdxPageHeader } from './MdxPageHeader';
diff --git a/docs/components/MdxProvider/MdxDataTable/MdxDataTable.tsx b/docs/components/MdxProvider/MdxDataTable/MdxDataTable.tsx
new file mode 100644
index 00000000000..07a95fc6c31
--- /dev/null
+++ b/docs/components/MdxProvider/MdxDataTable/MdxDataTable.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Table, rem } from '@mantine/core';
+import { DocsSection } from '@/components/DocsSection';
+
+interface DataTableProps {
+ data: React.ReactNode[][];
+ head?: string[];
+}
+
+function getTransformedScaledValue(value: unknown) {
+ if (typeof value !== 'string' || !value.includes('var(--mantine-scale)')) {
+ return value as string;
+ }
+
+ return value
+ .match(/^calc\((.*?)\)$/)?.[1]
+ .split('*')[0]
+ .trim();
+}
+
+export function MdxDataTable({ data, head }: DataTableProps) {
+ const rows = data.map((row, index) => {
+ const cells = row.map((cell, cellIndex) => (
+ {getTransformedScaledValue(cell)}
+ ));
+ return {cells} ;
+ });
+
+ const ths = Array.isArray(head)
+ ? head.map((cell, index) => {cell} )
+ : null;
+
+ return (
+
+
+
+
+ {ths && (
+
+ {ths}
+
+ )}
+ {rows}
+
+
+
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxInfo/MdxInfo.module.css b/docs/components/MdxProvider/MdxInfo/MdxInfo.module.css
new file mode 100644
index 00000000000..ebaf46a7a4d
--- /dev/null
+++ b/docs/components/MdxProvider/MdxInfo/MdxInfo.module.css
@@ -0,0 +1,62 @@
+.root {
+ margin-top: calc(var(--mantine-spacing-xl) * 1.5);
+ margin-bottom: var(--mantine-spacing-lg);
+
+ & > p {
+ line-height: 1.65;
+ font-size: rem(15px);
+ }
+
+ /* Considered to be a heading of the info */
+ & > p > strong:only-child {
+ font-family: var(--docs-font-primary);
+ font-size: var(--mantine-font-size-lg);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+
+ & > code {
+ font-size: 80%;
+ }
+ }
+
+ & > p:first-of-type {
+ margin-top: 0;
+ }
+
+ & > p:last-of-type {
+ margin-bottom: 0;
+ }
+
+ & :global(.mantine-Code-root) {
+ @mixin light {
+ background-color: var(--docs-bq-code-bg-light);
+ }
+
+ @mixin dark {
+ background-color: var(--docs-bq-code-bg-dark);
+ }
+ }
+
+ & :global(.mantine-CodeHighlight-root) {
+ margin-top: var(--mantine-spacing-md);
+
+ @mixin light {
+ background-color: var(--mantine-color-white);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-7);
+ }
+ }
+}
+
+.icon {
+ width: rem(28px);
+ height: rem(28px);
+}
diff --git a/docs/components/MdxProvider/MdxInfo/MdxInfo.tsx b/docs/components/MdxProvider/MdxInfo/MdxInfo.tsx
new file mode 100644
index 00000000000..7bb6ddc34f7
--- /dev/null
+++ b/docs/components/MdxProvider/MdxInfo/MdxInfo.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import cx from 'clsx';
+import { IconInfoCircle } from '@tabler/icons-react';
+import { Blockquote, BlockquoteProps, rgba, useMantineTheme } from '@mantine/core';
+import classes from './MdxInfo.module.css';
+
+export function MdxInfo({ className, ...others }: BlockquoteProps) {
+ const theme = useMantineTheme();
+ return (
+ }
+ radius="md"
+ __vars={{
+ '--docs-bq-code-bg-light': rgba(theme.colors.blue[6], 0.2),
+ '--docs-bq-code-bg-dark': rgba(theme.colors.blue[4], 0.2),
+ }}
+ {...others}
+ />
+ );
+}
diff --git a/docs/components/MdxProvider/MdxInstallScript/MdxInstallScript.tsx b/docs/components/MdxProvider/MdxInstallScript/MdxInstallScript.tsx
new file mode 100644
index 00000000000..b355423f8c5
--- /dev/null
+++ b/docs/components/MdxProvider/MdxInstallScript/MdxInstallScript.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { MdxNpmScript } from '../MdxNpmScript/MdxNpmScript';
+
+interface MdxInstallScriptProps {
+ packages: string;
+ dev?: boolean;
+}
+
+export function MdxInstallScript({ packages, dev }: MdxInstallScriptProps) {
+ // Hello good sir/lady. Seems like you are interested in adding pnpm here.
+ // Please do not do that, this contribution is not welcome.
+ // https://github.com/mantinedev/mantine/pulls?q=is%3Apr+pnpm
+
+ return (
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxKeyboardEventsTable/MdxKeyboardEventsTable.tsx b/docs/components/MdxProvider/MdxKeyboardEventsTable/MdxKeyboardEventsTable.tsx
new file mode 100644
index 00000000000..7481c608908
--- /dev/null
+++ b/docs/components/MdxProvider/MdxKeyboardEventsTable/MdxKeyboardEventsTable.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { Kbd, Table, Code, rem } from '@mantine/core';
+
+interface KeyboardEventsTableProps {
+ data: { key: string; description: string; condition: string }[];
+}
+
+export function MdxKeyboardEventsTable({ data }: KeyboardEventsTableProps) {
+ const hasCondition = data.some((item) => item.condition);
+ const rows = data.map((item, index) => (
+
+
+ {item.key}
+
+ {item.description}
+ {hasCondition && {item.condition ? {item.condition}
: '–'} }
+
+ ));
+
+ return (
+
+
+
+
+
+ Key
+ Description
+ {hasCondition && Condition }
+
+
+ {rows}
+
+
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxLink/MdxLink.module.css b/docs/components/MdxProvider/MdxLink/MdxLink.module.css
new file mode 100644
index 00000000000..f77e6e69d50
--- /dev/null
+++ b/docs/components/MdxProvider/MdxLink/MdxLink.module.css
@@ -0,0 +1,3 @@
+.link {
+ font-size: rem(15px);
+}
diff --git a/docs/components/MdxProvider/MdxLink/MdxLink.tsx b/docs/components/MdxProvider/MdxLink/MdxLink.tsx
new file mode 100644
index 00000000000..e20fae565a1
--- /dev/null
+++ b/docs/components/MdxProvider/MdxLink/MdxLink.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import Link from 'next/link';
+import { Anchor } from '@mantine/core';
+import classes from './MdxLink.module.css';
+
+export function MdxLink({ href, ...others }: React.ComponentPropsWithoutRef<'a'>) {
+ const replaced = href?.replace('https://mantine.dev', '')!;
+
+ if (!replaced?.startsWith('http') && replaced.trim().length > 0) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.module.css b/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.module.css
new file mode 100644
index 00000000000..1528420f4e3
--- /dev/null
+++ b/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.module.css
@@ -0,0 +1,23 @@
+.root {
+ border: rem(1px) solid;
+ border-radius: var(--mantine-radius-md);
+ overflow: hidden;
+ margin-bottom: var(--mantine-spacing-xl);
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-5);
+ }
+}
+
+.icon {
+ margin-right: rem(5px);
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: rem(5px);
+ }
+}
diff --git a/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.tsx b/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.tsx
new file mode 100644
index 00000000000..8afb1151b16
--- /dev/null
+++ b/docs/components/MdxProvider/MdxNpmScript/MdxNpmScript.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useLocalStorage } from '@mantine/hooks';
+import { CodeHighlightTabs } from '@mantine/code-highlight';
+import { YarnIcon, NpmIcon } from '@mantine/ds';
+import classes from './MdxNpmScript.module.css';
+
+interface MdxNpmScriptProps {
+ yarnScript: string;
+ npmScript: string;
+}
+
+export function MdxNpmScript({ yarnScript, npmScript }: MdxNpmScriptProps) {
+ // Hello good sir/lady. Seems like you are interested in adding pnpm here.
+ // Please do not do that, this contribution is not welcome.
+ // https://github.com/mantinedev/mantine/pulls?q=is%3Apr+pnpm
+
+ const [tab, setTab] = useLocalStorage({
+ key: 'script-tab',
+ defaultValue: 0,
+ });
+
+ return (
+ ,
+ },
+ {
+ fileName: 'npm',
+ code: npmScript,
+ language: 'bash',
+ icon: ,
+ },
+ ]}
+ />
+ );
+}
diff --git a/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.module.css b/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.module.css
new file mode 100644
index 00000000000..9f357e10dd5
--- /dev/null
+++ b/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.module.css
@@ -0,0 +1,19 @@
+.row {
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+
+ @mixin hover {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ }
+}
+
+.checkbox {
+ pointer-events: none;
+ display: flex;
+}
+
+.hiddenMobile {
+ @media (max-width: em(750px)) {
+ display: none;
+ }
+}
diff --git a/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.tsx b/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.tsx
new file mode 100644
index 00000000000..06684d85fce
--- /dev/null
+++ b/docs/components/MdxProvider/MdxPackagesInstallation/MdxPackagesInstallation.tsx
@@ -0,0 +1,82 @@
+import React, { useState } from 'react';
+import { Checkbox, Code, Text, Table } from '@mantine/core';
+import { MdxInstallScript } from '../MdxInstallScript/MdxInstallScript';
+import { MdxParagraph } from '../MdxTypography/MdxTypography';
+import { PACKAGES_DATA } from './data';
+import classes from './MdxPackagesInstallation.module.css';
+
+function getPackagesList(selection: string[], extraPackages: string[]) {
+ const packages = selection.reduce((acc, item) => {
+ acc.push(...PACKAGES_DATA.find((i) => i.package === item)!.dependencies);
+ return acc;
+ }, []);
+
+ const unique = Array.from(
+ new Set(['@mantine/core', '@mantine/hooks', ...packages, ...extraPackages])
+ );
+ return unique.join(' ');
+}
+
+interface MdxPackagesInstallation {
+ extraPackages?: string[];
+}
+
+export function MdxPackagesInstallation({ extraPackages = [] }: MdxPackagesInstallation) {
+ const [selection, setSelection] = useState(['@mantine/core', '@mantine/hooks']);
+ const toggleSelection = (item: string) =>
+ setSelection((current) =>
+ current.includes(item) ? current.filter((i) => i !== item) : [...current, item]
+ );
+ const selectAll = () =>
+ setSelection((current) =>
+ current.length === PACKAGES_DATA.length ? [] : PACKAGES_DATA.map((item) => item.package)
+ );
+
+ const rows = PACKAGES_DATA.map((item) => (
+ toggleSelection(item.package)}
+ >
+
+ {}}
+ className={classes.checkbox}
+ />
+
+
+ {item.package}
+
+
+ {item.description}
+
+
+ ));
+
+ return (
+ <>
+ Choose packages that you will use in your application:
+
+
+
+
+ 0}
+ indeterminate={selection.length < PACKAGES_DATA.length && selection.length > 0}
+ />
+
+ Package
+ Description
+
+
+ {rows}
+
+
+ Install dependencies:
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxPackagesInstallation/data.ts b/docs/components/MdxProvider/MdxPackagesInstallation/data.ts
new file mode 100644
index 00000000000..05dbcd58a18
--- /dev/null
+++ b/docs/components/MdxProvider/MdxPackagesInstallation/data.ts
@@ -0,0 +1,70 @@
+export const PACKAGES_DATA = [
+ {
+ package: '@mantine/hooks',
+ description: 'Hooks for state and UI management',
+ dependencies: ['@mantine/hooks'],
+ },
+ {
+ package: '@mantine/core',
+ description: 'Core components library: inputs, buttons, overlays, etc.',
+ dependencies: ['@mantine/hooks', '@mantine/core'],
+ },
+ {
+ package: '@mantine/form',
+ description: 'Form management library',
+ dependencies: ['@mantine/form'],
+ },
+ {
+ package: '@mantine/dates',
+ description: 'Date inputs, calendars',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/dates', 'dayjs'],
+ },
+ {
+ package: '@mantine/notifications',
+ description: 'Notifications system',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/notifications'],
+ },
+ {
+ package: '@mantine/code-highlight',
+ description: 'Code highlight with your theme colors and styles',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/code-highlight'],
+ },
+ {
+ package: '@mantine/tiptap',
+ description: 'Rich text editor based on Tiptap',
+ dependencies: [
+ '@mantine/hooks',
+ '@mantine/core',
+ '@mantine/tiptap',
+ '@tabler/icons-react',
+ '@tiptap/react',
+ '@tiptap/extension-link',
+ '@tiptap/starter-kit',
+ ],
+ },
+ {
+ package: '@mantine/dropzone',
+ description: 'Capture files with drag and drop',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/dropzone'],
+ },
+ {
+ package: '@mantine/carousel',
+ description: 'Embla based carousel component',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/carousel', 'embla-carousel-react'],
+ },
+ {
+ package: '@mantine/spotlight',
+ description: 'Overlay command center',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/spotlight'],
+ },
+ {
+ package: '@mantine/modals',
+ description: 'Centralized modals manager',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/modals'],
+ },
+ {
+ package: '@mantine/nprogress',
+ description: 'Navigation progress',
+ dependencies: ['@mantine/hooks', '@mantine/core', '@mantine/nprogress'],
+ },
+];
diff --git a/docs/components/MdxProvider/MdxPre/MdxPre.module.css b/docs/components/MdxProvider/MdxPre/MdxPre.module.css
new file mode 100644
index 00000000000..3fb8a985be7
--- /dev/null
+++ b/docs/components/MdxProvider/MdxPre/MdxPre.module.css
@@ -0,0 +1,14 @@
+.code {
+ border: rem(1px) solid;
+ border-radius: var(--mantine-radius-md);
+ margin-top: var(--mantine-spacing-md);
+ margin-bottom: var(--mantine-spacing-md);
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-5);
+ }
+}
diff --git a/docs/components/MdxProvider/MdxPre/MdxPre.tsx b/docs/components/MdxProvider/MdxPre/MdxPre.tsx
new file mode 100644
index 00000000000..1bdbfe7ef8b
--- /dev/null
+++ b/docs/components/MdxProvider/MdxPre/MdxPre.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import cx from 'clsx';
+import { CodeHighlight, CodeHighlightProps } from '@mantine/code-highlight';
+import classes from './MdxPre.module.css';
+
+interface PreProps {
+ children?: React.ReactNode;
+}
+
+function getLanguage(children: any) {
+ const matches = (children.props.className || '').match(/language-(?.*)/);
+ return matches && matches.groups && matches.groups.lang ? matches.groups.lang : 'tsx';
+}
+
+function getCode(children: any) {
+ return children.props.children;
+}
+
+export function MdxCodeHighlight({ className, ...others }: CodeHighlightProps) {
+ return ;
+}
+
+export function MdxPre({ children }: PreProps) {
+ return ;
+}
diff --git a/docs/components/MdxProvider/MdxProvider.tsx b/docs/components/MdxProvider/MdxProvider.tsx
new file mode 100644
index 00000000000..0f79cc3ddcc
--- /dev/null
+++ b/docs/components/MdxProvider/MdxProvider.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { Demo } from '@mantine/ds';
+import { MDXProvider } from '@mdx-js/react';
+import { MdxDataTable } from './MdxDataTable/MdxDataTable';
+import { MdxPre } from './MdxPre/MdxPre';
+import { MdxLink } from './MdxLink/MdxLink';
+import { MdxParagraph, MdxLi, MdxUl, MdxCode } from './MdxTypography/MdxTypography';
+import { h } from './MdxTitle/MdxTitle';
+import { MdxInfo } from './MdxInfo/MdxInfo';
+import { MdxKeyboardEventsTable } from './MdxKeyboardEventsTable/MdxKeyboardEventsTable';
+import { MdxInstallScript } from './MdxInstallScript/MdxInstallScript';
+import { MdxPackagesInstallation } from './MdxPackagesInstallation/MdxPackagesInstallation';
+import { MdxNpmScript } from './MdxNpmScript/MdxNpmScript';
+import { MdxTemplatesList } from './MdxTemplatesList/MdxTemplatesList';
+import {
+ MdxGetElementRef,
+ MdxPolymorphic,
+ MdxTargetComponent,
+ MdxInputFeatures,
+ MdxInputAccessibility,
+ MdxFlexboxGapSupport,
+ MdxGradient,
+ MdxStylesApiSelectors,
+ MdxGetTemplates,
+ MdxComboboxDisclaimer,
+ MdxComboboxData,
+ MdxComboboxLargeData,
+ MdxComboboxFiltering,
+ MdxComboboxProps,
+} from './MdxSharedContent';
+
+export function MdxProvider({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children as any}
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxComboboxData.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxData.tsx
new file mode 100644
index 00000000000..62b7d4ba6a9
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxData.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxComboboxDataProps {
+ component: string;
+}
+
+const getStringArrayCode = (component: string) => `
+import { ${component} } from '@mantine/core';
+function Demo() {
+ return <${component} data={['React', 'Angular']} />;
+}
+`;
+
+const getArrayCode = (component: string) => `
+import { ${component} } from '@mantine/core';
+
+function Demo() {
+ return (
+ <${component}
+ data={[
+ { value: 'react', label: 'React' },
+ { value: 'ng', label: 'Angular' },
+ ]}
+ />
+ );
+}
+`;
+
+const getStringGroupsCode = (component: string) => `
+import { ${component} } from '@mantine/core';
+
+function Demo() {
+ return (
+ <${component}
+ data={[
+ { group: 'Frontend', items: ['React', 'Angular'] },
+ { group: 'Backend', items: ['Express', 'Django'] },
+ ]}
+ />
+ );
+}
+`;
+
+const getGroupsCode = (component: string) => `
+import { ${component} } from '@mantine/core';
+
+function Demo() {
+ return (
+ <${component}
+ data={[
+ { group: 'Frontend', items: [{ value: 'react', label: 'React' }, { value: 'ng', label: 'Angular' }] },
+ { group: 'Backend', items: [{ value: 'express', label: 'Express' }, { value: 'django', label: 'Django' }] },
+ ]}
+ />
+ );
+}
+`;
+
+export function MdxComboboxData({ component }: MdxComboboxDataProps) {
+ return (
+ <>
+ Data formats
+
+ {component} data prop accepts data in one of the
+ following formats:
+
+
+ Array of strings:
+
+
+
+ {component !== 'Autocomplete' && (
+ <>
+
+ Array of object with value , label and optional{' '}
+ disabled keys:
+
+
+ >
+ )}
+
+ Array of groups with string options:
+
+
+ {component !== 'Autocomplete' && (
+ <>
+ Array of groups with object options:
+
+ >
+ )}
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxComboboxDisclaimer.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxDisclaimer.tsx
new file mode 100644
index 00000000000..40ccf63e4f8
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxDisclaimer.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxComboboxDisclaimerProps {
+ component: string;
+}
+
+export function MdxComboboxDisclaimer({ component }: MdxComboboxDisclaimerProps) {
+ return (
+ <>
+ Made with Combobox
+
+ {component} is an opinionated component built on top of{' '}
+ Combobox component. It has a limited set of
+ features to cover only the basic use cases. If you need more advanced features, you can
+ build your own component with Combobox . You can
+ find examples of custom {component.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase()}{' '}
+ components on the examples page .
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxComboboxFiltering.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxFiltering.tsx
new file mode 100644
index 00000000000..d94f1a766eb
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxFiltering.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { MdxParagraph, MdxCode, MdxLi, MdxUl } from '../MdxTypography/MdxTypography';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxComboboxFilteringProps {
+ component: string;
+}
+
+const valueFormat = '{ value: string; label: string; disabled?: boolean }';
+
+export function MdxComboboxFiltering({ component }: MdxComboboxFilteringProps) {
+ return (
+ <>
+ Options filtering
+ By default, {component} filters options by checking if the option label
+ contains input value. You can change this behavior with filter prop.
+ filter function receives an object with the following properties as a
+ single argument:
+
+
+ options – array of options or options groups, all options are in{' '}
+ {valueFormat} format
+
+
+ search – current search query
+
+
+ limit – value of limit prop passed to{' '}
+ {component}
+
+
+
+ Example of a custom filter function that matches options by words instead of letters
+ sequence:
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxComboboxLargeData.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxLargeData.tsx
new file mode 100644
index 00000000000..cec0554d087
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxLargeData.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxComboboxLargeDataProps {
+ component: string;
+}
+
+export function MdxComboboxLargeData({ component }: MdxComboboxLargeDataProps) {
+ return (
+ <>
+ Large data sets
+
+ The best strategy for large data sets is to limit the number of options that are rendered at
+ the same time. You can do it with limit prop. Note that if you use a
+ custom filter function, you need to implement your own logic to limit the
+ number of options in filter
+
+
+
+ Example of {component} with 100 000 options, 5 options are rendered at
+ the same time:
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxComboboxProps.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxProps.tsx
new file mode 100644
index 00000000000..87f4d213dbc
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxComboboxProps.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxLink } from '../MdxLink/MdxLink';
+
+const getCode = (component: string) => `
+import { ${component} } from '@mantine/core';
+
+function Demo() {
+ return <${component} comboboxProps={{ withinPortal: false }} data={[]} />;
+}
+`;
+
+interface MdxComboboxPropsProps {
+ component: string;
+}
+
+export function MdxComboboxProps({ component }: MdxComboboxPropsProps) {
+ return (
+ <>
+ Combobox props
+
+ You can override Combobox props with{' '}
+ comboboxProps . It is useful when you need to change some of the props
+ that are not exposed by {component} , for example{' '}
+ withinPortal :
+
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxFlexboxGapSupport.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxFlexboxGapSupport.tsx
new file mode 100644
index 00000000000..96698783969
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxFlexboxGapSupport.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxFlexboxGapSupportProps {
+ component: string;
+}
+
+export function MdxFlexboxGapSupport({ component }: MdxFlexboxGapSupportProps) {
+ return (
+ <>
+ Browser support
+
+ {component} uses{' '}
+ flexbox gap to add spacing between
+ children. In older browsers, {component} children may not have spacing.
+ You can install PostCSS{' '}
+
+ flex-gap-polyfill
+ {' '}
+ to add support for older browsers.
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxGetElementRef.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxGetElementRef.tsx
new file mode 100644
index 00000000000..1f7faf25bc6
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxGetElementRef.tsx
@@ -0,0 +1,40 @@
+/* eslint-disable react/no-unused-prop-types */
+import React from 'react';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+
+interface MdxGetElementRefProps {
+ component: string;
+ refType: string;
+ package?: string;
+}
+
+const commonRefs: Record = {
+ div: 'HTMLDivElement',
+ button: 'HTMLButtonElement',
+ input: 'HTMLInputElement',
+ textarea: 'HTMLTextAreaElement',
+ select: 'HTMLSelectElement',
+ a: 'HTMLAnchorElement',
+};
+
+function getRefCode(input: MdxGetElementRefProps) {
+ const refType = input.refType in commonRefs ? commonRefs[input.refType] : input.refType;
+ return `import { useRef } from 'react';
+import { ${input.component} } from '${input.package || '@mantine/core'}';
+
+function Demo() {
+ const ref = useRef<${refType}>(null);
+ return <${input.component} ref={ref} />;
+}
+ `;
+}
+
+export function MdxGetElementRef(props: MdxGetElementRefProps) {
+ return (
+ <>
+ Get element ref
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxGetTemplates.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxGetTemplates.tsx
new file mode 100644
index 00000000000..1b6c5f0d49d
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxGetTemplates.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Template } from '../MdxTemplatesList/data';
+import { MdxTemplatesList } from '../MdxTemplatesList/MdxTemplatesList';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxParagraph } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+
+interface MdxGetTemplatesProps {
+ type: Template['type'];
+}
+
+export function MdxGetTemplates({ type }: MdxGetTemplatesProps) {
+ return (
+ <>
+ Get started with a template
+
+ The easiest way to get started is to use one of the templates. All templates are configured
+ correctly: they include PostCSS setup ,{' '}
+ ColorSchemeScript and other essential
+ features. Some templates also include additional features like{' '}
+ Jest ,{' '}
+ Storybook and ESLint.
+
+
+
+ If you are not familiar with GitHub, you can find a detailed instruction on how to bootstrap
+ a project from a template on this page .
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxGradient.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxGradient.tsx
new file mode 100644
index 00000000000..1a6cf19032f
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxGradient.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxLink } from '../MdxLink/MdxLink';
+
+interface MdxGradientProps {
+ component: string;
+}
+
+export function MdxGradient({ component }: MdxGradientProps) {
+ return (
+ <>
+ Gradient variant
+
+ When variant prop is set to gradient , you can control
+ gradient with gradient prop, it accepts an object with{' '}
+ from , to and deg properties. If the
+ gradient prop is not set, {component} will use{' '}
+ theme.defaultGradient which can be configured on the{' '}
+ theme object . gradient {' '}
+ prop is ignored when variant is not gradient .
+
+
+
+ Note that variant="gradient" supports only linear gradients
+ with two colors. If you need a more complex gradient, then use{' '}
+ Styles API to modify{' '}
+ {component} styles.
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxInputAccessibility.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxInputAccessibility.tsx
new file mode 100644
index 00000000000..2b55df15853
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxInputAccessibility.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxInputAccessibilityProps {
+ component: string;
+ package?: string;
+}
+
+const getInaccessibleCode = (component: string, packageName: string) => `
+import { ${component} } from '${packageName}';
+
+// Inaccessible input – screen reader will not announce it properly
+function Demo() {
+ return <${component} />;
+}
+`;
+
+const getAriaLabelCode = (component: string, packageName: string) => `
+import { ${component} } from '${packageName}';
+
+// Accessible input – it has aria-label
+function Demo() {
+ return <${component} aria-label="My input" />;
+}
+`;
+
+const getLabelCode = (component: string, packageName: string) => `
+import { ${component} } from '${packageName}';
+
+// Accessible input – it has associated label element
+function Demo() {
+ return <${component} label="My input" />;
+}
+`;
+
+export function MdxInputAccessibility(props: MdxInputAccessibilityProps) {
+ const packageName = props.package || '@mantine/core';
+ return (
+ <>
+ Accessibility
+
+ If {props.component} is used without label prop, it
+ will not be announced properly by screen reader:
+
+
+
+
+ Set aria-label to make the input accessible. In this case label will not
+ be visible, but screen reader will announce it:
+
+
+
+
+ If label prop is set, input will be accessible it is not required to set{' '}
+ aria-label :
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxInputFeatures.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxInputFeatures.tsx
new file mode 100644
index 00000000000..316de942f83
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxInputFeatures.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+
+interface MdxInputFeaturesProps {
+ component: string;
+ element: string;
+}
+
+export function MdxInputFeatures({ component, element }: MdxInputFeaturesProps) {
+ return (
+
+ {component} component supports Input {' '}
+ and Input.Wrapper components features and all{' '}
+ {element} element props. {component} documentation does
+ not include all features supported by the component – see{' '}
+ Input documentation to learn about all available
+ features.
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxPolymorphic.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxPolymorphic.tsx
new file mode 100644
index 00000000000..52e44b3eea3
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxPolymorphic.tsx
@@ -0,0 +1,98 @@
+/* eslint-disable react/no-unused-prop-types */
+import React from 'react';
+import { TypeScriptCircleIcon } from '@mantine/ds';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+import { MdxInfo } from '../MdxInfo/MdxInfo';
+
+interface MdxPolymorphicProps {
+ component: string;
+ defaultElement: string;
+ changeToElement: string;
+ withNext?: string;
+ package?: string;
+}
+
+function getElementCode(input: MdxPolymorphicProps) {
+ return `import { ${input.component} } from '${input.package || '@mantine/core'}';
+
+function Demo() {
+ return <${input.component} component="${input.changeToElement}" />;
+}
+ `;
+}
+
+function getNextLinkCode(input: MdxPolymorphicProps) {
+ return `import Link from 'next/link';
+import { ${input.component} } from '${input.package || '@mantine/core'}';
+
+function Demo() {
+ return <${input.component} component={Link} href="/" />;
+}`;
+}
+
+function getInterfaceCode(input: MdxPolymorphicProps) {
+ return `import type { ${input.component}Props, ElementProps } from '${
+ input.package || '@mantine/core'
+ }';
+
+interface My${input.component}Props extends ${input.component}Props,
+ ElementProps<'${input.changeToElement}', keyof ${input.component}Props> {}`;
+}
+
+export function MdxPolymorphic(props: MdxPolymorphicProps) {
+ return (
+ <>
+ Polymorphic component
+
+ {props.component} is a{' '}
+ polymorphic component – its default root
+ element is {props.defaultElement} , but it can be changed to any other
+ element or component with component prop:
+
+
+
+ {props.withNext && (
+ <>
+
+ You can also use components in component prop, for example, Next.js{' '}
+ Link :
+
+
+ >
+ )}
+
+ } color="#3178C6">
+
+ Polymorphic components with TypeScript
+
+
+
+ Note that polymorphic components props types are different from regular components – they
+ do not extend HTML element props of the default element. For example,{' '}
+ {props.component}Props does not extend{' '}
+
+ React.ComponentPropsWithoutRef{"'<'"}div{"'>'"}
+ {' '}
+ although {props.defaultElement} is the default element.
+
+
+
+ If you want to create a wrapper for a polymorphic component that is not polymorphic (does
+ not support component prop), then your component props interface should
+ extend HTML element props, for example:{' '}
+
+
+
+
+
+ If you want your component to remain polymorphic after wrapping, use{' '}
+ createPolymorphicComponent function described in{' '}
+ this guide .
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxStylesApiSelectors.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxStylesApiSelectors.tsx
new file mode 100644
index 00000000000..c7765b02b3b
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxStylesApiSelectors.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { MdxParagraph, MdxCode } from '../MdxTypography/MdxTypography';
+import { MdxLink } from '../MdxLink/MdxLink';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+
+interface MdxStylesApiSelectorsProps {
+ component: string;
+}
+
+export function MdxStylesApiSelectors({ component }: MdxStylesApiSelectorsProps) {
+ return (
+ <>
+ Styles API
+
+ {component} supports{' '}
+ Styles API , you can add styles to any inner
+ element of the component with
+ classNames prop. Follow{' '}
+ Styles API documentation to learn more.
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/MdxTargetComponent.tsx b/docs/components/MdxProvider/MdxSharedContent/MdxTargetComponent.tsx
new file mode 100644
index 00000000000..0fc69b209e4
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/MdxTargetComponent.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import { MdxTitle } from '../MdxTitle/MdxTitle';
+import { MdxCode, MdxParagraph } from '../MdxTypography/MdxTypography';
+import { MdxCodeHighlight } from '../MdxPre/MdxPre';
+
+const getTargetCode = (component: string) => `import { ${component}, Button } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+ <${component}.Target>
+ Native button – ok
+ ${component}.Target>
+
+ {/* OK */}
+ <${component}.Target>
+ Mantine component – ok
+ ${component}.Target>
+
+ {/* String, NOT OK – will throw error */}
+ <${component}.Target>Raw string${component}.Target>
+
+ {/* Number, NOT OK – will throw error */}
+ <${component}.Target>{2}${component}.Target>
+
+ {/* Fragment, NOT OK – will throw error */}
+ <${component}.Target>
+ <>Fragment, NOT OK, will throw error>
+ ${component}.Target>
+
+ {/* Multiple nodes, NOT OK – will throw error */}
+ <${component}.Target>
+ More that one node
+ NOT OK, will throw error
+ ${component}.Target>
+ >
+ );
+}`;
+
+const getNoRefCode = (component: string) => `
+// Example of code that WILL NOT WORK
+import { ${component} } from '@mantine/core';
+
+function MyComponent() {
+ return My component
;
+}
+
+// This will not work – MyComponent does not support ref
+function Demo() {
+ return (
+ <${component}>
+ <${component}.Target>
+
+ ${component}.Target>
+ ${component}>
+ );
+}`;
+
+const getForwardRefCode = (component: string) => `
+// Example of code that will work
+import { forwardRef } from 'react';
+import { ${component} } from '@mantine/core';
+
+const MyComponent = forwardRef>((props, ref) => (
+
+ My component
+
+));
+
+// Works correctly – ref is forwarded
+function Demo() {
+ return (
+ <${component}>
+ <${component}.Target>
+
+ ${component}.Target>
+ ${component}>
+ );
+}
+`;
+
+interface MdxTargetComponentProps {
+ component: string;
+}
+
+export function MdxTargetComponent({ component }: MdxTargetComponentProps) {
+ return (
+ <>
+ {component}.Target children
+
+ {component}.Target requires an element or a component as a single child –
+ strings, fragments, numbers and multiple elements/components are not supported and{' '}
+ will throw error . Custom components must provide a prop to get root element ref, all
+ Mantine components support ref out of the box.
+
+
+
+
+ Required ref prop
+
+ Custom components that are rendered inside {component}.Target are required to support{' '}
+ ref prop:
+
+
+
+
+
+ Use forwardRef function to forward ref to root element:
+
+
+
+ >
+ );
+}
diff --git a/docs/components/MdxProvider/MdxSharedContent/index.ts b/docs/components/MdxProvider/MdxSharedContent/index.ts
new file mode 100644
index 00000000000..8a2c5af0dbe
--- /dev/null
+++ b/docs/components/MdxProvider/MdxSharedContent/index.ts
@@ -0,0 +1,14 @@
+export { MdxGetElementRef } from './MdxGetElementRef';
+export { MdxPolymorphic } from './MdxPolymorphic';
+export { MdxTargetComponent } from './MdxTargetComponent';
+export { MdxInputFeatures } from './MdxInputFeatures';
+export { MdxInputAccessibility } from './MdxInputAccessibility';
+export { MdxFlexboxGapSupport } from './MdxFlexboxGapSupport';
+export { MdxGradient } from './MdxGradient';
+export { MdxStylesApiSelectors } from './MdxStylesApiSelectors';
+export { MdxGetTemplates } from './MdxGetTemplates';
+export { MdxComboboxDisclaimer } from './MdxComboboxDisclaimer';
+export { MdxComboboxData } from './MdxComboboxData';
+export { MdxComboboxLargeData } from './MdxComboboxLargeData';
+export { MdxComboboxFiltering } from './MdxComboboxFiltering';
+export { MdxComboboxProps } from './MdxComboboxProps';
diff --git a/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.module.css b/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.module.css
new file mode 100644
index 00000000000..72c9000e856
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.module.css
@@ -0,0 +1,17 @@
+.externalLinkIcon {
+ width: rem(14px);
+ height: rem(14px);
+}
+
+.icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: rem(44px);
+}
+
+.iconCell {
+ @media (max-width: em(500px)) {
+ display: none;
+ }
+}
diff --git a/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.tsx b/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.tsx
new file mode 100644
index 00000000000..b0b3a82141d
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTemplatesList/MdxTemplatesList.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Table, Button, Text } from '@mantine/core';
+import { IconExternalLink } from '@tabler/icons-react';
+import { frameworkIcons } from '@/components/icons';
+import { TEMPLATES_DATA, Template } from './data';
+import classes from './MdxTemplatesList.module.css';
+
+interface MdxTemplatesListProps {
+ type?: Template['type'];
+ name?: string[];
+}
+
+export function MdxTemplatesList({ type, name }: MdxTemplatesListProps) {
+ const data = name
+ ? TEMPLATES_DATA.filter((template) => name.includes(template.name))
+ : type
+ ? TEMPLATES_DATA.filter((template) => template.type === type)
+ : TEMPLATES_DATA;
+
+ const rows = data.map((template) => {
+ const Icon = frameworkIcons[template.type];
+ return (
+
+
+
+
+
+
+
+
+ {template.name}
+
+ {template.description}
+
+
+ }
+ >
+ Use template
+
+
+
+ );
+ });
+
+ return (
+
+ );
+}
diff --git a/docs/components/MdxProvider/MdxTemplatesList/data.ts b/docs/components/MdxProvider/MdxTemplatesList/data.ts
new file mode 100644
index 00000000000..96955104987
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTemplatesList/data.ts
@@ -0,0 +1,85 @@
+export interface Template {
+ name: string;
+ link: string;
+ description: string;
+ type: 'next' | 'gatsby' | 'vite' | 'remix' | 'redwood';
+}
+
+export const TEMPLATES_DATA: Template[] = [
+ {
+ type: 'next',
+ name: 'next-app-template',
+ link: 'https://github.com/mantinedev/next-app-template',
+ description: 'Next.js template with app router and full setup: Jest, Storybook, ESLint',
+ },
+ {
+ type: 'next',
+ name: 'next-pages-template',
+ link: 'https://github.com/mantinedev/next-pages-template',
+ description: 'Next.js template with pages router and full setup: Jest, Storybook, ESLint',
+ },
+ {
+ type: 'next',
+ name: 'next-app-min-template',
+ link: 'https://github.com/mantinedev/next-app-min-template',
+ description:
+ 'Next.js template with app router and minimal setup – no additional tools included, only default Next.js configuration',
+ },
+ {
+ type: 'next',
+ name: 'next-pages-min-template',
+ link: 'https://github.com/mantinedev/next-pages-min-template',
+ description:
+ 'Next.js template with pages router and minimal setup – no additional tools included, only default Next.js configuration',
+ },
+ {
+ type: 'next',
+ name: 'next-vanilla-extract-template',
+ link: 'https://github.com/mantinedev/next-vanilla-extract-template',
+ description: 'Next.js template with Vanilla extract example',
+ },
+ {
+ type: 'vite',
+ name: 'vite-template',
+ link: 'https://github.com/mantinedev/vite-template',
+ description: 'Vite template with full setup: Jest, Storybook, ESLint',
+ },
+ {
+ type: 'vite',
+ name: 'vite-min-template',
+ link: 'https://github.com/mantinedev/vite-min-template',
+ description:
+ 'Vite template with minimal setup – no additional tools included, only default Vite configuration',
+ },
+ {
+ type: 'vite',
+ name: 'vite-vanilla-extract-template',
+ link: 'https://github.com/mantinedev/vite-vanilla-extract-template',
+ description: 'Vite template with Vanilla extract example',
+ },
+ {
+ type: 'remix',
+ name: 'remix-template',
+ link: 'https://github.com/mantinedev/remix-template',
+ description: 'Remix template with full setup: Jest, Storybook, ESLint',
+ },
+ {
+ type: 'remix',
+ name: 'remix-min-template',
+ link: 'https://github.com/mantinedev/remix-min-template',
+ description:
+ 'Remix template with minimal setup – no additional tools included, only default Remix configuration',
+ },
+ {
+ type: 'gatsby',
+ name: 'gatsby-template',
+ link: 'https://github.com/mantinedev/gatsby-template',
+ description: 'Gatsby template with basic setup',
+ },
+ {
+ type: 'redwood',
+ name: 'redwood-template',
+ link: 'https://github.com/mantinedev/redwood-template',
+ description: 'RedwoodJS template with basic setup',
+ },
+];
diff --git a/docs/components/MdxProvider/MdxTitle/MdxTitle.module.css b/docs/components/MdxProvider/MdxTitle/MdxTitle.module.css
new file mode 100644
index 00000000000..a990ec8362d
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTitle/MdxTitle.module.css
@@ -0,0 +1,31 @@
+.title {
+ margin-top: calc(var(--mantine-spacing-xl) * 1.2);
+ margin-bottom: var(--mantine-spacing-md);
+ word-break: break-word;
+ font-family: var(--docs-font-primary);
+ font-weight: 700;
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+
+ &[data-h1] {
+ font-weight: 900;
+ font-size: rem(40px);
+ margin-top: 0;
+ }
+}
+
+.link {
+ text-decoration: none;
+ color: inherit;
+}
+
+.offset {
+ position: relative;
+ top: rem(-62px);
+}
diff --git a/docs/components/MdxProvider/MdxTitle/MdxTitle.tsx b/docs/components/MdxProvider/MdxTitle/MdxTitle.tsx
new file mode 100644
index 00000000000..780419fc711
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTitle/MdxTitle.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import cx from 'clsx';
+import { Title, FOCUS_CLASS_NAMES } from '@mantine/core';
+import classes from './MdxTitle.module.css';
+
+export function MdxTitle({
+ id,
+ children,
+ order = 2,
+ ...others
+}: React.ComponentPropsWithoutRef) {
+ if (order === 1) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {children}
+
+
+ >
+ );
+}
+
+export const h = (order: 1 | 2 | 3 | 4 | 5 | 6) => (props: any) => (
+
+);
diff --git a/docs/components/MdxProvider/MdxTypography/MdxTypography.module.css b/docs/components/MdxProvider/MdxTypography/MdxTypography.module.css
new file mode 100644
index 00000000000..b3bebb7ecec
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTypography/MdxTypography.module.css
@@ -0,0 +1,17 @@
+.paragraph {
+ line-height: 1.65;
+ font-size: rem(15px);
+}
+
+.ul {
+ line-height: 1.65;
+ margin-bottom: rem(20px);
+ margin-top: rem(10px);
+ font-size: rem(15px);
+ padding-left: var(--mantine-spacing-xl);
+}
+
+.li {
+ margin-top: rem(4px);
+ font-size: rem(15px);
+}
diff --git a/docs/components/MdxProvider/MdxTypography/MdxTypography.tsx b/docs/components/MdxProvider/MdxTypography/MdxTypography.tsx
new file mode 100644
index 00000000000..fe091fd1b45
--- /dev/null
+++ b/docs/components/MdxProvider/MdxTypography/MdxTypography.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import cx from 'clsx';
+import { Code } from '@mantine/core';
+import classes from './MdxTypography.module.css';
+
+export function MdxParagraph({ className, ...others }: React.ComponentPropsWithoutRef<'p'>) {
+ return
;
+}
+
+export function MdxUl({ className, ...others }: React.ComponentPropsWithoutRef<'ul'>) {
+ return ;
+}
+
+export function MdxLi({ className, ...others }: React.ComponentPropsWithoutRef<'li'>) {
+ return ;
+}
+
+export function MdxCode(props: React.ComponentPropsWithoutRef<'code'>) {
+ return
;
+}
diff --git a/docs/components/MdxProvider/index.ts b/docs/components/MdxProvider/index.ts
new file mode 100644
index 00000000000..4a72ccdcfc1
--- /dev/null
+++ b/docs/components/MdxProvider/index.ts
@@ -0,0 +1,12 @@
+export * from './MdxSharedContent';
+
+export { MdxProvider } from './MdxProvider';
+export { MdxTitle } from './MdxTitle/MdxTitle';
+export { MdxDataTable } from './MdxDataTable/MdxDataTable';
+export { MdxKeyboardEventsTable } from './MdxKeyboardEventsTable/MdxKeyboardEventsTable';
+export { MdxInfo } from './MdxInfo/MdxInfo';
+export { MdxLink } from './MdxLink/MdxLink';
+export { MdxParagraph, MdxUl, MdxLi } from './MdxTypography/MdxTypography';
+export { MdxCodeHighlight } from './MdxPre/MdxPre';
+export { MdxInstallScript } from './MdxInstallScript/MdxInstallScript';
+export { MdxPackagesInstallation } from './MdxPackagesInstallation/MdxPackagesInstallation';
diff --git a/docs/components/MdxRawContent/MdxRawContent.module.css b/docs/components/MdxRawContent/MdxRawContent.module.css
new file mode 100644
index 00000000000..9b48a025be8
--- /dev/null
+++ b/docs/components/MdxRawContent/MdxRawContent.module.css
@@ -0,0 +1,34 @@
+.wrapper {
+ z-index: 1;
+ display: flex;
+ position: relative;
+ justify-content: space-between;
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+ padding-right: calc(var(--mantine-spacing-xl) * 2);
+ background-color: var(--mantine-color-body);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: var(--mantine-spacing-xl);
+ padding-right: var(--mantine-spacing-xl);
+ }
+}
+
+.container {
+ margin-top: var(--mantine-spacing-xl);
+ width: calc(100% - var(--docs-table-of-contents-width));
+ max-width: var(--docs-mdx-content-width);
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ width: 100%;
+ }
+}
+
+.tableOfContents {
+ flex: 0 0 var(--docs-table-of-contents-width);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ display: none;
+ }
+}
diff --git a/docs/components/MdxRawContent/MdxRawContent.tsx b/docs/components/MdxRawContent/MdxRawContent.tsx
new file mode 100644
index 00000000000..1d38118616f
--- /dev/null
+++ b/docs/components/MdxRawContent/MdxRawContent.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { Frontmatter } from '@/types';
+import { TableOfContents } from '@/components/TableOfContents';
+import { PageBase } from '@/components/PageBase';
+import { MdxSiblings } from '@/components/MdxSiblings';
+import classes from './MdxRawContent.module.css';
+
+interface MdxRawContentProps {
+ children: React.ReactNode;
+ meta: Frontmatter;
+}
+
+export function MdxRawContent({ children, meta }: MdxRawContentProps) {
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/MdxRawContent/index.ts b/docs/components/MdxRawContent/index.ts
new file mode 100644
index 00000000000..a22f873116b
--- /dev/null
+++ b/docs/components/MdxRawContent/index.ts
@@ -0,0 +1 @@
+export { MdxRawContent } from './MdxRawContent';
diff --git a/docs/components/MdxSiblings/MdxSiblings.module.css b/docs/components/MdxSiblings/MdxSiblings.module.css
new file mode 100644
index 00000000000..d5047b782c8
--- /dev/null
+++ b/docs/components/MdxSiblings/MdxSiblings.module.css
@@ -0,0 +1,84 @@
+.root {
+ display: flex;
+ align-items: center;
+ gap: var(--mantine-spacing-xl);
+ margin-top: rem(50px);
+
+ @media (max-width: em(700px)) {
+ flex-direction: column;
+ align-items: stretch;
+ gap: var(--mantine-spacing-md);
+ }
+}
+
+.link {
+ -webkit-tap-highlight-color: transparent;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ text-decoration: none;
+ padding: var(--mantine-spacing-md);
+ border-radius: var(--mantine-radius-md);
+
+ @mixin light {
+ color: var(--mantine-colors-black);
+ background-color: var(--mantine-color-white);
+ border: rem(1px) solid var(--mantine-color-gray-3);
+
+ @mixin hover {
+ background-color: var(--mantine-color-gray-0);
+ }
+ }
+
+ @mixin dark {
+ color: var(--mantine-colors-white);
+ background-color: var(--mantine-color-dark-6);
+ border: rem(1px) solid var(--mantine-color-dark-4);
+
+ @mixin hover {
+ background-color: var(--mantine-color-dark-5);
+ }
+ }
+}
+
+.body {
+ &[data-next] {
+ text-align: left;
+ margin-right: var(--mantine-spacing-md);
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: var(--mantine-spacing-md);
+ text-align: right;
+ }
+ }
+
+ &[data-prev] {
+ text-align: right;
+ margin-left: var(--mantine-spacing-md);
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-md);
+ text-align: left;
+ }
+ }
+}
+
+.title {
+ font-size: var(--mantine-font-size-lg);
+}
+
+.description {
+ font-size: var(--mantine-font-size-sm);
+}
+
+.icon {
+ width: rem(25px);
+ height: rem(25px);
+
+ @mixin rtl {
+ transform: rotate(180deg);
+ }
+}
diff --git a/docs/components/MdxSiblings/MdxSiblings.tsx b/docs/components/MdxSiblings/MdxSiblings.tsx
new file mode 100644
index 00000000000..3a0f6f374a5
--- /dev/null
+++ b/docs/components/MdxSiblings/MdxSiblings.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import Link from 'next/link';
+import cx from 'clsx';
+import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react';
+import { Text, Box, FOCUS_CLASS_NAMES } from '@mantine/core';
+import { getMdxSiblings } from './get-mdx-siblings';
+import classes from './MdxSiblings.module.css';
+import { Frontmatter } from '@/types';
+
+interface MdxSiblingsProps {
+ meta: Frontmatter;
+}
+
+export function MdxSiblings({ meta }: MdxSiblingsProps) {
+ const { next, prev } = getMdxSiblings(meta.slug);
+
+ if (meta.hideSiblings) {
+ return null;
+ }
+
+ return (
+
+ {prev && (
+
+
+
+ Previous
+
+ {prev.title}
+
+
+
+ )}
+
+ {next && (
+
+
+ Next
+
+ {next.title}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/docs/components/MdxSiblings/get-mdx-siblings.ts b/docs/components/MdxSiblings/get-mdx-siblings.ts
new file mode 100644
index 00000000000..dcf2b2bf049
--- /dev/null
+++ b/docs/components/MdxSiblings/get-mdx-siblings.ts
@@ -0,0 +1,13 @@
+import { Frontmatter } from '@/types';
+import { ALL_MDX_PAGES } from '@/mdx';
+
+export function getMdxSiblings(slug: string): {
+ prev: Frontmatter | undefined;
+ next: Frontmatter | undefined;
+} {
+ const index = ALL_MDX_PAGES.findIndex((page) => page.slug === slug);
+ const prev = ALL_MDX_PAGES[index - 1];
+ const next = ALL_MDX_PAGES[index + 1];
+
+ return { prev, next };
+}
diff --git a/docs/components/MdxSiblings/index.ts b/docs/components/MdxSiblings/index.ts
new file mode 100644
index 00000000000..e00ca862b59
--- /dev/null
+++ b/docs/components/MdxSiblings/index.ts
@@ -0,0 +1 @@
+export { MdxSiblings } from './MdxSiblings';
diff --git a/docs/components/MdxTabs/MdxTabs.module.css b/docs/components/MdxTabs/MdxTabs.module.css
new file mode 100644
index 00000000000..6402bc76632
--- /dev/null
+++ b/docs/components/MdxTabs/MdxTabs.module.css
@@ -0,0 +1,106 @@
+.tabsWrapper {
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+
+ @mixin light {
+ background-color: var(--mantine-color-gray-0);
+ border-bottom: rem(1px) solid var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-8);
+ border-bottom: rem(1px) solid var(--mantine-color-dark-6);
+ }
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: var(--mantine-spacing-xl);
+ }
+
+ @media (min-width: 86.25em) {
+ padding-left: 0;
+ }
+}
+
+.tabsList {
+ max-width: rem(1082px);
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: rem(-1px);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ max-width: 100%;
+ padding-right: 0;
+ }
+
+ &::before {
+ @mixin light {
+ border-color: var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-6);
+ }
+ }
+}
+
+.tab {
+ font-size: var(--mantine-font-size-md);
+ font-weight: 500;
+ height: rem(46px);
+ padding-left: var(--mantine-spacing-lg);
+ padding-right: var(--mantine-spacing-lg);
+ background-color: transparent;
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ font-size: var(--mantine-font-size-sm);
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ }
+
+ &[data-active] {
+ background-color: var(--mantine-color-body);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ border-color: var(--mantine-color-gray-2);
+ border-bottom-color: transparent;
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ border-color: var(--mantine-color-dark-6);
+ border-bottom-color: transparent;
+ }
+ }
+}
+
+.tabContent {
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+ padding-right: calc(var(--mantine-spacing-xl) * 2);
+ padding-top: var(--mantine-spacing-xs);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: var(--mantine-spacing-xl);
+ padding-right: var(--mantine-spacing-xl);
+ }
+}
+
+.main {
+ width: calc(100% - var(--docs-table-of-contents-width));
+ max-width: var(--docs-mdx-content-width);
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ width: 100%;
+ padding-right: 0;
+ }
+}
+
+.tableOfContents {
+ flex: 0 0 var(--docs-table-of-contents-width);
+ margin-top: var(--mantine-spacing-xl);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ display: none;
+ }
+}
diff --git a/docs/components/MdxTabs/MdxTabs.tsx b/docs/components/MdxTabs/MdxTabs.tsx
new file mode 100644
index 00000000000..2d64af1677d
--- /dev/null
+++ b/docs/components/MdxTabs/MdxTabs.tsx
@@ -0,0 +1,101 @@
+import React, { useState, useEffect } from 'react';
+import { Tabs, rem } from '@mantine/core';
+import { useRouter } from 'next/router';
+import { TableOfContents } from '@/components/TableOfContents';
+import { PageBase } from '@/components/PageBase';
+import { MdxSiblings } from '@/components/MdxSiblings';
+import { PropsTablesList } from '@/components/PropsTable';
+import { StylesApiTablesList } from '@/components/StylesApiTable';
+import { Frontmatter } from '@/types';
+import classes from './MdxTabs.module.css';
+
+interface MdxTabsProps {
+ children: React.ReactNode;
+ meta: Frontmatter;
+}
+
+export function MdxTabs({ children, meta }: MdxTabsProps) {
+ const router = useRouter();
+ const [activeTab, setActiveTab] = useState('docs');
+ const hasProps = Array.isArray(meta.props);
+ const hasStyles = Array.isArray(meta.styles);
+
+ useEffect(() => {
+ setActiveTab(window.location.search.replace('?t=', '') || 'docs');
+ }, []);
+
+ if (!hasProps && !hasStyles) {
+ return null;
+ }
+
+ return (
+
+ {
+ router.replace(value === 'docs' ? router.pathname : `${router.pathname}?t=${value}`);
+ setActiveTab(value!);
+ }}
+ >
+
+
+ Documentation
+ {hasProps && Props }
+ {hasStyles && Styles API }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/MdxTabs/index.ts b/docs/components/MdxTabs/index.ts
new file mode 100644
index 00000000000..5a87cdde268
--- /dev/null
+++ b/docs/components/MdxTabs/index.ts
@@ -0,0 +1 @@
+export { MdxTabs } from './MdxTabs';
diff --git a/docs/components/ModalsProviderDemo/ModalsProviderDemo.tsx b/docs/components/ModalsProviderDemo/ModalsProviderDemo.tsx
new file mode 100644
index 00000000000..9ea44e717a6
--- /dev/null
+++ b/docs/components/ModalsProviderDemo/ModalsProviderDemo.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Text, Button } from '@mantine/core';
+import { ModalsProvider, ContextModalProps } from '@mantine/modals';
+
+interface ModalsProviderDemoProps {
+ children: React.ReactNode;
+}
+
+const demonstrationModal = ({
+ context,
+ id,
+ innerProps,
+}: ContextModalProps<{ modalBody: string }>) => (
+ <>
+ {innerProps.modalBody}
+ context.closeModal(id)}>
+ Close modal
+
+ >
+);
+
+export function ModalsProviderDemo({ children }: ModalsProviderDemoProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/docs/components/ModalsProviderDemo/index.ts b/docs/components/ModalsProviderDemo/index.ts
new file mode 100644
index 00000000000..788a03e6060
--- /dev/null
+++ b/docs/components/ModalsProviderDemo/index.ts
@@ -0,0 +1 @@
+export { ModalsProviderDemo } from './ModalsProviderDemo';
diff --git a/docs/components/PageBase/PageBase.module.css b/docs/components/PageBase/PageBase.module.css
new file mode 100644
index 00000000000..c137150b923
--- /dev/null
+++ b/docs/components/PageBase/PageBase.module.css
@@ -0,0 +1,16 @@
+.content {
+ min-height: calc(100vh - rem(350px));
+ position: relative;
+ z-index: 1;
+ padding-bottom: rem(80px);
+
+ @mixin light {
+ background-color: var(--mantine-color-white);
+ box-shadow: var(--mantine-shadow-sm);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-7);
+ box-shadow: none;
+ }
+}
diff --git a/docs/components/PageBase/PageBase.tsx b/docs/components/PageBase/PageBase.tsx
new file mode 100644
index 00000000000..044261b5ef8
--- /dev/null
+++ b/docs/components/PageBase/PageBase.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Footer } from '@/components/Footer';
+import classes from './PageBase.module.css';
+
+interface PageBaseProps {
+ children: React.ReactNode;
+}
+
+export function PageBase({ children }: PageBaseProps) {
+ return (
+ <>
+ {children}
+
+ >
+ );
+}
diff --git a/docs/components/PageBase/index.ts b/docs/components/PageBase/index.ts
new file mode 100644
index 00000000000..798daa67d9e
--- /dev/null
+++ b/docs/components/PageBase/index.ts
@@ -0,0 +1 @@
+export { PageBase } from './PageBase';
diff --git a/docs/components/PageContentContainer/PageContentContainer.module.css b/docs/components/PageContentContainer/PageContentContainer.module.css
new file mode 100644
index 00000000000..70501906a05
--- /dev/null
+++ b/docs/components/PageContentContainer/PageContentContainer.module.css
@@ -0,0 +1,17 @@
+.wrapper {
+ padding-left: calc(var(--mantine-spacing-xl) * 2);
+ padding-right: calc(var(--mantine-spacing-xl) * 2);
+ padding-top: var(--mantine-spacing-xl);
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ padding-left: var(--mantine-spacing-xl);
+ padding-right: var(--mantine-spacing-xl);
+ }
+}
+
+.container {
+ width: 100%;
+ max-width: calc(var(--docs-mdx-content-width) + var(--docs-table-of-contents-width));
+ margin-left: auto;
+ margin-right: auto;
+}
diff --git a/docs/components/PageContentContainer/PageContentContainer.tsx b/docs/components/PageContentContainer/PageContentContainer.tsx
new file mode 100644
index 00000000000..4cdc61d929e
--- /dev/null
+++ b/docs/components/PageContentContainer/PageContentContainer.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import cx from 'clsx';
+import classes from './PageContentContainer.module.css';
+
+interface PageContentContainerProps extends React.ComponentPropsWithoutRef<'div'> {}
+
+export function PageContentContainer({
+ className,
+ children,
+ ...others
+}: PageContentContainerProps) {
+ return (
+
+ );
+}
diff --git a/docs/components/PageContentContainer/index.ts b/docs/components/PageContentContainer/index.ts
new file mode 100644
index 00000000000..132ef7feb66
--- /dev/null
+++ b/docs/components/PageContentContainer/index.ts
@@ -0,0 +1 @@
+export { PageContentContainer } from './PageContentContainer';
diff --git a/docs/components/PageHead/PageHead.tsx b/docs/components/PageHead/PageHead.tsx
new file mode 100644
index 00000000000..e1a8fe9fdb7
--- /dev/null
+++ b/docs/components/PageHead/PageHead.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import Head from 'next/head';
+
+interface PageHeadProps {
+ title: string | undefined;
+ description: string | undefined;
+}
+
+const metaDescription =
+ 'React components and hooks library with native dark theme support and focus on usability, accessibility and developer experience';
+
+export function PageHead({ title, description }: PageHeadProps) {
+ const _title = title ? `${title} | Mantine` : 'Mantine';
+ const _description = description || metaDescription;
+ return (
+
+ {_title}
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/PageHead/index.ts b/docs/components/PageHead/index.ts
new file mode 100644
index 00000000000..e7fce951758
--- /dev/null
+++ b/docs/components/PageHead/index.ts
@@ -0,0 +1 @@
+export { PageHead } from './PageHead';
diff --git a/docs/components/PropsTable/PropsTable.tsx b/docs/components/PropsTable/PropsTable.tsx
new file mode 100644
index 00000000000..bb1e4023c74
--- /dev/null
+++ b/docs/components/PropsTable/PropsTable.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { Table, Text, Highlight, rem } from '@mantine/core';
+import { DocsSection } from '@/components/DocsSection';
+import { HtmlText } from '@/components/HtmlText';
+import { TableInlineCode } from '@/components/TableInlineCode';
+import { TableError } from '@/components/TableError';
+import docgenData from '@/.docgen/docgen.json';
+
+export interface DocgenProp {
+ defaultValue: {
+ value: string;
+ };
+ description: string;
+ name: string;
+ required: boolean;
+ type: {
+ name: string;
+ };
+}
+
+export interface Docgen {
+ description: string;
+ displayName: string;
+ props: Record;
+}
+
+const PROPS_DATA: Record = docgenData as any;
+
+interface PropsTableProps {
+ component: string;
+ query: string;
+}
+
+export function PropsTable({ component, query }: PropsTableProps) {
+ if (!PROPS_DATA[component]) {
+ return ;
+ }
+
+ const rows = Object.keys(PROPS_DATA[component].props)
+ .filter((propKey) =>
+ PROPS_DATA[component].props[propKey].name.toLowerCase().includes(query.toLowerCase().trim())
+ )
+ .map((propKey) => {
+ const prop = PROPS_DATA[component].props[propKey];
+
+ return (
+
+
+
+ {prop.name}
+
+
+ {prop.required && (
+
+ {' '}
+ *
+
+ )}
+
+
+
+ {prop.type.name}
+
+
+ {prop.description}
+
+
+ );
+ });
+
+ if (rows.length === 0) {
+ return (
+
+ Nothing found
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ Name
+ Type
+ Description
+
+
+ {rows}
+
+
+
+
+ );
+}
diff --git a/docs/components/PropsTable/PropsTablesList.module.css b/docs/components/PropsTable/PropsTablesList.module.css
new file mode 100644
index 00000000000..787821a2ce6
--- /dev/null
+++ b/docs/components/PropsTable/PropsTablesList.module.css
@@ -0,0 +1,35 @@
+.searchIcon {
+ width: rem(22px);
+ height: rem(22px);
+
+ @mixin light {
+ color: var(--mantine-color-gray-4);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-3);
+ }
+}
+
+.title {
+ font-family: var(--docs-font-primary);
+ margin-bottom: var(--mantine-spacing-lg);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
+
+.section {
+ & + & {
+ margin-top: rem(50px);
+ }
+}
+
+.search {
+ margin-bottom: var(--mantine-spacing-xl);
+}
diff --git a/docs/components/PropsTable/PropsTablesList.tsx b/docs/components/PropsTable/PropsTablesList.tsx
new file mode 100644
index 00000000000..bc7b5f2a34e
--- /dev/null
+++ b/docs/components/PropsTable/PropsTablesList.tsx
@@ -0,0 +1,40 @@
+import React, { useState } from 'react';
+import { TextInput, Title } from '@mantine/core';
+import { IconSearch } from '@tabler/icons-react';
+import { PropsTable } from './PropsTable';
+import { getComponentName } from './getComponentName';
+import classes from './PropsTablesList.module.css';
+
+export interface PropsTablesListProps {
+ components: string[];
+ componentPrefix?: string;
+}
+
+export function PropsTablesList({ components, componentPrefix }: PropsTablesListProps) {
+ const [query, setQuery] = useState('');
+
+ const tables = components.map((component) => (
+
+
+ {getComponentName({ component, componentPrefix })} component props
+
+
+
+ ));
+
+ return (
+
+ setQuery(event.currentTarget.value)}
+ leftSection={ }
+ placeholder="Search props"
+ radius="md"
+ size="lg"
+ autoFocus
+ />
+ {tables}
+
+ );
+}
diff --git a/docs/components/PropsTable/getComponentName.ts b/docs/components/PropsTable/getComponentName.ts
new file mode 100644
index 00000000000..900e240cb6d
--- /dev/null
+++ b/docs/components/PropsTable/getComponentName.ts
@@ -0,0 +1,12 @@
+interface GetComponentNameInput {
+ component: string;
+ componentPrefix: string | undefined;
+}
+
+export function getComponentName({ component, componentPrefix }: GetComponentNameInput) {
+ return componentPrefix
+ ? componentPrefix === component
+ ? component
+ : `${componentPrefix}.${component.replace(componentPrefix, '')}`
+ : component;
+}
diff --git a/docs/components/PropsTable/index.ts b/docs/components/PropsTable/index.ts
new file mode 100644
index 00000000000..1a7d9be99a5
--- /dev/null
+++ b/docs/components/PropsTable/index.ts
@@ -0,0 +1,3 @@
+export { PropsTable } from './PropsTable';
+export { PropsTablesList } from './PropsTablesList';
+export { getComponentName } from './getComponentName';
diff --git a/docs/components/Search/Search.tsx b/docs/components/Search/Search.tsx
new file mode 100644
index 00000000000..697d3f6ae42
--- /dev/null
+++ b/docs/components/Search/Search.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { useRouter } from 'next/router';
+import { IconSearch } from '@tabler/icons-react';
+import { rem } from '@mantine/core';
+import { Spotlight, createSpotlight } from '@mantine/spotlight';
+import { ALL_MDX_PAGES } from '@/mdx';
+
+export const [searchStore, searchHandlers] = createSpotlight();
+
+export function Search() {
+ const router = useRouter();
+
+ const actions = ALL_MDX_PAGES.map((page) => ({
+ id: page.slug,
+ label: page.title,
+ description: page.search || page.description || page.date,
+ onClick: () => router.push(page.slug),
+ }));
+
+ return (
+ ,
+ placeholder: 'Search documentation...',
+ }}
+ />
+ );
+}
diff --git a/docs/components/Search/index.ts b/docs/components/Search/index.ts
new file mode 100644
index 00000000000..78772750003
--- /dev/null
+++ b/docs/components/Search/index.ts
@@ -0,0 +1 @@
+export { Search, searchHandlers } from './Search';
diff --git a/docs/components/Shell/Header/Header.module.css b/docs/components/Shell/Header/Header.module.css
new file mode 100644
index 00000000000..0084f26cb60
--- /dev/null
+++ b/docs/components/Shell/Header/Header.module.css
@@ -0,0 +1,74 @@
+.header {
+ background-color: var(--mantine-color-body);
+ height: var(--docs-header-height);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 100;
+
+ @mixin light {
+ border-bottom: rem(1px) solid var(--mantine-color-gray-2);
+ }
+
+ @mixin dark {
+ border-bottom: rem(1px) solid var(--mantine-color-dark-8);
+ }
+
+ &[data-desktop] {
+ @media (max-width: 47.5em) {
+ display: none;
+ }
+ }
+
+ &[data-mobile] {
+ @media (min-width: 47.5625em) {
+ display: none;
+ }
+ }
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+}
+
+.version {
+ margin-top: rem(4px);
+ margin-left: var(--mantine-spacing-md);
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+ padding: rem(4px) rem(10px);
+ padding-right: rem(8px);
+ border-radius: var(--mantine-radius-xl);
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-md);
+ }
+
+ @media (max-width: 50em) {
+ display: none;
+ }
+}
+
+.versionChevron {
+ display: block;
+ width: rem(14px);
+ height: rem(14px);
+ margin-left: rem(5px);
+}
+
+.versionExternalIcon {
+ width: rem(14px);
+ height: rem(14px);
+ color: var(--mantine-color-dimmed);
+}
diff --git a/docs/components/Shell/Header/Header.tsx b/docs/components/Shell/Header/Header.tsx
new file mode 100644
index 00000000000..b512c89e1de
--- /dev/null
+++ b/docs/components/Shell/Header/Header.tsx
@@ -0,0 +1,89 @@
+/* eslint-disable import/no-relative-packages */
+import React from 'react';
+import cx from 'clsx';
+import { IconChevronDown, IconExternalLink } from '@tabler/icons-react';
+import { Code, Group, Burger, RemoveScroll, Menu, UnstyledButton, Text, Box } from '@mantine/core';
+import { HeaderControls, ColorSchemeControl, SearchMobileControl } from '@mantine/ds';
+import { Logo } from '@/components/Logo';
+import { searchHandlers } from '@/components/Search';
+import packageJson from '../../../../package.json';
+import classes from './Header.module.css';
+
+interface HeaderProps {
+ navbarOpened: boolean;
+ onNavbarToggle(): void;
+}
+
+const versions = [
+ { v: 'v6', name: '6.0.21', link: 'https://v6.mantine.dev/' },
+ { v: 'v5', name: '5.10.5', link: 'https://v5.mantine.dev/' },
+ { v: 'v4', name: '4.2.12', link: 'https://v4.mantine.dev/' },
+ { v: 'v3', name: '3.6.14', link: 'https://v3.mantine.dev/' },
+ { v: 'v2', name: '2.5.1', link: 'https://v2.mantine.dev/' },
+ { v: 'v1', name: '1.3.1', link: 'https://v1.mantine.dev/' },
+];
+
+export function Header({ navbarOpened, onNavbarToggle }: HeaderProps) {
+ const versionItems = versions.map((item) => (
+ }
+ >
+ {item.v} {' '}
+
+ ({item.name})
+
+
+ ));
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/docs/components/Shell/Navbar/Navbar.module.css b/docs/components/Shell/Navbar/Navbar.module.css
new file mode 100644
index 00000000000..c22dddf3655
--- /dev/null
+++ b/docs/components/Shell/Navbar/Navbar.module.css
@@ -0,0 +1,61 @@
+.navbar {
+ border-right: rem(1px) solid var(--navbar-border-color);
+ position: fixed;
+ top: var(--docs-header-height);
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+ width: var(--docs-navbar-width);
+ display: flex;
+ flex-direction: column;
+
+ @mixin light {
+ --navbar-border-color: var(--mantine-color-gray-2);
+ background-color: var(--mantine-color-white);
+ }
+
+ @mixin dark {
+ --navbar-border-color: var(--mantine-color-dark-7);
+ background-color: var(--mantine-color-dark-8);
+ }
+
+ @mixin rtl {
+ border-left: rem(1px) solid var(--navbar-border-color);
+ border-right: 0;
+ left: unset;
+ right: 0;
+ }
+
+ &[data-hidden] {
+ @media (max-width: $docs-navbar-breakpoint) {
+ display: none !important;
+ }
+ }
+
+ &[data-mobile-only] {
+ display: none;
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ display: flex;
+ }
+ }
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ width: unset;
+ left: 0;
+ right: 0;
+ }
+}
+
+.scrollarea {
+ flex: 1;
+}
+
+.body {
+ padding-top: var(--mantine-spacing-md);
+ padding-bottom: rem(100px);
+}
+
+.groups {
+ padding-top: var(--mantine-spacing-xl);
+}
diff --git a/docs/components/Shell/Navbar/Navbar.tsx b/docs/components/Shell/Navbar/Navbar.tsx
new file mode 100644
index 00000000000..3d13d914d30
--- /dev/null
+++ b/docs/components/Shell/Navbar/Navbar.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import {
+ IconBrandMantine,
+ IconCompass,
+ IconApi,
+ IconHeartHandshake,
+ IconPalette,
+} from '@tabler/icons-react';
+import { Box, ScrollArea, rem } from '@mantine/core';
+import { MDX_PAGES_GROUPS } from '@/mdx';
+import { NavbarMainLink } from './NavbarMainLink/NavbarMainLink';
+import { NavbarLinksGroup } from './NavbarLinksGroup/NavbarLinksGroup';
+import classes from './Navbar.module.css';
+
+interface NavbarProps {
+ navbarOpened: boolean;
+ mobileNavbarOnly: boolean | undefined;
+ onNavbarClose(): void;
+}
+
+export function Navbar({ navbarOpened, onNavbarClose, mobileNavbarOnly }: NavbarProps) {
+ const groups = MDX_PAGES_GROUPS.map((group) => (
+
+ ));
+
+ return (
+
+
+
+
}
+ href="/getting-started"
+ onNavbarClose={onNavbarClose}
+ >
+ Getting started
+
+
}
+ href="/about"
+ onNavbarClose={onNavbarClose}
+ >
+ About Mantine
+
+
}
+ href="/overview"
+ onNavbarClose={onNavbarClose}
+ >
+ API Overview
+
+
}
+ href="/contribute"
+ onNavbarClose={onNavbarClose}
+ >
+ Contribute
+
+
}
+ href="/colors-generator"
+ onNavbarClose={onNavbarClose}
+ >
+ Colors generator
+
+
}
+ href="https://ui.mantine.dev"
+ onNavbarClose={onNavbarClose}
+ >
+ Mantine UI
+
+
{groups}
+
+
+
+ );
+}
diff --git a/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.module.css b/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.module.css
new file mode 100644
index 00000000000..c0f969afbf5
--- /dev/null
+++ b/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.module.css
@@ -0,0 +1,196 @@
+.group {
+ margin-bottom: 0;
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+
+ &[data-opened] {
+ margin-bottom: calc(var(--mantine-spacing-xl) * 1.2);
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ width: calc(100% + var(--mantine-spacing-md));
+ height: rem(32px);
+ cursor: pointer;
+
+ @mixin light {
+ color: var(--mantine-color-gray-7);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
+
+.chevron {
+ width: rem(15px);
+ height: rem(15px);
+ margin-right: var(--mantine-spacing-md);
+ transition: transform 150ms ease;
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: var(--mantine-spacing-md);
+ }
+
+ &[data-collapsed] {
+ transform: rotate(-90deg);
+ }
+}
+
+.title {
+ user-select: none;
+ font-weight: 700;
+ font-family: var(--docs-font-primary);
+ line-height: 1;
+ padding-top: rem(4px);
+ letter-spacing: rem(0.5px);
+ word-spacing: rem(1px);
+ text-transform: uppercase;
+ font-size: var(--mantine-font-size-xs);
+}
+
+.category {
+ padding-top: rem(12px);
+}
+
+.categoryTitle {
+ position: relative;
+ padding-left: rem(23px);
+ margin-left: rem(7px);
+ margin-bottom: rem(5px);
+ border-left: rem(1px) solid;
+ height: rem(34px);
+ display: flex;
+ align-items: center;
+ font-size: var(--mantine-font-size-xs);
+ border-top-right-radius: var(--mantine-radius-sm);
+ border-bottom-right-radius: var(--mantine-radius-sm);
+ text-transform: capitalize;
+
+ @mixin light {
+ background-color: var(--mantine-color-gray-0);
+ border-color: var(--mantine-color-gray-3);
+ color: var(--mantine-color-gray-6);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-7);
+ border-color: var(--mantine-color-dark-6);
+ color: var(--mantine-color-dark-2);
+ }
+
+ @mixin rtl {
+ padding-left: 0;
+ padding-right: rem(23px);
+ border-left: 0;
+ border-right: rem(1px) solid;
+ margin-left: 0;
+ margin-right: rem(7px);
+ border-radius: 0;
+ border-top-left-radius: var(--mantine-radius-sm);
+ border-bottom-left-radius: var(--mantine-radius-sm);
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-6);
+ }
+ }
+
+ &::before {
+ content: '';
+ position: absolute;
+ bottom: rem(-5px);
+ left: rem(-1px);
+ height: rem(5px);
+ width: rem(1px);
+
+ @mixin light {
+ background-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-6);
+ }
+
+ @mixin rtl {
+ left: unset;
+ right: rem(-1px);
+ }
+ }
+}
+
+.categoryIcon {
+ margin-right: var(--mantine-spacing-xs);
+ width: rem(14px);
+ height: rem(14px);
+
+ @mixin rtl {
+ margin-right: 0;
+ margin-left: var(--mantine-spacing-xs);
+ }
+}
+
+.link {
+ display: block;
+ border-left: rem(1px) solid;
+ padding-left: rem(23px);
+ padding-right: var(--mantine-spacing-md);
+ margin-left: rem(7px);
+ height: rem(34px);
+ line-height: rem(34px);
+ border-top-right-radius: var(--mantine-radius-sm);
+ border-bottom-right-radius: var(--mantine-radius-sm);
+ font-size: var(--mantine-font-size-sm);
+ user-select: none;
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ color: var(--mantine-color-gray-7);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-6);
+ color: var(--mantine-color-dark-1);
+ }
+
+ @mixin rtl {
+ padding-left: var(--mantine-spacing-md);
+ padding-right: rem(23px);
+ border-left: 0;
+ border-right: rem(1px) solid;
+ margin-left: 0;
+ margin-right: rem(7px);
+ border-radius: 0;
+ border-top-left-radius: var(--mantine-radius-sm);
+ border-bottom-left-radius: var(--mantine-radius-sm);
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-6);
+ }
+ }
+
+ &[data-active] {
+ border-color: var(--mantine-color-blue-5) !important;
+ font-weight: 500;
+
+ @mixin light {
+ color: var(--mantine-color-blue-8);
+ background-color: var(--mantine-color-blue-0);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-blue-1);
+ background-color: rgba(24, 100, 171, 0.45);
+ }
+ }
+}
diff --git a/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.tsx b/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.tsx
new file mode 100644
index 00000000000..c0422556d68
--- /dev/null
+++ b/docs/components/Shell/Navbar/NavbarLinksGroup/NavbarLinksGroup.tsx
@@ -0,0 +1,127 @@
+import React, { useEffect, useRef, useState } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { IconChevronDown } from '@tabler/icons-react';
+import { UnstyledButton, Text, Box } from '@mantine/core';
+import { MdxPagesGroup, MdxPagesCategory, Frontmatter } from '@/types';
+import { CATEGORY_ICONS } from './category-icons';
+import classes from './NavbarLinksGroup.module.css';
+
+function hasCategory(page: Frontmatter | MdxPagesCategory): page is MdxPagesCategory {
+ return 'category' in page;
+}
+
+function hasActiveLink(data: MdxPagesGroup, pathname: string) {
+ return data.pages.some((page) => {
+ if (hasCategory(page)) {
+ return page.pages.some((nestedPage) => nestedPage.slug === pathname);
+ }
+
+ return page.slug === pathname;
+ });
+}
+
+interface NavbarLinkProps {
+ data: Frontmatter;
+ onNavbarClose(): void;
+ linkRef: React.ForwardedRef;
+}
+
+function NavbarLink({ data, onNavbarClose, linkRef }: NavbarLinkProps) {
+ const router = useRouter();
+ return (
+
+ {data.title}
+
+ );
+}
+
+interface NavbarLinksGroupProps {
+ data: MdxPagesGroup;
+ onNavbarClose(): void;
+}
+
+export function NavbarLinksGroup({ data, onNavbarClose }: NavbarLinksGroupProps) {
+ const router = useRouter();
+ const [opened, setOpened] = useState(hasActiveLink(data, router.pathname));
+ const itemRefs = useRef>({});
+
+ const scrollToLink = (pathname: string) => {
+ const element = itemRefs.current[pathname];
+
+ if (!element) {
+ return;
+ }
+
+ const height = typeof window !== 'undefined' ? window.innerHeight : 0;
+ const { top, bottom } = element.getBoundingClientRect();
+
+ if (top < 60 || bottom > height) {
+ element.scrollIntoView({ block: 'center' });
+ }
+ };
+
+ useEffect(() => {
+ if (hasActiveLink(data, router.pathname)) {
+ setOpened(true);
+ setTimeout(() => scrollToLink(router.pathname), 10);
+ }
+ }, [router.pathname]);
+
+ const pages = data.pages.map((page) => {
+ if (hasCategory(page)) {
+ const sorted = page.pages.sort((a, b) => a.title.localeCompare(b.title));
+ const nested = sorted.map((nestedPage) => (
+ {
+ itemRefs.current[nestedPage.slug] = node!;
+ }}
+ />
+ ));
+
+ const Icon = CATEGORY_ICONS[page.category];
+
+ return (
+
+
+
+ {page.category}
+
+
+ {nested}
+
+ );
+ }
+
+ return (
+ {
+ itemRefs.current[page.slug] = node!;
+ }}
+ />
+ );
+ });
+
+ return (
+
+ setOpened((o) => !o)}>
+
+ {data.group.replace('-', ' ')}
+
+ {opened && pages}
+
+ );
+}
diff --git a/docs/components/Shell/Navbar/NavbarLinksGroup/category-icons.ts b/docs/components/Shell/Navbar/NavbarLinksGroup/category-icons.ts
new file mode 100644
index 00000000000..0c4f8561b5d
--- /dev/null
+++ b/docs/components/Shell/Navbar/NavbarLinksGroup/category-icons.ts
@@ -0,0 +1,32 @@
+import {
+ IconRefresh,
+ IconBulb,
+ IconForms,
+ IconBox,
+ IconLayout2,
+ IconClick,
+ IconMenu2,
+ IconNotebook,
+ IconBoxMultiple,
+ IconLetterCase,
+ IconSpeakerphone,
+ IconComponents,
+ IconSelector,
+} from '@tabler/icons-react';
+
+export const CATEGORY_ICONS: Record = {
+ 'State management': IconBox,
+ 'UI and Dom': IconForms,
+ Utilities: IconBulb,
+ Lifecycle: IconRefresh,
+ 'Data display': IconNotebook,
+ overlays: IconBoxMultiple,
+ navigation: IconMenu2,
+ inputs: IconForms,
+ misc: IconComponents,
+ feedback: IconSpeakerphone,
+ typography: IconLetterCase,
+ layout: IconLayout2,
+ buttons: IconClick,
+ combobox: IconSelector,
+};
diff --git a/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.module.css b/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.module.css
new file mode 100644
index 00000000000..413d7736320
--- /dev/null
+++ b/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.module.css
@@ -0,0 +1,48 @@
+.link {
+ --offset: rem(6px);
+
+ height: rem(48px);
+ display: flex;
+ align-items: center;
+ padding-left: calc(var(--mantine-spacing-md) - var(--offset));
+ padding-right: calc(var(--mantine-spacing-md) - var(--offset));
+ margin-left: var(--offset);
+ margin-right: var(--offset);
+ border-radius: var(--mantine-radius-md);
+ color: var(--mantine-color-black);
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+
+ &[data-active] {
+ background-color: var(--mantine-color-blue-light-hover);
+
+ & > .icon {
+ background: transparent;
+ }
+ }
+}
+
+.icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: rem(4px);
+ border-radius: var(--mantine-radius-md);
+ background-color: var(--mantine-color-blue-light-hover);
+ color: var(--mantine-color-blue-light-color);
+ width: rem(34px);
+ height: rem(34px);
+}
+
+.label {
+ font-size: var(--mantine-font-size-sm);
+ margin-left: var(--mantine-spacing-sm);
+ font-weight: 500;
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-sm);
+ }
+}
diff --git a/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.tsx b/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.tsx
new file mode 100644
index 00000000000..69620d337b7
--- /dev/null
+++ b/docs/components/Shell/Navbar/NavbarMainLink/NavbarMainLink.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import Link from 'next/link';
+import { UnstyledButton } from '@mantine/core';
+import { useRouter } from 'next/router';
+import classes from './NavbarMainLink.module.css';
+
+interface NavbarMainLinkProps extends React.ComponentPropsWithoutRef<'a'> {
+ external?: boolean;
+ children: React.ReactNode;
+ icon: React.ReactNode;
+ onNavbarClose(): void;
+}
+
+export function NavbarMainLink({
+ href,
+ external,
+ children,
+ icon,
+ onNavbarClose,
+}: NavbarMainLinkProps) {
+ const router = useRouter();
+ return (
+
+ component={external ? 'a' : Link}
+ href={href}
+ target={external ? '_blank' : undefined}
+ className={classes.link}
+ mod={{ active: router.pathname === href }}
+ onClick={onNavbarClose}
+ >
+ {icon}
+ {children}
+
+ );
+}
diff --git a/docs/components/Shell/Shell.module.css b/docs/components/Shell/Shell.module.css
new file mode 100644
index 00000000000..52e37507226
--- /dev/null
+++ b/docs/components/Shell/Shell.module.css
@@ -0,0 +1,25 @@
+.main {
+ padding-top: var(--docs-header-height);
+ padding-left: var(--docs-navbar-width);
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ padding-left: 0;
+ }
+
+ @mixin rtl {
+ padding-left: 0;
+ padding-right: var(--docs-navbar-width);
+
+ @media (max-width: $docs-navbar-breakpoint) {
+ padding-right: 0;
+ }
+ }
+
+ @mixin light {
+ background-color: var(--mantine-color-white);
+ }
+
+ @mixin dark {
+ background-color: var(--mantine-color-dark-7);
+ }
+}
diff --git a/docs/components/Shell/Shell.tsx b/docs/components/Shell/Shell.tsx
new file mode 100644
index 00000000000..b303c07f8f0
--- /dev/null
+++ b/docs/components/Shell/Shell.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { useUncontrolled } from '@mantine/hooks';
+import { Header } from './Header/Header';
+import { Navbar } from './Navbar/Navbar';
+import classes from './Shell.module.css';
+
+interface ShellProps {
+ children: React.ReactNode;
+ withNavbar?: boolean;
+ navbarOpened?: boolean;
+ mobileNavbarOnly?: boolean;
+ onNavbarOpenedChange?(opened: boolean): void;
+}
+
+export function Shell({
+ children,
+ withNavbar = true,
+ mobileNavbarOnly = false,
+ navbarOpened,
+ onNavbarOpenedChange,
+}: ShellProps) {
+ const [opened, setNavbarOpened] = useUncontrolled({
+ value: navbarOpened,
+ defaultValue: false,
+ finalValue: false,
+ onChange: onNavbarOpenedChange,
+ });
+
+ const toggleNavbar = () => setNavbarOpened(!opened);
+ const closeNavbar = () => setNavbarOpened(false);
+
+ return (
+ <>
+
+ {withNavbar && (
+
+ )}
+
+ {children}
+
+ >
+ );
+}
diff --git a/docs/components/Shell/index.ts b/docs/components/Shell/index.ts
new file mode 100644
index 00000000000..0a26c6f2bc9
--- /dev/null
+++ b/docs/components/Shell/index.ts
@@ -0,0 +1 @@
+export { Shell } from './Shell';
diff --git a/docs/components/SocialCards/SocialCards.module.css b/docs/components/SocialCards/SocialCards.module.css
new file mode 100644
index 00000000000..a89b0cbe1a6
--- /dev/null
+++ b/docs/components/SocialCards/SocialCards.module.css
@@ -0,0 +1,49 @@
+.cards {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--mantine-spacing-lg);
+}
+
+.card {
+ flex: 1;
+ min-width: rem(300px);
+ display: flex;
+ color: var(--mantine-color-white);
+ border-radius: var(--mantine-radius-md);
+ padding: var(--mantine-spacing-md);
+ text-decoration: none;
+ transition: transform 200ms ease, box-shadow 200ms ease;
+
+ @mixin hover {
+ transform: scale(1.03);
+ box-shadow: var(--mantine-shadow-xl);
+ }
+}
+
+.discord {
+ background-color: light-dark(#5865f2, #4450da);
+}
+
+.twitter {
+ background-color: light-dark(#239de9, #228acb);
+}
+
+.github {
+ background-color: var(--mantine-color-dark-9);
+}
+
+.icon {
+ margin-right: var(--mantine-spacing-lg);
+ height: rem(30px);
+ width: rem(30px);
+ min-width: rem(30px);
+}
+
+.title {
+ font-weight: 500;
+}
+
+.description {
+ font-size: var(--mantine-font-size-xs);
+ margin-top: rem(5px);
+}
diff --git a/docs/components/SocialCards/SocialCards.tsx b/docs/components/SocialCards/SocialCards.tsx
new file mode 100644
index 00000000000..aedc0fcf249
--- /dev/null
+++ b/docs/components/SocialCards/SocialCards.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import cx from 'clsx';
+import { DiscordIcon, GithubIcon, TwitterIcon, meta } from '@mantine/ds';
+import classes from './SocialCards.module.css';
+
+interface CardBaseProps extends React.ComponentPropsWithoutRef<'a'> {
+ icon: 'discord' | 'twitter' | 'github';
+ title: string;
+ description: string;
+ href: string;
+}
+
+const icons: Record = {
+ discord: ,
+ twitter: ,
+ github: ,
+};
+
+export function CardBase({ icon, title, description, className, ...others }: CardBaseProps) {
+ return (
+
+ {icons[icon]}
+
+
{title}
+
{description}
+
+
+ );
+}
+
+export function DiscordCard() {
+ return (
+
+ );
+}
+
+export function GitHubCard() {
+ return (
+
+ );
+}
+
+export function TwitterCard() {
+ return (
+
+ );
+}
+
+interface SocialCardsProps {
+ discord?: boolean;
+ github?: boolean;
+ twitter?: boolean;
+}
+
+export function SocialCards({ discord = true, github = true, twitter = true }: SocialCardsProps) {
+ return (
+
+ {discord && }
+ {github && }
+ {twitter && }
+
+ );
+}
diff --git a/docs/components/SocialCards/index.ts b/docs/components/SocialCards/index.ts
new file mode 100644
index 00000000000..39433c521e3
--- /dev/null
+++ b/docs/components/SocialCards/index.ts
@@ -0,0 +1 @@
+export { SocialCards, DiscordCard } from './SocialCards';
diff --git a/docs/components/StylePropsTable/StylePropsTable.tsx b/docs/components/StylePropsTable/StylePropsTable.tsx
new file mode 100644
index 00000000000..6c751a73246
--- /dev/null
+++ b/docs/components/StylePropsTable/StylePropsTable.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { STYlE_PROPS_DATA, Code, Breadcrumbs } from '@mantine/core';
+import { MdxDataTable } from '@/components/MdxProvider';
+
+const THEME_KEYS: Record = {
+ color: 'theme.colors',
+ fontSize: 'theme.fontSizes',
+ spacing: 'theme.spacing',
+ lineHeight: 'theme.lineHeights',
+};
+
+export function StylePropsTable({ source = STYlE_PROPS_DATA }: { source: any }) {
+ const data = Object.keys(source).map((propName) => {
+ const propData = source[propName];
+ const themeKey = THEME_KEYS[propData.type];
+ return [
+ {propName}
,
+
+ {Array.isArray(propData.property) ? (
+ propData.property.map((prop: any) => {prop}
)
+ ) : (
+ {propData.property}
+ )}
+ ,
+ themeKey ? {themeKey}
: '–',
+ ];
+ });
+
+ return ;
+}
diff --git a/docs/components/StylePropsTable/index.ts b/docs/components/StylePropsTable/index.ts
new file mode 100644
index 00000000000..4d64de7760b
--- /dev/null
+++ b/docs/components/StylePropsTable/index.ts
@@ -0,0 +1 @@
+export { StylePropsTable } from './StylePropsTable';
diff --git a/docs/components/StylesApiTable/ModifiersTable.tsx b/docs/components/StylesApiTable/ModifiersTable.tsx
new file mode 100644
index 00000000000..dee5ce658e6
--- /dev/null
+++ b/docs/components/StylesApiTable/ModifiersTable.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Table, TableProps } from '@mantine/core';
+import { HtmlText } from '@/components/HtmlText';
+import { TableInlineCode } from '@/components/TableInlineCode';
+import type { StylesApiData } from './StylesApiTable';
+
+interface ModifiersTableProps extends TableProps {
+ data: StylesApiData;
+}
+
+export function ModifiersTable({ data, ...others }: ModifiersTableProps) {
+ const hasConditions = data.modifiers?.some((modifier) => !!modifier.condition);
+ const hasValues = data.modifiers?.some((modifier) => !!modifier.value);
+
+ const rows =
+ data.modifiers?.map((modifier, index) => (
+
+
+ {Array.isArray(modifier.selector) ? modifier.selector.join(', ') : modifier.selector}
+
+
+ {modifier.modifier}
+
+ {hasConditions && (
+
+ {modifier.condition || '–'}
+
+ )}
+ {hasValues && (
+
+ {modifier.value || '–'}
+
+ )}
+
+ )) || [];
+
+ return (
+
+
+
+
+ Selector
+ Attribute
+ {hasConditions && Condition }
+ {hasValues && Value }
+
+
+ {rows}
+
+
+ );
+}
diff --git a/docs/components/StylesApiTable/SelectorsTable.tsx b/docs/components/StylesApiTable/SelectorsTable.tsx
new file mode 100644
index 00000000000..38007d607b6
--- /dev/null
+++ b/docs/components/StylesApiTable/SelectorsTable.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Table, TableProps } from '@mantine/core';
+import { HtmlText } from '@/components/HtmlText';
+import { TableInlineCode } from '@/components/TableInlineCode';
+import type { StylesApiData } from './StylesApiTable';
+
+interface SelectorsTableProps extends TableProps {
+ data: StylesApiData;
+ component: string;
+}
+
+export function SelectorsTable({ data, component, ...others }: SelectorsTableProps) {
+ const rows = Object.keys(data.selectors).map((selector) => (
+
+ {selector}
+
+
+ .mantine-{component}-{selector}
+
+
+
+ {data.selectors[selector]}
+
+
+ ));
+
+ return (
+
+
+
+
+ Selector
+ Static selector
+ Description
+
+
+ {rows}
+
+
+ );
+}
diff --git a/docs/components/StylesApiTable/StylesApiTable.module.css b/docs/components/StylesApiTable/StylesApiTable.module.css
new file mode 100644
index 00000000000..8d14d6c565c
--- /dev/null
+++ b/docs/components/StylesApiTable/StylesApiTable.module.css
@@ -0,0 +1,32 @@
+.groupsHeader {
+ margin-bottom: rem(30px);
+}
+
+.group {
+ --_group-spacing: rem(40px);
+
+ & + & {
+ margin-top: var(--_group-spacing);
+ padding-top: var(--_group-spacing);
+ border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
+ }
+}
+
+.title {
+ font-family: var(--docs-font-primary);
+ margin-bottom: var(--mantine-spacing-sm);
+
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
+
+.section {
+ & + & {
+ margin-top: rem(40px);
+ }
+}
diff --git a/docs/components/StylesApiTable/StylesApiTable.tsx b/docs/components/StylesApiTable/StylesApiTable.tsx
new file mode 100644
index 00000000000..f5259d44c5e
--- /dev/null
+++ b/docs/components/StylesApiTable/StylesApiTable.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { rem, Title } from '@mantine/core';
+import * as stylesData from '@mantine/styles-api';
+import type { Modifier } from '@mantine/styles-api';
+import { TableError } from '@/components/TableError';
+import { getComponentName } from '@/components/PropsTable';
+import { SelectorsTable } from './SelectorsTable';
+import { VariablesTable } from './VariablesTable';
+import { ModifiersTable } from './ModifiersTable';
+import classes from './StylesApiTable.module.css';
+
+export interface StylesApiData {
+ selectors: Record;
+ vars: Record>;
+ modifiers: Modifier[];
+}
+
+const STYLES_API_DATA: Record = stylesData as any;
+
+export interface StylesApiTableProps {
+ component: string;
+ componentPrefix: string | undefined;
+}
+
+export function StylesApiTable({ component, componentPrefix }: StylesApiTableProps) {
+ const data = STYLES_API_DATA[`${component}StylesApi`];
+ if (!data) {
+ return ;
+ }
+
+ const componentName = getComponentName({ component, componentPrefix });
+
+ return (
+ <>
+
+
+
+
+ {componentName} selectors
+
+
+
+
+ {Object.keys(data.vars).length > 0 && (
+
+
+ {componentName} CSS variables
+
+
+
+ )}
+
+ {Array.isArray(data.modifiers) && data.modifiers.length > 0 && (
+
+
+ {componentName} data attributes
+
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/docs/components/StylesApiTable/StylesApiTablesList.tsx b/docs/components/StylesApiTable/StylesApiTablesList.tsx
new file mode 100644
index 00000000000..8103920124d
--- /dev/null
+++ b/docs/components/StylesApiTable/StylesApiTablesList.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Code } from '@mantine/core';
+import { MdxParagraph, MdxLink, MdxTitle } from '@/components/MdxProvider';
+import { StylesApiTable } from './StylesApiTable';
+import classes from './StylesApiTable.module.css';
+
+interface StylesApiTablesListProps {
+ components: string[];
+ componentPrefix?: string;
+}
+
+export function StylesApiTablesList({ components, componentPrefix }: StylesApiTablesListProps) {
+ const tables = components.map((component) => (
+
+
+
+ ));
+
+ return (
+ <>
+
+ Styles API
+
+ {components[0]}
component supports{' '}
+ Styles API . With Styles API, you can
+ customize styles of any inner element. Follow{' '}
+ the documentation to learn how to use CSS
+ modules, CSS variables and inline styles to get full control over component styles.
+
+
+ {tables}
+ >
+ );
+}
diff --git a/docs/components/StylesApiTable/VariablesTable.tsx b/docs/components/StylesApiTable/VariablesTable.tsx
new file mode 100644
index 00000000000..d6dde7ef17c
--- /dev/null
+++ b/docs/components/StylesApiTable/VariablesTable.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { Table, TableProps } from '@mantine/core';
+import { HtmlText } from '@/components/HtmlText';
+import { TableInlineCode } from '@/components/TableInlineCode';
+import type { StylesApiData } from './StylesApiTable';
+
+interface VariablesTableProps extends TableProps {
+ data: StylesApiData;
+}
+
+export function VariablesTable({ data, ...others }: VariablesTableProps) {
+ const rows = Object.keys(data.vars).reduce((acc, selector) => {
+ Object.keys(data.vars[selector]).forEach((variable, index) => {
+ acc.push(
+
+ {index === 0 && (
+ {selector}
+ )}
+
+ {variable}
+
+
+ {data.vars[selector][variable]}
+
+
+ );
+ });
+
+ return acc;
+ }, []);
+
+ return (
+
+
+
+
+ Selector
+ Variable
+ Description
+
+
+ {rows}
+
+
+ );
+}
diff --git a/docs/components/StylesApiTable/index.ts b/docs/components/StylesApiTable/index.ts
new file mode 100644
index 00000000000..ac5fc88db6a
--- /dev/null
+++ b/docs/components/StylesApiTable/index.ts
@@ -0,0 +1,5 @@
+export { StylesApiTable } from './StylesApiTable';
+export { StylesApiTablesList } from './StylesApiTablesList';
+export { ModifiersTable } from './ModifiersTable';
+export { SelectorsTable } from './SelectorsTable';
+export { VariablesTable } from './VariablesTable';
diff --git a/docs/components/TableError/TableError.tsx b/docs/components/TableError/TableError.tsx
new file mode 100644
index 00000000000..c1c42efc890
--- /dev/null
+++ b/docs/components/TableError/TableError.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Text, Anchor } from '@mantine/core';
+
+interface TableErrorProps {
+ errorOf: string;
+}
+
+export function TableError({ errorOf }: TableErrorProps) {
+ return (
+
+
+ Error loading component {errorOf} data.{' '}
+
+ If you see this message please let us know by{' '}
+
+ opening an issue on GitHub
+
+ .
+
+ );
+}
diff --git a/docs/components/TableError/index.ts b/docs/components/TableError/index.ts
new file mode 100644
index 00000000000..41cdd64171d
--- /dev/null
+++ b/docs/components/TableError/index.ts
@@ -0,0 +1 @@
+export { TableError } from './TableError';
diff --git a/docs/components/TableInlineCode/TableInlineCode.module.css b/docs/components/TableInlineCode/TableInlineCode.module.css
new file mode 100644
index 00000000000..e8dac477410
--- /dev/null
+++ b/docs/components/TableInlineCode/TableInlineCode.module.css
@@ -0,0 +1,12 @@
+.code {
+ font-family: var(--mantine-font-family-monospace);
+ font-size: var(--mantine-font-size-xs);
+
+ @mixin light {
+ color: var(--mantine-color-indigo-filled);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-red-filled);
+ }
+}
diff --git a/docs/components/TableInlineCode/TableInlineCode.tsx b/docs/components/TableInlineCode/TableInlineCode.tsx
new file mode 100644
index 00000000000..8dca25e005c
--- /dev/null
+++ b/docs/components/TableInlineCode/TableInlineCode.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import cx from 'clsx';
+import { Text, TextProps, ElementProps } from '@mantine/core';
+import classes from './TableInlineCode.module.css';
+
+interface TableInlineCodeProps extends TextProps, ElementProps<'span', 'color'> {}
+
+export function TableInlineCode({ className, ...others }: TableInlineCodeProps) {
+ return ;
+}
diff --git a/docs/components/TableInlineCode/index.ts b/docs/components/TableInlineCode/index.ts
new file mode 100644
index 00000000000..17d27febdd6
--- /dev/null
+++ b/docs/components/TableInlineCode/index.ts
@@ -0,0 +1 @@
+export { TableInlineCode } from './TableInlineCode';
diff --git a/docs/components/TableOfContents/TableOfContents.module.css b/docs/components/TableOfContents/TableOfContents.module.css
new file mode 100644
index 00000000000..c727d4bc763
--- /dev/null
+++ b/docs/components/TableOfContents/TableOfContents.module.css
@@ -0,0 +1,120 @@
+.wrapper {
+ padding-left: var(--mantine-spacing-md);
+ position: sticky;
+ top: var(--mantine-spacing-xl);
+ right: 0;
+ padding-top: rem(55px);
+ flex: 0 0 calc(var(--docs-table-of-contents-width) - rem(20px));
+
+ @media (max-width: $docs-mdx-breakpoint) {
+ display: none;
+ }
+
+ @mixin rtl {
+ padding-left: 0;
+ padding-right: var(--mantine-spacing-md);
+ right: auto;
+ left: 0;
+ }
+
+ &[data-with-tabs] {
+ padding-top: 0;
+ top: calc(var(--mantine-spacing-xl) + rem(60px));
+ }
+}
+
+.inner {
+ padding-bottom: var(--mantine-spacing-xl);
+ padding-left: var(--mantine-spacing-md);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ @mixin rtl {
+ padding-left: 0;
+ padding-right: var(--mantine-spacing-md);
+ }
+}
+
+.link {
+ display: block;
+ border-left: rem(1px) solid transparent;
+ padding: rem(8px) var(--mantine-spacing-md);
+ margin-left: rem(-1px);
+ padding-left: calc(var(--toc-link-offset) * var(--mantine-spacing-lg));
+ border-top-right-radius: var(--mantine-radius-sm);
+ border-bottom-right-radius: var(--mantine-radius-sm);
+
+ @mixin light {
+ color: var(--mantine-color-gray-7);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-dark-1);
+ }
+
+ @mixin rtl {
+ border-left: 0;
+ border-right: rem(1px) solid transparent;
+ margin-left: 0;
+ margin-right: rem(-1px);
+ border-top-left-radius: var(--mantine-radius-sm);
+ border-bottom-left-radius: var(--mantine-radius-sm);
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ &[data-active] {
+ border-color: var(--mantine-color-blue-5);
+
+ @mixin light {
+ color: var(--mantine-color-blue-8);
+ background-color: var(--mantine-color-blue-0);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-blue-1);
+ background-color: rgba(24, 100, 171, 0.45);
+ }
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ margin-bottom: var(--mantine-spacing-md);
+}
+
+.title {
+ margin-left: var(--mantine-spacing-md);
+
+ @mixin rtl {
+ margin-left: 0;
+ margin-right: var(--mantine-spacing-md);
+ }
+}
+
+.items {
+ border-left: rem(1px) solid;
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-4);
+ }
+
+ @mixin rtl {
+ border-left: 0;
+ border-right: rem(1px) solid;
+
+ @mixin light {
+ border-color: var(--mantine-color-gray-3);
+ }
+
+ @mixin dark {
+ border-color: var(--mantine-color-dark-4);
+ }
+ }
+}
diff --git a/docs/components/TableOfContents/TableOfContents.tsx b/docs/components/TableOfContents/TableOfContents.tsx
new file mode 100644
index 00000000000..50ee1a77790
--- /dev/null
+++ b/docs/components/TableOfContents/TableOfContents.tsx
@@ -0,0 +1,94 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { useRouter } from 'next/router';
+import { Text, ScrollArea, rem, Box } from '@mantine/core';
+import { IconList } from '@tabler/icons-react';
+import { getHeadings, Heading } from './get-headings';
+import classes from './TableOfContents.module.css';
+
+interface TableOfContentsProps {
+ withTabs: boolean;
+}
+
+function getActiveElement(rects: DOMRect[]) {
+ if (rects.length === 0) {
+ return -1;
+ }
+
+ const closest = rects.reduce(
+ (acc, item, index) => {
+ if (Math.abs(acc.position) < Math.abs(item.y)) {
+ return acc;
+ }
+
+ return {
+ index,
+ position: item.y,
+ };
+ },
+ { index: 0, position: rects[0].y }
+ );
+
+ return closest.index;
+}
+
+export function TableOfContents({ withTabs }: TableOfContentsProps) {
+ const [active, setActive] = useState(0);
+ const [headings, setHeadings] = useState([]);
+ const headingsRef = useRef([]);
+ const router = useRouter();
+
+ const filteredHeadings = headings.filter((heading) => heading.depth > 1);
+
+ const handleScroll = () => {
+ setActive(
+ getActiveElement(headingsRef.current.map((d) => d.getNode().getBoundingClientRect()))
+ );
+ };
+
+ useEffect(() => {
+ const _headings = getHeadings();
+ headingsRef.current = _headings;
+ setHeadings(_headings);
+ setActive(getActiveElement(_headings.map((d) => d.getNode().getBoundingClientRect())));
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ if (filteredHeadings.length === 0) {
+ return null;
+ }
+
+ const items = filteredHeadings.map((heading, index) => (
+
+ key={heading.id}
+ component="a"
+ fz="sm"
+ className={classes.link}
+ mod={{ active: active === index }}
+ href={`#${heading.id}`}
+ __vars={{ '--toc-link-offset': `${heading.depth - 1}` }}
+ onClick={(event) => {
+ event.preventDefault();
+ router.replace(`${router.pathname}#${heading.id}`);
+ }}
+ >
+ {heading.content}
+
+ ));
+
+ return (
+
+
+
+
+
+ Table of contents
+
+
+ {items}
+
+
+
+
+ );
+}
diff --git a/docs/components/TableOfContents/get-headings.ts b/docs/components/TableOfContents/get-headings.ts
new file mode 100644
index 00000000000..27e07465767
--- /dev/null
+++ b/docs/components/TableOfContents/get-headings.ts
@@ -0,0 +1,34 @@
+export interface Heading {
+ depth: number;
+ content: string;
+ id: string;
+ getNode: () => HTMLHeadingElement;
+}
+
+function getHeadingsData(headings: HTMLHeadingElement[]): Heading[] {
+ const result: Heading[] = [];
+
+ for (let i = 0; i < headings.length; i += 1) {
+ const heading = headings[i];
+ if (heading.id) {
+ result.push({
+ depth: parseInt(heading.getAttribute('data-order')!, 10),
+ content: heading.getAttribute('data-heading') || '',
+ id: heading.id,
+ getNode: () => document.getElementById(heading.id) as HTMLHeadingElement,
+ });
+ }
+ }
+
+ return result;
+}
+
+export function getHeadings(): Heading[] {
+ const root = document.getElementById('mdx');
+
+ if (!root) {
+ return [];
+ }
+
+ return getHeadingsData(Array.from(root.querySelectorAll('[data-heading]')));
+}
diff --git a/docs/components/TableOfContents/index.ts b/docs/components/TableOfContents/index.ts
new file mode 100644
index 00000000000..ffe4593ca76
--- /dev/null
+++ b/docs/components/TableOfContents/index.ts
@@ -0,0 +1 @@
+export { TableOfContents } from './TableOfContents';
diff --git a/docs/components/ThemeColors/ColorsGroup/ColorsGroup.module.css b/docs/components/ThemeColors/ColorsGroup/ColorsGroup.module.css
new file mode 100644
index 00000000000..aaf688ba136
--- /dev/null
+++ b/docs/components/ThemeColors/ColorsGroup/ColorsGroup.module.css
@@ -0,0 +1,47 @@
+$theme-colors-breakpoint: em(820px);
+
+.wrapper {
+ & + & {
+ margin-top: var(--mantine-spacing-xl);
+ }
+}
+
+.color {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 calc(10% - rem(7px));
+
+ @media (max-width: $theme-colors-breakpoint) {
+ flex: 0 0 calc(20% - rem(7px));
+ }
+}
+
+.swatch {
+ width: 100%;
+ height: 0;
+ padding-bottom: 100%;
+}
+
+.title {
+ text-transform: capitalize;
+ margin-bottom: var(--mantine-spacing-xs);
+}
+
+.group {
+ display: flex;
+ gap: rem(7px);
+
+ @media (max-width: $theme-colors-breakpoint) {
+ flex-wrap: wrap;
+ }
+}
+
+.colorName {
+ @mixin light {
+ color: var(--mantine-color-black);
+ }
+
+ @mixin dark {
+ color: var(--mantine-color-white);
+ }
+}
diff --git a/docs/components/ThemeColors/ColorsGroup/ColorsGroup.tsx b/docs/components/ThemeColors/ColorsGroup/ColorsGroup.tsx
new file mode 100644
index 00000000000..9e5d425a05a
--- /dev/null
+++ b/docs/components/ThemeColors/ColorsGroup/ColorsGroup.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { useMantineTheme, Text, ColorSwatch, Box } from '@mantine/core';
+import classes from './ColorsGroup.module.css';
+
+interface ColorsGroupProps {
+ group: string;
+}
+
+export function ColorsGroup({ group }: ColorsGroupProps) {
+ const theme = useMantineTheme();
+
+ const colors = theme.colors[group].map((color, index) => (
+
+
+
+
+ {group} {index}
+
+
+ {color}
+
+
+
+ ));
+
+ return (
+
+ );
+}
diff --git a/docs/components/ThemeColors/ThemeColors.module.css b/docs/components/ThemeColors/ThemeColors.module.css
new file mode 100644
index 00000000000..7c7734c5b78
--- /dev/null
+++ b/docs/components/ThemeColors/ThemeColors.module.css
@@ -0,0 +1,4 @@
+.root {
+ margin-top: var(--mantine-spacing-xl);
+ margin-bottom: var(--mantine-spacing-xl);
+}
diff --git a/docs/components/ThemeColors/ThemeColors.tsx b/docs/components/ThemeColors/ThemeColors.tsx
new file mode 100644
index 00000000000..5ebd17946cc
--- /dev/null
+++ b/docs/components/ThemeColors/ThemeColors.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { DEFAULT_THEME } from '@mantine/core';
+import { ColorsGroup } from './ColorsGroup/ColorsGroup';
+import classes from './ThemeColors.module.css';
+
+export function ThemeColors() {
+ const groups = Object.keys(DEFAULT_THEME.colors).map((group) => (
+
+ ));
+
+ return {groups}
;
+}
diff --git a/docs/components/ThemeColors/index.ts b/docs/components/ThemeColors/index.ts
new file mode 100644
index 00000000000..d18a6137b83
--- /dev/null
+++ b/docs/components/ThemeColors/index.ts
@@ -0,0 +1,2 @@
+export { ColorsGroup } from './ColorsGroup/ColorsGroup';
+export { ThemeColors } from './ThemeColors';
diff --git a/docs/components/icons/GatsbyIcon.tsx b/docs/components/icons/GatsbyIcon.tsx
new file mode 100644
index 00000000000..319e3dddc18
--- /dev/null
+++ b/docs/components/icons/GatsbyIcon.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Box, rem } from '@mantine/core';
+import { IconProps } from './types';
+import classes from './icons.module.css';
+
+export function GatsbyIcon({ size = 28, ...others }: IconProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/docs/src/components/FrameworksControls/icons/NextIcon.tsx b/docs/components/icons/NextIcon.tsx
similarity index 78%
rename from docs/src/components/FrameworksControls/icons/NextIcon.tsx
rename to docs/components/icons/NextIcon.tsx
index d9ee571dac6..fdd7be6355b 100644
--- a/docs/src/components/FrameworksControls/icons/NextIcon.tsx
+++ b/docs/components/icons/NextIcon.tsx
@@ -1,19 +1,24 @@
import React from 'react';
-import { useMantineTheme, rem } from '@mantine/core';
+import { Box, rem } from '@mantine/core';
+import { IconProps } from './types';
+import classes from './icons.module.css';
-export function NextIcon() {
- const theme = useMantineTheme();
+export function NextIcon({ size = 40, ...others }: IconProps) {
return (
-
-
+
);
}
diff --git a/docs/components/icons/RedwoodIcon.tsx b/docs/components/icons/RedwoodIcon.tsx
new file mode 100644
index 00000000000..c5e31c2414c
--- /dev/null
+++ b/docs/components/icons/RedwoodIcon.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Box, rem } from '@mantine/core';
+import { IconProps } from './types';
+import classes from './icons.module.css';
+
+export function RedwoodIcon({ size = 28, ...others }: IconProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/docs/components/icons/RemixIcon.tsx b/docs/components/icons/RemixIcon.tsx
new file mode 100644
index 00000000000..a8dbb9999a6
--- /dev/null
+++ b/docs/components/icons/RemixIcon.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Box, rem } from '@mantine/core';
+import { IconProps } from './types';
+import classes from './icons.module.css';
+
+export function RemixIcon({ size = 40, ...others }: IconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/docs/components/icons/ViteIcon.tsx b/docs/components/icons/ViteIcon.tsx
new file mode 100644
index 00000000000..9b6c3188180
--- /dev/null
+++ b/docs/components/icons/ViteIcon.tsx
@@ -0,0 +1,42 @@
+import React, { useId } from 'react';
+import { Box, rem } from '@mantine/core';
+import { IconProps } from './types';
+import classes from './icons.module.css';
+
+export function ViteIcon({ size = 28, ...others }: IconProps) {
+ const id = useId();
+ const idA = `vite-a-${id}`;
+ const idB = `vite-b-${id}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/icons/icons.module.css b/docs/components/icons/icons.module.css
new file mode 100644
index 00000000000..cc365396ebd
--- /dev/null
+++ b/docs/components/icons/icons.module.css
@@ -0,0 +1,9 @@
+.nextIcon {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ width: var(--icon-size);
+}
+
+.icon {
+ width: var(--icon-size);
+ height: var(--icon-size);
+}
diff --git a/docs/components/icons/index.ts b/docs/components/icons/index.ts
new file mode 100644
index 00000000000..7463713d269
--- /dev/null
+++ b/docs/components/icons/index.ts
@@ -0,0 +1,16 @@
+import { NextIcon } from './NextIcon';
+import { ViteIcon } from './ViteIcon';
+import { GatsbyIcon } from './GatsbyIcon';
+import { RemixIcon } from './RemixIcon';
+import { RedwoodIcon } from './RedwoodIcon';
+import type { Template } from '../MdxProvider/MdxTemplatesList/data';
+
+export const frameworkIcons: Record = {
+ next: NextIcon,
+ vite: ViteIcon,
+ gatsby: GatsbyIcon,
+ redwood: RedwoodIcon,
+ remix: RemixIcon,
+};
+
+export { NextIcon, ViteIcon, GatsbyIcon, RemixIcon, RedwoodIcon };
diff --git a/docs/components/icons/types.ts b/docs/components/icons/types.ts
new file mode 100644
index 00000000000..5a885f66319
--- /dev/null
+++ b/docs/components/icons/types.ts
@@ -0,0 +1,5 @@
+import { ElementProps } from '@mantine/core';
+
+export interface IconProps extends ElementProps<'svg', 'display'> {
+ size?: number;
+}
diff --git a/docs/dropzone-demos/Dropzone.demo.disabled.tsx b/docs/dropzone-demos/Dropzone.demo.disabled.tsx
new file mode 100644
index 00000000000..28d44554279
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.disabled.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { MantineDemo } from '@mantine/ds';
+import { BaseDemo } from './_base';
+import classes from './Dropzone.disabled.module.css';
+
+const cssCode = `
+.disabled {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
+ cursor: not-allowed;
+
+ & * {
+ color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
+ }
+}
+`;
+
+const code = `
+import { Dropzone } from '@mantine/dropzone';
+import classes from './Demo.module.css';
+
+function Demo() {
+ return ;
+}
+`;
+
+function Demo() {
+ return ;
+}
+
+export const disabled: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code: [
+ { fileName: 'Demo.tsx', code, language: 'tsx' },
+ { fileName: 'Demo.module.css', code: cssCode, language: 'scss' },
+ ],
+};
diff --git a/docs/dropzone-demos/Dropzone.demo.enableChildPointerEvent.tsx b/docs/dropzone-demos/Dropzone.demo.enableChildPointerEvent.tsx
new file mode 100644
index 00000000000..33e7316a308
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.enableChildPointerEvent.tsx
@@ -0,0 +1,50 @@
+import React, { useRef } from 'react';
+import { Button, Group } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { MantineDemo } from '@mantine/ds';
+
+const code = `
+import { useRef } from 'react';
+import { Button, Group } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+
+function Demo() {
+ const openRef = useRef<() => void>(null);
+
+ return (
+ {}}
+ activateOnClick={false}
+ styles={{ inner: { pointerEvents: 'all' } }}
+ >
+
+ openRef.current?.()}>Select files
+
+
+ );
+}
+`;
+
+function Demo() {
+ const openRef = useRef<() => void>(null);
+
+ return (
+ {}}
+ activateOnClick={false}
+ styles={{ inner: { pointerEvents: 'all' } }}
+ >
+
+ openRef.current?.()}>Select files
+
+
+ );
+}
+
+export const enableChildPointerEvent: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code,
+};
diff --git a/docs/dropzone-demos/Dropzone.demo.fullScreen.tsx b/docs/dropzone-demos/Dropzone.demo.fullScreen.tsx
new file mode 100644
index 00000000000..d314a2bacff
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.fullScreen.tsx
@@ -0,0 +1,97 @@
+/* eslint-disable no-console */
+import React, { useState } from 'react';
+import { Button, Group } from '@mantine/core';
+import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+import { MantineDemo } from '@mantine/ds';
+import { DropzoneDemoChildren } from './_base';
+
+const code = `
+import { useState } from 'react';
+import { Group, Text, Button, rem } from '@mantine/core';
+import { IconUpload, IconPhoto, IconX } from '@tabler/icons-react';
+import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+
+function Demo() {
+ const [active, setActive] = useState(false);
+
+ return (
+ <>
+
+ setActive((d) => !d)}>
+ {active ? 'Deactivate' : 'Activate'} full screen dropzone
+
+
+
+ {
+ console.log(files);
+ setActive(false);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag images here or click to select files
+
+
+ Attach as many files as you like, each file should not exceed 5mb
+
+
+
+
+ >
+ );
+}
+`;
+
+function Demo() {
+ const [active, setActive] = useState(false);
+
+ return (
+ <>
+
+ setActive((d) => !d)}>
+ {active ? 'Deactivate' : 'Activate'} full screen dropzone
+
+
+
+ {
+ console.log(files);
+ setActive(false);
+ }}
+ >
+
+
+ >
+ );
+}
+
+export const fullScreen: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code,
+};
diff --git a/src/mantine-demos/src/demos/dropzone/Dropzone.demo.loading.tsx b/docs/dropzone-demos/Dropzone.demo.loading.tsx
similarity index 86%
rename from src/mantine-demos/src/demos/dropzone/Dropzone.demo.loading.tsx
rename to docs/dropzone-demos/Dropzone.demo.loading.tsx
index dd0eb2bd6d9..30aa0ac2580 100644
--- a/src/mantine-demos/src/demos/dropzone/Dropzone.demo.loading.tsx
+++ b/docs/dropzone-demos/Dropzone.demo.loading.tsx
@@ -7,7 +7,7 @@ import { Dropzone } from '@mantine/dropzone';
function Demo() {
return (
-
+ {}}>
{/* children */}
);
@@ -19,7 +19,7 @@ function Demo() {
}
export const loading: MantineDemo = {
- type: 'demo',
+ type: 'code',
component: Demo,
code,
};
diff --git a/docs/dropzone-demos/Dropzone.demo.manual.tsx b/docs/dropzone-demos/Dropzone.demo.manual.tsx
new file mode 100644
index 00000000000..6ba80bf9627
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.manual.tsx
@@ -0,0 +1,45 @@
+import React, { useRef } from 'react';
+import { Button, Group } from '@mantine/core';
+import { MantineDemo } from '@mantine/ds';
+import { BaseDemo } from './_base';
+
+const code = `
+import { useRef } from 'react';
+import { Button, Group } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+
+function Demo() {
+ const openRef = useRef<() => void>(null);
+
+ return (
+ <>
+ {}}>
+ {/* children */}
+
+
+
+ openRef.current?.()}>Select files
+
+ >
+ );
+}
+`;
+
+function Demo() {
+ const openRef = useRef<() => void>(null);
+
+ return (
+ <>
+
+
+ openRef.current?.()}>Select files
+
+ >
+ );
+}
+
+export const manual: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code,
+};
diff --git a/docs/dropzone-demos/Dropzone.demo.preview.tsx b/docs/dropzone-demos/Dropzone.demo.preview.tsx
new file mode 100644
index 00000000000..31909b0fc3c
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.preview.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react';
+import { Text, Image, SimpleGrid } from '@mantine/core';
+import { MantineDemo } from '@mantine/ds';
+import { Dropzone, IMAGE_MIME_TYPE, FileWithPath } from '@mantine/dropzone';
+
+const code = `
+import { useState } from 'react';
+import { Text, Image, SimpleGrid } from '@mantine/core';
+import { Dropzone, IMAGE_MIME_TYPE, FileWithPath } from '@mantine/dropzone';
+
+function Demo() {
+ const [files, setFiles] = useState([]);
+
+ const previews = files.map((file, index) => {
+ const imageUrl = URL.createObjectURL(file);
+ return URL.revokeObjectURL(imageUrl)} />;
+ });
+
+ return (
+
+
+ Drop images here
+
+
+ 0 ? 'xl' : 0}>
+ {previews}
+
+
+ );
+}
+`;
+
+function Demo() {
+ const [files, setFiles] = useState([]);
+
+ const previews = files.map((file, index) => {
+ const imageUrl = URL.createObjectURL(file);
+ return URL.revokeObjectURL(imageUrl)} />;
+ });
+
+ return (
+
+
+ Drop images here
+
+
+ 0 ? 'xl' : 0}>
+ {previews}
+
+
+ );
+}
+
+export const preview: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code,
+};
diff --git a/docs/dropzone-demos/Dropzone.demo.stylesApi.module.css b/docs/dropzone-demos/Dropzone.demo.stylesApi.module.css
new file mode 100644
index 00000000000..7a1dfbfccfe
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.stylesApi.module.css
@@ -0,0 +1,18 @@
+.root {
+ min-height: rem(120px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 0;
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+
+ &[data-accept] {
+ color: var(--mantine-color-white);
+ background-color: var(--mantine-color-blue-6);
+ }
+
+ &[data-reject] {
+ color: var(--mantine-color-white);
+ background-color: var(--mantine-color-red-6);
+ }
+}
diff --git a/docs/dropzone-demos/Dropzone.demo.stylesApi.tsx b/docs/dropzone-demos/Dropzone.demo.stylesApi.tsx
new file mode 100644
index 00000000000..d6676968f81
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.stylesApi.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Text } from '@mantine/core';
+import { MantineDemo } from '@mantine/ds';
+import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+import classes from './Dropzone.demo.stylesApi.module.css';
+
+const cssCode = `
+.root {
+ min-height: rem(120px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 0;
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+
+ &[data-accept] {
+ color: var(--mantine-color-white);
+ background-color: var(--mantine-color-blue-6);
+ }
+
+ &[data-reject] {
+ color: var(--mantine-color-white);
+ background-color: var(--mantine-color-red-6);
+ }
+}
+
+`;
+
+const code = `
+import { Text } from '@mantine/core';
+import { Dropzone, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+import classes from './Demo.module.css';
+
+function Demo() {
+ return (
+ {}} accept={IMAGE_MIME_TYPE} className={classes.root}>
+ Drop images here
+
+ );
+}
+`;
+
+function Demo() {
+ return (
+ {}} accept={IMAGE_MIME_TYPE} className={classes.root}>
+ Drop images here
+
+ );
+}
+
+export const stylesApi: MantineDemo = {
+ type: 'code',
+ component: Demo,
+ code: [
+ { fileName: 'Demo.tsx', code, language: 'tsx' },
+ { fileName: 'Demo.module.css', code: cssCode, language: 'scss' },
+ ],
+};
diff --git a/docs/dropzone-demos/Dropzone.demo.usage.tsx b/docs/dropzone-demos/Dropzone.demo.usage.tsx
new file mode 100644
index 00000000000..48d4e38eb9b
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.demo.usage.tsx
@@ -0,0 +1,56 @@
+import { MantineDemo } from '@mantine/ds';
+import { BaseDemo } from './_base';
+
+const code = `
+import { Group, Text, rem } from '@mantine/core';
+import { IconUpload, IconPhoto, IconX } from '@tabler/icons-react';
+import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+
+export function BaseDemo(props: Partial) {
+ return (
+ console.log('accepted files', files)}
+ onReject={(files) => console.log('rejected files', files)}
+ maxSize={3 * 1024 ** 2}
+ accept={IMAGE_MIME_TYPE}
+ {...props}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag images here or click to select files
+
+
+ Attach as many files as you like, each file should not exceed 5mb
+
+
+
+
+ );
+}
+`;
+
+export const usage: MantineDemo = {
+ type: 'code',
+ component: BaseDemo,
+ code,
+};
diff --git a/docs/dropzone-demos/Dropzone.disabled.module.css b/docs/dropzone-demos/Dropzone.disabled.module.css
new file mode 100644
index 00000000000..d533474b328
--- /dev/null
+++ b/docs/dropzone-demos/Dropzone.disabled.module.css
@@ -0,0 +1,9 @@
+.disabled {
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
+ border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
+ cursor: not-allowed;
+
+ & * {
+ color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
+ }
+}
diff --git a/docs/dropzone-demos/_base.tsx b/docs/dropzone-demos/_base.tsx
new file mode 100644
index 00000000000..28b2355f1a6
--- /dev/null
+++ b/docs/dropzone-demos/_base.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable no-console */
+import React from 'react';
+import { Group, Text, rem } from '@mantine/core';
+import { IconUpload, IconPhoto, IconX } from '@tabler/icons-react';
+import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE } from '@mantine/dropzone';
+
+export function DropzoneDemoChildren() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag images here or click to select files
+
+
+ Attach as many files as you like, each file should not exceed 5mb
+
+
+
+ );
+}
+
+export function BaseDemo(props: Partial) {
+ return (
+ console.log('accepted files', files)}
+ onReject={(files) => console.log('rejected files', files)}
+ maxSize={3 * 1024 ** 2}
+ accept={IMAGE_MIME_TYPE}
+ {...props}
+ >
+
+
+ );
+}
diff --git a/docs/dropzone-demos/index.ts b/docs/dropzone-demos/index.ts
new file mode 100644
index 00000000000..4dea4dbfeb4
--- /dev/null
+++ b/docs/dropzone-demos/index.ts
@@ -0,0 +1,10 @@
+// For some reason next.js cannot handle dropzone demos in @mantine/demos
+
+export { usage } from './Dropzone.demo.usage';
+export { loading } from './Dropzone.demo.loading';
+export { disabled } from './Dropzone.demo.disabled';
+export { manual } from './Dropzone.demo.manual';
+export { enableChildPointerEvent } from './Dropzone.demo.enableChildPointerEvent';
+export { fullScreen } from './Dropzone.demo.fullScreen';
+export { stylesApi } from './Dropzone.demo.stylesApi';
+export { preview } from './Dropzone.demo.preview';
diff --git a/docs/fonts/FontsStyle.tsx b/docs/fonts/FontsStyle.tsx
new file mode 100644
index 00000000000..766c28e4b99
--- /dev/null
+++ b/docs/fonts/FontsStyle.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import Head from 'next/head';
+import { DEFAULT_THEME } from '@mantine/core';
+import { greycliffCF } from './GreycliffCF/GreycliffCF';
+
+export function FontsStyle() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/src/fonts/GreycliffCF/GreycliffCF-Bold.woff2 b/docs/fonts/GreycliffCF/GreycliffCF-Bold.woff2
similarity index 100%
rename from docs/src/fonts/GreycliffCF/GreycliffCF-Bold.woff2
rename to docs/fonts/GreycliffCF/GreycliffCF-Bold.woff2
diff --git a/docs/src/fonts/GreycliffCF/GreycliffCF-Heavy.woff2 b/docs/fonts/GreycliffCF/GreycliffCF-Heavy.woff2
similarity index 100%
rename from docs/src/fonts/GreycliffCF/GreycliffCF-Heavy.woff2
rename to docs/fonts/GreycliffCF/GreycliffCF-Heavy.woff2
diff --git a/docs/fonts/GreycliffCF/GreycliffCF.ts b/docs/fonts/GreycliffCF/GreycliffCF.ts
new file mode 100644
index 00000000000..45faa93a202
--- /dev/null
+++ b/docs/fonts/GreycliffCF/GreycliffCF.ts
@@ -0,0 +1,8 @@
+import localFont from 'next/font/local';
+
+export const greycliffCF = localFont({
+ src: [
+ { path: './GreycliffCF-Bold.woff2', weight: '700', style: 'normal' },
+ { path: './GreycliffCF-Heavy.woff2', weight: '900', style: 'normal' },
+ ],
+});
diff --git a/docs/src/fonts/README.md b/docs/fonts/README.md
similarity index 100%
rename from docs/src/fonts/README.md
rename to docs/fonts/README.md
diff --git a/docs/fonts/index.ts b/docs/fonts/index.ts
new file mode 100644
index 00000000000..ae7d996bf57
--- /dev/null
+++ b/docs/fonts/index.ts
@@ -0,0 +1,2 @@
+export { greycliffCF } from './GreycliffCF/GreycliffCF';
+export { FontsStyle } from './FontsStyle';
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
deleted file mode 100644
index b4c86e50927..00000000000
--- a/docs/gatsby-config.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const remarkSlug = require('remark-slug');
-
-module.exports = {
- flags: {
- DEV_SSR: false,
- FAST_DEV: true,
- },
-
- siteMetadata: {
- title: 'Mantine Docs',
- description: 'Mantine documentation',
- author: '@rtivital',
- siteUrl: 'https://mantine.dev',
- },
-
- plugins: [
- 'gatsby-plugin-cname',
- 'gatsby-plugin-tsconfig-paths',
- {
- resolve: 'gatsby-plugin-layout',
- options: {
- component: `${__dirname}/src/components/Layout/Layout`,
- },
- },
- {
- resolve: 'gatsby-plugin-mdx',
- options: {
- remarkPlugins: [remarkSlug],
- },
- },
- {
- resolve: 'gatsby-source-filesystem',
- options: {
- name: 'images',
- path: `${__dirname}/src/images`,
- },
- },
-
- {
- resolve: 'gatsby-source-filesystem',
- options: {
- name: 'docs',
- path: `${__dirname}/src/docs/`,
- },
- },
-
- {
- resolve: 'gatsby-plugin-manifest',
- options: {
- name: 'Mantine Docs',
- short_name: 'mantine',
- start_url: '/',
- background_color: '#228be6',
- theme_color: '#228be6',
- display: 'minimal-ui',
- icon: 'src/images/favicon.svg',
- },
- },
-
- {
- resolve: 'gatsby-plugin-nprogress',
- options: {
- color: '#228be6',
- showSpinner: false,
- },
- },
- ],
-};
diff --git a/docs/gatsby-ssr.js b/docs/gatsby-ssr.js
deleted file mode 100644
index 7a517b10bd5..00000000000
--- a/docs/gatsby-ssr.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable react/jsx-filename-extension */
-import React from 'react';
-import { renderToString } from 'react-dom/server';
-import { createStylesServer, ServerStyles } from '@mantine/ssr';
-
-const stylesServer = createStylesServer();
-
-export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString, setHeadComponents }) => {
- const html = renderToString(bodyComponent);
- setHeadComponents([ ]);
- replaceBodyHTMLString(html);
-};
-
-export const onRenderBody = ({ setHtmlAttributes }) => {
- setHtmlAttributes({ lang: 'en' });
-};
diff --git a/docs/mdx/data/mdx-core-data.ts b/docs/mdx/data/mdx-core-data.ts
new file mode 100644
index 00000000000..8d22d74bf06
--- /dev/null
+++ b/docs/mdx/data/mdx-core-data.ts
@@ -0,0 +1,1118 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_CORE_DATA: Record = {
+ Box: {
+ title: 'Box',
+ package: '@mantine/core',
+ slug: '/core/box',
+ description: 'Base component for all Mantine components',
+ import: "import { Box } from '@mantine/core';",
+ source: 'mantine-core/src/components/Box/Box.tsx',
+ docs: 'core/button.mdx',
+ },
+
+ Button: {
+ title: 'Button',
+ package: '@mantine/core',
+ slug: '/core/button',
+ description: 'Button component to render button or link',
+ componentPrefix: 'Button',
+ props: ['Button', 'ButtonGroup'],
+ styles: ['Button', 'ButtonGroup'],
+ import: "import { Button } from '@mantine/core';",
+ source: 'mantine-core/src/components/Button/Button.tsx',
+ docs: 'core/button.mdx',
+ },
+ Loader: {
+ title: 'Loader',
+ package: '@mantine/core',
+ slug: '/core/loader',
+ description: 'Indicate loading state',
+ props: ['Loader'],
+ styles: ['Loader'],
+ import: "import { Loader } from '@mantine/core';",
+ source: 'mantine-core/src/components/Loader/Loader.tsx',
+ docs: 'core/loader.mdx',
+ },
+ Container: {
+ title: 'Container',
+ package: '@mantine/core',
+ slug: '/core/container',
+ description: 'Center content with padding and max-width',
+ props: ['Container'],
+ styles: ['Container'],
+ import: "import { Container } from '@mantine/core';",
+ source: 'mantine-core/src/components/Container/Container.tsx',
+ docs: 'core/container.mdx',
+ },
+ Anchor: {
+ title: 'Anchor',
+ package: '@mantine/core',
+ slug: '/core/anchor',
+ description: 'Display link with theme styles',
+ props: ['Anchor'],
+ styles: ['Anchor'],
+ import: "import { Anchor } from '@mantine/core';",
+ source: 'mantine-core/src/components/Anchor/Anchor.tsx',
+ docs: 'core/anchor.mdx',
+ },
+ Input: {
+ title: 'Input',
+ package: '@mantine/core',
+ slug: '/core/input',
+ description: 'Base component to create custom inputs',
+ componentPrefix: 'Input',
+ props: ['Input', 'InputWrapper', 'InputLabel', 'InputDescription', 'InputError'],
+ styles: ['Input', 'InputWrapper'],
+ polymorphic: true,
+ import: "import { Input } from '@mantine/core';",
+ source: 'mantine-core/src/components/Input/Input.tsx',
+ docs: 'core/input.mdx',
+ },
+ ActionIcon: {
+ title: 'ActionIcon',
+ package: '@mantine/core',
+ slug: '/core/action-icon',
+ description: 'Icon button',
+ componentPrefix: 'ActionIcon',
+ props: ['ActionIcon', 'ActionIconGroup'],
+ styles: ['ActionIcon', 'ActionIconGroup'],
+ polymorphic: true,
+ import: "import { ActionIcon } from '@mantine/core';",
+ source: 'mantine-core/src/components/ActionIcon/ActionIcon.tsx',
+ docs: 'core/action-icon.mdx',
+ },
+ CloseButton: {
+ title: 'CloseButton',
+ package: '@mantine/core',
+ slug: '/core/close-button',
+ description: 'ActionIcon with close icon',
+ props: ['CloseButton'],
+ styles: ['CloseButton'],
+ polymorphic: true,
+ import: "import { CloseButton } from '@mantine/core';",
+ source: 'mantine-core/src/components/CloseButton/CloseButton.tsx',
+ docs: 'core/close-button.mdx',
+ },
+ CopyButton: {
+ title: 'CopyButton',
+ package: '@mantine/core',
+ slug: '/core/copy-button',
+ description: 'Copies given text to clipboard',
+ props: ['CopyButton'],
+ import: "import { CopyButton } from '@mantine/core';",
+ source: 'mantine-core/src/components/CopyButton/CopyButton.tsx',
+ docs: 'core/copy-button.mdx',
+ },
+ FileButton: {
+ title: 'FileButton',
+ package: '@mantine/core',
+ slug: '/core/file-button',
+ description: 'Open file picker with a button click',
+ props: ['FileButton'],
+ import: "import { FileButton } from '@mantine/core';",
+ source: 'mantine-core/src/components/FileButton/FileButton.tsx',
+ docs: 'core/file-button.mdx',
+ },
+ UnstyledButton: {
+ title: 'UnstyledButton',
+ package: '@mantine/core',
+ slug: '/core/unstyled-button',
+ description: 'Unstyled polymorphic button',
+ polymorphic: true,
+ import: "import { UnstyledButton } from '@mantine/core';",
+ source: 'mantine-core/src/components/UnstyledButton/UnstyledButton.tsx',
+ docs: 'core/unstyled-button.mdx',
+ },
+ Tabs: {
+ title: 'Tabs',
+ package: '@mantine/core',
+ slug: '/core/tabs',
+ props: ['Tabs', 'TabsList', 'TabsTab', 'TabsPanel'],
+ styles: ['Tabs'],
+ description: 'Switch between different views',
+ import: "import { Tabs } from '@mantine/core';",
+ source: 'mantine-core/src/components/Tabs/Tabs.tsx',
+ docs: 'core/tabs.mdx',
+ },
+ BackgroundImage: {
+ title: 'BackgroundImage',
+ package: '@mantine/core',
+ slug: '/core/background-image',
+ description: 'Displays image as background',
+ polymorphic: true,
+ props: ['BackgroundImage'],
+ styles: ['BackgroundImage'],
+ import: "import { BackgroundImage } from '@mantine/core';",
+ source: 'mantine-core/src/components/BackgroundImage/BackgroundImage.tsx',
+ docs: 'core/background-image.mdx',
+ },
+ Blockquote: {
+ title: 'Blockquote',
+ package: '@mantine/core',
+ slug: '/core/blockquote',
+ props: ['Blockquote'],
+ styles: ['Blockquote'],
+ description: 'Blockquote with optional cite',
+ import: "import { Blockquote } from '@mantine/core';",
+ source: 'mantine-core/src/components/Blockquote/Blockquote.tsx',
+ docs: 'core/blockquote.mdx',
+ },
+ Breadcrumbs: {
+ title: 'Breadcrumbs',
+ package: '@mantine/core',
+ slug: '/core/breadcrumbs',
+ props: ['Breadcrumbs'],
+ styles: ['Breadcrumbs'],
+ description: 'Separates list of react nodes with given separator',
+ import: "import { Breadcrumbs } from '@mantine/core';",
+ source: 'mantine-core/src/components/Breadcrumbs/Breadcrumbs.tsx',
+ docs: 'core/breadcrumbs.mdx',
+ },
+ Burger: {
+ title: 'Burger',
+ package: '@mantine/core',
+ slug: '/core/burger',
+ props: ['Burger'],
+ styles: ['Burger'],
+ description: 'Open/close navigation button',
+ import: "import { Burger } from '@mantine/core';",
+ source: 'mantine-core/src/components/Burger/Burger.tsx',
+ docs: 'core/burger.mdx',
+ },
+ Center: {
+ title: 'Center',
+ package: '@mantine/core',
+ slug: '/core/center',
+ props: ['Center'],
+ styles: ['Center'],
+ polymorphic: true,
+ description: 'Centers content vertically and horizontally',
+ import: "import { Center } from '@mantine/core';",
+ source: 'mantine-core/src/components/Center/Center.tsx',
+ docs: 'core/center.mdx',
+ },
+ Code: {
+ title: 'Code',
+ package: '@mantine/core',
+ slug: '/core/code',
+ props: ['Code'],
+ styles: ['Code'],
+ description: 'Inline and block code',
+ import: "import { Code } from '@mantine/core';",
+ source: 'mantine-core/src/components/Code/Code.tsx',
+ docs: 'core/code.mdx',
+ },
+ Collapse: {
+ title: 'Collapse',
+ package: '@mantine/core',
+ slug: '/core/collapse',
+ props: ['Collapse'],
+ description: 'Animate presence with slide down/up transition',
+ import: "import { Collapse } from '@mantine/core';",
+ source: 'mantine-core/src/components/Collapse/Collapse.tsx',
+ docs: 'core/collapse.mdx',
+ },
+ ColorPicker: {
+ title: 'ColorPicker',
+ package: '@mantine/core',
+ slug: '/core/color-picker',
+ props: ['ColorPicker'],
+ styles: ['ColorPicker'],
+ description: 'Pick colors',
+ import: "import { ColorPicker } from '@mantine/core';",
+ source: 'mantine-core/src/components/ColorPicker/ColorPicker.tsx',
+ docs: 'core/color-picker.mdx',
+ },
+ ColorSwatch: {
+ title: 'ColorSwatch',
+ package: '@mantine/core',
+ slug: '/core/color-swatch',
+ props: ['ColorSwatch'],
+ styles: ['ColorSwatch'],
+ polymorphic: true,
+ description: 'Displays color',
+ import: "import { ColorSwatch } from '@mantine/core';",
+ source: 'mantine-core/src/components/ColorSwatch/ColorSwatch.tsx',
+ docs: 'core/color-swatch.mdx',
+ },
+ FocusTrap: {
+ title: 'FocusTrap',
+ package: '@mantine/core',
+ slug: '/core/focus-trap',
+ props: ['FocusTrap'],
+ description: 'Trap focus at child node',
+ import: "import { FocusTrap } from '@mantine/core';",
+ source: 'mantine-core/src/components/FocusTrap/FocusTrap.tsx',
+ docs: 'core/focus-trap.mdx',
+ },
+ Group: {
+ title: 'Group',
+ package: '@mantine/core',
+ slug: '/core/group',
+ props: ['Group'],
+ styles: ['Group'],
+ description: 'Compose elements and components in a horizontal flex container',
+ import: "import { Group } from '@mantine/core';",
+ source: 'mantine-core/src/components/Group/Group.tsx',
+ docs: 'core/group.mdx',
+ },
+ Highlight: {
+ title: 'Highlight',
+ package: '@mantine/core',
+ slug: '/core/highlight',
+ props: ['Highlight'],
+ styles: ['Highlight'],
+ polymorphic: true,
+ description: 'Highlight given part of a string with mark',
+ import: "import { Highlight } from '@mantine/core';",
+ source: 'mantine-core/src/components/Highlight/Highlight.tsx',
+ docs: 'core/highlight.mdx',
+ },
+ Kbd: {
+ title: 'Kbd',
+ package: '@mantine/core',
+ slug: '/core/kbd',
+ props: ['Kbd'],
+ styles: ['Kbd'],
+ description: 'Display keyboard key',
+ import: "import { Kbd } from '@mantine/core';",
+ source: 'mantine-core/src/components/Kbd/Kbd.tsx',
+ docs: 'core/kbd.mdx',
+ },
+ Mark: {
+ title: 'Mark',
+ package: '@mantine/core',
+ slug: '/core/mark',
+ props: ['Mark'],
+ styles: ['Mark'],
+ description: 'Highlight part of the text',
+ import: "import { Mark } from '@mantine/core';",
+ source: 'mantine-core/src/components/Mark/Mark.tsx',
+ docs: 'core/mark.mdx',
+ },
+ NativeSelect: {
+ title: 'NativeSelect',
+ package: '@mantine/core',
+ slug: '/core/native-select',
+ props: ['NativeSelect'],
+ styles: ['NativeSelect'],
+ description: 'Native select element based on Input',
+ import: "import { NativeSelect } from '@mantine/core';",
+ source: 'mantine-core/src/components/NativeSelect/NativeSelect.tsx',
+ docs: 'core/native-select.mdx',
+ },
+ Notification: {
+ title: 'Notification',
+ package: '@mantine/core',
+ slug: '/core/notification',
+ props: ['Notification'],
+ styles: ['Notification'],
+ description: 'Show dynamic notifications and alerts to user, part of notifications system',
+ import: "import { Notification } from '@mantine/core';",
+ source: 'mantine-core/src/components/Notification/Notification.tsx',
+ docs: 'core/notification.mdx',
+ },
+ Paper: {
+ title: 'Paper',
+ package: '@mantine/core',
+ slug: '/core/paper',
+ props: ['Paper'],
+ styles: ['Paper'],
+ description: 'Renders white or dark background depending on color scheme',
+ polymorphic: true,
+ import: "import { Paper } from '@mantine/core';",
+ source: 'mantine-core/src/components/Paper/Paper.tsx',
+ docs: 'core/paper.mdx',
+ },
+ Popover: {
+ title: 'Popover',
+ package: '@mantine/core',
+ slug: '/core/popover',
+ componentPrefix: 'Popover',
+ props: ['Popover', 'PopoverTarget'],
+ styles: ['Popover'],
+ description: 'Display popover section relative to given target element',
+ import: "import { Popover } from '@mantine/core';",
+ source: 'mantine-core/src/components/Popover/Popover.tsx',
+ docs: 'core/popover.mdx',
+ },
+ Portal: {
+ title: 'Portal',
+ package: '@mantine/core',
+ slug: '/core/portal',
+ props: ['Portal'],
+ description: 'Renders component outside of parent element tree',
+ import: "import { Portal } from '@mantine/core';",
+ source: 'mantine-core/src/components/Portal/Portal.tsx',
+ docs: 'core/portal.mdx',
+ },
+ ScrollArea: {
+ title: 'ScrollArea',
+ package: '@mantine/core',
+ slug: '/core/scroll-area',
+ props: ['ScrollArea'],
+ styles: ['ScrollArea'],
+ description: 'Area with custom scrollbars',
+ import: "import { ScrollArea } from '@mantine/core';",
+ source: 'mantine-core/src/components/ScrollArea/ScrollArea.tsx',
+ docs: 'core/scroll-area.mdx',
+ },
+ SegmentedControl: {
+ title: 'SegmentedControl',
+ package: '@mantine/core',
+ slug: '/core/segmented-control',
+ props: ['SegmentedControl'],
+ styles: ['SegmentedControl'],
+ description: 'A linear set of two or more segments',
+ import: "import { SegmentedControl } from '@mantine/core';",
+ source: 'mantine-core/src/components/SegmentedControl/SegmentedControl.tsx',
+ docs: 'core/segmented-control.mdx',
+ },
+ Slider: {
+ title: 'Slider',
+ package: '@mantine/core',
+ slug: '/core/slider',
+ props: ['Slider', 'RangeSlider'],
+ styles: ['Slider'],
+ description: 'Slider and RangeSlider components',
+ import: "import { Slider } from '@mantine/core';",
+ source: 'mantine-core/src/components/Slider/Slider.tsx',
+ docs: 'core/slider.mdx',
+ },
+ Stack: {
+ title: 'Stack',
+ package: '@mantine/core',
+ slug: '/core/stack',
+ props: ['Stack'],
+ styles: ['Stack'],
+ description: 'Compose elements and components in a vertical flex container',
+ import: "import { Stack } from '@mantine/core';",
+ source: 'mantine-core/src/components/Stack/Stack.tsx',
+ docs: 'core/stack.mdx',
+ },
+ Switch: {
+ title: 'Switch',
+ package: '@mantine/core',
+ slug: '/core/switch',
+ props: ['Switch', 'SwitchGroup'],
+ styles: ['Switch'],
+ description: 'Capture boolean input from user',
+ import: "import { Switch } from '@mantine/core';",
+ source: 'mantine-core/src/components/Switch/Switch.tsx',
+ docs: 'core/switch.mdx',
+ },
+ Table: {
+ title: 'Table',
+ package: '@mantine/core',
+ slug: '/core/table',
+ props: ['Table'],
+ styles: ['Table'],
+ description: 'Render table with theme styles',
+ import: "import { Table } from '@mantine/core';",
+ source: 'mantine-core/src/components/Table/Table.tsx',
+ docs: 'core/table.mdx',
+ },
+ Text: {
+ title: 'Text',
+ package: '@mantine/core',
+ slug: '/core/text',
+ props: ['Text'],
+ styles: ['Text'],
+ description: 'Display text',
+ import: "import { Text } from '@mantine/core';",
+ source: 'mantine-core/src/components/Text/Text.tsx',
+ docs: 'core/text.mdx',
+ },
+ TextInput: {
+ title: 'TextInput',
+ package: '@mantine/core',
+ slug: '/core/text-input',
+ props: ['TextInput'],
+ styles: ['TextInput'],
+ description: 'Capture string input from user',
+ import: "import { TextInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/TextInput/TextInput.tsx',
+ docs: 'core/text-input.mdx',
+ },
+ Title: {
+ title: 'Title',
+ package: '@mantine/core',
+ slug: '/core/title',
+ props: ['Title'],
+ styles: ['Title'],
+ description: 'h1-h6 heading',
+ import: "import { Title } from '@mantine/core';",
+ source: 'mantine-core/src/components/Title/Title.tsx',
+ docs: 'core/title.mdx',
+ },
+ Badge: {
+ title: 'Badge',
+ package: '@mantine/core',
+ slug: '/core/badge',
+ props: ['Badge'],
+ styles: ['Badge'],
+ polymorphic: true,
+ description: 'Display badge, pill or tag',
+ import: "import { Badge } from '@mantine/core';",
+ source: 'mantine-core/src/components/Badge/Badge.tsx',
+ docs: 'core/badge.mdx',
+ },
+ Tooltip: {
+ title: 'Tooltip',
+ package: '@mantine/core',
+ slug: '/core/tooltip',
+ props: ['Tooltip'],
+ styles: ['Tooltip'],
+ description: 'Renders tooltip at given element on mouse over or other event',
+ import: "import { Tooltip } from '@mantine/core';",
+ source: 'mantine-core/src/components/Tooltip/Tooltip.tsx',
+ docs: 'core/tooltip.mdx',
+ },
+ Transition: {
+ title: 'Transition',
+ package: '@mantine/core',
+ slug: '/core/transition',
+ props: ['Transition'],
+ description: 'Animate presence of component with pre-made animations',
+ import: "import { Transition } from '@mantine/core';",
+ source: 'mantine-core/src/components/Transition/Transition.tsx',
+ docs: 'core/transition.mdx',
+ },
+ TypographyStylesProvider: {
+ title: 'TypographyStylesProvider',
+ package: '@mantine/core',
+ slug: '/core/typography-styles-provider',
+ description: 'Styles provider for html content',
+ import: "import { TypographyStylesProvider } from '@mantine/core';",
+ source: 'mantine-core/src/components/TypographyStylesProvider/TypographyStylesProvider.tsx',
+ docs: 'core/typography-styles-provider.mdx',
+ },
+ VisuallyHidden: {
+ title: 'VisuallyHidden',
+ package: '@mantine/core',
+ slug: '/core/visually-hidden',
+ description: 'Hide element visually but keep it accessible for screen readers',
+ import: "import { VisuallyHidden } from '@mantine/core';",
+ source: 'mantine-core/src/components/VisuallyHidden/VisuallyHidden.tsx',
+ docs: 'core/visually-hidden.mdx',
+ },
+ Divider: {
+ title: 'Divider',
+ package: '@mantine/core',
+ slug: '/core/divider',
+ props: ['Divider'],
+ styles: ['Divider'],
+ description: 'Horizontal line with optional label or vertical divider',
+ import: "import { Divider } from '@mantine/core';",
+ source: 'mantine-core/src/components/Divider/Divider.tsx',
+ docs: 'core/divider.mdx',
+ },
+ AspectRatio: {
+ title: 'AspectRatio',
+ package: '@mantine/core',
+ slug: '/core/aspect-ratio',
+ props: ['AspectRatio'],
+ styles: ['AspectRatio'],
+ description: 'Maintain responsive consistent width/height ratio',
+ import: "import { AspectRatio } from '@mantine/core';",
+ source: 'mantine-core/src/components/AspectRatio/AspectRatio.tsx',
+ docs: 'core/aspect-ratio.mdx',
+ },
+ Overlay: {
+ title: 'Overlay',
+ package: '@mantine/core',
+ slug: '/core/overlay',
+ props: ['Overlay'],
+ styles: ['Overlay'],
+ polymorphic: true,
+ description: 'Overlays parent element with div element with any color and opacity',
+ import: "import { Overlay } from '@mantine/core';",
+ source: 'mantine-core/src/components/Overlay/Overlay.tsx',
+ docs: 'core/overlay.mdx',
+ },
+ Avatar: {
+ title: 'Avatar',
+ package: '@mantine/core',
+ slug: '/core/avatar',
+ props: ['Avatar'],
+ styles: ['Avatar'],
+ polymorphic: true,
+ description: 'Display user profile image, initials or fallback icon',
+ import: "import { Avatar } from '@mantine/core';",
+ source: 'mantine-core/src/components/Avatar/Avatar.tsx',
+ docs: 'core/avatar.mdx',
+ },
+
+ Alert: {
+ title: 'Alert',
+ package: '@mantine/core',
+ slug: '/core/alert',
+ props: ['Alert'],
+ styles: ['Alert'],
+ description: 'Attract user attention with important static message',
+ import: "import { Alert } from '@mantine/core';",
+ source: 'mantine-core/src/components/Alert/Alert.tsx',
+ docs: 'core/alert.mdx',
+ },
+
+ Affix: {
+ title: 'Affix',
+ package: '@mantine/core',
+ slug: '/core/affix',
+ props: ['Affix'],
+ styles: ['Affix'],
+ description: 'Renders children inside portal at fixed position',
+ import: "import { Affix } from '@mantine/core';",
+ source: 'mantine-core/src/components/Affix/Affix.tsx',
+ docs: 'core/affix.mdx',
+ },
+
+ Fieldset: {
+ title: 'Fieldset',
+ package: '@mantine/core',
+ slug: '/core/fieldset',
+ props: ['Fieldset'],
+ styles: ['Fieldset'],
+ description: 'Group related elements in a form',
+ import: "import { Fieldset } from '@mantine/core';",
+ source: 'mantine-core/src/components/Fieldset/Fieldset.tsx',
+ docs: 'core/fieldset.mdx',
+ },
+
+ Checkbox: {
+ title: 'Checkbox',
+ package: '@mantine/core',
+ slug: '/core/checkbox',
+ componentPrefix: 'Checkbox',
+ props: ['Checkbox', 'CheckboxGroup'],
+ styles: ['Checkbox', 'CheckboxGroup'],
+ description: 'Capture boolean input from user',
+ import: "import { Checkbox } from '@mantine/core';",
+ source: 'mantine-core/src/components/Checkbox/Checkbox.tsx',
+ docs: 'core/checkbox.mdx',
+ },
+
+ Combobox: {
+ title: 'Combobox',
+ package: '@mantine/core',
+ slug: '/core/combobox',
+ componentPrefix: 'Combobox',
+ props: [
+ 'Combobox',
+ 'ComboboxOption',
+ 'ComboboxTarget',
+ 'ComboboxDropdownTarget',
+ 'ComboboxEventsTarget',
+ 'ComboboxDropdown',
+ 'ComboboxGroup',
+ ],
+ styles: ['Combobox'],
+ description: 'Create custom select, autocomplete or multiselect inputs',
+ import: "import { Combobox } from '@mantine/core';",
+ source: 'mantine-core/src/components/Combobox/Combobox.tsx',
+ docs: 'core/combobox.mdx',
+ },
+
+ Modal: {
+ title: 'Modal',
+ package: '@mantine/core',
+ slug: '/core/modal',
+ props: ['Modal'],
+ styles: ['Modal'],
+ description: 'An accessible overlay dialog',
+ import: "import { Modal } from '@mantine/core';",
+ source: 'mantine-core/src/components/Modal/Modal.tsx',
+ docs: 'core/modal.mdx',
+ },
+
+ Drawer: {
+ title: 'Drawer',
+ package: '@mantine/core',
+ slug: '/core/drawer',
+ props: ['Drawer'],
+ styles: ['Drawer'],
+ description: 'Display overlay area at any side of the screen',
+ import: "import { Drawer } from '@mantine/core';",
+ source: 'mantine-core/src/components/Drawer/Drawer.tsx',
+ docs: 'core/drawer.mdx',
+ },
+
+ Accordion: {
+ title: 'Accordion',
+ package: '@mantine/core',
+ slug: '/core/accordion',
+ props: ['Accordion', 'AccordionItem', 'AccordionControl'],
+ styles: ['Accordion'],
+ componentPrefix: 'Accordion',
+ description: 'Divide content into collapsible sections',
+ import: "import { Accordion } from '@mantine/core';",
+ source: 'mantine-core/src/components/Accordion/Accordion.tsx',
+ docs: 'core/accordion.mdx',
+ },
+
+ Pill: {
+ title: 'Pill',
+ package: '@mantine/core',
+ slug: '/core/pill',
+ props: ['Pill', 'PillGroup'],
+ styles: ['Pill', 'PillGroup'],
+ description: 'Removable and non-removable tags',
+ import: "import { Pill } from '@mantine/core';",
+ source: 'mantine-core/src/components/Pill/Pill.tsx',
+ docs: 'core/pill.mdx',
+ },
+
+ PillsInput: {
+ title: 'PillsInput',
+ package: '@mantine/core',
+ slug: '/core/pills-input',
+ props: ['PillsInput', 'PillsInputField'],
+ styles: ['PillsInput', 'PillsInputField'],
+ description: 'Base component for custom tags inputs and multi selects',
+ import: "import { PillsInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/PillsInput/PillsInput.tsx',
+ docs: 'core/pills-input.mdx',
+ },
+
+ Autocomplete: {
+ title: 'Autocomplete',
+ package: '@mantine/core',
+ slug: '/core/autocomplete',
+ props: ['Autocomplete'],
+ styles: ['Autocomplete'],
+ description: 'Autocomplete user input with any list of options',
+ import: "import { Autocomplete } from '@mantine/core';",
+ source: 'mantine-core/src/components/Autocomplete/Autocomplete.tsx',
+ docs: 'core/autocomplete.mdx',
+ },
+
+ TagsInput: {
+ title: 'TagsInput',
+ package: '@mantine/core',
+ slug: '/core/tags-input',
+ props: ['TagsInput'],
+ styles: ['TagsInput'],
+ description: 'Capture a list of values from user with free input and suggestions',
+ import: "import { TagsInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/TagsInput/TagsInput.tsx',
+ docs: 'core/tags-input.mdx',
+ },
+
+ Select: {
+ title: 'Select',
+ package: '@mantine/core',
+ slug: '/core/select',
+ props: ['Select'],
+ styles: ['Select'],
+ description: 'Custom searchable select',
+ import: "import { Select } from '@mantine/core';",
+ source: 'mantine-core/src/components/Select/Select.tsx',
+ docs: 'core/select.mdx',
+ },
+
+ MultiSelect: {
+ title: 'MultiSelect',
+ package: '@mantine/core',
+ slug: '/core/multi-select',
+ props: ['MultiSelect'],
+ styles: ['MultiSelect'],
+ description: 'Custom searchable multi select',
+ import: "import { MultiSelect } from '@mantine/core';",
+ source: 'mantine-core/src/components/MultiSelect/MultiSelect.tsx',
+ docs: 'core/multi-select.mdx',
+ },
+
+ Pagination: {
+ title: 'Pagination',
+ package: '@mantine/core',
+ slug: '/core/pagination',
+ props: ['Pagination'],
+ styles: ['Pagination'],
+ description: 'Display active page and navigate between multiple pages',
+ import: "import { Pagination } from '@mantine/core';",
+ source: 'mantine-core/src/components/Pagination/Pagination.tsx',
+ docs: 'core/pagination.mdx',
+ },
+
+ AppShell: {
+ title: 'AppShell',
+ package: '@mantine/core',
+ slug: '/core/app-shell',
+ props: ['AppShell', 'AppShellNavbar', 'AppShellHeader', 'AppShellAside', 'AppShellFooter'],
+ styles: ['AppShell'],
+ componentPrefix: 'AppShell',
+ description: 'Responsive shell for your application with header, navbar, aside and footer',
+ import: "import { AppShell } from '@mantine/core';",
+ source: 'mantine-core/src/components/AppShell/AppShell.tsx',
+ docs: 'core/app-shell.mdx',
+ },
+
+ Skeleton: {
+ title: 'Skeleton',
+ package: '@mantine/core',
+ slug: '/core/skeleton',
+ props: ['Skeleton'],
+ styles: ['Skeleton'],
+ description: 'Indicate content loading state',
+ import: "import { Skeleton } from '@mantine/core';",
+ source: 'mantine-core/src/components/Skeleton/Skeleton.tsx',
+ docs: 'core/skeleton.mdx',
+ },
+
+ SimpleGrid: {
+ title: 'SimpleGrid',
+ package: '@mantine/core',
+ slug: '/core/simple-grid',
+ props: ['SimpleGrid'],
+ styles: ['SimpleGrid'],
+ description: 'Responsive grid in which each item takes equal amount of space',
+ import: "import { SimpleGrid } from '@mantine/core';",
+ source: 'mantine-core/src/components/SimpleGrid/SimpleGrid.tsx',
+ docs: 'core/simple-grid.mdx',
+ },
+
+ Grid: {
+ title: 'Grid',
+ package: '@mantine/core',
+ slug: '/core/grid',
+ props: ['Grid', 'GridCol'],
+ styles: ['Grid'],
+ componentPrefix: 'Grid',
+ description: 'Responsive grid system',
+ import: "import { Grid } from '@mantine/core';",
+ source: 'mantine-core/src/components/Grid/Grid.tsx',
+ docs: 'core/grid.mdx',
+ },
+
+ HoverCard: {
+ title: 'HoverCard',
+ package: '@mantine/core',
+ slug: '/core/hover-card',
+ props: ['HoverCard', 'HoverCardTarget'],
+ styles: ['HoverCard'],
+ componentPrefix: 'HoverCard',
+ description: 'Display popover section when target element is hovered',
+ import: "import { HoverCard } from '@mantine/core';",
+ source: 'mantine-core/src/components/HoverCard/HoverCard.tsx',
+ docs: 'core/hover-card.mdx',
+ },
+
+ Menu: {
+ title: 'Menu',
+ package: '@mantine/core',
+ slug: '/core/menu',
+ props: ['Menu', 'MenuTarget', 'MenuItem'],
+ styles: ['Menu'],
+ componentPrefix: 'Menu',
+ description: 'Combine a list of secondary actions into single interactive area',
+ import: "import { Menu } from '@mantine/core';",
+ source: 'mantine-core/src/components/Menu/Menu.tsx',
+ docs: 'core/menu.mdx',
+ },
+
+ Progress: {
+ title: 'Progress',
+ package: '@mantine/core',
+ slug: '/core/progress',
+ props: ['Progress', 'ProgressRoot', 'ProgressSection'],
+ styles: ['Progress'],
+ componentPrefix: 'Progress',
+ description: 'Give user feedback for status of the task',
+ import: "import { Progress } from '@mantine/core';",
+ source: 'mantine-core/src/components/Progress/Progress.tsx',
+ docs: 'core/progress.mdx',
+ },
+
+ RingProgress: {
+ title: 'RingProgress',
+ package: '@mantine/core',
+ slug: '/core/ring-progress',
+ props: ['RingProgress'],
+ styles: ['RingProgress'],
+ description: 'Give user feedback for status of the task with circle diagram',
+ import: "import { RingProgress } from '@mantine/core';",
+ source: 'mantine-core/src/components/RingProgress/RingProgress.tsx',
+ docs: 'core/ring-progress.mdx',
+ },
+
+ Chip: {
+ title: 'Chip',
+ package: '@mantine/core',
+ slug: '/core/chip',
+ props: ['Chip', 'ChipGroup'],
+ styles: ['Chip'],
+ componentPrefix: 'Chip',
+ description: 'Pick one or multiple values with inline controls',
+ import: "import { Chip } from '@mantine/core';",
+ source: 'mantine-core/src/components/Chip/Chip.tsx',
+ docs: 'core/chip.mdx',
+ },
+
+ PinInput: {
+ title: 'PinInput',
+ package: '@mantine/core',
+ slug: '/core/pin-input',
+ props: ['PinInput'],
+ styles: ['PinInput'],
+ description: 'Capture pin code or one time password from the user',
+ import: "import { PinInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/PinInput/PinInput.tsx',
+ docs: 'core/pin-input.mdx',
+ },
+
+ Rating: {
+ title: 'Rating',
+ package: '@mantine/core',
+ slug: '/core/rating',
+ props: ['Rating'],
+ styles: ['Rating'],
+ description: 'Pick and display rating',
+ import: "import { Rating } from '@mantine/core';",
+ source: 'mantine-core/src/components/Rating/Rating.tsx',
+ docs: 'core/rating.mdx',
+ },
+
+ Space: {
+ title: 'Space',
+ package: '@mantine/core',
+ slug: '/core/space',
+ description: 'Add horizontal or vertical spacing from theme',
+ import: "import { Space } from '@mantine/core';",
+ source: 'mantine-core/src/components/Space/Space.tsx',
+ docs: 'core/space.mdx',
+ },
+
+ Indicator: {
+ title: 'Indicator',
+ package: '@mantine/core',
+ slug: '/core/indicator',
+ props: ['Indicator'],
+ styles: ['Indicator'],
+ description: 'Display element at the corner of another element',
+ import: "import { Indicator } from '@mantine/core';",
+ source: 'mantine-core/src/components/Indicator/Indicator.tsx',
+ docs: 'core/indicator.mdx',
+ },
+
+ Textarea: {
+ title: 'Textarea',
+ package: '@mantine/core',
+ slug: '/core/textarea',
+ props: ['Textarea'],
+ styles: ['Textarea'],
+ description: 'Autosize or regular textarea',
+ import: "import { Textarea } from '@mantine/core';",
+ source: 'mantine-core/src/components/Textarea/Textarea.tsx',
+ docs: 'core/textarea.mdx',
+ },
+
+ JsonInput: {
+ title: 'JsonInput',
+ package: '@mantine/core',
+ slug: '/core/json-input',
+ props: ['JsonInput'],
+ styles: ['JsonInput'],
+ description: 'Capture json data from user',
+ import: "import { JsonInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/JsonInput/JsonInput.tsx',
+ docs: 'core/json-input.mdx',
+ },
+
+ Image: {
+ title: 'Image',
+ package: '@mantine/core',
+ slug: '/core/image',
+ props: ['Image'],
+ styles: ['Image'],
+ polymorphic: true,
+ description: 'Image with optional fallback',
+ import: "import { Image } from '@mantine/core';",
+ source: 'mantine-core/src/components/Image/Image.tsx',
+ docs: 'core/image.mdx',
+ },
+
+ Card: {
+ title: 'Card',
+ package: '@mantine/core',
+ slug: '/core/card',
+ props: ['Card', 'CardSection'],
+ styles: ['Card'],
+ componentPrefix: 'Card',
+ description: 'Card with sections',
+ import: "import { Card } from '@mantine/core';",
+ source: 'mantine-core/src/components/Card/Card.tsx',
+ docs: 'core/card.mdx',
+ },
+
+ PasswordInput: {
+ title: 'PasswordInput',
+ package: '@mantine/core',
+ slug: '/core/password-input',
+ props: ['PasswordInput'],
+ styles: ['PasswordInput'],
+ description: 'Capture password data from user',
+ import: "import { PasswordInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/PasswordInput/PasswordInput.tsx',
+ docs: 'core/password-input.mdx',
+ },
+
+ FileInput: {
+ title: 'FileInput',
+ package: '@mantine/core',
+ slug: '/core/file-input',
+ props: ['FileInput'],
+ styles: ['FileInput'],
+ description: 'Capture files from user',
+ import: "import { FileInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/FileInput/FileInput.tsx',
+ docs: 'core/file-input.mdx',
+ },
+
+ Stepper: {
+ title: 'Stepper',
+ package: '@mantine/core',
+ slug: '/core/stepper',
+ props: ['Stepper', 'StepperStep'],
+ styles: ['Stepper'],
+ componentPrefix: 'Stepper',
+ description: 'Display content divided into a steps sequence',
+ import: "import { Stepper } from '@mantine/core';",
+ source: 'mantine-core/src/components/Stepper/Stepper.tsx',
+ docs: 'core/stepper.mdx',
+ },
+
+ ColorInput: {
+ title: 'ColorInput',
+ package: '@mantine/core',
+ slug: '/core/color-input',
+ props: ['ColorInput'],
+ styles: ['ColorInput'],
+ description: 'Capture color from user',
+ import: "import { ColorInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/ColorInput/ColorInput.tsx',
+ docs: 'core/color-input.mdx',
+ },
+
+ ThemeIcon: {
+ title: 'ThemeIcon',
+ package: '@mantine/core',
+ slug: '/core/theme-icon',
+ props: ['ThemeIcon'],
+ styles: ['ThemeIcon'],
+ description: 'Render icon inside element with theme colors',
+ import: "import { ThemeIcon } from '@mantine/core';",
+ source: 'mantine-core/src/components/ThemeIcon/ThemeIcon.tsx',
+ docs: 'core/theme-icon.mdx',
+ },
+
+ NumberInput: {
+ title: 'NumberInput',
+ package: '@mantine/core',
+ slug: '/core/number-input',
+ props: ['NumberInput'],
+ styles: ['NumberInput'],
+ description: 'Capture number from user',
+ import: "import { NumberInput } from '@mantine/core';",
+ source: 'mantine-core/src/components/NumberInput/NumberInput.tsx',
+ docs: 'core/number-input.mdx',
+ },
+ LoadingOverlay: {
+ title: 'LoadingOverlay',
+ package: '@mantine/core',
+ slug: '/core/loading-overlay',
+ props: ['LoadingOverlay'],
+ styles: ['LoadingOverlay'],
+ description: 'An overlay with centered loader',
+ import: "import { LoadingOverlay } from '@mantine/core';",
+ source: 'mantine-core/src/components/LoadingOverlay/LoadingOverlay.tsx',
+ docs: 'core/loading-overlay.mdx',
+ },
+
+ Radio: {
+ title: 'Radio',
+ package: '@mantine/core',
+ slug: '/core/radio',
+ componentPrefix: 'Radio',
+ props: ['Radio', 'RadioGroup'],
+ styles: ['Radio', 'RadioGroup'],
+ description: 'Wrapper for input type radio',
+ import: "import { Radio } from '@mantine/core';",
+ source: 'mantine-core/src/components/Radio/Radio.tsx',
+ docs: 'core/radio.mdx',
+ },
+
+ Timeline: {
+ title: 'Timeline',
+ package: '@mantine/core',
+ slug: '/core/timeline',
+ componentPrefix: 'Timeline',
+ props: ['Timeline', 'TimelineItem'],
+ styles: ['Timeline'],
+ description: 'Display list of events in chronological order',
+ import: "import { Timeline } from '@mantine/core';",
+ source: 'mantine-core/src/components/Timeline/Timeline.tsx',
+ docs: 'core/timeline.mdx',
+ },
+
+ Dialog: {
+ title: 'Dialog',
+ package: '@mantine/core',
+ slug: '/core/dialog',
+ props: ['Dialog'],
+ styles: ['Dialog'],
+ description: 'Display a fixed overlay dialog at any side of the screen',
+ import: "import { Dialog } from '@mantine/core';",
+ source: 'mantine-core/src/components/Dialog/Dialog.tsx',
+ docs: 'core/dialog.mdx',
+ },
+
+ Flex: {
+ title: 'Flex',
+ package: '@mantine/core',
+ slug: '/core/flex',
+ props: ['Flex'],
+ styles: ['Flex'],
+ description: 'Compose elements in a flex container',
+ import: "import { Flex } from '@mantine/core';",
+ source: 'mantine-core/src/components/Flex/Flex.tsx',
+ docs: 'core/flex.mdx',
+ },
+
+ List: {
+ title: 'List',
+ package: '@mantine/core',
+ slug: '/core/list',
+ componentPrefix: 'List',
+ props: ['List', 'ListItem'],
+ styles: ['List'],
+ description: 'Display ordered or unordered list',
+ import: "import { List } from '@mantine/core';",
+ source: 'mantine-core/src/components/List/List.tsx',
+ docs: 'core/list.mdx',
+ },
+
+ Spoiler: {
+ title: 'Spoiler',
+ package: '@mantine/core',
+ slug: '/core/spoiler',
+ props: ['Spoiler'],
+ styles: ['Spoiler'],
+ description: 'Hide long sections of content under a spoiler',
+ import: "import { Spoiler } from '@mantine/core';",
+ source: 'mantine-core/src/components/Spoiler/Spoiler.tsx',
+ docs: 'core/spoiler.mdx',
+ },
+
+ NavLink: {
+ title: 'NavLink',
+ package: '@mantine/core',
+ slug: '/core/nav-link',
+ props: ['NavLink'],
+ styles: ['NavLink'],
+ description: 'Navigation link',
+ import: "import { NavLink } from '@mantine/core';",
+ source: 'mantine-core/src/components/NavLink/NavLink.tsx',
+ docs: 'core/nav-link.mdx',
+ },
+};
diff --git a/docs/mdx/data/mdx-dates-data.ts b/docs/mdx/data/mdx-dates-data.ts
new file mode 100644
index 00000000000..0f26280ad10
--- /dev/null
+++ b/docs/mdx/data/mdx-dates-data.ts
@@ -0,0 +1,142 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_DATES_DATA: Record = {
+ GettingStartedDates: {
+ title: 'Getting started',
+ package: '@mantine/dates',
+ license: 'MIT',
+ slug: '/dates/getting-started',
+ docs: 'dates/getting-started.mdx',
+ hideInSearch: true,
+ },
+
+ Calendar: {
+ title: 'Calendar',
+ package: '@mantine/dates',
+ slug: '/dates/calendar',
+ description: 'Base component for custom date pickers',
+ props: ['Calendar'],
+ styles: ['Calendar'],
+ import: "import { Calendar } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/Calendar/Calendar.tsx',
+ docs: 'dates/calendar.mdx',
+ },
+
+ DateInput: {
+ title: 'DateInput',
+ package: '@mantine/dates',
+ slug: '/dates/date-input',
+ description: 'Free form date input',
+ props: ['DateInput'],
+ styles: ['DateInput'],
+ import: "import { DateInput } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/DateInput/DateInput.tsx',
+ docs: 'dates/date-input.mdx',
+ },
+
+ DatePicker: {
+ title: 'DatePicker',
+ package: '@mantine/dates',
+ slug: '/dates/date-picker',
+ description: 'Inline date, multiple dates and dates range picker',
+ props: ['DatePicker'],
+ styles: ['DatePicker'],
+ import: "import { DatePicker } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/DatePicker/DatePicker.tsx',
+ docs: 'dates/date-picker.mdx',
+ },
+
+ DatePickerInput: {
+ title: 'DatePickerInput',
+ package: '@mantine/dates',
+ slug: '/dates/date-picker-input',
+ description: 'Date, multiple dates and dates range picker input',
+ props: ['DatePickerInput'],
+ styles: ['DatePickerInput'],
+ import: "import { DatePickerInput } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/DatePickerInput/DatePickerInput.tsx',
+ docs: 'dates/date-picker-input.mdx',
+ },
+
+ DatesProvider: {
+ title: 'DatesProvider',
+ package: '@mantine/dates',
+ slug: '/dates/dates-provider',
+ description: 'Settings provider for @mantine/dates components',
+ import: "import { DatesProvider } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/DatesProvider/DatesProvider.tsx',
+ docs: 'dates/dates-provider.mdx',
+ },
+
+ DateTimePicker: {
+ title: 'DateTimePicker',
+ package: '@mantine/dates',
+ slug: '/dates/date-time-picker',
+ props: ['DateTimePicker'],
+ styles: ['DateTimePicker'],
+ description: 'Capture datetime from the user',
+ import: "import { DateTimePicker } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/DateTimePicker/DateTimePicker.tsx',
+ docs: 'dates/date-time-picker.mdx',
+ },
+
+ MonthPicker: {
+ title: 'MonthPicker',
+ package: '@mantine/dates',
+ slug: '/dates/month-picker',
+ props: ['MonthPicker'],
+ styles: ['MonthPicker'],
+ description: 'Inline month, multiple months and months range picker',
+ import: "import { MonthPicker } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/MonthPicker/MonthPicker.tsx',
+ docs: 'dates/month-picker.mdx',
+ },
+
+ MonthPickerInput: {
+ title: 'MonthPickerInput',
+ package: '@mantine/dates',
+ slug: '/dates/month-picker-input',
+ props: ['MonthPickerInput'],
+ styles: ['MonthPickerInput'],
+ description: 'Month, multiple months and months range picker input',
+ import: "import { MonthPickerInput } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/MonthPickerInput/MonthPickerInput.tsx',
+ docs: 'dates/month-picker-input.mdx',
+ },
+
+ TimeInput: {
+ title: 'TimeInput',
+ package: '@mantine/dates',
+ slug: '/dates/time-input',
+ props: ['TimeInput'],
+ styles: ['TimeInput'],
+ description: 'Capture time from the user',
+ import: "import { TimeInput } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/TimeInput/TimeInput.tsx',
+ docs: 'dates/time-input.mdx',
+ },
+
+ YearPicker: {
+ title: 'YearPicker',
+ package: '@mantine/dates',
+ slug: '/dates/year-picker',
+ props: ['YearPicker'],
+ styles: ['YearPicker'],
+ description: 'Inline year, multiple years and years range picker',
+ import: "import { YearPicker } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/YearPicker/YearPicker.tsx',
+ docs: 'dates/year-picker.mdx',
+ },
+
+ YearPickerInput: {
+ title: 'YearPickerInput',
+ package: '@mantine/dates',
+ slug: '/dates/year-picker-input',
+ props: ['YearPickerInput'],
+ styles: ['YearPickerInput'],
+ description: 'Inline year, multiple years and years range picker',
+ import: "import { YearPickerInput } from '@mantine/dates';",
+ source: 'mantine-dates/src/components/YearPickerInput/YearPickerInput.tsx',
+ docs: 'dates/year-picker-input.mdx',
+ },
+};
diff --git a/docs/mdx/data/mdx-form-data.ts b/docs/mdx/data/mdx-form-data.ts
new file mode 100644
index 00000000000..1ae6ee74b61
--- /dev/null
+++ b/docs/mdx/data/mdx-form-data.ts
@@ -0,0 +1,77 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_FORM_DATA: Record = {
+ useForm: {
+ title: 'use-form',
+ package: '@mantine/form',
+ slug: '/form/use-form',
+ description: 'Manage form state',
+ source: 'mantine-form/src',
+ license: 'MIT',
+ docs: 'form/use-form.mdx',
+ },
+
+ createFormContext: {
+ title: 'Form context',
+ package: '@mantine/form',
+ slug: '/form/create-form-context',
+ description: 'Add context support to use-form with createFormContext',
+ docs: 'form/use-form.mdx',
+ },
+
+ formErrors: {
+ title: 'Form errors',
+ package: '@mantine/form',
+ slug: '/form/errors',
+ description: 'Manipulate form errors with use-form hook',
+ docs: 'form/errors.mdx',
+ },
+
+ formNested: {
+ title: 'Nested fields',
+ package: '@mantine/form',
+ slug: '/form/nested',
+ description: 'Manage nested arrays and object state with use-form hook',
+ docs: 'form/nested.mdx',
+ },
+
+ formRecipes: {
+ title: 'Recipes',
+ package: '@mantine/form',
+ slug: '/form/recipes',
+ description: 'use-form examples',
+ docs: 'form/recipes.mdx',
+ },
+
+ formStatus: {
+ title: 'Touched & dirty',
+ package: '@mantine/form',
+ slug: '/form/status',
+ description: 'Get fields and form touched and dirty status',
+ docs: 'form/status.mdx',
+ },
+
+ formValidation: {
+ title: 'Form validation',
+ package: '@mantine/form',
+ slug: '/form/validation',
+ description: 'Validate fields with use-form hook',
+ docs: 'form/validation.mdx',
+ },
+
+ formValidators: {
+ title: 'Form validators',
+ package: '@mantine/form',
+ slug: '/form/validators',
+ description: 'Premade validation functions',
+ docs: 'form/validators.mdx',
+ },
+
+ formValues: {
+ title: 'Form values',
+ package: '@mantine/form',
+ slug: '/form/values',
+ description: 'Manipulate form values with use-form',
+ docs: 'form/values.mdx',
+ },
+};
diff --git a/docs/mdx/data/mdx-guides-data.ts b/docs/mdx/data/mdx-guides-data.ts
new file mode 100644
index 00000000000..9355d375760
--- /dev/null
+++ b/docs/mdx/data/mdx-guides-data.ts
@@ -0,0 +1,87 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_GUIDES_DATA: Record = {
+ Polymorphic: {
+ title: 'Polymorphic components',
+ slug: '/guides/polymorphic',
+ search: 'createPolymorphicComponent, change root element, component prop',
+ hideHeader: true,
+ },
+
+ NextJs: {
+ title: 'Usage with Next.js',
+ slug: '/guides/next',
+ search: 'Get started with Next.js',
+ hideHeader: true,
+ },
+
+ Vite: {
+ title: 'Usage with Vite',
+ slug: '/guides/vite',
+ search: 'Get started with Vite',
+ hideHeader: true,
+ },
+
+ Remix: {
+ title: 'Usage with Remix',
+ slug: '/guides/remix',
+ search: 'Get started with Remix',
+ hideHeader: true,
+ },
+
+ Gatsby: {
+ title: 'Usage with Gatsby',
+ slug: '/guides/gatsby',
+ search: 'Get started with Gatsby',
+ hideHeader: true,
+ },
+
+ Redwood: {
+ title: 'Usage with Redwood',
+ slug: '/guides/redwood',
+ search: 'Get started with Redwood',
+ hideHeader: true,
+ },
+
+ Storybook: {
+ title: 'Usage with Storybook',
+ slug: '/guides/storybook',
+ search: 'Setup Mantine with Storybook',
+ hideHeader: true,
+ },
+
+ TypeScript: {
+ title: 'Usage with TypeScript',
+ slug: '/guides/typescript',
+ search: 'Usage with TypeScript',
+ hideHeader: true,
+ },
+
+ JavaScript: {
+ title: 'Usage with JavaScript',
+ slug: '/guides/javascript',
+ search: 'Usage with JavaScript',
+ hideHeader: true,
+ },
+
+ Icons: {
+ title: 'Icons libraries',
+ slug: '/guides/icons',
+ search: 'Usage of icons libraries with Mantine',
+ hideHeader: true,
+ },
+
+ Jest: {
+ title: 'Testing with Jest',
+ slug: '/guides/jest',
+ search: 'Testing with Jest and React Testing Library',
+ hideHeader: true,
+ },
+
+ SixToSeven: {
+ title: '6.x to 7.x migration',
+ slug: '/guides/6x-to-7x',
+ search: 'Styles migration guide from 6.x to 7.x',
+ hideHeader: true,
+ },
+};
diff --git a/docs/mdx/data/mdx-hooks-data.ts b/docs/mdx/data/mdx-hooks-data.ts
new file mode 100644
index 00000000000..df3bc348ec0
--- /dev/null
+++ b/docs/mdx/data/mdx-hooks-data.ts
@@ -0,0 +1,131 @@
+import { Frontmatter } from '@/types';
+
+function hDocs(hook: string, description: string): Frontmatter {
+ const name = hook.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
+ return {
+ title: name,
+ package: '@mantine/hooks',
+ slug: `/hooks/${name}`,
+ description,
+ import: `import { ${hook} } from '@mantine/hooks';`,
+ source: `mantine-hooks/src/${name}/${name}.ts`,
+ docs: `hooks/${name}.mdx`,
+ };
+}
+
+export const MDX_HOOKS_DATA: Record = {
+ useClickOutside: hDocs(
+ 'useClickOutside',
+ 'Detects click and touch events outside of given element'
+ ),
+
+ useClipboard: hDocs('useClipboard', 'Wrapper around navigator.clipboard with feedback timeout'),
+
+ useColorScheme: hDocs(
+ 'useColorScheme',
+ 'Detects user system color scheme with window.matchMedia API'
+ ),
+
+ useCounter: hDocs('useCounter', 'Increments/decrements state within given boundaries'),
+ useDebouncedState: hDocs('useDebouncedState', 'Debounces value changes'),
+ useDebouncedValue: hDocs('useDebouncedValue', 'Debounces value changes'),
+
+ useDidUpdate: hDocs(
+ 'useDidUpdate',
+ 'Calls function in useEffect when value changes, but not when component mounts'
+ ),
+
+ useDisclosure: hDocs('useDisclosure', 'Manages boolean state and provides controls to toggle it'),
+ useDocumentTitle: hDocs('useDocumentTitle', 'Sets document.title to given string'),
+ useDocumentVisibility: hDocs('useDocumentVisibility', 'Detects if current tab is active'),
+
+ useElementSize: hDocs(
+ 'useElementSize',
+ 'Returns element width and height and subscribe to changes'
+ ),
+
+ useEventListener: hDocs('useEventListener', 'Subscribes to events with a ref'),
+ useEyeDropper: hDocs('useEyeDropper', 'Pick color from any pixel on the screen'),
+ useFavicon: hDocs('useFavicon', 'Changes favicon'),
+
+ useFocusReturn: hDocs(
+ 'useFocusReturn',
+ 'Captures last focused element on the page and returns focus to it once the condition is met'
+ ),
+
+ useFocusTrap: hDocs('useFocusTrap', 'Traps focus inside given node'),
+ useFocusWithin: hDocs('useFocusWithin', 'Detects if any element within other element has focus'),
+ useForceUpdate: hDocs('useForceUpdate', 'Force component to rerender without state change'),
+ useFullscreen: hDocs('useFullscreen', 'Enter/exit fullscreen mode'),
+ useHash: hDocs('useHash', 'Get and set hash value in url'),
+
+ useHeadroom: hDocs(
+ 'useHeadroom',
+ 'Create headers that are hidden after user scrolls past given distance'
+ ),
+
+ useHotkeys: hDocs('useHotkeys', 'Listen for keys combinations on document element'),
+ useHover: hDocs('useHover', 'Detects if element is hovered'),
+ useId: hDocs('useId', 'Generates memoized random id'),
+ useIdle: hDocs('useIdle', 'Detects if user does nothing on page'),
+ useInputState: hDocs('useInputState', 'Manages input state'),
+ useIntersection: hDocs('useIntersection', 'Detects if element is visible in viewport'),
+ useInterval: hDocs('useInterval', 'Calls function in given interval'),
+
+ useIsomorphicEffect: hDocs(
+ 'useIsomorphicEffect',
+ 'useLayoutEffect that does not show warning when used in SSR'
+ ),
+
+ useListState: hDocs('useListState', 'Manages array state'),
+
+ useLocalStorage: hDocs(
+ 'useLocalStorage',
+ 'Use localStorage value as react state, sync state across opened tabs'
+ ),
+
+ useLogger: hDocs('useLogger', 'Log given values to console when component renders'),
+ useMediaQuery: hDocs('useMediaQuery', 'Subscribes to media queries with window.matchMedia'),
+ useMergedRef: hDocs('useMergedRef', 'Merges multiple refs into one'),
+ useMouse: hDocs('useMouse', 'Tracks mouse position over the viewport or given element'),
+
+ useMove: hDocs(
+ 'useMove',
+ 'Handles move behavior over any element, use to build custom sliders, color pickers, etc.'
+ ),
+
+ useNetwork: hDocs('useNetwork', 'Returns current connection status'),
+ useOs: hDocs('useOs', 'Detects user operating system'),
+ usePageLeave: hDocs('usePageLeave', 'Calls given function when mouse leaves the page'),
+ usePrevious: hDocs('usePrevious', 'Returns previous value of given state'),
+ useQueue: hDocs('useQueue', 'Manages queue of values'),
+ useReducedMotion: hDocs('useReducedMotion', 'Detects if user prefers reduced motion'),
+ useResizeObserver: hDocs('useResizeObserver', 'Tracks element size and position changes'),
+ useScrollIntoView: hDocs('useScrollIntoView', 'Scrolls element into view'),
+ useSetState: hDocs('useSetState', 'Manages state with setState-like API'),
+
+ useShallowEffect: hDocs(
+ 'useShallowEffect',
+ 'useEffect drop in replacement with dependencies shallow comparison'
+ ),
+
+ useTextSelection: hDocs('useTextSelection', 'Returns current selected text on the page'),
+ useTimeout: hDocs('useTimeout', 'Calls function in given timeout'),
+ useToggle: hDocs('useToggle', 'Switch between given values'),
+
+ useUncontrolled: hDocs(
+ 'useUncontrolled',
+ 'Manage state of both controlled and uncontrolled components'
+ ),
+
+ useValidatedState: hDocs('useValidatedState', 'Manages state with validation'),
+ useViewportSize: hDocs('useViewportSize', 'Returns current viewport size'),
+
+ useWindowEvent: hDocs(
+ 'useWindowEvent',
+ 'Adds event listener to window on component mount and removes it on unmount'
+ ),
+
+ useWindowScroll: hDocs('useWindowScroll', 'Tracks window scroll position'),
+ usePagination: hDocs('usePagination', 'Manages pagination state'),
+};
diff --git a/docs/mdx/data/mdx-meta-data.ts b/docs/mdx/data/mdx-meta-data.ts
new file mode 100644
index 00000000000..faff7a85978
--- /dev/null
+++ b/docs/mdx/data/mdx-meta-data.ts
@@ -0,0 +1,64 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_META_DATA: Record = {
+ NotFound: {
+ title: '404',
+ slug: '/404',
+ hideHeader: true,
+ hideInSearch: true,
+ hideSiblings: true,
+ },
+
+ About: {
+ title: 'About Mantine',
+ slug: '/about',
+ search: 'Browser support, releases cycle, previous versions documentation',
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ GettingStarted: {
+ title: 'Getting started',
+ slug: '/getting-started',
+ hideInSearch: true,
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ TemplatesUsage: {
+ title: 'Getting started with a template',
+ slug: '/templates-usage',
+ hideInSearch: true,
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ Overview: {
+ title: 'Mantine API overview',
+ slug: '/overview',
+ search: 'Overview of Mantine components API',
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ Contribute: {
+ title: 'Contributing to Mantine',
+ slug: '/contribute',
+ search: 'Learn how to contribute to Mantine',
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ Changelog700: {
+ title: 'Version v7.0.0',
+ slug: '/changelog/7-0-0',
+ release: 'https://github.com/mantinedev/mantine/releases/tag/7.0.0',
+ date: 'Some time this year',
+ },
+
+ PreviousChangelogs: {
+ title: 'Previous versions',
+ slug: '/changelog/previous-versions',
+ hideHeader: true,
+ },
+};
diff --git a/docs/mdx/data/mdx-others-data.ts b/docs/mdx/data/mdx-others-data.ts
new file mode 100644
index 00000000000..589cb1390cb
--- /dev/null
+++ b/docs/mdx/data/mdx-others-data.ts
@@ -0,0 +1,103 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_OTHERS_DATA: Record = {
+ Notifications: {
+ title: 'Notifications system',
+ package: '@mantine/notifications',
+ slug: '/others/notifications',
+ props: ['Notifications'],
+ styles: ['Notifications'],
+ description: 'Mantine notifications system',
+ source: 'mantine-notifications/src',
+ license: 'MIT',
+ docs: 'others/notifications.mdx',
+ },
+
+ Spotlight: {
+ title: 'Spotlight',
+ package: '@mantine/spotlight',
+ slug: '/others/spotlight',
+ props: [
+ 'Spotlight',
+ 'SpotlightRoot',
+ 'SpotlightAction',
+ 'SpotlightActionsGroup',
+ 'SpotlightSearch',
+ ],
+ styles: ['Spotlight'],
+ description: 'Command center for your application',
+ source: 'mantine-spotlight/src',
+ license: 'MIT',
+ docs: 'others/spotlight.mdx',
+ },
+
+ Carousel: {
+ title: 'Carousel',
+ package: '@mantine/carousel',
+ slug: '/others/carousel',
+ props: ['Carousel'],
+ styles: ['Carousel'],
+ description: 'Embla based carousel component',
+ source: 'mantine-carousel/src',
+ license: 'MIT',
+ docs: 'others/carousel.mdx',
+ },
+
+ Dropzone: {
+ title: 'Dropzone',
+ package: '@mantine/dropzone',
+ slug: '/others/dropzone',
+ props: ['Dropzone'],
+ styles: ['Dropzone'],
+ description: 'Capture files from user with drag and drop',
+ source: 'mantine-dropzone/src',
+ license: 'MIT',
+ docs: 'others/dropzone.mdx',
+ },
+
+ Nprogress: {
+ title: 'NavigationProgress',
+ package: '@mantine/nprogress',
+ slug: '/others/nprogress',
+ props: ['NavigationProgress'],
+ description: 'Navigation progress bar',
+ source: 'mantine-nprogress/src',
+ license: 'MIT',
+ docs: 'others/nprogress.mdx',
+ },
+
+ CodeHighlight: {
+ title: 'CodeHighlight',
+ package: '@mantine/code-highlight',
+ slug: '/others/code-highlight',
+ props: ['CodeHighlight', 'CodeHighlightTabs', 'InlineCodeHighlight'],
+ styles: ['CodeHighlight', 'CodeHighlightTabs', 'InlineCodeHighlight'],
+ description: 'Highlight code with highlight.js',
+ source: 'mantine-code-highlight/src',
+ license: 'MIT',
+ docs: 'others/code-highlight.mdx',
+ },
+
+ Modals: {
+ title: 'Modals manager',
+ package: '@mantine/modals',
+ slug: '/others/modals',
+ props: ['ModalsProvider'],
+ description: 'Centralized modals manager with option to handle state of multi-step modals',
+ source: 'mantine-modals/src',
+ license: 'MIT',
+ docs: 'others/modals.mdx',
+ },
+
+ TipTap: {
+ title: 'Rich text editor',
+ package: '@mantine/tiptap',
+ slug: '/others/tiptap',
+ props: ['RichTextEditor'],
+ styles: ['RichTextEditor'],
+ description: 'Tiptap based rich text editor',
+ source: 'mantine-tiptap/src',
+ license: 'MIT',
+ docs: 'others/tiptap.mdx',
+ },
+};
diff --git a/docs/mdx/data/mdx-styles-data.ts b/docs/mdx/data/mdx-styles-data.ts
new file mode 100644
index 00000000000..05ab041c78a
--- /dev/null
+++ b/docs/mdx/data/mdx-styles-data.ts
@@ -0,0 +1,122 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_STYLES_DATA: Record = {
+ CSSModules: {
+ title: 'CSS modules',
+ slug: '/styles/css-modules',
+ search: 'Get started with CSS modules',
+ hideHeader: true,
+ },
+
+ VanillaExtract: {
+ title: 'Vanilla extract',
+ slug: '/styles/vanilla-extract',
+ search: 'Mantine + Vanilla extract integration',
+ hideHeader: true,
+ },
+
+ PostCSSPreset: {
+ title: 'PostCSS preset',
+ slug: '/styles/postcss-preset',
+ search: 'postcss-preset-mantine, mixins, CSS functions',
+ hideHeader: true,
+ },
+
+ GlobalStyles: {
+ title: 'Global styles',
+ slug: '/styles/global-styles',
+ search: 'Global styles that are required for Mantine components to work properly',
+ hideHeader: true,
+ },
+
+ CssVariables: {
+ title: 'CSS variables',
+ slug: '/styles/css-variables',
+ search: 'Mantine CSS variables and resolvers',
+ hideHeader: true,
+ },
+
+ CSSVariablesList: {
+ title: 'CSS variables list',
+ slug: '/styles/css-variables-list',
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ CSSFilesList: {
+ title: 'CSS files list',
+ slug: '/styles/css-files-list',
+ hideSiblings: true,
+ hideHeader: true,
+ },
+
+ Rem: {
+ title: 'rem, em and px units',
+ slug: '/styles/rem',
+ search: 'rem and em units with Mantine components, units conversion',
+ hideHeader: true,
+ },
+
+ StyleProp: {
+ title: 'style prop',
+ slug: '/styles/style',
+ search: 'style prop usage',
+ hideHeader: true,
+ },
+
+ ResponsiveStyles: {
+ title: 'Responsive styles',
+ slug: '/styles/responsive',
+ search: 'Media queries, breakpoints, inline responsive styles',
+ hideHeader: true,
+ },
+
+ StylesApi: {
+ title: 'Styles API',
+ slug: '/styles/styles-api',
+ search: 'classNames, styles and unstyled props',
+ hideHeader: true,
+ },
+
+ DataAttributes: {
+ title: 'data-* attributes',
+ slug: '/styles/data-attributes',
+ search: 'Usage of data-* attributes to apply styles',
+ hideHeader: true,
+ },
+
+ VariantsAndSizes: {
+ title: 'Variants and sizes',
+ slug: '/styles/variants-sizes',
+ search: 'Customize components sizes and variants',
+ hideHeader: true,
+ },
+
+ StyleProps: {
+ title: 'Style props',
+ slug: '/styles/style-props',
+ search: 'Props to add inline styles',
+ hideHeader: true,
+ },
+
+ Rtl: {
+ title: 'RTL (right-to-left)',
+ slug: '/styles/rtl',
+ search: 'Change text direction to right-to-left',
+ hideHeader: true,
+ },
+
+ StylesPerformance: {
+ title: 'Styles performance',
+ slug: '/styles/styles-performance',
+ search: 'Learn how to improve your styles performance',
+ hideHeader: true,
+ },
+
+ UnstyledComponents: {
+ title: 'Unstyled / headless',
+ slug: '/styles/unstyled',
+ search: 'Headless/unstyled components',
+ hideHeader: true,
+ },
+};
diff --git a/docs/mdx/data/mdx-theming-data.ts b/docs/mdx/data/mdx-theming-data.ts
new file mode 100644
index 00000000000..7ccc15bc9fd
--- /dev/null
+++ b/docs/mdx/data/mdx-theming-data.ts
@@ -0,0 +1,58 @@
+import { Frontmatter } from '@/types';
+
+export const MDX_THEMING_DATA: Record = {
+ MantineProvider: {
+ title: 'MantineProvider',
+ slug: '/theming/mantine-provider',
+ search: 'Theme context, CSS reset, CSS variables, context classes and styles',
+ hideHeader: true,
+ },
+
+ ThemeObject: {
+ title: 'Theme object',
+ slug: '/theming/theme-object',
+ search: 'use-mantine-theme, theme override, MantineTheme',
+ hideHeader: true,
+ },
+
+ ColorSchemes: {
+ title: 'Color schemes',
+ slug: '/theming/color-schemes',
+ search: 'use-mantine-color-scheme, color schemes management, data-mantine-color-scheme',
+ hideHeader: true,
+ },
+
+ Colors: {
+ title: 'Colors',
+ slug: '/theming/colors',
+ search: 'Default colors, primaryColor, primaryShade',
+ hideHeader: true,
+ },
+
+ ColorFunctions: {
+ title: 'Color functions',
+ slug: '/styles/color-functions',
+ search: 'Functions to manipulate colors, darken, lighten, parse',
+ hideHeader: true,
+ },
+
+ Typography: {
+ title: 'Typography',
+ slug: '/theming/typography',
+ search: 'Change fonts, @font-face',
+ hideHeader: true,
+ },
+
+ DefaultProps: {
+ title: 'Default props',
+ slug: '/theming/default-props',
+ search: 'Default props for components',
+ hideHeader: true,
+ },
+
+ DefaultTheme: {
+ title: 'Default theme',
+ slug: '/theming/default-theme',
+ hideHeader: true,
+ },
+};
diff --git a/docs/mdx/index.ts b/docs/mdx/index.ts
new file mode 100644
index 00000000000..ce6fbaf6969
--- /dev/null
+++ b/docs/mdx/index.ts
@@ -0,0 +1,2 @@
+export { MDX_PAGES_GROUPS, ALL_MDX_PAGES } from './mdx-pages-group';
+export { MDX_DATA } from './mdx-data';
diff --git a/docs/mdx/mdx-data.ts b/docs/mdx/mdx-data.ts
new file mode 100644
index 00000000000..ce5c6d1f517
--- /dev/null
+++ b/docs/mdx/mdx-data.ts
@@ -0,0 +1,22 @@
+import { Frontmatter } from '@/types';
+import { MDX_META_DATA } from './data/mdx-meta-data';
+import { MDX_THEMING_DATA } from './data/mdx-theming-data';
+import { MDX_STYLES_DATA } from './data/mdx-styles-data';
+import { MDX_GUIDES_DATA } from './data/mdx-guides-data';
+import { MDX_CORE_DATA } from './data/mdx-core-data';
+import { MDX_OTHERS_DATA } from './data/mdx-others-data';
+import { MDX_HOOKS_DATA } from './data/mdx-hooks-data';
+import { MDX_FORM_DATA } from './data/mdx-form-data';
+import { MDX_DATES_DATA } from './data/mdx-dates-data';
+
+export const MDX_DATA: Record = {
+ ...MDX_META_DATA,
+ ...MDX_THEMING_DATA,
+ ...MDX_STYLES_DATA,
+ ...MDX_GUIDES_DATA,
+ ...MDX_CORE_DATA,
+ ...MDX_HOOKS_DATA,
+ ...MDX_OTHERS_DATA,
+ ...MDX_FORM_DATA,
+ ...MDX_DATES_DATA,
+};
diff --git a/docs/mdx/mdx-pages-group.ts b/docs/mdx/mdx-pages-group.ts
new file mode 100644
index 00000000000..de837f72834
--- /dev/null
+++ b/docs/mdx/mdx-pages-group.ts
@@ -0,0 +1,349 @@
+import { Frontmatter, MdxPagesCategory, MdxPagesGroup } from '@/types';
+import { MDX_DATA } from './mdx-data';
+
+export const MDX_PAGES_GROUPS: MdxPagesGroup[] = [
+ {
+ group: 'theming',
+ pages: [
+ MDX_DATA.MantineProvider,
+ MDX_DATA.ThemeObject,
+ MDX_DATA.Colors,
+ MDX_DATA.ColorSchemes,
+ MDX_DATA.Typography,
+ MDX_DATA.DefaultProps,
+ ],
+ },
+ {
+ group: 'styles',
+ pages: [
+ MDX_DATA.CSSModules,
+ MDX_DATA.PostCSSPreset,
+ MDX_DATA.VanillaExtract,
+ MDX_DATA.GlobalStyles,
+ MDX_DATA.CssVariables,
+ MDX_DATA.Rem,
+ MDX_DATA.StyleProp,
+ MDX_DATA.ResponsiveStyles,
+ MDX_DATA.StylesApi,
+ MDX_DATA.DataAttributes,
+ MDX_DATA.VariantsAndSizes,
+ MDX_DATA.UnstyledComponents,
+ MDX_DATA.StyleProps,
+ MDX_DATA.ColorFunctions,
+ MDX_DATA.Rtl,
+ MDX_DATA.StylesPerformance,
+ ],
+ },
+ {
+ group: 'guides',
+ pages: [
+ MDX_DATA.Polymorphic,
+ MDX_DATA.Icons,
+ MDX_DATA.NextJs,
+ MDX_DATA.Vite,
+ MDX_DATA.Remix,
+ MDX_DATA.Gatsby,
+ MDX_DATA.Redwood,
+ MDX_DATA.Storybook,
+ MDX_DATA.TypeScript,
+ MDX_DATA.JavaScript,
+ MDX_DATA.Jest,
+ MDX_DATA.SixToSeven,
+ ],
+ },
+ {
+ group: 'mantine-hooks',
+ pages: [
+ {
+ category: 'UI and Dom',
+ pages: [
+ MDX_DATA.useClickOutside,
+ MDX_DATA.useColorScheme,
+ MDX_DATA.useElementSize,
+ MDX_DATA.useEventListener,
+ MDX_DATA.useFocusReturn,
+ MDX_DATA.useFocusTrap,
+ MDX_DATA.useFocusWithin,
+ MDX_DATA.useFullscreen,
+ MDX_DATA.useHotkeys,
+ MDX_DATA.useHover,
+ MDX_DATA.useIntersection,
+ MDX_DATA.useMediaQuery,
+ MDX_DATA.useMouse,
+ MDX_DATA.useMove,
+ MDX_DATA.useReducedMotion,
+ MDX_DATA.useResizeObserver,
+ MDX_DATA.useScrollIntoView,
+ MDX_DATA.useViewportSize,
+ MDX_DATA.useWindowEvent,
+ MDX_DATA.useWindowScroll,
+ ],
+ },
+
+ {
+ category: 'State management',
+ pages: [
+ MDX_DATA.useCounter,
+ MDX_DATA.useDebouncedState,
+ MDX_DATA.useDebouncedValue,
+ MDX_DATA.useDisclosure,
+ MDX_DATA.useId,
+ MDX_DATA.useInputState,
+ MDX_DATA.useListState,
+ MDX_DATA.useLocalStorage,
+ MDX_DATA.usePrevious,
+ MDX_DATA.useQueue,
+ MDX_DATA.useSetState,
+ MDX_DATA.useToggle,
+ MDX_DATA.useUncontrolled,
+ MDX_DATA.useValidatedState,
+ MDX_DATA.usePagination,
+ ],
+ },
+
+ {
+ category: 'Utilities',
+ pages: [
+ MDX_DATA.useClipboard,
+ MDX_DATA.useDocumentTitle,
+ MDX_DATA.useDocumentVisibility,
+ MDX_DATA.useEyeDropper,
+ MDX_DATA.useFavicon,
+ MDX_DATA.useHash,
+ MDX_DATA.useHeadroom,
+ MDX_DATA.useIdle,
+ MDX_DATA.useInterval,
+ MDX_DATA.useMergedRef,
+ MDX_DATA.useNetwork,
+ MDX_DATA.useOs,
+ MDX_DATA.usePageLeave,
+ MDX_DATA.useTextSelection,
+ MDX_DATA.useTimeout,
+ ],
+ },
+
+ {
+ category: 'Lifecycle',
+ pages: [
+ MDX_DATA.useDidUpdate,
+ MDX_DATA.useForceUpdate,
+ MDX_DATA.useIsomorphicEffect,
+ MDX_DATA.useLogger,
+ MDX_DATA.useShallowEffect,
+ ],
+ },
+ ],
+ },
+ {
+ group: 'mantine-form',
+ pages: [
+ MDX_DATA.useForm,
+ MDX_DATA.formValues,
+ MDX_DATA.formErrors,
+ MDX_DATA.formValidation,
+ MDX_DATA.formValidators,
+ MDX_DATA.formNested,
+ MDX_DATA.formStatus,
+ MDX_DATA.createFormContext,
+ MDX_DATA.formRecipes,
+ ],
+ },
+ {
+ group: 'mantine-core',
+ pages: [
+ {
+ category: 'layout',
+ pages: [
+ MDX_DATA.Container,
+ MDX_DATA.Center,
+ MDX_DATA.Group,
+ MDX_DATA.Stack,
+ MDX_DATA.AspectRatio,
+ MDX_DATA.AppShell,
+ MDX_DATA.SimpleGrid,
+ MDX_DATA.Grid,
+ MDX_DATA.Space,
+ MDX_DATA.Flex,
+ ],
+ },
+ {
+ category: 'inputs',
+ pages: [
+ MDX_DATA.Input,
+ MDX_DATA.ColorPicker,
+ MDX_DATA.NativeSelect,
+ MDX_DATA.SegmentedControl,
+ MDX_DATA.Slider,
+ MDX_DATA.Switch,
+ MDX_DATA.TextInput,
+ MDX_DATA.Checkbox,
+ MDX_DATA.Radio,
+ MDX_DATA.Fieldset,
+ MDX_DATA.PinInput,
+ MDX_DATA.Chip,
+ MDX_DATA.Rating,
+ MDX_DATA.Textarea,
+ MDX_DATA.JsonInput,
+ MDX_DATA.PasswordInput,
+ MDX_DATA.FileInput,
+ MDX_DATA.ColorInput,
+ MDX_DATA.NumberInput,
+ ],
+ },
+ {
+ category: 'combobox',
+ pages: [
+ MDX_DATA.Combobox,
+ MDX_DATA.Pill,
+ MDX_DATA.PillsInput,
+ MDX_DATA.Autocomplete,
+ MDX_DATA.TagsInput,
+ MDX_DATA.Select,
+ MDX_DATA.MultiSelect,
+ ],
+ },
+ {
+ category: 'buttons',
+ pages: [
+ MDX_DATA.ActionIcon,
+ MDX_DATA.Button,
+ MDX_DATA.CloseButton,
+ MDX_DATA.CopyButton,
+ MDX_DATA.FileButton,
+ MDX_DATA.UnstyledButton,
+ ],
+ },
+ {
+ category: 'navigation',
+ pages: [
+ MDX_DATA.Anchor,
+ MDX_DATA.Tabs,
+ MDX_DATA.Breadcrumbs,
+ MDX_DATA.Burger,
+ MDX_DATA.Pagination,
+ MDX_DATA.Stepper,
+ MDX_DATA.NavLink,
+ ],
+ },
+ {
+ category: 'feedback',
+ pages: [
+ MDX_DATA.Loader,
+ MDX_DATA.Alert,
+ MDX_DATA.Notification,
+ MDX_DATA.Skeleton,
+ MDX_DATA.Progress,
+ MDX_DATA.RingProgress,
+ ],
+ },
+ {
+ category: 'overlays',
+ pages: [
+ MDX_DATA.Popover,
+ MDX_DATA.Tooltip,
+ MDX_DATA.Overlay,
+ MDX_DATA.Affix,
+ MDX_DATA.Modal,
+ MDX_DATA.Drawer,
+ MDX_DATA.HoverCard,
+ MDX_DATA.Menu,
+ MDX_DATA.LoadingOverlay,
+ MDX_DATA.Dialog,
+ ],
+ },
+ {
+ category: 'Data display',
+ pages: [
+ MDX_DATA.BackgroundImage,
+ MDX_DATA.ColorSwatch,
+ MDX_DATA.Kbd,
+ MDX_DATA.Badge,
+ MDX_DATA.Avatar,
+ MDX_DATA.Accordion,
+ MDX_DATA.Indicator,
+ MDX_DATA.Card,
+ MDX_DATA.Image,
+ MDX_DATA.ThemeIcon,
+ MDX_DATA.Timeline,
+ MDX_DATA.Spoiler,
+ ],
+ },
+ {
+ category: 'typography',
+ pages: [
+ MDX_DATA.Blockquote,
+ MDX_DATA.Code,
+ MDX_DATA.Highlight,
+ MDX_DATA.Mark,
+ MDX_DATA.Table,
+ MDX_DATA.Text,
+ MDX_DATA.Title,
+ MDX_DATA.TypographyStylesProvider,
+ MDX_DATA.List,
+ ],
+ },
+ {
+ category: 'misc',
+ pages: [
+ MDX_DATA.Collapse,
+ MDX_DATA.FocusTrap,
+ MDX_DATA.Paper,
+ MDX_DATA.Portal,
+ MDX_DATA.ScrollArea,
+ MDX_DATA.Transition,
+ MDX_DATA.VisuallyHidden,
+ MDX_DATA.Divider,
+ MDX_DATA.Box,
+ ],
+ },
+ ],
+ },
+ {
+ group: 'mantine-dates',
+ pages: [
+ MDX_DATA.GettingStartedDates,
+ MDX_DATA.DatesProvider,
+ MDX_DATA.Calendar,
+ MDX_DATA.DateInput,
+ MDX_DATA.DateTimePicker,
+ MDX_DATA.DatePicker,
+ MDX_DATA.DatePickerInput,
+ MDX_DATA.MonthPicker,
+ MDX_DATA.MonthPickerInput,
+ MDX_DATA.YearPicker,
+ MDX_DATA.YearPickerInput,
+ MDX_DATA.TimeInput,
+ ],
+ },
+ {
+ group: 'other packages',
+ pages: [
+ MDX_DATA.CodeHighlight,
+ MDX_DATA.Notifications,
+ MDX_DATA.Spotlight,
+ MDX_DATA.Carousel,
+ MDX_DATA.Dropzone,
+ MDX_DATA.Nprogress,
+ MDX_DATA.Modals,
+ MDX_DATA.TipTap,
+ ],
+ },
+ {
+ group: 'changelog',
+ pages: [MDX_DATA.Changelog700, MDX_DATA.PreviousChangelogs],
+ },
+];
+
+export const ALL_MDX_PAGES: Frontmatter[] = MDX_PAGES_GROUPS.reduce((acc, group) => {
+ group.pages.forEach((item) => {
+ if (item.category) {
+ const categoryPages = [...(item as MdxPagesCategory).pages];
+ categoryPages.sort((a, b) => a.title.localeCompare(b.title));
+ acc.push(...categoryPages);
+ } else {
+ acc.push(item as Frontmatter);
+ }
+ });
+
+ return acc;
+}, []);
diff --git a/docs/next-sitemap.config.js b/docs/next-sitemap.config.js
new file mode 100644
index 00000000000..3c74a0b8845
--- /dev/null
+++ b/docs/next-sitemap.config.js
@@ -0,0 +1,7 @@
+/** @type {import('next-sitemap').IConfig} */
+module.exports = {
+ siteUrl: process.env.SITE_URL || 'https://mantine.dev',
+ generateRobotsTxt: true,
+ output: 'export',
+ generateIndexSitemap: false,
+};
diff --git a/docs/next.config.js b/docs/next.config.js
new file mode 100644
index 00000000000..4936b8cc3d4
--- /dev/null
+++ b/docs/next.config.js
@@ -0,0 +1,36 @@
+const remarkSlug = require('remark-slug');
+
+const withMDX = require('@next/mdx')({
+ extension: /\.mdx?$/,
+ options: {
+ providerImportSource: '@mdx-js/react',
+ remarkPlugins: [remarkSlug],
+ },
+});
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ pageExtensions: ['tsx', 'mdx'],
+ reactStrictMode: true,
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ webpack: (config, { isServer }) => {
+ if (!isServer) {
+ config.resolve = {
+ ...config.resolve,
+ fallback: {
+ ...config.resolve.fallback,
+ child_process: false,
+ fs: false,
+ 'builtin-modules': false,
+ worker_threads: false,
+ },
+ };
+ }
+
+ return config;
+ },
+};
+
+module.exports = withMDX(nextConfig);
diff --git a/docs/package.json b/docs/package.json
index 5c4edd70c07..1ec272cc421 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,44 +1,73 @@
{
- "name": "mantine-docs",
+ "name": "docs",
+ "version": "0.1.0",
"private": true,
- "description": "Mantine docs",
- "version": "1.0.0",
- "author": "Vitaly Rtishchev ",
- "resolutions": {
- "@types/react": "18.0.9"
+ "scripts": {
+ "dev": "next dev -p 7545",
+ "clean": "rm -rf .next",
+ "build": "next build && next export && cp CNAME out/CNAME && touch out/.nojekyll",
+ "postbuild": "next-sitemap",
+ "start": "next start",
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
- "@mdx-js/mdx": "^1.6.22",
- "@mdx-js/react": "^1.6.22",
- "@types/mdx-js__react": "^1.5.5",
- "@types/react": "18.0.9",
- "@types/react-dom": "18.0.4",
- "gatsby": "^4.19.0",
- "gatsby-plugin-cname": "^1.0.0",
- "gatsby-plugin-layout": "^3.19.0",
- "gatsby-plugin-manifest": "^4.19.0",
- "gatsby-plugin-mdx": "^3.19.0",
- "gatsby-plugin-nprogress": "^4.19.0",
- "gatsby-plugin-tsconfig-paths": "^1.0.5",
- "gatsby-source-filesystem": "^4.19.0",
- "github-slugger": "^1.4.0",
- "lodash.debounce": "^4.0.8",
- "prism-react-renderer": "^1.2.1",
- "remark-slug": "^6.0.0"
+ "@floating-ui/react": "^0.24.8",
+ "@hello-pangea/dnd": "^16.3.0",
+ "@mantine/carousel": "link:../src/mantine-carousel",
+ "@mantine/code-highlight": "link:../src/mantine-code-highlight",
+ "@mantine/colors-generator": "link:../src/mantine-colors-generator",
+ "@mantine/core": "link:../src/mantine-core",
+ "@mantine/dates": "link:../src/mantine-dates",
+ "@mantine/demos": "link:../src/mantine-demos",
+ "@mantine/dropzone": "link:../src/mantine-dropzone",
+ "@mantine/ds": "link:../src/mantine-ds",
+ "@mantine/form": "link:../src/mantine-form",
+ "@mantine/hooks": "link:../src/mantine-hooks",
+ "@mantine/modals": "link:../src/mantine-modals",
+ "@mantine/notifications": "link:../src/mantine-notifications",
+ "@mantine/nprogress": "link:../src/mantine-nprogress",
+ "@mantine/spotlight": "link:../src/mantine-spotlight",
+ "@mantine/store": "link:../src/mantine-store",
+ "@mantine/styles-api": "link:../src/mantine-styles-api",
+ "@mantine/tiptap": "link:../src/mantine-tiptap",
+ "@mdx-js/loader": "^2.3.0",
+ "@mdx-js/react": "^2.3.0",
+ "@next/mdx": "^13.2.1",
+ "@tabler/icons-react": "^2.29.0",
+ "@tiptap/core": "^2.0.4",
+ "@tiptap/extension-code-block": "^2.0.4",
+ "@tiptap/extension-code-block-lowlight": "^2.0.4",
+ "@tiptap/extension-color": "^2.0.4",
+ "@tiptap/extension-highlight": "^2.0.4",
+ "@tiptap/extension-link": "^2.0.4",
+ "@tiptap/extension-placeholder": "^2.0.4",
+ "@tiptap/extension-subscript": "^2.0.4",
+ "@tiptap/extension-superscript": "^2.0.4",
+ "@tiptap/extension-text-align": "^2.0.4",
+ "@tiptap/extension-text-style": "^2.0.4",
+ "@tiptap/extension-underline": "^2.0.4",
+ "@tiptap/react": "^2.0.4",
+ "@tiptap/starter-kit": "^2.0.4",
+ "@types/node": "^18.14.2",
+ "@types/react": "18.2.17",
+ "chroma-js": "^2.4.2",
+ "dayjs": "^1.10.5",
+ "embla-carousel-autoplay": "^7.0.0",
+ "embla-carousel-react": "^7.0.0",
+ "github-slugger": "^2.0.0",
+ "highlight.js": "^11.7.0",
+ "lowlight": "^2.7.0",
+ "next": "13.2.1",
+ "next-sitemap": "^4.2.3",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-dropzone": "14.2.3",
+ "react-number-format": "^5.2.2",
+ "remark-slug": "^6.0.0",
+ "typescript": "5.1.6"
},
"devDependencies": {
- "@babel/core": "^7.14.3",
- "@types/github-slugger": "^1.3.0",
- "lmdb-store": "^1.6.8",
- "typescript": "4.9.3"
- },
- "license": "MIT",
- "scripts": {
- "build": "gatsby build",
- "start": "gatsby develop -H 0.0.0.0 -p 7521",
- "serve": "gatsby serve -H 0.0.0.0 -p 9521",
- "clean": "gatsby clean",
- "typecheck": "tsc --noEmit",
- "lint": "eslint src --ext .ts,.tsx"
+ "@types/chroma-js": "^2.4.0",
+ "@types/github-slugger": "^2.0.0"
}
}
diff --git a/docs/pages/404.mdx b/docs/pages/404.mdx
new file mode 100644
index 00000000000..ed4498dc4ce
--- /dev/null
+++ b/docs/pages/404.mdx
@@ -0,0 +1,10 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+
+export default Layout(MDX_DATA.NotFound);
+
+# 404
+
+Nothing found. If some of the Mantine documentation pages are lining to
+this page, please let us know by opening an [issue](https://github.com/mantinedev/mantine/issues/new/choose).
+Otherwise, get back to the [home page](/).
diff --git a/docs/pages/_app.tsx b/docs/pages/_app.tsx
new file mode 100644
index 00000000000..6aafaf3fece
--- /dev/null
+++ b/docs/pages/_app.tsx
@@ -0,0 +1,75 @@
+import '@mantine/core/styles.css';
+import '@mantine/dates/styles.css';
+import '@mantine/notifications/styles.css';
+import '@mantine/code-highlight/styles.css';
+import '@mantine/dropzone/styles.css';
+import '@mantine/nprogress/styles.css';
+import '@mantine/spotlight/styles.css';
+import '@mantine/carousel/styles.css';
+import '@mantine/tiptap/styles.css';
+import '@mantine/ds/styles.css';
+import '@mantine/demos/styles.css';
+
+import React from 'react';
+import { AppProps } from 'next/app';
+import Head from 'next/head';
+import { MantineProvider, DirectionProvider } from '@mantine/core';
+import { Notifications } from '@mantine/notifications';
+import { MdxProvider } from '@/components/MdxProvider';
+import { HotKeysHandler } from '@/components/HotKeysHandler';
+import { Search } from '@/components/Search';
+import { FontsStyle } from '@/fonts';
+import { Shell } from '@/components/Shell';
+import { GaScript } from '@/components/GaScript';
+import { theme } from '../theme';
+import '../styles/variables.css';
+import '../styles/global.css';
+import { ModalsProviderDemo } from '@/components/ModalsProviderDemo';
+
+const excludeShell = ['/', '/combobox', '/app-shell'];
+
+export default function App({ Component, pageProps, router }: AppProps) {
+ const shouldRenderShell = !excludeShell.includes(router.pathname);
+
+ return (
+ <>
+
+ Mantine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {shouldRenderShell ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/docs/pages/_document.tsx b/docs/pages/_document.tsx
new file mode 100644
index 00000000000..3af9f46a86a
--- /dev/null
+++ b/docs/pages/_document.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Html, Head, Main, NextScript } from 'next/document';
+import { ColorSchemeScript } from '@mantine/core';
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/pages/about.mdx b/docs/pages/about.mdx
new file mode 100644
index 00000000000..8aa98adcae5
--- /dev/null
+++ b/docs/pages/about.mdx
@@ -0,0 +1,70 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { GuidesDemos } from '@mantine/demos';
+import { LogoAssets } from '@/components/LogoAssets';
+
+export default Layout(MDX_DATA.About);
+
+# About Mantine
+
+Mantine is a React components library focused on providing great user and developer experience.
+Mantine development was started in January 2021 and the 1.0 version was released on [May 3rd, 2021](https://github.com/mantinedev/mantine/releases/tag/1.0.0),
+and since then, [more than 150 releases have been published](https://github.com/mantinedev/mantine/releases).
+
+## Browser support
+
+Mantine is tested on real devices in the following browsers before each minor and major release: Chrome, Safari, Firefox,
+Edge, Safari for iOS, Chrome for Android. Testing in other browsers is not performed.
+
+All Mantine components and hooks were tested to work in the following browsers:
+
+- Chromium browsers 108+ – Chrome, Edge, Chrome for Android, etc.
+- Firefox 101+
+- Safari 15.4+
+- IE (any version) is not supported
+
+In most cases, Mantine components and hooks will work in any browser that is supported
+by your React version that you are using. But you may experience some minor visual bugs
+caused by unsupported CSS properties/selectors (for example, [flexbox gap](https://caniuse.com/flexbox-gap), [dvh units](https://caniuse.com/mdn-css_types_length_viewport_percentage_units_dynamic) and [has pseudo-class](https://caniuse.com/css-has)).
+
+If you need to support older browsers, you should:
+
+- check the component `Browser support` section before usage and decide whether this component will work for your case
+- install corresponding polyfills that are required for hook/component to work in older browsers
+- check that component works in those browsers on your side (we do not test Mantine in browsers that are older than those that are listed above)
+
+## Releases cycle
+
+All `@mantine/*` packages follow [semver](https://semver.org/):
+
+- Patches (1.0.X) are usually released every one or two weeks
+- Minor versions (1.X.0) are usually released on the first Tuesday of each month or once every two months
+- Major versions (X.0.0) are released when breaking changes are required, usually a new major version is released once every 6 months/1 year
+
+## Previous versions documentation
+
+- v1 (1.3.1) – [v1.mantine.dev](https://v1.mantine.dev/)
+- v2 (2.5.1) – [v2.mantine.dev](https://v2.mantine.dev/)
+- v3 (3.6.14) – [v3.mantine.dev](https://v3.mantine.dev/)
+- v4 (4.2.12) – [v4.mantine.dev](https://v4.mantine.dev/)
+- v5 (5.10.5) – [v5.mantine.dev](https://v5.mantine.dev/)
+- v6 (6.0.21) – [v6.mantine.dev](https://v6.mantine.dev/)
+
+## Project maintenance
+
+Mantine is built and maintained by [Vitaly Rtishchev](https://github.com/rtivital) and [more than 300 other contributors](https://github.com/mantinedev/mantine/graphs/contributors).
+Most of new features and components/hooks are added to the library based on feedback from the community –
+you can participate in new features discussions on [GitHub](https://github.com/mantinedev/mantine/discussions) or [Discord](https://discord.gg/wbH82zuWMN).
+
+## Mantine logo
+
+Download Mantine logos in `.svg` format:
+
+
+
+You can also install `@mantine/ds` package and import `MantineLogo` component.
+Note that the package depends on `@mantine/core` and `@mantine/hooks` packages.
+
+
+
+
diff --git a/docs/pages/app-shell.tsx b/docs/pages/app-shell.tsx
new file mode 100644
index 00000000000..3451b396511
--- /dev/null
+++ b/docs/pages/app-shell.tsx
@@ -0,0 +1,3 @@
+import { AppShellPage } from '@/app-shell-examples';
+
+export default AppShellPage;
diff --git a/docs/pages/changelog/7-0-0.mdx b/docs/pages/changelog/7-0-0.mdx
new file mode 100644
index 00000000000..6bf107bd467
--- /dev/null
+++ b/docs/pages/changelog/7-0-0.mdx
@@ -0,0 +1,555 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { Button } from '@mantine/core';
+import {
+ ThemingDemos,
+ LoaderDemos,
+ TableDemos,
+ GroupDemos,
+ StylesDemos,
+ FieldsetDemos,
+ ComboboxDemos,
+ TagsInputDemos,
+ InputDemos,
+ SimpleGridDemos,
+ GridDemos,
+ ProgressDemos,
+ SpotlightDemos,
+ ImageDemos,
+ CarouselDemos,
+ NumberInputDemos,
+ CodeHighlightDemos,
+} from '@mantine/demos';
+
+export default Layout(MDX_DATA.Changelog700);
+
+## Migration to native CSS
+
+Mantine no longer depends on [Emotion](https://emotion.sh/) for styles generation. All `@mantine/*`
+packages are now shipped with native CSS files which can be imported from `@mantine/{package}/styles.css`,
+for example:
+
+```tsx
+import '@mantine/core/styles.css';
+```
+
+This change improves performance, reduces bundle size of the library and allows using Mantine
+in environments where CSS-in-JS is not supported (or supported with limitations), for example,
+Next.js app directory.
+
+Important breaking changes:
+
+- `createStyles` function is no longer available, use [CSS modules](/styles/css-modules) or any other styling solution of your choice instead
+- Components no longer support `sx` prop. You can use `className` or `style` props instead.
+- `styles` prop no longer supports nested selectors
+
+It is now recommended to use [CSS modules](/styles/css-modules) to style Mantine components.
+To update your project to [CSS modules](/styles/css-modules), follow the [6.x → 7.x migration guide](/guides/6x-to-7x).
+
+## Vanilla extract integration
+
+If you prefer CSS-in-JS syntax for styling, you can use [Vanilla extract](/styles/vanilla-extract)
+as a TypeScript CSS preprocessor. You will be able to use most of Mantine styling features
+with [Vanilla extract](/styles/vanilla-extract).
+
+## System color scheme support
+
+All components now support system color scheme – when `colorScheme` value is `auto`,
+components will use `prefers-color-scheme` media query to determine if the user prefers light or dark color scheme.
+
+Note that `auto` is not the default value. You need to set it manually to enable system color scheme support
+both on [MantineProvider](/theming/mantine-provider) and in [ColorSchemeScript](/theming/color-schemes#colorschemescript):
+
+```tsx
+import { MantineProvider, ColorSchemeScript } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+```
+
+## Built-in color scheme manager
+
+[MantineProvider](/theming/mantine-provider) now comes with a built-in color scheme manager.
+It is no longer required to use any other components to set up color scheme logic.
+Color scheme can be changed with [useMantineColorScheme hook](/theming/color-schemes#use-mantine-color-scheme-hook):
+
+
+
+## CSS modules and PostCSS preset
+
+[CSS modules](/styles/css-modules) is now the recommended way to style Mantine components,
+although it is not required – you can choose any other styling solution of your choice.
+
+It is also recommended to use [postcss-preset-mantine](/styles/postcss-preset). It includes
+mixins and functions to simplify styling of Mantine components. [postcss-preset-mantine](/styles/postcss-preset)
+is included in all templates.
+
+## Global styles
+
+Mantine no longer includes normalize.css. Instead, it uses a bare minimum set of [global styles](/styles/global-styles).
+These styles are part of the `@mantine/core` package and are applied automatically when you import
+`@mantine/core/styles.css` in your application. Note that these styles cannot be decoupled from the
+rest of the library.
+
+## Mantine as a headless UI library
+
+You can now use Mantine as a [headless](/styles/unstyled) library. To achieve that, just do not import
+`@mantine/*/styles.css` in your application. Then you will be able to apply styles with
+[Styles API](/styles/styles-api).
+
+## createTheme function
+
+`createTheme` function is a replacement for `MantineThemeOverride` type. Use it to create a theme
+override, it will give you autocomplete for all theme properties:
+
+```tsx
+import { createTheme, MantineProvider } from '@mantine/core';
+
+const theme = createTheme({
+ fontFamily: 'sans-serif',
+ primaryColor: 'orange',
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## Components extend functions
+
+All components that support [default props](/theming/default-props) or [Styles API](/styles/styles-api) now have a static `extend` function which allows getting autocomplete when customizing
+[defaultProps](/theming/default-props), [classNames and styles](/styles/styles-api) of the component
+on [theme](/theming/theme-object):
+
+```tsx
+import { useState } from 'react';
+import { TextInput, MantineProvider, createTheme } from '@mantine/core';
+import classes from './Demo.module.css';
+
+const theme = createTheme({
+ components: {
+ TextInput: TextInput.extends({
+ styles: (theme, props) => ({
+ input: {
+ fontSize: props.size === 'compact' ? theme.fontSizes.sm : undefined,
+ }
+ })
+ classNames: {
+ root: classes.root,
+ input: classes.input,
+ label: classes.label,
+ },
+
+ defaultProps: {
+ size: 'compact',
+ },
+ }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## classNames based on component props
+
+You can now get component props in [classNames and styles](/styles/styles-api) to conditionally apply styles.
+This feature is a more powerful replacement for styles params.
+
+
+
+## Components CSS variables
+
+You can now customize components [CSS variables](/styles/styles-api) to change component styles based on its props.
+For example, it can be used to add new [sizes](/styles/variants-sizes):
+
+
+
+## New variants system
+
+All components now have `data-variant` attribute on the root element, even if the component does not have any predefined variants.
+You can use it to apply styles to all components of the same type on [theme](/theming/theme-object):
+
+
+
+## New sizes system
+
+There are multiple ways to customize component sizes:
+
+- With `data-size` attribute
+- With component [CSS variables](/styles/styles-api)
+- With [static CSS variables](/styles/variants-sizes#sizes-with-static-css-variables)
+
+Example of customizing [Button](/core/button) size with `data-size` attribute:
+
+
+
+## theme.variantColorResolver
+
+[Button](/core/button), [Badge](/core/badge), [ActionIcon](/core/action-icon), [ThemeIcon](/core/theme-icon) and other
+components now support custom variants with [variantColorResolver](/theming/colors#colors-variant-resolver)
+– it supports both changing colors of existing variants and adding new variants.
+
+
+
+## rem units scaling
+
+It is now possible to scale [rem](/styles/rem#rem-units-scaling) units. It is useful when you want to change
+font-size of `html`/`:root` element and preserve Mantine components sizes. For example, if you would like to set `html` font-size to `10px` and scale Mantine components accordingly, you need
+to set `scale` to `1 / (10 / 16)` (16 – default font-size) = `1 / 0.625` = `1.6`:
+
+```css
+:root {
+ font-size: 10px;
+}
+```
+
+```tsx
+import { MantineProvider, createTheme } from '@mantine/core';
+
+const theme = createTheme({
+ scale: 1.6,
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## color prop improvements
+
+All components that support `color` prop now support the following color values:
+
+- Key of `theme.colors`, for example, `blue`
+- `theme.colors` index reference, for example, `blue.5`
+- Any valid CSS color value, for example, `#fff`, `rgba(0, 0, 0, 0.5)`, `hsl(0, 0%, 100%)`
+
+
+
+## Components classes
+
+Classes of each component are now available in `Component.classes` object. For example, you can
+find [Button](/core/button) classes in `Button.classes`:
+
+ [key, Button.classes[key]])}
+/>
+
+You can use these classes to create components with the same styles as Mantine components:
+
+```tsx
+import { Button } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+```
+
+## Theme object changes
+
+- `theme.lineHeight` is now `theme.lineHeights` – it is now possible to specify multiple line heights. `theme.lineHeights` values are used in the [Text](/core/text) component.
+- `theme.colorScheme` is no longer available, use [useMantineColorScheme](/theming/color-schemes#use-mantine-color-scheme-hook) to get color scheme value
+- `theme.dir` is no longer needed, direction is now managed by [DirectionProvider](/styles/rtl)
+- `theme.loader` was removed, you can now configure default loader with [default props](/theming/default-props) of [Loader](/core/loader) component
+- `theme.transitionTimingFunction` was removed
+- `theme.focusRingStyles` was replaced with `theme.focusClassName`
+- `theme.activeStyles` was replaced with `theme.activeClassName`
+- `theme.globalStyles` was removed
+- `theme.fn` functions were removed, some of the functions are available as [standalone utilities](/styles/color-functions)
+- New [theme.scale](/styles/rem#rem-units-scaling) property controls rem units scaling
+- New `theme.fontSmoothing` property determines whether font smoothing styles should be applied to the body element
+- New [theme.variantColorResolver](/theming/colors#colors-variant-resolver) property allows customizing component colors per variant
+
+## Colors generator
+
+New [@mantine/colors-generator](/theming/colors#colors-generation) package is now available to generate
+color palettes based on single color value. It is also available as [online tool](/colors-generator/).
+Note that it is usually better to generate colors in advance to avoid contrast issues.
+
+```tsx
+import { MantineProvider } from '@mantine/core';
+import { generateColors } from '@mantine/colors-generator';
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+## New setup for RTL
+
+`@mantine/core` package now exports [DirectionProvider](/styles/rtl) component, which should be used to configure the direction of the application.
+`useDirection` hook can be used to toggle direction. All components now include RTL styles by default, it is no
+longer required to set up additional plugins. See [RTL documentation](/styles/rtl) to learn more.
+
+## React 18+ only
+
+Starting from version 7.0 Mantine no longer supports older React versions. The minimum supported version is now React 18.
+It is required because Mantine components now use [useId](https://react.dev/reference/react/useId) and [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
+hooks, which are available only in React 18.
+
+## left and right section
+
+Components that previously had `rightSection` and `icon` props, now use `leftSection` instead of `icon`.
+Example of [Button](/core/button) sections:
+
+```tsx
+import { Button } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ Label
+
+ );
+}
+```
+
+## NumberInput changes
+
+[NumberInput](/core/number-input) was migrated to [react-number-format](https://s-yadav.github.io/react-number-format/).
+It now supports more features and has improvements in cursor position management.
+Due to migration, some of the props were renamed – follow the [documentation](/core/number-input) to learn about the changes.
+
+
+
+## Loader changes
+
+[Loader](/core/loader) component is now built with CSS only. This change improves performance and reduces
+HTML output of the component.
+
+
+
+[Theme](/theming/theme-object) object no longer supports `theme.loader` property –
+it is also now possible to add custom loaders on [theme](/theming/theme-object) with [default props](/theming/default-props).
+Specified [Loader](/core/loader) will be used in all components that use it under the hood ([LoadingOverlay](/core/loading-overlay), [Button](/core/button), [ActionIcon](/core/action-icon), [Stepper](/core/stepper), etc.).
+
+
+
+## Progress changes
+
+[Progress](/core/progress) component now supports compound components pattern.
+Advanced features that were previously implemented in [Progress](/core/progress) are now supposed to be implemented with
+compound components instead.
+
+
+
+## Table changes
+
+[Table](/core/table) component changes:
+
+- [Styles API](/styles/styles-api) support
+- It is now required to use compound components instead of elements: `Table.Tr`, `Table.Td`, etc.
+- It is now easier to override styles – all styles are added with classes instead of deeply nested selectors on root element
+- New props: `borderColor`, `withRowBorders`, `stripedColor`, `highlightOnHoverColor`
+- `withBorder` prop was renamed to `withTableBorder`
+- `fontSize` prop was removed, use `fz` [style prop](/styles/style-props) instead
+- New `Table.ScrollContainer` component to create scrollable table
+
+
+
+## Group changes
+
+[Group](/core/group) component changes:
+
+- `position` prop was renamed to `justify` – it now supports all `justify-content` values
+- `noWrap` prop was replaced with `wrap="nowrap"`, `wrap` prop now supports all `flex-wrap` values
+- `spacing` prop was replaced with `gap`
+- `Group` now supports new `preventGrowOverflow` prop which allows customizing how group items will behave when they grow larger than their dedicated space
+
+
+
+## Tabs changes
+
+- Styles API selector `tabsList` renamed to `list`
+- `TabProps` type was renamed to `TabsTabProps`
+- `onTabChange` prop was renamed to `onChange`
+- `Tabs.List` `position` prop was renamed to `justify`, it now supports all `justify-content` values
+
+## Button changes
+
+- `compact` prop was removed, use `size="compact-XXX"` instead
+- `leftIcon` and `rightIcon` props were renamed to `leftSection` and `rightSection`
+- `uppercase` prop was removed, use `tt` [style prop](/styles/style-props) instead
+- `loaderPosition` prop was removed, [Loader](/core/loader) is now always rendered in the center to prevent layout shifts
+- Styles API selectors were changed, see [Button Styles API](/core/button?t=styles-api) documentation for more details
+
+## AppShell changes
+
+[AppShell](/core/app-shell) component was completely rewritten, it now supports more features:
+
+- `AppShell` now uses compound components pattern: `Navbar`, `Aside`, `Header` and `Footer` are no longer exported from `@mantine/core` package. Instead, use `AppShell.Navbar`, `AppShell.Aside`, etc.
+- `AppShell` now supports animations when navbar/aside are opened/closed
+- Navbar/aside can now be collapsed on desktop – state is handled separately for mobile and desktop
+- Header/footer can now be collapsed the same way as navbar/aside. For example, the header can be collapsed based on scroll position or direction.
+- `AppShell` no longer supports `fixed` prop – all components have `position: fixed` styles, static positioning is no longer supported
+- The documentation was updated, it now includes [10+ examples on a separate page](/app-shell?e=BasicAppShell)
+
+## SimpleGrid changes
+
+[SimpleGrid](/core/simple-grid) now uses object format to define grid breakpoints and spacing,
+it works the same way as [style props](/styles/style-props).
+
+
+
+## Grid changes
+
+[Grid](/core/grid) now uses object format in `gutter`, `offset`, `span` and order props,
+all props now work the same way as [style props](/styles/style-props).
+
+
+
+## Image changes
+
+[Image](/core/image) component changes:
+
+- `Image` component no longer includes `figure` and other associated elements
+- `caption` prop is no longer available
+- `width` and `height` props are replaced with `w` and `h` [style props](/styles/style-props)
+- Placeholder functionality was replaced with fallback image
+
+
+
+## Spotlight changes
+
+[Spotlight](/others/spotlight) changes:
+
+- The logic is no longer based on React context
+- `SpotlightProvider` was renamed to `Spotlight`
+- `useSpotlight` hook was removed, use `spotlight` object instead
+- `actions` prop now uses a different data format
+- It is now possible to have multiple spotlights in the same app
+- `Spotlight` component now uses compound components pattern for advanced customization
+
+
+
+## Carousel changes
+
+[Carousel](/others/carousel) now uses object format for responsive values in
+`slideSize` and `slideGap` props instead of `breakpoints` prop:
+
+
+
+## @mantine/prism deprecation
+
+`@mantine/prism` package was deprecated in favor of `@mantine/code-highlight` package. [The new package](/others/code-highlight) uses [highlight.js](https://highlightjs.org/) instead of [Prism](https://prismjs.com/).
+
+
+
+## Fieldset component
+
+New [Fieldset](/core/fieldset) component:
+
+
+
+## Combobox component
+
+The new [Combobox](/core/combobox) component allows building custom select, autocomplete, tags input, multiselect and other
+similar components. It is used as a base for updated [Autocomplete](/core/autocomplete), [Select](/core/select),
+[TagsInput](/core/tags-input) and [MultiSelect](/core/multi-select) components.
+
+[Combobox](/core/combobox) is very flexible and allows you to have full control over the component rendering and logic.
+Advanced features that were previously implemented in [Autocomplete](/core/autocomplete), [Select](/core/select)
+and [MultiSelect](/core/multi-select) are now supposed to be implemented with [Combobox](/core/combobox) instead.
+
+You can find 50+ `Combobox` examples on [this page](/combobox).
+
+
+
+## TagsInput component
+
+New [TagsInput](/core/tags-input) component based on [Combobox](/core/combobox) component.
+The component is similar to [MultiSelect](/core/multi-select) but allows entering custom values.
+
+
+
+## withErrorStyles prop
+
+All inputs now support `withErrorStyles` prop, which allows removing error styles from the input.
+It can be used to apply custom error styles without override, or use other techniques to
+indicate error state. For example, it can be used to render an icon in the right section:
+
+
+
+## hiddenFrom and visibleFrom props
+
+All Mantine components now support `hiddenFrom` and `visibleFrom` props.
+These props accept breakpoint (`xs`, `sm`, `md`, `lg`, `xl`) and hide the component when
+viewport width is less than or greater than the specified breakpoint:
+
+
+
+## Other changes
+
+- New [VisuallyHidden](/core/visually-hidden) component
+- New [ActionIcon.Group](/core/action-icon#actionicongroup) component
+- [DatesProvider](/dates/dates-provider) now supports setting timezone
+- All transitions are now disabled during color scheme change
+- `theme.respectReducedMotion` is now set to `false` by default – it caused a lot of confusion for users who did not know about it
+- [Notifications system](/others/notifications) now exports `notifications.updateState` function to update notifications state with a given callback
+- [Blockquote](/core/blockquote) component has new design
+- [Breadcrumbs](/core/breadcrumbs) component now supports `separatorMargin` prop
+- [Tooltip](/core/tooltip) component now supports `mainAxis` and `crossAxis` offset configuration
+- [Slider and RangeSlider](/core/slider) components `radius` prop now affects thumb as well as track
+- [NativeSelect](/core/native-select/) component now supports setting options as `children` and options groups
+- [Anchor](/core/anchor) component now supports `underline` prop which lets you configure how link will be underlined: `hover` (default), `always` or `never`
+- [Affix](/core/affix) component no longer supports `target` prop, use `portalProps` instead
+- [Container](/core/container) component no longer supports `sizes` prop, use [CSS variables](/styles/styles-api) to customize sizes on [theme](/theming/theme-object) instead
+- `useComponentDefaultProps` hook was renamed to [useProps](/theming/default-props#useprops-hook)
+- `withinPortal` prop is now true by default in all overlay components ([Popover](/core/popover), [HoverCard](/core/hover-card), [Tooltip](/core/tooltip), etc.)
+- `AlphaSlider` and `HueSlider` components are no longer available, they can be used only as a part of [ColorPicker](/core/color-picker)
+- [Text](/core/text) default root element is now `
`
+- [Title](/core/title) no longer supports all [Text](/core/text) props, only [style props](/styles/style-props) are available
+- `MediaQuery` component was removed – use [CSS modules](/styles/css-modules) to apply responsive styles
+- [Highlight](/core/highlight) component prop `highlightColor` was renamed to `color`
+- [Tooltip and Tooltip.Floating](/core/tooltip) components no longer support `width` prop, use `w` [style prop](/styles/style-props) instead
+- [Stack](/core/stack) component `spacing` prop was renamed to `gap`
+- [Input](/core/input) and other `Input` based components `icon` prop was renamed to `leftSection`
+- [Loader](/core/loader) component `variant` prop was renamed to `type`
+- `@mantine/core` package no longer depends on [Radix UI ScrollArea](https://www.radix-ui.com/docs/primitives/components/scroll-area#scroll-area), [ScrollArea](/core/scroll-area) component is now a native Mantine component – it reduces bundle size, allows setting CSP for styles and improves performance (all styles are now applied with classes instead of inline `` tags)
+- [Overlay](/core/overlay) `opacity` prop was renamed to `backgroundOpacity` to avoid collision with `opacity` [style prop](/styles/style-props)
+- [Badge](/core/badge) Styles API selectors were changed, see [Badge Styles API](/core/badge?t=styles-api) documentation for more details
+- [Slider](/core/slider) Styles API selectors were changed, see [Slider Styles API](/core/slider?t=styles-api) documentation for more details
+- [Text](/core/text) component no longer supports `underline`, `color`, `strikethrough`, `italic`, `transform`, `align` and `weight` prop – use [style props](/styles/style-props) instead
+- [Portal](/core/portal) component `innerRef` prop was renamed to `ref`
+- [ScrollArea](/core/scroll-area) now supports `x` and `y` values in `offsetScrollbars` prop
+- `TransferList` component is no longer available as a part of `@mantine/core` package, instead you can implement it with [Combobox](/core/combobox) component ([example](/combobox?e=TransferList))
+- [Chip](/core/chip) component now supports custom check icon
+- [PasswordInput](/core/password-input) no longer supports `visibilityToggleLabel` and `toggleTabIndex` props, use `visibilityToggleButtonProps` prop instead
+- [Stepper](/core/stepper) now longer supports `breakpoint` prop, you can apply responsive styles with Styles API
+- [ColorInput](/core/color-input) no longer supports `dropdownZIndex`, `transitionProps`, `withinPortal`, `portalProps` and `shadow` props, you can now pass these props with `popoverProps` prop
+- [LoadingOverlay](/core/loading-overlay) props are now grouped by the component they are passed down to: `overlayProps`, `loaderProps` and `transitionProps` now replace props that were previously passed to `LoadingOverlay` directly
+- [Dropzone](/others/dropzone) component no longer supports `padding` prop, use `p` style prop instead
+- [Dialog](/core/dialog) component now supports all [Paper](/core/paper) and [Affix](/core/affix) props, `transitionDuration`, `transition` and other transition related props were replaced with `transitionProps`
+- [Checkbox](/core/checkbox), [Radio](/core/radio), [Chip](/core/chip) and [Switch](/core/switch) components now support `rootRef` prop which allows using them with [Tooltip](/core/tooltip) and other similar components
diff --git a/docs/src/docs/changelog/previous-versions.mdx b/docs/pages/changelog/previous-versions.mdx
similarity index 92%
rename from docs/src/docs/changelog/previous-versions.mdx
rename to docs/pages/changelog/previous-versions.mdx
index 586040d8702..1fbaa536518 100644
--- a/docs/src/docs/changelog/previous-versions.mdx
+++ b/docs/pages/changelog/previous-versions.mdx
@@ -1,13 +1,9 @@
----
-group: 'changelog'
-title: 'Previous versions'
-order: 1
-slug: /changelog/previous-versions/
----
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
-# Previous changelogs
+export default Layout(MDX_DATA.PreviousChangelogs);
-Note that when following links of 1.x – 5.x changelogs you will be redirected to `v5` version website.
+# Previous changelogs
- [Version 1.1.0](https://v5.mantine.dev/changelog/1-1-0/) – May 19th, 2021
- [Version 1.3.0](https://v5.mantine.dev/changelog/1-3-0/) – June 1st, 2021
@@ -38,3 +34,4 @@ Note that when following links of 1.x – 5.x changelogs you will be redirected
- [Version 5.8.0](https://v5.mantine.dev/changelog/5-8-0/) – November 16th, 2022
- [Version 5.9.0](https://v5.mantine.dev/changelog/5-9-0/) – December 1st, 2022
- [Version 5.10.0](https://v5.mantine.dev/changelog/5-10-0/) – January 3rd, 2023
+- [Version 6.0.0](https://v6.mantine.dev/changelog/6-0-0/) – March 2nd, 2023
diff --git a/docs/pages/colors-generator.tsx b/docs/pages/colors-generator.tsx
new file mode 100644
index 00000000000..75b9df9c134
--- /dev/null
+++ b/docs/pages/colors-generator.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { ContentPageBase } from '@/components/ContentPageBase';
+import { ColorsGenerator } from '@/components/ColorsGenerator';
+
+export default function ColorsGeneratorPage() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/pages/combobox.tsx b/docs/pages/combobox.tsx
new file mode 100644
index 00000000000..fb583fad3ed
--- /dev/null
+++ b/docs/pages/combobox.tsx
@@ -0,0 +1,3 @@
+import { ComboboxPage } from '@/combobox-examples';
+
+export default ComboboxPage;
diff --git a/docs/pages/contribute.mdx b/docs/pages/contribute.mdx
new file mode 100644
index 00000000000..92b648c4652
--- /dev/null
+++ b/docs/pages/contribute.mdx
@@ -0,0 +1,61 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+
+export default Layout(MDX_DATA.Contribute);
+
+# Contributing to Mantine
+
+First of all, thank you for showing interest in contributing to Mantine! All your contributions are extremely valuable to the project!
+
+## Ways to contribute
+
+- **Improve documentation:** Fix incomplete or missing docs, bad wording, examples or explanations.
+- **Give feedback:** We are constantly working on making Mantine better. Please share how you use Mantine, what features are missing and what is done well via [GitHub Discussions](https://github.com/mantinedev/mantine/discussions/new) or [Discord](https://discord.gg/wbH82zuWMN).
+- **Share Mantine:** Share links to the Mantine docs with everyone who might be interested! [Share Mantine on Twitter here](https://twitter.com/intent/tweet?text=Mantine%20%E2%80%93%20new%20React%20library%20with%20100%2B%20components%20and%20hooks.%20It%20has%20native%20dark%20theme%20support%20and%20focuses%20on%20accessibility%20and%20usability.%0A%0Ahttp%3A%2F%2Fmantine.dev%0A%0A%23reactjs%20).
+- **Contribute to the codebase:** Propose new features via [GitHub Issues](https://github.com/mantinedev/mantine/issues/new), or find an [existing issue](https://github.com/mantinedev/mantine/labels/help%20wanted) that you are interested in and work on it!
+- **Give us a code review:** Help us identify problems with the [source code](https://github.com/mantinedev/mantine/tree/master/src) or make Mantine more performant.
+
+## Contributing workflow
+
+- Decide on what you want to contribute.
+- If you would like to implement a new feature, discuss it with the maintainer ([GitHub Discussions](https://github.com/mantinedev/mantine/discussions/new) or [Discord](https://discord.gg/wbH82zuWMN)) before jumping into coding.
+- After finalizing issue details, as you begin working on the code, please make sure to follow commit conventions.
+- Run tests with `npm test` and submit a PR once all tests have passed.
+- Get a code review and fix all issues noticed by the maintainer.
+- If you cannot finish your task or if you change your mind – that's totally fine! Just let us know in the GitHub issue that you created during the first step of this process. The Mantine community is friendly – we won't judge or ask any questions if you decide to cancel your submission.
+- Your PR is merged. You are awesome ❤️!
+
+## Get started with Mantine locally
+
+- Install the [editorconfig](https://editorconfig.org/) extension for your editor.
+- Fork the [repository](https://github.com/mantinedev/mantine), then clone or download your fork.
+- Install dependencies with yarn – `yarn`
+- Build local version of all packages – `npm run build:all`
+- Build local version of specific packages – `npm run build @mantine/core @mantine/demos @mantine/hooks`
+- To start storybook – `npm run storybook`
+- To start docs – `npm run docs`
+- To rebuild props descriptions – `npm run docs:docgen`
+
+## npm scripts
+
+All npm scripts are located at [main package.json](https://github.com/mantinedev/mantine/blob/master/package.json).
+Individual packages do not have dedicated scripts.
+
+### Development scripts
+
+- `storybook` – Starts the storybook development server. To start storybook for a specific component, use the `npm run storybook Tooltip` command.
+- `docs` – Starts the docs development server.
+
+### Testing scripts
+
+- `syncpack` – runs [syncpack](https://www.npmjs.com/package/syncpack)
+- `typecheck` – runs TypeScript typechecking with `tsc --noEmit` on all packages and docs
+- `lint` – runs ESLint on src folder
+- `jest` – runs tests with jest
+- `test` – runs all above testing scripts
+
+### Docs scripts
+
+- `docs:docgen` – generates components types information with [docgen script](https://github.com/mantinedev/mantine/blob/master/scripts/docgen.ts)
+- `docs:build` – builds docs for production
+- `docs:deploy` – builds and deploys docs to the GitHub Pages
diff --git a/docs/pages/core/accordion.mdx b/docs/pages/core/accordion.mdx
new file mode 100644
index 00000000000..409ef34e7d3
--- /dev/null
+++ b/docs/pages/core/accordion.mdx
@@ -0,0 +1,226 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AccordionDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Accordion);
+
+## Usage
+
+Data used in Accordion examples:
+
+```tsx
+const groceries = [
+ {
+ emoji: '🍎',
+ value: 'Apples',
+ description:
+ 'Crisp and refreshing fruit. Apples are known for their versatility and nutritional benefits. They come in a variety of flavors and are great for snacking, baking, or adding to salads.',
+ },
+ {
+ emoji: '🍌',
+ value: 'Bananas',
+ description:
+ 'Naturally sweet and potassium-rich fruit. Bananas are a popular choice for their energy-boosting properties and can be enjoyed as a quick snack, added to smoothies, or used in baking.',
+ },
+ {
+ emoji: '🥦',
+ value: 'Broccoli',
+ description:
+ 'Nutrient-packed green vegetable. Broccoli is packed with vitamins, minerals, and fiber. It has a distinct flavor and can be enjoyed steamed, roasted, or added to stir-fries.',
+ },
+];
+```
+
+
+
+## Custom control label
+
+
+
+## Change chevron
+
+
+
+## With icons
+
+
+
+## Change transition
+
+To change transition duration, set `transitionDuration` prop:
+
+
+
+To disable transitions, set `transitionDuration` to 0:
+
+
+
+## Default opened items
+
+When `multiple={false}`, set `defaultValue` as string:
+
+```tsx
+import { Accordion } from '@mantine/core';
+
+function Demo() {
+ // Second item will be opened by default
+ return (
+
+
+ control-1
+ panel-1
+
+
+
+ control-2
+ panel-2
+
+
+ );
+}
+```
+
+When `multiple={true}`, set `defaultValue` as an array of strings:
+
+```tsx
+import { Accordion } from '@mantine/core';
+
+function Demo() {
+ // Both items will be opened by default
+ return (
+
+
+ control-1
+ panel-1
+
+
+
+ control-2
+ panel-2
+
+
+ );
+}
+```
+
+## Control state
+
+When `multiple={false}`, set `value` as string:
+
+```tsx
+import { useState } from 'react';
+import { Accordion } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState(null);
+
+ return (
+
+
+ control-1
+ panel-1
+
+
+
+ control-2
+ panel-2
+
+
+ );
+}
+```
+
+When `multiple={true}`, set `value` as an array of strings:
+
+```tsx
+import { useState } from 'react';
+import { Accordion } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+
+ return (
+
+
+ control-1
+ panel-1
+
+
+
+ control-2
+ panel-2
+
+
+ );
+}
+```
+
+## Compose controls
+
+You can add any additional elements that will be displayed next to `Accordion.Control`,
+for example, you can add [ActionIcon](/core/action-icon/) or [Menu](/core/menu/):
+
+
+
+## Disabled items
+
+Set `disabled` prop on `Accordion.Control` component to disable it.
+Disabled items cannot be activated with mouse or keyboard, will be skipped when user navigates with arrow keys:
+
+
+
+## Unstyled Accordion
+
+Set `unstyled` prop on Accordion component to remove all non-essential library styles.
+It can be used to style component with [Styles API](/styles/styles-api/) without overriding any styles.
+
+
+
+
+
+Use [Styles API](/styles/styles-api/) to customize Accordion styles:
+
+
+
+Use [Styles API](/styles/styles-api/) to customize Accordion styles:
+
+
+
+## TypeScript
+
+`AccordionProps` type exported from `@mantine/core` is a generic, it accepts boolean type that
+describes `multiple` state:
+
+```tsx
+import type { AccordionProps } from '@mantine/core';
+
+type MultipleAccordionProps = AccordionProps;
+type DefaultAccordionProps = AccordionProps;
+```
+
+## Accessibility
+
+Accordion component follows [WAI-ARIA recommendations](https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html) on accessibility.
+
+Set `order` on `Accordion` component to wrap accordion controls with h2-h6 heading.
+The following example will wrap controls with h3 tag:
+
+```tsx
+import { Accordion } from '@mantine/core';
+
+function Demo() {
+ return {/* ...items */} ;
+}
+```
+
+Keyboard interactions:
+
+
diff --git a/docs/pages/core/action-icon.mdx b/docs/pages/core/action-icon.mdx
new file mode 100644
index 00000000000..9d721f3b910
--- /dev/null
+++ b/docs/pages/core/action-icon.mdx
@@ -0,0 +1,142 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ActionIconDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.ActionIcon);
+
+## Usage
+
+
+
+
+
+
+
+## Size
+
+You can use any valid CSS value in `size` prop, it is used to set `width`, `min-width`, `min-height` and `height`
+properties. Note that `size` prop does not control child [icon](/guides/icons) size, you need to
+set it manually on icon component. When `size` is a number, the value is treated as `px` units and
+converted to [rem](/styles/rem) units.
+
+
+
+## Disabled state
+
+To make `ActionIcon` disabled set `disabled` prop, this will prevent any interactions with the button
+and add disabled styles. If you want the button to just look disabled but still be interactive,
+set `data-disabled` prop instead. Note that disabled styles are the same for all variants.
+
+
+
+## Disabled state when ActionIcon is link
+
+` ` element does not support `disabled` attribute. To make `ActionIcon` disabled when it is
+rendered as a link, set `data-disabled` attribute instead and prevent default behavior in
+`onClick` event handler.
+
+
+
+## Customize disabled styles
+
+To customize disabled styles, it is recommended to use both `&:disabled` and `&[data-disabled]`
+selectors:
+
+- `&:disabled` is used to style the button when `disabled` prop is set and
+ also when the button is disabled by the parent component (for example, when `disabled` prop is set on a
+ ` ` element which contains `ActionIcon`).
+- `&[data-disabled]` is used to style the button when it is not actually disabled but should look like
+ it is (for example, `data-disabled` should be used if you need to use [Tooltip](/core/tooltip) with disabled `ActionIcon`
+ or when `ActionIcon` is used as a link)
+
+
+
+## Disabled button with Tooltip
+
+`onMouseLeave` event [is not triggered](https://github.com/facebook/react/issues/18753) when `ActionIcon` is disabled, so if you need to use
+[Tooltip](/core/tooltip) with disabled `ActionIcon` you need to set `data-disabled` prop on `ActionIcon`
+instead of `disabled`. Note that it is also required to change `onClick` event handler to
+`(event) => event.preventDefault()` as `ActionIcon` is not actually disabled and will still trigger
+`onClick` event.
+
+
+
+## Loading state
+
+When `loading` prop is set, `ActionIcon` will be disabled and [Loader](/core/loader) with overlay will be rendered
+in the center of the button. [Loader](/core/loader) color depends on `ActionIcon` variant.
+
+
+
+## Loader props
+
+You can customize [Loader](/core/loader) with `loaderProps` prop, it accepts all props that
+[Loader](/core/loader) component has:
+
+
+
+## Add custom variants
+
+To add new `ActionIcon` variants, use [data-variant](/styles/variants-sizes) attribute.
+Usually new variants are added on [theme](/theming/theme-object), this way they are
+available in all `ActionIcon` components in your application.
+
+
+
+## Add custom sizes
+
+`ActionIcon` sizes are defined by `--ai-size-{x}` CSS variables. The easiest way to add new sizes is to
+define additional `--ai-size-{x}` variables on the `root` element:
+
+
+
+## ActionIcon.Group
+
+
+
+Note that you must not wrap child `ActionIcon` components with any additional elements:
+
+```tsx
+import { ActionIcon } from '@mantine/core';
+
+// Will not work correctly
+function Demo() {
+ return (
+
+
+ ActionIcons will have incorrect borders
+
+ );
+}
+```
+
+
+
+
+
+## Accessibility
+
+To make `ActionIcon` accessible for screen readers, you need to either set `aria-label` or
+use [VisuallyHidden](/core/visually-hidden) component:
+
+```tsx
+import { ActionIcon, VisuallyHidden } from '@mantine/core';
+import { IconHeart } from '@tabler/icons-react';
+
+function Demo() {
+ return (
+ <>
+
+
+
+
+
+ Like post
+
+
+ >
+ );
+}
+```
diff --git a/docs/pages/core/affix.mdx b/docs/pages/core/affix.mdx
new file mode 100644
index 00000000000..cb6845a59a4
--- /dev/null
+++ b/docs/pages/core/affix.mdx
@@ -0,0 +1,12 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AffixDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Affix);
+
+## Usage
+
+`Affix` renders a div element with a fixed position inside the [Portal](/core/portal) component.
+Use it to display elements fixed at any position on the screen, for example, scroll to top button:
+
+
diff --git a/docs/pages/core/alert.mdx b/docs/pages/core/alert.mdx
new file mode 100644
index 00000000000..bb5e7d93d03
--- /dev/null
+++ b/docs/pages/core/alert.mdx
@@ -0,0 +1,38 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AlertDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Alert);
+
+## Usage
+
+
+
+
+
+
+
+## Accessibility
+
+- Root element role set to `alert`
+- `aria-describedby` set to body element id, `aria-labelledby` set to title element id if `title` is provided
+- Set `closeButtonLabel` prop to make close button accessible
+
+```tsx
+import { Alert } from '@mantine/core';
+
+function Invalid() {
+ // -> not ok
+ return ;
+}
+
+function Valid() {
+ // -> ok
+ return ;
+}
+
+function AlsoValid() {
+ // -> ok, without close button, closeButtonLabel is not needed
+ return ;
+}
+```
diff --git a/docs/pages/core/anchor.mdx b/docs/pages/core/anchor.mdx
new file mode 100644
index 00000000000..7a9652769ff
--- /dev/null
+++ b/docs/pages/core/anchor.mdx
@@ -0,0 +1,47 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AnchorDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Anchor);
+
+## Usage
+
+
+
+## Underline
+
+Use `underline` prop to configure `text-decoration` property. It accepts the following values:
+
+- `always` - link is always underlined
+- `hover` - link is underlined on hover
+- `never` - link is never underlined
+
+
+
+You can also configure `underline` prop for all `Anchor` components with [default props](/theming/default-props):
+
+```tsx
+import { Anchor, createTheme, MantineProvider } from '@mantine/core';
+
+const theme = createTheme({
+ components: {
+ Anchor: Anchor.extend({
+ defaultProps: {
+ underline: 'always',
+ },
+ }),
+ },
+});
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+
+
+
diff --git a/docs/pages/core/app-shell.mdx b/docs/pages/core/app-shell.mdx
new file mode 100644
index 00000000000..7bc883df4fd
--- /dev/null
+++ b/docs/pages/core/app-shell.mdx
@@ -0,0 +1,385 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+
+export default Layout(MDX_DATA.AppShell);
+
+## Examples
+
+This page includes only documentation. Since all associated `AppShell` components have fixed
+position, it is not possible to include demos on this page. You can find 10+ examples on
+[this page](/app-shell?e=BasicAppShell)
+
+## Usage
+
+`AppShell` is a layout component. It can be used to implement a common Header – Navbar – Footer – Aside
+layout pattern. All `AppShell` components have `position: fixed` style – they are not scrolled with
+the page.
+
+[Basic AppShell example](/app-shell?e=BasicAppShell) with header and navbar.
+Navbar is hidden on mobile by default and toggled with the burger button.
+
+```tsx
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Burger } from '@mantine/core';
+
+function Demo() {
+ const [opened, { toggle }] = useDisclosure();
+
+ return (
+
+
+
+ Logo
+
+
+ Navbar
+
+ Main
+
+ );
+}
+```
+
+## Configuration
+
+`AppShell` component accepts, `header`, `footer`, `navbar` and `aside` props to configure
+corresponding sections. It is required to set these props if you want to use corresponding
+components. For example, if you want to use `AppShell.Header` component, you need to set `header`
+prop on the `AppShell` component.
+
+`header` and `footer` configuration objects are the same and have the following properties:
+
+```tsx
+interface Configuration {
+ /** Height of the section: number, string or
+ ** object with breakpoints as keys and height as value */
+ height: AppShellSize | AppShellResponsiveSize;
+
+ /** If section is collapsed,
+ ** it is hidden from the viewport and is not offset in AppShell.Main */
+ collapsed?: boolean;
+
+ /** Determines whether the section should be offset by the AppShell.Main.
+ ** For example, it is useful if you want to
+ ** hide header based on the scroll position. */
+ offset?: boolean;
+}
+```
+
+`navbar` and `aside` configuration objects are the same as well and have the following properties:
+
+```tsx
+interface Configuration {
+ /** Width of the section: number, string or
+ ** object with breakpoints as keys and width as value */
+ width: AppShellSize | AppShellResponsiveSize;
+
+ /** Breakpoint at which section should switch to mobile mode
+ ** In mobile mode the section always has 100% width and its
+ ** collapsed state is controlled by the `collapsed.mobile`
+ ** instead of `collapsed.desktop` */
+ breakpoint: MantineBreakpoint | (string & {}) | number;
+
+ /** Determines whether the section should be collapsed */
+ collapsed?: { desktop?: boolean; mobile?: boolean };
+}
+```
+
+## layout prop
+
+`layout` prop controls how `AppShell.Header`/`AppShell.Footer` and `AppShell.Navbar`/`AppShell.Aside`
+are positioned relative to each other. It accepts `alt` and `default` values:
+
+- `alt` – `AppShell.Navbar`/`AppShell.Aside` height is equal to viewport height, `AppShell.Header`/`AppShell.Footer` width is equal to viewport width - `AppShell.Navbar` and `AppShell.Aside` width ([example](/app-shell?e=AltLayout))
+- `default` – `AppShell.Navbar`/`AppShell.Aside` height is equal to viewport height - `AppShell.Header`/`AppShell.Footer` height, `AppShell.Header`/`AppShell.Footer` width is equal to viewport width ([example](/app-shell?e=FullLayout))
+
+## Height configuration
+
+`height` property in `header` and `footer` configuration objects works the following way:
+
+- If you pass a number, the value will be converted to [rem](/styles/rem) and used as
+ height at all viewport sizes.
+- To change height based on viewport width, use object with breakpoints as keys and height as
+ values. It works the same way as [style props](/styles/style-props#responsive-styles).
+
+Examples:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Height is a number, it will be converted to rem
+// and used as height at all viewport sizes
+function Demo() {
+ return (
+
+ Header
+
+ );
+}
+```
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Height is an object with breakpoints:
+// - height is 48 when viewport width is < theme.breakpoints.sm
+// - height is 60 when viewport width is >= theme.breakpoints.sm and < theme.breakpoints.lg
+// - height is 76 when viewport width is >= theme.breakpoints.lg
+function Demo() {
+ return (
+
+ Header
+
+ );
+}
+```
+
+## Width configuration
+
+`width` property in `navbar` and `aside` configuration objects works the following way:
+
+- If you pass a number, the value will be converted to [rem](/styles/rem) and used as
+ width when the viewport is larger than `breakpoint`.
+- To change width based on viewport width, use object with breakpoints as keys and width as
+ values. It works the same way as [style props](/styles/style-props#responsive-styles).
+ Note that width is always 100% when the viewport is smaller than `breakpoint`.
+
+Examples:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Width is a number, it will be converted to rem
+// and used as width when viewport is larger than theme.breakpoints.sm
+function Demo() {
+ return (
+
+ Navbar
+
+ );
+}
+```
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Width is an object with breakpoints:
+// - width is 100% when viewport width is < theme.breakpoints.sm
+// - width is 200 when viewport width is >= theme.breakpoints.sm and < theme.breakpoints.lg
+// - width is 300 when viewport width is >= theme.breakpoints.lg
+function Demo() {
+ return (
+
+ Navbar
+
+ );
+}
+```
+
+## padding prop
+
+`padding` prop controls the padding of the `AppShell.Main` component. It is important to use it
+instead of setting padding on the `AppShell.Main` directly because padding of the `AppShell.Main` is
+also used to offset `AppShell.Header`, `AppShell.Navbar`, `AppShell.Aside` and `AppShell.Footer` components.
+
+`padding` prop works the same way as [style props](/styles/style-props#responsive-styles) and
+accepts numbers, strings and objects with breakpoints as keys and padding as values. You can
+reference `theme.spacing` values or use any valid CSS values.
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Padding is always theme.spacing.md
+function Demo() {
+ return {/* AppShell content */} ;
+}
+```
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// Padding is:
+// - 10 when viewport width is < theme.breakpoints.sm
+// - 15 when viewport width is >= theme.breakpoints.sm and < theme.breakpoints.lg
+// - theme.spacing.xl when viewport width is >= theme.breakpoints.lg
+function Demo() {
+ return {/* AppShell content */} ;
+}
+```
+
+## Header offset configuration
+
+`header` prop has `offset` property which allows removing `AppShell.Header` offset from `AppShell.Main` component.
+It is useful when you want to collapse `AppShell.Header` based on the scroll position. For example, you can use
+[use-headroom](/hooks/use-headroom) hook to collapse header when user scrolls down and expand it when user scrolls up ([example](/app-shell?e=Headroom)).
+
+```tsx
+import { useHeadroom } from '@mantine/hooks';
+import { AppShell, rem } from '@mantine/core';
+
+function Demo() {
+ const pinned = useHeadroom({ fixedAt: 120 });
+
+ return (
+
+ Header
+
+
+ {/* Content */}
+
+
+ );
+}
+```
+
+## Collapsed navbar/aside configuration
+
+`navbar` and `aside` props have `collapsed` property. The property accepts an object
+`{ mobile: boolean; desktop: boolean }` which allows to configure collapsed state
+depending on the viewport width.
+
+[Example](/app-shell?e=CollapseDesktop) with separate collapsed state for mobile and desktop:
+
+```tsx
+import { useDisclosure } from '@mantine/hooks';
+import { AppShell, Button } from '@mantine/core';
+
+export function CollapseDesktop() {
+ const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
+ const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
+
+ return (
+
+ Header
+ Navbar
+
+
+ Toggle navbar
+
+
+ Toggle navbar
+
+
+
+ );
+}
+```
+
+## withBorder prop
+
+`withBorder` prop is available on `AppShell` and associated sections: `AppShell.Header`, `AppShell.Navbar`, `AppShell.Aside` and `AppShell.Footer`.
+By default, `withBorder` prop is `true` – all components have a border on the side that is adjacent to the `AppShell.Main` component.
+For example, `AppShell.Header` is located at the top of the page – it has a border on the bottom side,
+`AppShell.Navbar` is located on the left side of the page – it has a border on the right side.
+
+To remove the border from all components, set `withBorder={false}` on the `AppShell`:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// None of the components will have a border
+function Demo() {
+ return {/* AppShell content */} ;
+}
+```
+
+To remove the border from a specific component, set `withBorder={false}` on that component:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// AppShell.Header does not have a border
+// AppShell.Navbar and AppShell.Aside have a border
+function Demo() {
+ return (
+
+ Header
+ Navbar
+ Aside
+
+ );
+}
+```
+
+## zIndex prop
+
+`zIndex` prop is available on `AppShell` and associated sections: `AppShell.Header`, `AppShell.Navbar`, `AppShell.Aside` and `AppShell.Footer`.
+By default, all sections `z-index` is `200`.
+
+To change `z-index` of all sections, set `zIndex` prop on the `AppShell`:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// All sections will have z-index of 100
+function Demo() {
+ return {/* AppShell content */} ;
+}
+```
+
+To change `z-index` of a specific section, set `zIndex` prop on that section:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+// AppShell.Header has z-index of 100
+// AppShell.Navbar and AppShell.Aside have z-index of 300
+function Demo() {
+ return (
+
+ Header
+ Navbar
+ Aside
+
+ );
+}
+```
+
+## Control transitions
+
+Set `transitionDuration` and `transitionTimingFunction` props on the `AppShell` to control transitions:
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ {/* AppShell content */}
+
+ );
+}
+```
+
+## disabled prop
+
+Set `disabled` prop on the `AppShell` to prevent all sections except `AppShell.Main` from rendering.
+It is useful when you want to hide the shell on some pages of your application.
+
+```tsx
+import { AppShell } from '@mantine/core';
+
+function Demo() {
+ return {/* AppShell content */} ;
+}
+```
+
+## Semantic elements
+
+- `AppShell.Header` root element is `header`
+- `AppShell.Navbar` root element is `nav`
+- `AppShell.Aside` root element is `aside`
+- `AppShell.Footer` root element is `footer`
+- `AppShell.Main` root element is `main` – **!important:** do not use `main` element inside `AppShell.Main` component
diff --git a/docs/pages/core/aspect-ratio.mdx b/docs/pages/core/aspect-ratio.mdx
new file mode 100644
index 00000000000..73e7d423ded
--- /dev/null
+++ b/docs/pages/core/aspect-ratio.mdx
@@ -0,0 +1,20 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AspectRatioDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.AspectRatio);
+
+## Usage
+
+`AspectRatio` allows maintaining a consistent width/height ratio.
+It can be used to display images, maps, videos and other media.
+
+
+
+## Map embed
+
+
+
+## Video embed
+
+
diff --git a/docs/pages/core/autocomplete.mdx b/docs/pages/core/autocomplete.mdx
new file mode 100644
index 00000000000..fb98f86551c
--- /dev/null
+++ b/docs/pages/core/autocomplete.mdx
@@ -0,0 +1,100 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AutocompleteDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Autocomplete);
+
+
+
+## Usage
+
+`Autocomplete` provides user a list of suggestions based on the input,
+however user is not limited to suggestions and can type anything.
+
+
+
+## Controlled
+
+`Autocomplete` value must be a string, other types are not supported.
+`onChange` function is called with a string value as a single argument.
+
+```tsx
+import { useState } from 'react';
+import { Autocomplete } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return ;
+}
+```
+
+
+
+
+
+
+
+## Sort options
+
+By default, options are sorted by their position in the data array. You can change this behavior
+with `filter` function:
+
+
+
+
+
+
+
+## Scrollable dropdown
+
+By default, the options list is wrapped with [ScrollArea.Autosize](/core/scroll-area).
+You can control dropdown max-height with `maxDropdownHeight` prop if you do not change the default settings.
+
+If you want to use native scrollbars, set `withScrollArea={false}`. Note that in this case,
+you will need to change dropdown styles with [Styles API](/styles/styles-api).
+
+
+
+## Group options
+
+
+
+## Disabled options
+
+When option is disabled, it cannot be selected and is ignored in keyboard navigation.
+
+
+
+
+
+## Input props
+
+
+
+
+
+## Read only
+
+Set `readOnly` to make the input read only. When `readOnly` is set,
+`Autocomplete` will not show suggestions and will not call `onChange` function.
+
+
+
+## Disabled
+
+Set `disabled` to disable the input. When `disabled` is set,
+user cannot interact with the input and `Autocomplete` will not show suggestions.
+
+
+
+## Error state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/avatar.mdx b/docs/pages/core/avatar.mdx
new file mode 100644
index 00000000000..4fb075780eb
--- /dev/null
+++ b/docs/pages/core/avatar.mdx
@@ -0,0 +1,89 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { AvatarDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Avatar);
+
+## Usage
+
+
+
+## Placeholder
+
+If the image cannot be loaded or not provided, `Avatar` will display a placeholder instead. By default,
+placeholder is an icon, but it can be changed to any React node:
+
+
+
+## Variants
+
+
+
+## Avatar.Group
+
+`Avatar.Group` component combines multiple avatars into a stack:
+
+
+
+Note that you must not wrap child `Avatar` components with any additional elements,
+but you can use wrap `Avatar` with components that do not render any HTML elements
+in the current tree, for example [Tooltip](/core/tooltip).
+
+```tsx
+import { Avatar } from '@mantine/core';
+
+// Will not work correctly
+function Demo() {
+ return (
+
+
+
+
+ +5
+
+ );
+}
+```
+
+Example of usage with [Tooltip](/core/tooltip/):
+
+
+
+
+
+
+
+You can combine it with [Tooltip](/core/tooltip/) or [Popover](/core/popover/) to show additional information on hover
+
+
+
+## Accessibility
+
+Avatar renders ` ` HTML element. Set `alt` prop to describe image,
+it is also used as `title` attribute for avatar placeholder when the image cannot be loaded.
+
+```tsx
+import { Avatar } from '@mantine/core';
+
+function NotOk() {
+ // Not ok, no alt for image
+ return ;
+}
+
+function Ok() {
+ // Ok, alt is set on tag
+ return ;
+}
+
+function Ehh() {
+ // Ehh, title is not required, but still recommended
+ return RJ ;
+}
+
+function OkPlaceholder() {
+ // Ok, title is set on placeholder
+ return RJ ;
+}
+```
diff --git a/docs/pages/core/background-image.mdx b/docs/pages/core/background-image.mdx
new file mode 100644
index 00000000000..1b7d14feaae
--- /dev/null
+++ b/docs/pages/core/background-image.mdx
@@ -0,0 +1,11 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { BackgroundImageDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.BackgroundImage);
+
+## Usage
+
+
+
+
diff --git a/docs/pages/core/badge.mdx b/docs/pages/core/badge.mdx
new file mode 100644
index 00000000000..2b7cd7f599c
--- /dev/null
+++ b/docs/pages/core/badge.mdx
@@ -0,0 +1,29 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { BadgeDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Badge);
+
+## Usage
+
+
+
+
+
+
+
+## Left and right sections
+
+
+
+## Full width
+
+Set `fullWidth` to make badge span full width of its parent element:
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/blockquote.mdx b/docs/pages/core/blockquote.mdx
new file mode 100644
index 00000000000..2f7efd04cec
--- /dev/null
+++ b/docs/pages/core/blockquote.mdx
@@ -0,0 +1,9 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { BlockquoteDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Blockquote);
+
+## Usage
+
+
diff --git a/docs/pages/core/box.mdx b/docs/pages/core/box.mdx
new file mode 100644
index 00000000000..a69e1235339
--- /dev/null
+++ b/docs/pages/core/box.mdx
@@ -0,0 +1,26 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+
+export default Layout(MDX_DATA.Box);
+
+## Usage
+
+`Box` component is used as a base for all other components. `Box` supports the following features:
+
+- [component prop](/guides/polymorphic)
+- [style props](/styles/style-props)
+- [style prop](/styles/style)
+
+You can use `Box` as a base for your own components or as a replacement for HTML elements:
+
+```tsx
+import { Box } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ My component
+
+ );
+}
+```
diff --git a/docs/pages/core/breadcrumbs.mdx b/docs/pages/core/breadcrumbs.mdx
new file mode 100644
index 00000000000..3300f1c54c1
--- /dev/null
+++ b/docs/pages/core/breadcrumbs.mdx
@@ -0,0 +1,12 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { BreadcrumbsDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Breadcrumbs);
+
+## Usage
+
+`Breadcrumbs` component accepts any number of React nodes as children
+and adds a given separator (defaults to `/`) between them:
+
+
diff --git a/docs/pages/core/burger.mdx b/docs/pages/core/burger.mdx
new file mode 100644
index 00000000000..337c2c06671
--- /dev/null
+++ b/docs/pages/core/burger.mdx
@@ -0,0 +1,36 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { BurgerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Burger);
+
+## Usage
+
+`Burger` component renders open/close menu button.
+Set `opened` and `onClick` props to control Burger state.
+If `opened` prop is set, cross will be rendered, otherwise – burger.
+
+
+
+
+
+## Accessibility
+
+To make `Burger` accessible for screen readers, you need to either set `aria-label` or
+use [VisuallyHidden](/core/visually-hidden) component:
+
+```tsx
+import { Burger, VisuallyHidden } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+
+ Toggle navigation
+
+ >
+ );
+}
+```
diff --git a/docs/pages/core/button.mdx b/docs/pages/core/button.mdx
new file mode 100644
index 00000000000..a94d7331ea2
--- /dev/null
+++ b/docs/pages/core/button.mdx
@@ -0,0 +1,148 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ButtonDemos, StylesDemos, ThemingDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Button);
+
+## Usage
+
+
+
+## Full width
+
+If `fullWidth` prop is set `Button` will take 100% of parent width:
+
+
+
+## Left and right sections
+
+`leftSection` and `rightSection` allow adding icons or any other element to the left and right side of the button.
+When a section is added, padding on the corresponding side is reduced.
+
+Note that `leftSection` and `rightSection` are flipped in [RTL](/styles/rtl) mode
+(`leftSection` is displayed on the right and `rightSection` is displayed on the left).
+
+
+
+## Sections position
+
+`justify` prop sets `justify-content` of `inner` element. You can use it to change the alignment of
+left and right sections. For example, to spread them across the button set `justify="space-between"`.
+
+If you need to align just one section to the side of the button set `justify` to `space-between`
+and add empty ` ` to the opposite section.
+
+
+
+## Compact size
+
+`Button` supports `xs` – `xl` and `compact-xs` – `compact-xl` sizes. `compact` sizes have
+the same font-size as `xs` – `xl` but reduced padding and height.
+
+
+
+
+
+
+
+## Disabled state
+
+To make `Button` disabled, set `disabled` prop, this will prevent any interactions with the button
+and add disabled styles. If you want the button to just look disabled but still be interactive,
+set `data-disabled` prop instead. Note that disabled styles are the same for all variants.
+
+
+
+## Disabled state when Button is link
+
+` ` element does not support `disabled` attribute. To make `Button` disabled when it is
+rendered as a link, set `data-disabled` attribute instead and prevent default behavior in
+`onClick` event handler.
+
+
+
+## Customize disabled styles
+
+To customize disabled styles, it is recommended to use both `&:disabled` and `&[data-disabled]`
+selectors:
+
+- `&:disabled` is used to style the button when `disabled` prop is set and
+ also when the button is disabled by the parent component (for example, when `disabled` prop is set on a
+ ` ` element which contains `Button`).
+- `&[data-disabled]` is used to style the button when it is not actually disabled but should look like
+ it is (for example, `data-disabled` should be used if you need to use [Tooltip](/core/tooltip) with disabled `Button`
+ or when `Button` is used as a link)
+
+
+
+## Disabled button with Tooltip
+
+`onMouseLeave` event [is not triggered](https://github.com/facebook/react/issues/18753) when `Button` is disabled, so if you need to use
+[Tooltip](/core/tooltip) with disabled `Button` you need to set `data-disabled` prop on `Button`
+instead of `disabled`. Note that it is also required to change `onClick` event handler to
+`(event) => event.preventDefault()` as `Button` is not actually disabled and will still trigger
+`onClick` event.
+
+
+
+## Loading state
+
+When `loading` prop is set, `Button` will be disabled and [Loader](/core/loader) with overlay will be rendered
+in the center of the button. [Loader](/core/loader) color depends on `Button` variant.
+
+
+
+## Loader props
+
+You can customize [Loader](/core/loader) with `loaderProps` prop, it accepts all props that
+[Loader](/core/loader) component has:
+
+
+
+
+
+
+
+Example of customizing `Button` with [Styles API](/styles/styles-api) and [data-\* attributes](/styles/data-attributes):
+
+
+
+## Custom variants
+
+To add new `Button` variants, use [data-variant](/styles/variants-sizes) attribute.
+Usually new variants are added on [theme](/theming/theme-object), this way they are
+available in all `Button` components in your application.
+
+
+
+## Customize variants colors
+
+You can customize colors for `Button` and other components variants by adding
+[variantColorResolver](/theming/colors#colors-variant-resolver) to your theme.
+
+
+
+## Button.Group
+
+
+
+Note that you must not wrap child `Button` components with any additional elements:
+
+```tsx
+import { Button } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ This will not work
+
+ Buttons will have incorrect borders
+
+ );
+}
+```
+
+
+
+
diff --git a/docs/pages/core/card.mdx b/docs/pages/core/card.mdx
new file mode 100644
index 00000000000..39799016852
--- /dev/null
+++ b/docs/pages/core/card.mdx
@@ -0,0 +1,84 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CardDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Card);
+
+## Usage
+
+`Card` is a wrapper around [Paper](/core/paper/) component with some additional styles and `Card.Section`
+component that allows to split card into sections. If you do not need sections, you use [Paper](/core/paper/) component instead.
+
+
+
+## Polymorphic component
+
+Card is a [polymorphic component](/guides/polymorphic/) component, you can change its root element:
+
+
+
+## Card.Section
+
+`Card.Section` is a special component that is used to remove Card padding from its children while other elements still have horizontal spacing.
+`Card.Section` works the following way:
+
+- If component is the first child in Card, then it has negative top, left and right margins
+- If it is the last child in Card, then it has negative bottom, left and right margins
+- If it is in the middle then, only the left and right margins will be negative
+
+```tsx
+import { Card } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ {/* top, right, left margins are negative – -1 * theme.spacing.xl */}
+ First section
+
+ {/* Content that is not inside Card.Section will have theme.spacing.xl spacing on all sides relative to Card */}
+ Some other content
+
+ {/* right, left margins are negative – -1 * theme.spacing.xl */}
+ Middle section
+
+ {/* bottom, right, left margins are negative – -1 * theme.spacing.xl */}
+ Last section
+
+ );
+}
+```
+
+Note that `Card` relies on mapping direct children and you cannot use fragments or others wrappers for `Card.Section`:
+
+```tsx
+import { Card } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ Won't work
+
+
+ <>
+ Won't work either
+ >
+
+ Works fine
+
+ );
+}
+```
+
+## Polymorphic Card.Section
+
+`Card.Section` is a [polymorphic component](/guides/polymorphic/) component, you can change its root element:
+
+
+
+## withBorder and inheritPadding props
+
+- `withBorder` prop adds top and bottom border to `Card.Section` depending on its position relative to other content and sections
+- `inheritPadding` prop adds the same left and right padding to `Card.Section` as set in `Card` component
+
+
diff --git a/docs/pages/core/center.mdx b/docs/pages/core/center.mdx
new file mode 100644
index 00000000000..b9218ad7e35
--- /dev/null
+++ b/docs/pages/core/center.mdx
@@ -0,0 +1,18 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CenterDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Center);
+
+## Usage
+
+
+
+## Inline
+
+To use `Center` with inline elements set `inline` prop.
+For example, you can center link icon and label:
+
+
+
+
diff --git a/docs/pages/core/checkbox.mdx b/docs/pages/core/checkbox.mdx
new file mode 100644
index 00000000000..8a983d075a1
--- /dev/null
+++ b/docs/pages/core/checkbox.mdx
@@ -0,0 +1,89 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CheckboxDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Checkbox);
+
+## Usage
+
+
+
+## States
+
+
+
+## Replace icon
+
+
+
+## Indeterminate state
+
+`Checkbox` supports indeterminate state. When `indeterminate` prop is true,
+`checked` prop is ignored:
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Checkbox } from '@mantine/core';
+
+function Demo() {
+ const [checked, setChecked] = useState(false);
+ return (
+ setChecked(event.currentTarget.checked)} />
+ );
+}
+```
+
+## Label with link
+
+
+
+## Checkbox.Group
+
+
+
+## Controlled Checkbox.Group
+
+```tsx
+import { useState } from 'react';
+import { Checkbox } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+
+ return (
+
+
+
+
+ );
+}
+```
+
+
+
+## Accessibility
+
+Set `aria-label` or `label` prop to make checkbox accessible:
+
+```tsx
+import { Checkbox } from '@mantine/core';
+
+// Not ok, input is not labeled
+function Bad() {
+ return ;
+}
+
+// Ok, input is labelled by aria-label
+function GoodAriaLabel() {
+ return ;
+}
+
+// Ok, input is labelled by label element
+function GoodLabel() {
+ return ;
+}
+```
diff --git a/docs/pages/core/chip.mdx b/docs/pages/core/chip.mdx
new file mode 100644
index 00000000000..0e4fceac85f
--- /dev/null
+++ b/docs/pages/core/chip.mdx
@@ -0,0 +1,81 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ChipDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Chip);
+
+## Usage
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Chip } from '@mantine/core';
+
+function Demo() {
+ const [checked, setChecked] = useState(false);
+
+ return (
+ setChecked((v) => !v)}>
+ My chip
+
+ );
+}
+```
+
+## Change checked icon
+
+
+
+## States
+
+
+
+## Chip.Group
+
+`Chip.Group` component manages state of child Chip components,
+set `multiple` prop to allow multiple chips to be selected at a time:
+
+
+
+## Controlled Chip.Group
+
+```tsx
+import { useState } from 'react';
+import { Chip } from '@mantine/core';
+
+function Single() {
+ // string value when multiple is false (default)
+ const [value, setValue] = useState('react');
+
+ return (
+
+ React
+ Angular
+ Svelte
+ Vue
+
+ );
+}
+
+function Multiple() {
+ // array of strings value when multiple is true
+ const [value, setValue] = useState(['react']);
+
+ return (
+
+ React
+ Angular
+ Svelte
+ Vue
+
+ );
+}
+```
+
+## Accessibility
+
+`Chip` and `Chip.Group` components are accessible by default – they are built with native radio/checkbox inputs,
+all keyboard events work the same as with native controls.
diff --git a/docs/pages/core/close-button.mdx b/docs/pages/core/close-button.mdx
new file mode 100644
index 00000000000..d6dba495309
--- /dev/null
+++ b/docs/pages/core/close-button.mdx
@@ -0,0 +1,32 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CloseButtonDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.CloseButton);
+
+## Usage
+
+`CloseButton` renders a button with `X` icon inside. It is used in other Mantine components like [Drawer](/core/drawer) or [Modal](/core/modal).
+
+
+
+## Accessibility
+
+To make `CloseButton` accessible for screen readers, you need to either set `aria-label` or
+use [VisuallyHidden](/core/visually-hidden) component:
+
+```tsx
+import { CloseButton, VisuallyHidden } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+
+ Close modal
+
+ >
+ );
+}
+```
diff --git a/docs/pages/core/code.mdx b/docs/pages/core/code.mdx
new file mode 100644
index 00000000000..4c6f040c6b6
--- /dev/null
+++ b/docs/pages/core/code.mdx
@@ -0,0 +1,24 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CodeDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Code);
+
+## Usage
+
+By default, Code component renders inline `code` html element:
+
+
+
+## Block code
+
+To render code in `pre` element pass `block` prop to Code component:
+
+
+
+## Custom color
+
+By default, code color is gray, you can change it to any valid CSS color or to one
+of the [theme.colors](/theming/colors):
+
+
diff --git a/docs/pages/core/collapse.mdx b/docs/pages/core/collapse.mdx
new file mode 100644
index 00000000000..a3ad6592834
--- /dev/null
+++ b/docs/pages/core/collapse.mdx
@@ -0,0 +1,23 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CollapseDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Collapse);
+
+## Usage
+
+
+
+## Change transition
+
+Set following props to control transition:
+
+- `transitionDuration` – duration in ms
+- `transitionTimingFunction` – timing function (ease, linear, etc.), defaults to `ease`
+- `onTransitionEnd` – called when transition ends (both open and close)
+
+
+
+## Nested Collapse components
+
+
diff --git a/docs/pages/core/color-input.mdx b/docs/pages/core/color-input.mdx
new file mode 100644
index 00000000000..6f404b2ba02
--- /dev/null
+++ b/docs/pages/core/color-input.mdx
@@ -0,0 +1,73 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ColorInputDemos, ColorPickerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.ColorInput);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { ColorInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return ;
+}
+```
+
+## Formats
+
+Component supports hex, hexa, rgb, rgba, hsl and hsla color formats.
+Slider to change opacity is displayed only for hexa, rgba and hsla formats:
+
+
+
+## Disable free input
+
+To disable free input set `disallowInput` prop:
+
+
+
+## With swatches
+
+You can add any amount of predefined color swatches:
+
+
+
+By default, there will be 10 swatches per row, you can change this with `swatchesPerRow` prop,
+like in [ColorPicker](/core/color-picker/) component:
+
+
+
+If you need to restrict color picking to certain colors – disable color picker and disallow free input:
+
+
+
+## Eye dropper
+
+By default, if [EyeDropper API](https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper_API)
+is available, eye dropper icon will be displayed at the right section of the input.
+To disable it, set `withEyeDropper={false}`.
+
+## Error state
+
+
+
+## Disabled state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/color-picker.mdx b/docs/pages/core/color-picker.mdx
new file mode 100644
index 00000000000..ccd0ef884ee
--- /dev/null
+++ b/docs/pages/core/color-picker.mdx
@@ -0,0 +1,65 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ColorPickerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.ColorPicker);
+
+## Usage
+
+
+
+## Color format
+
+`ColorPicker` supports hex, hexa, rgb, rgba, hsl and hsla color formats.
+Slider to change opacity and color preview are displayed only for hexa, rgba and hsla formats:
+
+
+
+## With swatches
+
+You can add predefined color swatches with `swatches` prop:
+
+
+
+By default, `ColorPicker` will display 7 swatches per row, you can configure it with `swatchesPerRow` prop:
+
+
+
+To display swatches without picker set `withPicker={false}` and `fullWidth` props:
+
+
+
+## Size
+
+`ColorPicker` has 5 predefined sizes: `xs`, `sm`, `md`, `lg` and `xl`:
+
+
+
+## fullWidth
+
+Set `fullWidth` prop to stretch component to 100% of parent width. In this case the picker will not
+have fixed width, but you can still use `size` prop to control sizes of sliders.
+
+
+
+
+
+
+
+## Accessibility
+
+ColorPicker component is accessible by default:
+
+- Saturation, hue and alpha sliders are focusable
+- When moused is used to interact with the slider, focus is moved to the slider
+- All values can be changed with arrows
+
+To make component accessible for screen readers, set `saturationLabel`, `hueLabel` and `alphaLabel`:
+
+```tsx
+import { ColorPicker } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+```
diff --git a/docs/pages/core/color-swatch.mdx b/docs/pages/core/color-swatch.mdx
new file mode 100644
index 00000000000..6e3932088fa
--- /dev/null
+++ b/docs/pages/core/color-swatch.mdx
@@ -0,0 +1,20 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ColorSwatchDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.ColorSwatch);
+
+## Usage
+
+
+
+## withShadow
+
+By default, `ColorSwatch` has an inner box-shadow to make it more visible on light backgrounds,
+you can disable it by setting `withShadow={false}` prop:
+
+
+
+
+
+
diff --git a/docs/pages/core/combobox.mdx b/docs/pages/core/combobox.mdx
new file mode 100644
index 00000000000..51f6c7f65c8
--- /dev/null
+++ b/docs/pages/core/combobox.mdx
@@ -0,0 +1,309 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ComboboxDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Combobox);
+
+## Usage
+
+`Combobox` provides a set of components and hooks to custom select, multiselect or autocomplete components.
+The component is very flexible – you have full control of the rendering and logic.
+
+
+
+## Examples
+
+This page contains only a small set of examples, as the full code of the demos is long.
+You can find all 50+ examples on a [separate page](/combobox?e=BasicSelect).
+
+## useCombobox hook
+
+`useCombobox` hook provides combobox store. The store contains the current state of the component
+and handlers to update it. Created store must be passed to the `store` prop of `Combobox`:
+
+```tsx
+import { useCombobox, Combobox } from '@mantine/core';
+
+function Demo() {
+ const combobox = useCombobox();
+ return {/* Your implementation */} ;
+}
+```
+
+## useCombobox options
+
+`useCombobox` hooks accepts an options object with the following properties:
+
+```tsx
+interface UseComboboxOptions {
+ /** Default value for `dropdownOpened`, `false` by default */
+ defaultOpened?: boolean;
+
+ /** Controlled `dropdownOpened` state */
+ opened?: boolean;
+
+ /** Called when `dropdownOpened` state changes */
+ onOpenedChange?(opened: boolean): void;
+
+ /** Called when dropdown closes with event source: keyboard, mouse or unknown */
+ onDropdownClose?(eventSource: ComboboxDropdownEventSource): void;
+
+ /** Called when dropdown opens with event source: keyboard, mouse or unknown */
+ onDropdownOpen?(eventSource: ComboboxDropdownEventSource): void;
+
+ /** Determines whether arrow key presses should loop though items (first to last and last to first), `true` by default */
+ loop?: boolean;
+
+ /** `behavior` passed down to `element.scrollIntoView`, `'instant'` by default */
+ scrollBehavior?: ScrollBehavior;
+}
+```
+
+You can import `UseComboboxOptions` type from `@mantine/core` package:
+
+```tsx
+import type { UseComboboxOptions } from '@mantine/core';
+```
+
+## Combobox store
+
+Combobox store is an object with the following properties:
+
+```tsx
+interface ComboboxStore {
+ /** Current dropdown opened state */
+ dropdownOpened: boolean;
+
+ /** Opens dropdown */
+ openDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
+
+ /** Closes dropdown */
+ closeDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
+
+ /** Toggles dropdown opened state */
+ toggleDropdown(eventSource?: 'keyboard' | 'mouse' | 'unknown'): void;
+
+ /** Selected option index */
+ selectedOptionIndex: number;
+
+ /** Selects `Combobox.Option` by index */
+ selectOption(index: number): void;
+
+ /** Selects first `Combobox.Option` with `active` prop.
+ * If there are no such options, the function does nothing.
+ */
+ selectActiveOption(): string | null;
+
+ /** Selects first `Combobox.Option` that is not disabled.
+ * If there are no such options, the function does nothing.
+ * */
+ selectFirstOption(): string | null;
+
+ /** Selects next `Combobox.Option` that is not disabled.
+ * If the current option is the last one, the function selects first option, if `loop` is true.
+ */
+ selectNextOption(): string | null;
+
+ /** Selects previous `Combobox.Option` that is not disabled.
+ * If the current option is the first one, the function selects last option, if `loop` is true.
+ * */
+ selectPreviousOption(): string | null;
+
+ /** Resets selected option index to -1, removes `data-combobox-selected` from selected option */
+ resetSelectedOption(): void;
+
+ /** Triggers `onClick` event of selected option.
+ * If there is no selected option, the function does nothing.
+ */
+ clickSelectedOption(): void;
+
+ /** Updates selected option index to currently selected or active option.
+ * The function is required to be used with searchable components to update selected option index
+ * when options list changes based on search query.
+ */
+ updateSelectedOptionIndex(target?: 'active' | 'selected'): void;
+
+ /** List id, used for `aria-*` attributes */
+ listId: string | null;
+
+ /** Sets list id */
+ setListId(id: string): void;
+
+ /** Ref of `Combobox.Search` input */
+ searchRef: React.MutableRefObject;
+
+ /** Moves focus to `Combobox.Search` input */
+ focusSearchInput(): void;
+
+ /** Ref of the target element */
+ targetRef: React.MutableRefObject;
+
+ /** Moves focus to the target element */
+ focusTarget(): void;
+}
+```
+
+You can import `ComboboxStore` type from `@mantine/core` package:
+
+```tsx
+import type { ComboboxStore } from '@mantine/core';
+```
+
+## useCombobox handlers
+
+Combobox store handlers can be used to control `Combobox` state.
+For example, to open the dropdown, call `openDropdown` handler:
+
+```tsx
+import { useCombobox, Combobox, Button } from '@mantine/core';
+
+function Demo() {
+ const combobox = useCombobox();
+
+ return (
+
+
+ combobox.openDropdown()}>Open dropdown
+
+
+ {/* Your implementation */}
+
+ );
+}
+```
+
+You can use store handlers in `useCombobox` options. For example, you can
+call `selectFirstOption` when the dropdown is opened and `resetSelectedOption`
+when it is closed:
+
+```tsx
+import { useCombobox, Combobox } from '@mantine/core';
+
+function Demo() {
+ const combobox = useCombobox({
+ onDropdownOpen: () => combobox.selectFirstOption(),
+ onDropdownClose: () => combobox.resetSelectedOption(),
+ });
+
+ return {/* Your implementation */} ;
+}
+```
+
+## Combobox.Target
+
+`Combobox.Target` should be used as a wrapper for the target element or component.
+`Combobox.Target` marks its child as a target for dropdown and sets `aria-*` attributes
+and adds keyboard event listeners to it.
+
+`Combobox.Target` requires a single child element or component. The child component
+must accept `ref` and `...others` props. You can use any Mantine component as a target without
+any additional configuration, for example, [Button](/core/button/), [TextInput](/core/text-input/)
+or [InputBase](/core/input/#inputbase-component).
+
+Example of using `Combobox.Target` with [TextInput](/core/text-input/) component:
+
+
+
+Example of using `Combobox.Target` with [Button](/core/button) component:
+
+
+
+## Split events and dropdown targets
+
+In some cases, you might need to use different elements as an events target and as a dropdown.
+Use `Combobox.EventsTarget` to add `aria-*` attributes and keyboard event handlers, and
+`Combobox.DropdownTarget` to position the dropdown relative to the target.
+
+You can have as many `Combobox.EventsTarget` as you need, but only one `Combobox.DropdownTarget`
+per `Combobox`.
+
+Example of using `Combobox.EventsTarget` and `Combobox.DropdownTarget` with [PillsInput](/core/pills-input) component
+to create a searchable multiselect component:
+
+
+
+## Update selected option index
+
+`updateSelectedOptionIndex` handler is required to be called when options list changes.
+Usually, the options list changes when options are filtered based on the search query. In this case,
+you need to call `updateSelectedOptionIndex` in `onChange` handler of the search input.
+
+Example of using `updateSelectedOptionIndex` handler in searchable select component:
+
+
+
+## Search input
+
+If you prefer search input inside the dropdown, use `Combobox.Search` component.
+To focus the search input, call `combobox.focusSearchInput`, usually it is done
+when the dropdown is opened. To prevent focus loss after the dropdown is closed,
+call `combobox.focusTarget`:
+
+
+
+## Select first option
+
+Use `combobox.selectFirstOption` function to select the first option. It is useful
+if you want to select the first option when user searching for options in the list.
+If there are no options available, it will do nothing.
+
+
+
+## Active option
+
+Set `active` prop on `Combobox.Option` component to mark it as active.
+By default, an active option does not have any styles, you can use `data-combobox-active`
+[data attribute](/styles/data-attributes) to style it.
+
+`combobox.selectActiveOption` function selects active option. Usually, it is called
+when the dropdown is opened:
+
+
+
+## Options groups
+
+Render `Combobox.Option` components inside `Combobox.Group` to create options group.
+`Combobox.Group` label will be automatically hidden if the group does not have any
+children.
+
+
+
+## Scrollable list
+
+Set `max-height` style on either `Combobox.Dropdown` or `Combobox.Options` to make the
+options list scrollable. You can use `mah` [style prop](/styles/style-props) to set
+`max-height`.
+
+
+
+## Scrollable list with ScrollArea
+
+You can also use [ScrollArea or ScrollArea.Autosize](/core/scroll-area) components
+instead of native scrollbars:
+
+
+
+## Hide dropdown
+
+Set `hidden` prop on `Combobox.Dropdown` to hide the dropdown. For example,
+it can be useful when you want to show the dropdown only when there is at least
+one option available:
+
+
+
+## Control dropdown opened state
+
+To control the dropdown opened state, pass `opened` to `useCombobox` hook:
+
+
+
+## Without dropdown
+
+You can use `Combobox` without dropdown. To do so, use `Combobox.EventsTarget` instead
+of `Combobox.Target`:
+
+
+
+
+
+
diff --git a/docs/pages/core/container.mdx b/docs/pages/core/container.mdx
new file mode 100644
index 00000000000..e705f8dcdf4
--- /dev/null
+++ b/docs/pages/core/container.mdx
@@ -0,0 +1,35 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ContainerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Container);
+
+## Usage
+
+`Container` centers content and limits its `max-width` to the value specified in `size` prop.
+Note that the `size` prop does not make `max-width` responsive, for example,
+when it set to `lg` it will always be `lg` regardless of screen size.
+
+
+
+## Fluid
+
+Set `fluid` prop to make container fluid, it will take 100% of available width,
+it is the same as setting `size="100%"`.
+
+
+
+## Customize sizes
+
+You can customize existing `Container` sizes and add new ones with [CSS variables](/styles/styles-api)
+on [theme](/theming/theme-object):
+
+
+
+## Responsive max-width
+
+To make `Container` `max-width` responsive, use [Styles API](/styles/styles-api) to set
+`classNames`. For example, you can add `responsive` size that will make `Container`
+`max-width` different depending on screen size:
+
+
diff --git a/docs/pages/core/copy-button.mdx b/docs/pages/core/copy-button.mdx
new file mode 100644
index 00000000000..975640f1b59
--- /dev/null
+++ b/docs/pages/core/copy-button.mdx
@@ -0,0 +1,26 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { CopyButtonDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.CopyButton);
+
+## Usage
+
+`CopyButton` is based on [use-clipboard](/hooks/use-clipboard/) hook.
+Its children is a function that receives an object with the following properties:
+
+- `copied` – boolean value that indicates that a given value was recently copied to the clipboard, it resets after a given timeout (defaults to 500ms)
+- `copy` – function that should be called to copy given value to clipboard
+
+
+
+## Security
+
+Due to security reasons `CopyButton` component will not work in iframes and may not work with local files opened with `file://` protocol
+(component will work fine with local websites that are using `http://` protocol). You can learn more about `navigator.clipboard` [here](https://web.dev/async-clipboard/).
+
+## Timeout
+
+You can provide a custom `copied` reset `timeout`:
+
+
diff --git a/docs/pages/core/dialog.mdx b/docs/pages/core/dialog.mdx
new file mode 100644
index 00000000000..0a492c8d030
--- /dev/null
+++ b/docs/pages/core/dialog.mdx
@@ -0,0 +1,37 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { DialogDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Dialog);
+
+## Usage
+
+`Dialog` is a simplified version of [Modal](/core/modal/) component.
+It does not include most of accessibility and usability [Modal](/core/modal/) features:
+
+- Focus trap is not available
+- Does not close on click outside
+- Does not have overlay
+
+Use `Dialog` to attract attention with not important information or action,
+for example, you can create an email subscription form:
+
+
+
+## Change position
+
+`Dialog` is rendered in [Portal](/core/portal/) and has fixed position, set `position` prop to control dialog's position:
+
+```tsx
+// Dialog in top left corner
+
+
+// Dialog in bottom left corner
+
+```
+
+## Accessibility
+
+`Dialog` is not accessible and most likely will not be announced by screen reader,
+make sure you do not put any important information. In most cases it would be better
+to select [Modal](/core/modal/), [Drawer](/core/drawer/) or [Notifications](/others/notifications/).
diff --git a/docs/pages/core/divider.mdx b/docs/pages/core/divider.mdx
new file mode 100644
index 00000000000..a56f40222bc
--- /dev/null
+++ b/docs/pages/core/divider.mdx
@@ -0,0 +1,21 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { DividerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Divider);
+
+## Usage
+
+
+
+## With label
+
+
+
+## Sizes
+
+
+
+## Vertical orientation
+
+
diff --git a/docs/pages/core/drawer.mdx b/docs/pages/core/drawer.mdx
new file mode 100644
index 00000000000..ea659c1e5cd
--- /dev/null
+++ b/docs/pages/core/drawer.mdx
@@ -0,0 +1,134 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { DrawerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Drawer);
+
+## Usage
+
+
+
+## Position
+
+Drawer can be placed on `left` (default), `top`, `right` and `bottom`. Control drawer position with `position` prop,
+for example ` `.
+
+
+
+## Customize overlay
+
+`Drawer` uses [Overlay](/core/overlay/) component, you can set any props that [Overlay](/core/overlay/)
+supports with `overlayProps`:
+
+
+
+## Sizes
+
+You can change drawer width/height (depends on `position`) by setting `size` prop to predefined size or any valid width,
+for example, `size="55%"` or `size={200}`:
+
+```tsx
+import { Drawer } from '@mantine/core';
+
+function Demo() {
+ return (
+ {}}>
+ {/* Drawer content */}
+
+ );
+}
+```
+
+
+
+## Remove header
+
+To remove header set `withCloseButton={false}`
+
+
+
+## Drawer with scroll
+
+
+
+## Usage with ScrollArea
+
+
+
+## Change transition
+
+`Drawer` is built with [Transition](/core/transition/) component. Use `transitionProps`
+prop to customize any [Transition](/core/transition/) properties:
+
+
+
+## Initial focus
+
+`Drawer` uses [FocusTrap](/core/focus-trap/) to trap focus. Add `data-autofocus`
+attribute to the element that should receive initial focus.
+
+
+
+## Control behavior
+
+The following props can be used to control `Drawer` behavior.
+In most cases it is not recommended to turn these features off –
+it will make the component less accessible.
+
+- `trapFocus` – determines whether focus should be trapped inside drawer
+- `closeOnEscape` – determines whether the drawer should be closed when `Escape` key is pressed
+- `closeOnClickOutside` – determines whether the drawer should be closed when user clicks on the overlay
+- `returnFocus` – determines whether focus should be returned to the element that was focused before the drawer was opened
+
+## Compound components
+
+You can use the following compound components to have full control over the `Drawer` rendering:
+
+- `Drawer.Root` – context provider
+- `Drawer.Overlay` – render [Overlay](/core/overlay/)
+- `Drawer.Content` – main drawer element, should include all drawer content
+- `Drawer.Header` – sticky header, usually contains `Drawer.Title` and `Drawer.CloseButton`
+- `Drawer.Title` – `h2` element, `aria-labelledby` of `Drawer.Content` is pointing to this element, usually is rendered inside `Drawer.Header`
+- `Drawer.CloseButton` – close button, usually rendered inside `Drawer.Header`
+- `Drawer.Body` – a place for main content, `aria-describedby` of `Drawer.Content` is pointing to this element
+
+
+
+## Fixed elements offset
+
+`Drawer` component uses [react-remove-scroll](https://github.com/theKashey/react-remove-scroll)
+package to lock scroll. To properly size these `elements` add a `className` to them ([documentation](https://github.com/theKashey/react-remove-scroll#positionfixed-elements)):
+
+```tsx
+import { RemoveScroll } from '@mantine/core';
+
+// to make "width: 100%"
+
+
+// to make "right: 0"
+
+```
+
+## Accessibility
+
+`Drawer` component follows [WAI-ARIA recommendations](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog) on accessibility.
+
+Set `title` props to make component accessible, will add `aria-labelledby` to the content element:
+
+```tsx
+import { Drawer } from '@mantine/core';
+
+function Demo() {
+ return {}} />;
+}
+```
+
+To set close button `aria-label` use `closeButtonProps`:
+
+```tsx
+import { Drawer } from '@mantine/core';
+
+function Demo() {
+ return {}} />;
+}
+```
diff --git a/docs/pages/core/fieldset.mdx b/docs/pages/core/fieldset.mdx
new file mode 100644
index 00000000000..075968b9b89
--- /dev/null
+++ b/docs/pages/core/fieldset.mdx
@@ -0,0 +1,15 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { FieldsetDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Fieldset);
+
+## Usage
+
+
+
+## Disabled
+
+Set `disabled` prop to disable all inputs and buttons inside the fieldset:
+
+
diff --git a/docs/pages/core/file-button.mdx b/docs/pages/core/file-button.mdx
new file mode 100644
index 00000000000..b3ceed1f564
--- /dev/null
+++ b/docs/pages/core/file-button.mdx
@@ -0,0 +1,22 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { FileButtonDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.FileButton);
+
+## Usage
+
+
+
+## Multiple files
+
+Set `multiple` prop to allow picking multiple files:
+
+
+
+## Reset file
+
+`resetRef` should be used to fix issue with stale value on hidden input element as input type file cannot be controlled.
+Call `resetRef` when user selection is cleared:
+
+
diff --git a/docs/pages/core/file-input.mdx b/docs/pages/core/file-input.mdx
new file mode 100644
index 00000000000..f67d1877d45
--- /dev/null
+++ b/docs/pages/core/file-input.mdx
@@ -0,0 +1,69 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { FileInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.FileInput);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+When `multiple` is `false`:
+
+```tsx
+import { useState } from 'react';
+import { FileInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState(null);
+ return ;
+}
+```
+
+When `multiple` is `true`:
+
+```tsx
+import { useState } from 'react';
+import { FileInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+ return ;
+}
+```
+
+## Multiple
+
+Set `multiple` to allow user to pick more than one file:
+
+
+
+## Accept
+
+Set `accept` prop to restrict files selection to specific mime types:
+
+
+
+## Custom value component
+
+
+
+## Error state
+
+
+
+## Disabled state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/flex.mdx b/docs/pages/core/flex.mdx
new file mode 100644
index 00000000000..75b399e6aee
--- /dev/null
+++ b/docs/pages/core/flex.mdx
@@ -0,0 +1,21 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { FlexDemos } from '@mantine/demos';
+import { FLEX_STYLE_PROPS_DATA } from '@mantine/core';
+import { StylePropsTable } from '@/components/StylePropsTable';
+
+export default Layout(MDX_DATA.Flex);
+
+## Usage
+
+
+
+## Supported props
+
+
+
+## Responsive props
+
+`Flex` component props can have responsive values the same way as other [style props](/styles/style-props/):
+
+
diff --git a/docs/pages/core/focus-trap.mdx b/docs/pages/core/focus-trap.mdx
new file mode 100644
index 00000000000..77dea44b177
--- /dev/null
+++ b/docs/pages/core/focus-trap.mdx
@@ -0,0 +1,26 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { FocusTrapDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.FocusTrap);
+
+## Usage
+
+FocusTrap is a component implementation of [use-focus-trap](/hooks/use-focus-trap/) hook,
+it is used in all Mantine components that require focus trap ([Modal](/core/modal/), [DatePicker](/dates/date-picker/), [Popover](/core/popover/), etc.).
+
+
+
+## Initial focus
+
+To define the element that will receive initial focus set `data-autofocus` attribute:
+
+
+
+## Focus trapping logic
+
+- Focus is trapped within child node if `active` prop is `true`
+- When FocusTrap component is mounted or when `active` prop changes from `false` to `true` first element with `data-autofocus` attribute is focused
+- If there are no elements with `data-autofocus` attribute, then the first element that supports keyboard interaction is focused
+- If the target element does not have focusable elements or does not support `ref`, then the focus trap will not work
+- Trap stops working when element outside of the `FocusTrap` child is focused
diff --git a/docs/pages/core/grid.mdx b/docs/pages/core/grid.mdx
new file mode 100644
index 00000000000..4cf0607986b
--- /dev/null
+++ b/docs/pages/core/grid.mdx
@@ -0,0 +1,97 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { GridDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Grid);
+
+## Usage
+
+
+
+## Columns span
+
+`Grid.Col` `span` prop controls the ratio of column width to the total width of the row.
+By default, grid uses 12 columns layout, so `span` prop can be any number from 1 to 12.
+
+Examples:
+
+- ` ` – 3 / 12 = 25% of row width
+- ` ` – 4 / 12 = 33% of row width
+- ` ` – 6 / 12 = 50% of row width
+- ` ` – 12 / 12 = 100% of row width
+
+`span` prop also supports object syntax to change column width based on viewport width,
+it accepts `xs`, `sm`, `md`, `lg` and `xl` keys and values from 1 to 12. The syntax
+is the same as in [style props](/styles/style-props).
+
+In the following example `span={{ base: 12, md: 6, lg: 3 }}`:
+
+- `base` – 12 / 12 = 100% of row width when viewport width is less than `md` breakpoint
+- `md` – 6 / 12 = 50% of row width when viewport width is between `md` and `lg` breakpoints
+- `lg` – 3 / 12 = 25% of row width when viewport width is greater than `lg` breakpoint
+
+
+
+## Gutter
+
+Set `gutter` prop to control spacing between columns. The prop works the same
+way as [style props](/styles/style-props) – you can reference `theme.spacing` values
+with `xs`, `sm`, `md`, `lg` and `xl` strings and use object syntax to change gutter
+based on viewport width:
+
+
+
+## Grow
+
+If `grow` prop is set, column will grow to fill the remaining space in the row:
+
+
+
+## Column offset
+
+Set `offset` prop on `Grid.Col` component to add gaps to the grid. `offset` prop
+supports the same syntax as `span` prop: a number from 1 to 12 or an object with `xs`, `sm`, `md`, `lg` and `xl` keys and values from 1 to 12.
+
+
+
+## Order
+
+Set the `order` prop on `Grid.Col` component to change the order of columns. `order` prop
+supports the same syntax as `span` prop: a number from 1 to 12 or an object with `xs`, `sm`, `md`, `lg` and `xl` keys and values from 1 to 12.
+
+
+
+## Multiple rows
+
+Once columns `span` and `offset` sum exceeds `columns` prop (12 by default),
+columns are moved to the next row:
+
+
+
+## Justify and align
+
+You can control `justify-content` and `align-items` CSS properties with `justify` and `align` props on `Grid` component:
+
+
+
+## Auto sized columns
+
+All columns in a row with `span="auto"` grow as much as they can to fill the row.
+In the following example, the second column takes up 50% of the row while the other two columns automatically resize to fill the remaining space:
+
+
+
+## Fit column content
+
+If you set `span="content"`, the column's size will automatically adjust to match the width of its content:
+
+
+
+## Change columns count
+
+By default, grid uses 12 columns layout, you can change it by setting `columns` prop on `Grid` component.
+Note that in this case, columns span and offset will be calculated relative to this value.
+
+In the following example, first column takes 50% with 12 span (12/24), second and third take 25% (6/24):
+
+
diff --git a/docs/pages/core/group.mdx b/docs/pages/core/group.mdx
new file mode 100644
index 00000000000..093d3b01d00
--- /dev/null
+++ b/docs/pages/core/group.mdx
@@ -0,0 +1,47 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { GroupDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Group);
+
+## Usage
+
+`Group` is a horizontal flex container. If you need a vertical flex container, use [Stack](/core/stack)
+component instead. If you need to have full control over flex container properties, use [Flex](/core/flex) component.
+
+
+
+## preventGrowOverflow
+
+`preventGrowOverflow` prop allows you to control how `Group` children should behave when there is not enough
+space to fit them all on one line. By default, children are not allowed to take more space than
+`(1 / children.length) * 100%` of parent width (`preventGrowOverflow` is set to `true`). To change
+this behavior, set `preventGrowOverflow` to `false` and children will be allowed to grow and take
+as much space as they need.
+
+
+
+## Group children
+
+**!important** `Group` works correctly only with React elements.
+Strings, numbers, fragments may have incorrect styles if `grow` prop is set:
+
+```tsx
+// Invalid Group usage, do not do this
+import { Group } from '@mantine/core';
+
+function InvalidDemo() {
+ return (
+
+ First string
+ <>
+ element inside fragment
+ another inside fragment
+ >
+ {20}
+
+ );
+}
+```
+
+
diff --git a/docs/pages/core/highlight.mdx b/docs/pages/core/highlight.mdx
new file mode 100644
index 00000000000..6a00bbe154b
--- /dev/null
+++ b/docs/pages/core/highlight.mdx
@@ -0,0 +1,34 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { HighlightDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Highlight);
+
+## Usage
+
+Use Highlight component to highlight a substring in a given string with a mark tag.
+
+Pass the main string as children to Highlight component and string part that should be highlighted to `highlight` prop.
+If the main string does not include `highlight` part, it will be ignored.
+`Highlight` ignores trailing whitespace and highlights all matched characters sequences.
+
+
+
+## Highlight multiple substrings
+
+To highlight multiple substrings, provide an array of values:
+
+
+
+## Change highlight styles
+
+Default [Mark](/core/mark/) styles can be overwritten with `highlightStyles` prop, it accepts either a function with a subscription to theme
+or an object with styles:
+
+
+
+## Text props
+
+Highlight is based on [Text](/core/text/) component, all its props are available:
+
+
diff --git a/docs/pages/core/hover-card.mdx b/docs/pages/core/hover-card.mdx
new file mode 100644
index 00000000000..344b8d01460
--- /dev/null
+++ b/docs/pages/core/hover-card.mdx
@@ -0,0 +1,118 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { HoverCardDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.HoverCard);
+
+## Usage
+
+
+
+## Delays
+
+Set open and close delays in ms with `openDelay` and `closeDelay` props:
+
+
+
+## With interactive elements
+
+`HoverCard` is displayed only when the mouse is over the target element or dropdown,
+you can use anchors and buttons within dropdowns, using inputs is not recommended:
+
+
+
+## HoverCard.Target children
+
+`HoverCard.Target` requires an element or a component as a single child –
+strings, fragments, numbers and multiple elements/components are not supported and **will throw an error**.
+Custom components must provide a prop to get root element ref, all Mantine components support ref out of the box.
+
+```tsx
+import { HoverCard, Badge } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+ Native button – ok
+
+
+ {/* OK */}
+
+ Mantine component – ok
+
+
+ {/* String, NOT OK – will throw error */}
+ Raw string
+
+ {/* Number, NOT OK – will throw error */}
+ {2}
+
+ {/* Fragment, NOT OK – will throw error */}
+
+ <>Fragment, NOT OK, will throw error>
+
+
+ {/* Multiple nodes, NOT OK – will throw error */}
+
+ More that one node
+ NOT OK, will throw error
+
+ >
+ );
+}
+```
+
+## Required ref prop
+
+Custom components that are rendered inside `HoverCard.Target` are required to support `ref` prop:
+
+```tsx
+// Example of code that WILL NOT WORK
+import { HoverCard } from '@mantine/core';
+
+function MyComponent() {
+ return My component
;
+}
+
+// This will not work – MyComponent does not support ref
+function Demo() {
+ return (
+
+
+
+
+
+ );
+}
+```
+
+Use `forwardRef` function to forward ref to root element:
+
+```tsx
+// Example of code that will work
+import { forwardRef } from 'react';
+import { HoverCard } from '@mantine/core';
+
+const MyComponent = forwardRef((props, ref) => (
+
+ My component
+
+));
+
+// Works correctly – ref is forwarded
+function Demo() {
+ return (
+
+
+
+
+
+ );
+}
+```
+
+## Accessibility
+
+`HoverCard` is ignored by screen readers and cannot be activated with keyboard, use it to display only additional information
+that is not required to understand the context.
diff --git a/docs/pages/core/image.mdx b/docs/pages/core/image.mdx
new file mode 100644
index 00000000000..6f55ac9fe5a
--- /dev/null
+++ b/docs/pages/core/image.mdx
@@ -0,0 +1,48 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ImageDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Image);
+
+## Usage
+
+`Image` is a wrapper for `img` with minimal styles. By default, the image
+will take 100% of parent width. The image size can be controlled with `w`
+and `h` [style props](/styles/style-props).
+
+
+
+## Image height
+
+In most case, you will need to set image height to prevent layout jumps when
+image is loading. You can do so with `h` [style props](/styles/style-props).
+
+
+
+## Image fit
+
+By default the image has `object-fit: cover` style - it will
+resize to cover parent element. To change this behavior, set `w="auto"` and `fit="contain"` props.
+
+
+
+## Fallback image
+
+Set `fallbackSrc` prop to display fallback image when image fails to load:
+
+
+
+## Usage with Next.js Image
+
+`Image` component is a [polymorphic component](/guides/polymorphic), its root element can be changed with `component` prop.
+You can use it with `next/image` and other similar components.
+
+```tsx
+import NextImage from 'next/image';
+import { Image } from '@mantine/core';
+import myImage from './my-image.jpg';
+
+function Demo() {
+ return ;
+}
+```
diff --git a/docs/pages/core/indicator.mdx b/docs/pages/core/indicator.mdx
new file mode 100644
index 00000000000..d460db2dcb2
--- /dev/null
+++ b/docs/pages/core/indicator.mdx
@@ -0,0 +1,34 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { IndicatorDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Indicator);
+
+## Usage
+
+
+
+## Inline
+
+When the target element has a fixed width, set `inline` prop to add `display: inline-block;` styles to
+Indicator container. Alternatively, you can set width and height with `style` prop if you still want the root
+element to keep `display: block`.
+
+
+
+## Offset
+
+Set `offset` to change indicator position. It is useful when Indicator component is
+used with children that have border-radius:
+
+
+
+## Processing animation
+
+
+
+## Disabled
+
+Set `disabled` to hide the indicator:
+
+
diff --git a/docs/pages/core/input.mdx b/docs/pages/core/input.mdx
new file mode 100644
index 00000000000..c03a5fa506f
--- /dev/null
+++ b/docs/pages/core/input.mdx
@@ -0,0 +1,229 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { InputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Input);
+
+## Disclaimer
+
+**!important:** In most cases, you should not use `Input` in your application.
+`Input` is a base for other inputs and was not designed to be used directly.
+Use `Input` to create custom inputs, for other cases prefer [TextInput](/core/text-input/)
+or other component.
+
+```tsx
+import { Input, TextInput } from '@mantine/core';
+
+// Incorrect usage, input is not accessible
+function Incorrect() {
+ return (
+
+
+
+ );
+}
+
+// Use TextInput instead of Input everywhere you want to use Input,
+// it is accessible by default and includes Input.Wrapper
+function Correct() {
+ return ;
+}
+```
+
+## Usage
+
+`Input` component is used as base for some other inputs ([NativeSelect](/core/native-select/), [TextInput](/core/text-input/), [Textarea](/core/textarea/), etc.).
+The purpose of the `Input` is to provide shared styles and features to other inputs.
+
+
+
+## Left and right sections
+
+You can use `leftSection` and `rightSection` props to add icons or other elements to the left and right side of the input.
+You can control the following sections styles with props:
+
+- `leftSectionWidth` / `rightSectionWidth` – width of the section
+- `leftSectionPointerEvents` / `rightSectionPointerEvents` – pointer-events CSS property
+
+
+
+## Change input element
+
+Input is a [polymorphic component](/guides/polymorphic), the default root element is `input`,
+but it can be changed to any other element or component.
+
+Example of using `Input` as `button` and `select`:
+
+
+
+Example of using [react-imask](https://github.com/uNmAnNeR/imaskjs/tree/master/packages/react-imask) with `Input`:
+
+
+
+## Input.Wrapper component
+
+`Input.Wrapper` component is used in all other inputs
+([TextInput](/core/text-input/), [NativeSelect](/core/native-select/), [Textarea](/core/textarea/), etc.)
+under the hood, you _do not need to wrap your inputs with it, as it is already included in all of them_.
+Use `Input.Wrapper` only when you want to create custom inputs.
+
+
+
+## inputWrapperOrder
+
+`inputWrapperOrder` allows configuring the order of `Input.Wrapper` parts.
+It accepts an array of four elements: `label`, `input`, `error` and `description`.
+Note that it is not required to include all of them, you can use only those that you need
+– parts that are not included will not be rendered.
+
+
+
+## inputContainer
+
+With `inputContainer` prop, you can enhance inputs that use `Input.Wrapper` under the hood,
+for example, you can add [Tooltip](/core/tooltip/) to the [TextInput](/core/text-input/) when
+the input is focused:
+
+
+
+## required and withAsterisk props
+
+All components that are based on `Input.Wrapper` support `required` and `withAsterisk` props.
+When set to true, both of these props will add a red asterisk to the end of the label.
+The only difference is whether input element will have `required` attribute, example with
+[TextInput](/core/text-input/) component:
+
+```tsx
+import { TextInput } from '@mantine/core';
+
+// Will display required asterisk and add `required` attribute to the input element
+function RequiredDemo() {
+ return ;
+}
+
+// Will only display the asterisk, `required` attribute is not added to the input element
+function AsteriskDemo() {
+ return ;
+}
+```
+
+## error prop
+
+All inputs that use `Input.Wrapper` under the hood support `error` prop.
+When set to `true`, it will add a red border to the input. You can also pass a React node to display
+an error message below the input. To only display error message without a red border, set `error` prop
+to React node and `withErrorStyles={false}`:
+
+
+
+## Input.Label, Input.Description and Input.Error components
+
+`Input.Label`, `Input.Error` and `Input.Description` components can be used to create custom
+form layouts if the default `Input.Wrapper` layout does not meet your requirements.
+
+
+
+## Input.Placeholder component
+
+`Input.Placeholder` component can be used to add placeholder to `Input` and `InputBase` components that are based on `button` element
+or do not support placeholder property natively:
+
+
+
+## Default props on theme
+
+You can add [default props](/theming/default-props/) on [theme](/theming/theme-object/)
+to `Input` and `Input.Wrapper` components. These default props will be inherited by all inputs
+that use `Input` and `Input.Wrapper` under the hood ([TextInput](/core/text-input/), [NativeSelect](/core/native-select/), [Textarea](/core/textarea/), etc.):
+
+
+
+## Styles on theme
+
+Same as with default props, you can use `Input` and `Input.Wrapper` [Styles API](/styles/styles-api/)
+on [theme](/theming/theme-object/) to add styles to all inputs:
+
+
+
+## Change focus styles
+
+Use `&:focus-within` selector to change inputs focus styles. You can apply these styles to
+one component with `classNames` prop or to all inputs with [Styles API](/styles/styles-api/)
+on [theme](/theming/theme-object/).
+
+
+
+## InputBase component
+
+`InputBase` component combines `Input` and `Input.Wrapper` components and supports `component` prop:
+
+
+
+## Styles API
+
+`Input` and `Input.Wrapper` components support [Styles API](/styles/styles-api) –
+you can customize styles of any inner element with `classNames` and `styles` props.
+
+`Input` Styles API selectors:
+
+
+
+`Input.Wrapper` Styles API selectors:
+
+
+
+
+
+## Accessibility
+
+If you use `Input` component without associated label element, set `aria-label`:
+
+```tsx
+import { Input } from '@mantine/core';
+
+// ok – the input is labelled by the aria-label
+function WithAriaLabel() {
+ return ;
+}
+
+// ok – the input is labelled by the label element
+function WithLabel() {
+ return (
+ <>
+ Your email
+
+ >
+ );
+}
+```
+
+When you use `Input` with `Input.Wrapper` it is required to set `id` on both components
+to connect label and other elements with the input:
+
+```tsx
+import { Input } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+You can use [use-id](/hooks/use-id) to generate unique ids:
+
+```tsx
+import { useId } from '@mantine/hooks';
+import { Input } from '@mantine/core';
+
+function Demo() {
+ const id = useId();
+ return (
+
+
+
+ );
+}
+```
diff --git a/docs/pages/core/json-input.mdx b/docs/pages/core/json-input.mdx
new file mode 100644
index 00000000000..e6453b700b1
--- /dev/null
+++ b/docs/pages/core/json-input.mdx
@@ -0,0 +1,42 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { JsonInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.JsonInput);
+
+## Usage
+
+`JsonInput` is based on [Textarea](/core/textarea/) component,
+it includes json validation logic and option to format input value on blur:
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { JsonInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return ;
+}
+```
+
+## Input props
+
+
+
+
+
+## Disabled state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/kbd.mdx b/docs/pages/core/kbd.mdx
new file mode 100644
index 00000000000..d877718fc86
--- /dev/null
+++ b/docs/pages/core/kbd.mdx
@@ -0,0 +1,9 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { KbdDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Kbd);
+
+## Usage
+
+
diff --git a/docs/pages/core/list.mdx b/docs/pages/core/list.mdx
new file mode 100644
index 00000000000..0384c246956
--- /dev/null
+++ b/docs/pages/core/list.mdx
@@ -0,0 +1,27 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ListDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.List);
+
+## Usage
+
+
+
+## With icons
+
+You can replace list bullets with icon. To do so provide following props:
+
+- `icon` on List component will be used as default icon for all list elements
+- `icon` on List.Item component will override context icon from List
+- `spacing` – spacing between list items from theme or any valid CSS value to set spacing, defaults to `0`
+- `center` – center item content with icon
+- `size` – set font size from theme
+
+
+
+## Nested lists
+
+Set `withPadding` prop to offset nested lists and `listStyleType` to control bullet type:
+
+
diff --git a/docs/pages/core/loader.mdx b/docs/pages/core/loader.mdx
new file mode 100644
index 00000000000..19cdf6f2425
--- /dev/null
+++ b/docs/pages/core/loader.mdx
@@ -0,0 +1,47 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { LoaderDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Loader);
+
+## Usage
+
+`Loader` component supports 3 types of loaders: `oval`, `bars` and `dots` by default. All
+loaders are animated with CSS for better performance.
+
+
+
+## Size prop
+
+You can pass any valid CSS values and numbers to `size` prop. Numbers are treated as px, but
+converted to [rem](/styles/rem). For example, `size={32}` will produce
+`--loader-size: 2rem` CSS variable.
+
+
+
+## Adding custom loaders
+
+`Loader` component is used in other components ([Button](/core/button), [ActionIcon](/core/action-icon), [LoadingOverlay](/core/loading-overlay), etc.).
+You can change loader type with [default props](/theming/default-props) by setting `type`.
+You can also add a custom CSS or SVG loader with `loaders` [default prop](/theming/default-props).
+
+### Custom CSS only loader
+
+Note that in order for `size` and `color` props to work with custom loaders, you need to
+use `--loader-size` and `--loader-color` CSS variables in your loader styles.
+
+
+
+### Custom SVG loader
+
+It is recommended to use CSS only loaders, as SVG based animations may have the following issues:
+
+- High CPU usage – loader may look glitchy on low-end devices
+- Loader animation may not start playing until js is loaded – user may see static loader
+
+In your SVG loader, you need to use `--loader-size` and `--loader-color` variables the same
+way as in CSS only custom loader in order for `size` and `color` props to work. Usually,
+you would need to set `width` and `height` to `var(--loader-size)` and `fill`/`stroke` to
+`var(--loader-color)`.
+
+
diff --git a/docs/pages/core/loading-overlay.mdx b/docs/pages/core/loading-overlay.mdx
new file mode 100644
index 00000000000..88876f4599e
--- /dev/null
+++ b/docs/pages/core/loading-overlay.mdx
@@ -0,0 +1,21 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { LoadingOverlayDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.LoadingOverlay);
+
+## Usage
+
+`LoadingOverlay` renders an overlay with a loader over the parent element with relative position.
+It is usually used to indicate loading state of forms.
+Note that elements under overlay are still focusable with keyboard, remember to add additional logic to handle this case.
+
+`LoadingOverlay` rendering is controlled by `visible` prop:
+
+
+
+## Loader props
+
+You can pass props down to the [Loader](/core/loader) component with `loaderProps`:
+
+
diff --git a/docs/pages/core/mark.mdx b/docs/pages/core/mark.mdx
new file mode 100644
index 00000000000..ae244ee0d59
--- /dev/null
+++ b/docs/pages/core/mark.mdx
@@ -0,0 +1,9 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { MarkDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Mark);
+
+## Usage
+
+
diff --git a/docs/pages/core/menu.mdx b/docs/pages/core/menu.mdx
new file mode 100644
index 00000000000..fcc581a3748
--- /dev/null
+++ b/docs/pages/core/menu.mdx
@@ -0,0 +1,121 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { MenuDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Menu);
+
+## Usage
+
+
+
+## Controlled
+
+Dropdown opened state can be controlled with `opened` and `onChange` props:
+
+```tsx
+import { useState } from 'react';
+import { Menu } from '@mantine/core';
+
+function Demo() {
+ const [opened, setOpened] = useState(false);
+ return (
+
+ {/* Menu content */}
+
+ );
+}
+```
+
+## Show menu on hover
+
+Set `trigger="hover"` to reveal dropdown when hovers over menu target and dropdown.
+`closeDelay` and `openDelay` props can be used to control open and close delay in ms.
+Note that:
+
+- If you set `closeDelay={0}` then menu will close before user will reach dropdown, set `offset={0}` to remove space between target element and dropdown
+- Menu with `trigger="hover"` is not accessible – users that navigate with keyboard will not be able to use it
+
+
+
+## Disabled items
+
+
+
+## Dropdown position
+
+
+
+## Transitions
+
+Menu dropdown can be animated with any of premade transitions from [Transition](/core/transition/) component:
+
+
+
+## Custom component as Menu.Item
+
+By default, `Menu.Item` renders as button element, to change that set `component` prop:
+
+
+
+## Custom component as target
+
+
+
+
+
+
+
+## Accessibility
+
+Menu follows [WAI-ARIA recommendations](https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html):
+
+- Dropdown element has `role="menu"` and `aria-labelledby="target-id"` attributes
+- Target element has `aria-haspopup="menu"`, `aria-expanded`, `aria-controls="dropdown-id"` attributes
+- Menu item has `role="menuitem"` attribute
+
+## Supported target elements
+
+Uncontrolled Menu with `trigger="click"` (default) will be accessible only when used with `button` element or component that renders it ([Button](/core/button/), [ActionIcon](/core/action-icon/), etc.).
+Other elements will not support `Space` and `Enter` key presses.
+
+## Hover menu
+
+Menu with `trigger="hover"` is not accessible – it cannot be accessed with keyboard,
+use it only if you do not care about accessibility.
+
+## Keyboard interactions
+
+
diff --git a/docs/pages/core/modal.mdx b/docs/pages/core/modal.mdx
new file mode 100644
index 00000000000..f63c6140e57
--- /dev/null
+++ b/docs/pages/core/modal.mdx
@@ -0,0 +1,158 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ModalDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Modal);
+
+## Usage
+
+
+
+## Center modal vertically
+
+
+
+## Remove header
+
+To remove header set `withCloseButton={false}`:
+
+
+
+## Change size
+
+You can change modal width by setting `size` prop to predefined size or any valid width, for example, `55%` or `50rem`.
+`Modal` width cannot exceed `100vw`.
+
+```tsx
+import { Modal } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+```
+
+
+
+## Size auto
+
+`Modal` with `size="auto"` will have width to fit its content:
+
+
+
+## Fullscreen
+
+Fullscreen modal will take the entire screen, it is usually better to change transition to `fade`
+when `fullScreen` prop is set:
+
+
+
+To switch Modal to fullscreen on devices with small screens only use [use-media-query](/hooks/use-media-query/) hook.
+`size` prop is ignored if `fullScreen` prop is set:
+
+
+
+## Customize overlay
+
+`Modal` uses [Overlay](/core/overlay/) component, you can set any props that [Overlay](/core/overlay/)
+supports with `overlayProps`:
+
+
+
+## Modal with scroll
+
+
+
+## Usage with ScrollArea
+
+
+
+## Change offsets
+
+Use `xOffset`/`yOffset` to configure horizontal/vertical content offsets:
+
+
+
+## Change transitions
+
+`Modal` is built with [Transition](/core/transition/) component. Use `transitionProps`
+prop to customize any [Transition](/core/transition/) properties:
+
+
+
+## Initial focus
+
+Modal uses [FocusTrap](/core/focus-trap/) to trap focus. Add `data-autofocus`
+attribute to the element that should receive initial focus.
+
+
+
+## Control behavior
+
+The following props can be used to control `Modal` behavior.
+In most cases, it is not recommended to turn these features off –
+it will make the component less accessible.
+
+- `trapFocus` – determines whether focus should be trapped inside modal
+- `closeOnEscape` – determines whether the modal should be closed when `Escape` key is pressed
+- `closeOnClickOutside` – determines whether the modal should be closed when user clicks on the overlay
+- `returnFocus` – determines whether focus should be returned to the element that was focused before the modal was opened
+
+## Compound components
+
+You can use the following compound components to have full control over the `Modal` rendering:
+
+- `Modal.Root` – context provider
+- `Modal.Overlay` – render [Overlay](/core/overlay/)
+- `Modal.Content` – main modal element, should include all modal content
+- `Modal.Header` – sticky header, usually contains `Modal.Title` and `Modal.CloseButton`
+- `Modal.Title` – `h2` element, `aria-labelledby` of `Modal.Content` is pointing to this element, usually is rendered inside `Modal.Header`
+- `Modal.CloseButton` – close button, usually rendered inside `Modal.Header`
+- `Modal.Body` – a place for main content, `aria-describedby` of `Modal.Content` is pointing to this element
+
+
+
+## Fixed elements offset
+
+`Modal` component uses [react-remove-scroll](https://github.com/theKashey/react-remove-scroll)
+package to lock scroll. To properly size these `elements` add a `className` to them ([documentation](https://github.com/theKashey/react-remove-scroll#positionfixed-elements)):
+
+```tsx
+import { RemoveScroll } from '@mantine/core';
+
+// to make "width: 100%"
+
+
+// to make "right: 0"
+
+```
+
+## Accessibility
+
+`Modal` component follows [WAI-ARIA recommendations](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog) on accessibility.
+
+Set `title` props to make component accessible, will add `aria-labelledby` to the content element:
+
+```tsx
+import { Modal } from '@mantine/core';
+
+function Demo() {
+ return {}} />;
+}
+```
+
+To set close button `aria-label` use `closeButtonProps`:
+
+```tsx
+import { Modal } from '@mantine/core';
+
+function Demo() {
+ return {}} />;
+}
+```
diff --git a/docs/pages/core/multi-select.mdx b/docs/pages/core/multi-select.mdx
new file mode 100644
index 00000000000..fd6e2c64948
--- /dev/null
+++ b/docs/pages/core/multi-select.mdx
@@ -0,0 +1,136 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { MultiSelectDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.MultiSelect);
+
+
+
+## Usage
+
+`MultiSelect` provides a way to enter multiple values.
+`MultiSelect` is similar to [TagsInput](/core/tags-input), but it does not allow entering custom values.
+
+
+
+## Controlled
+
+`MultiSelect` value must be an array of strings, other types are not supported.
+`onChange` function is called with an array of strings as a single argument.
+
+```tsx
+import { useState } from 'react';
+import { MultiSelect } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+ return ;
+}
+```
+
+## Searchable
+
+Set `searchable` prop to allow filtering options by user input:
+
+
+
+## Nothing found
+
+Set `nothingFoundMessage` prop to display given message when no options match search query.
+If `nothingFoundMessage` is not set, `MultiSelect` dropdown will be hidden when no options match search query.
+The message is not displayed when trimmed search query is empty.
+
+
+
+## Checked option icon
+
+Set `checkIconPosition` prop to `left` or `right` to control position of check icon in active option.
+To remove the check icon, set `withCheckIcon={false}`.
+
+
+
+## Max selected values
+
+You can limit the number of selected values with `maxValues` prop. This will not allow adding more values
+once the limit is reached.
+
+
+
+## Hide selected options
+
+To remove selected options from the list of available options, set `hidePickedOptions` prop:
+
+
+
+
+
+
+
+
+
+## Sort options
+
+By default, options are sorted by their position in the data array. You can change this behavior
+with `filter` function:
+
+
+
+
+
+
+
+## Scrollable dropdown
+
+By default, the options list is wrapped with [ScrollArea.Autosize](/core/scroll-area).
+You can control dropdown max-height with `maxDropdownHeight` prop if you do not change the default settings.
+
+If you want to use native scrollbars, set `withScrollArea={false}`. Note that in this case,
+you will need to change dropdown styles with [Styles API](/styles/styles-api).
+
+
+
+## Group options
+
+
+
+## Disabled options
+
+When option is disabled, it cannot be selected and is ignored in keyboard navigation.
+Note that user can still enter disabled option as a value. If you want to prohibit certain values,
+use controlled component and filter them out in `onChange` function.
+
+
+
+
+
+## Input props
+
+
+
+
+
+## Read only
+
+Set `readOnly` to make the input read only. When `readOnly` is set,
+`MultiSelect` will not show suggestions and will not call `onChange` function.
+
+
+
+## Disabled
+
+Set `disabled` to disable the input. When `disabled` is set,
+user cannot interact with the input and `MultiSelect` will not show suggestions.
+
+
+
+## Error state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/native-select.mdx b/docs/pages/core/native-select.mdx
new file mode 100644
index 00000000000..ebd58dea7cc
--- /dev/null
+++ b/docs/pages/core/native-select.mdx
@@ -0,0 +1,155 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { NativeSelectDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.NativeSelect);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { NativeSelect } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+
+ return (
+ setValue(event.currentTarget.value)}
+ data={['React', 'Angular', 'Svelte', 'Vue']}
+ />
+ );
+}
+```
+
+## Adding options
+
+`NativeSelect` allows passing options in two ways:
+
+- `data` prop array
+- `children` prop with `option` components
+
+Note that if `children` is used, `data` will be ignored.
+
+### data prop
+
+`data` prop accepts values in one of the following formats:
+
+1. Array of strings:
+
+```tsx
+import { NativeSelect } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+```
+
+2. Array of objects with `label`, `value` and `disabled` keys:
+
+```tsx
+import { NativeSelect } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+3. Array of grouped options (string format):
+
+```tsx
+import { NativeSelect } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+4. Array of grouped options (object format):
+
+```tsx
+import { NativeSelect } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
+
+Example of `data` prop with array of grouped options:
+
+
+
+### children options
+
+To add options with `children` prop, use `option` elements to add options and `optgroup`
+elements to group them:
+
+
+
+## Left and right sections
+
+
+
+## disabled
+
+
+
+## error
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/nav-link.mdx b/docs/pages/core/nav-link.mdx
new file mode 100644
index 00000000000..e29e7e85306
--- /dev/null
+++ b/docs/pages/core/nav-link.mdx
@@ -0,0 +1,25 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { NavLinkDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.NavLink);
+
+## Usage
+
+
+
+## Active
+
+Set `active` prop to add active styles to `NavLink`. You can customize active styles with `color` and `variant` props:
+
+
+
+## Nested NavLinks
+
+To create nested links put `NavLink` as children of another `NavLink`:
+
+
+
+
+
+
diff --git a/docs/pages/core/notification.mdx b/docs/pages/core/notification.mdx
new file mode 100644
index 00000000000..55f88a2e645
--- /dev/null
+++ b/docs/pages/core/notification.mdx
@@ -0,0 +1,32 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { NotificationDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Notification);
+
+## Usage
+
+Notification is a base component for notification system.
+Build your own or use [@mantine/notifications](/others/notifications/) package.
+
+
+
+## With icon
+
+
+
+
+
+
+
+## Accessibility
+
+To support screen readers, set close button aria-label or title with `closeButtonProps`:
+
+```tsx
+import { Notification } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+```
diff --git a/docs/pages/core/number-input.mdx b/docs/pages/core/number-input.mdx
new file mode 100644
index 00000000000..66ca102ae78
--- /dev/null
+++ b/docs/pages/core/number-input.mdx
@@ -0,0 +1,111 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { NumberInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.NumberInput);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { NumberInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return ;
+}
+```
+
+## min and max
+
+Set `min` and `max` props to limit the input value:
+
+
+
+## Clamp behavior
+
+By default, the value is clamped when the input is blurred. If you set `clampBehavior="strict"`,
+it will not be possible to enter value outside of min/max range. Note that this option
+may cause issues if you have tight `min` and `max`, for example `min={10}` and `max={20}`.
+If you need to disable value clamping entirely, set `clampBehavior="none"`.
+
+
+
+## Prefix and suffix
+
+Set `prefix` and `suffix` props to add given string to the start or end of the input value:
+
+
+
+## Negative numbers
+
+By default, negative numbers are allowed. Set `allowNegative={false}` to allow only positive numbers.
+
+
+
+## Decimal numbers
+
+By default, decimal numbers are allowed. Set `allowDecimal={false}` to allow only integers.
+
+
+
+## Decimal scale
+
+`decimalScale` controls how many decimal places are allowed:
+
+
+
+## Fixed decimal scale
+
+Set `fixedDecimalScale` to always display fixed number of decimal places:
+
+
+
+## Decimal separator
+
+Set `decimalSeparator` to change decimal separator character:
+
+
+
+## Thousand separator
+
+Set `thousandSeparator` prop to separate thousands with a character. You can control
+grouping logic with `thousandsGroupStyle`, it accepts: `thousand`, `lakh`, `wan`, `none` values.
+
+
+
+## Right section
+
+By default, the right section is occupied by increment and decrement buttons.
+To hide them, set `hideControls` prop. You can also use `rightSection` prop to render anything
+in the right section to replace the default controls.
+
+
+
+## Custom increment and decrement controls
+
+You can get a ref with `increment` and `decrement` functions to create custom controls:
+
+
+
+## Error state
+
+
+
+## Disabled state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/overlay.mdx b/docs/pages/core/overlay.mdx
new file mode 100644
index 00000000000..4f65d8490d9
--- /dev/null
+++ b/docs/pages/core/overlay.mdx
@@ -0,0 +1,30 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { OverlayDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Overlay);
+
+## Usage
+
+`Overlay` takes 100% of width and height of parent container or viewport if `fixed` prop is set.
+Set `color` and `backgroundOpacity` props to change `Overlay` background-color. Note that `backgroundOpacity` prop
+does not change CSS opacity property, it changes background-color. For example, if you set
+`color="#000"` and `backgroundOpacity={0.85}` background-color will be `rgba(0, 0, 0, 0.85)`:
+
+
+
+## Gradient
+
+Set `gradient` prop to use background-image instead of background-color. When `gradient` prop is set,
+`color` and `backgroundOpacity` props are ignored.
+
+
+
+## Blur
+
+Set `blur` prop to add `backdrop-filter: blur({value})` styles.
+Note that `backdrop-filter` [is not supported in all browsers](https://caniuse.com/css-backdrop-filter).
+
+
+
+
diff --git a/docs/pages/core/pagination.mdx b/docs/pages/core/pagination.mdx
new file mode 100644
index 00000000000..fdcd0661c7d
--- /dev/null
+++ b/docs/pages/core/pagination.mdx
@@ -0,0 +1,65 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PaginationDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Pagination);
+
+## Usage
+
+
+
+## Controlled
+
+To control component state provide `value` and `onChange` props:
+
+```tsx
+import { useState } from 'react';
+import { Pagination } from '@mantine/core';
+
+function Demo() {
+ const [activePage, setPage] = useState(1);
+ return ;
+}
+```
+
+## Siblings
+
+Control number of active item siblings with `siblings` prop:
+
+
+
+## Boundaries
+
+Control number of items displayed after previous and before next buttons with `boundaries` prop:
+
+
+
+
+
+
+
+## Compound components
+
+You can use the following compound components to have full control over the `Modal` rendering:
+
+- `Pagination.Root` – context provider
+- `Pagination.Items` – items list
+- `Pagination.Next` – next control
+- `Pagination.Previous` – previous control
+- `Pagination.First` – first control
+- `Pagination.Last` – last control
+
+
+
+## Controls as links
+
+
+
+## Change icons
+
+
+
+## use-pagination hook
+
+If you need more flexibility `@mantine/hooks` package exports [use-pagination](/hooks/use-pagination/) hook,
+you can use it to create custom pagination components.
diff --git a/docs/pages/core/paper.mdx b/docs/pages/core/paper.mdx
new file mode 100644
index 00000000000..0c8f0bf44e8
--- /dev/null
+++ b/docs/pages/core/paper.mdx
@@ -0,0 +1,11 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PaperDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Paper);
+
+## Usage
+
+
+
+
diff --git a/docs/pages/core/password-input.mdx b/docs/pages/core/password-input.mdx
new file mode 100644
index 00000000000..427dbc28748
--- /dev/null
+++ b/docs/pages/core/password-input.mdx
@@ -0,0 +1,69 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PasswordInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.PasswordInput);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { PasswordInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return setValue(event.currentTarget.value)} />;
+}
+```
+
+## Controlled visibility toggle
+
+Control visibility state with `visible` and `onVisibilityChange` props,
+for example, the props can be used to sync visibility state between two inputs:
+
+
+
+## Change visibility toggle icon
+
+To change visibility toggle icon, pass a React component that accepts `reveal` prop to `visibilityToggleIcon`:
+
+
+
+## Strength meter example
+
+Password strength meter example with [Progress](/core/progress/) and [Popover](/core/popover/) components:
+
+
+
+## Error state
+
+
+
+
+
+
+
+
+
+
+
+To set `aria-label` on the visibility toggle button, use `visibilityToggleButtonProps` prop:
+
+```tsx
+import { PasswordInput } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ );
+}
+```
diff --git a/docs/pages/core/pill.mdx b/docs/pages/core/pill.mdx
new file mode 100644
index 00000000000..7e6bbe5b3f8
--- /dev/null
+++ b/docs/pages/core/pill.mdx
@@ -0,0 +1,20 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PillDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Pill);
+
+## Usage
+
+
+
+## Inside inputs
+
+`Pill` component is designed to be used inside inputs. It can be used to create custom
+multi select or tag inputs.
+
+
+
+
+
+
diff --git a/docs/pages/core/pills-input.mdx b/docs/pages/core/pills-input.mdx
new file mode 100644
index 00000000000..72e7cd8acb9
--- /dev/null
+++ b/docs/pages/core/pills-input.mdx
@@ -0,0 +1,73 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PillsInputDemos, ComboboxDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.PillsInput);
+
+## Usage
+
+`PillsInput` is a utility component that can be used to create custom tag inputs, multi selects and
+other similar components. By itself it does not include any logic, it only renders given children.
+Usually, `PillsInput` is used in combination with [Pill](/core/pill) component.
+
+
+
+## Input props
+
+
+
+
+
+## Searchable select example
+
+Combine `PillsInput` with [Combobox](/core/combobox) to create searchable multiselect:
+
+
+
+## Accessibility
+
+If `PillsInput` is used without label prop, it will not be announced properly by screen reader:
+
+```tsx
+import { PillsInput } from '@mantine/core';
+
+// Inaccessible input – screen reader will not announce it properly
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+Set `aria-label` on the `PillsInput.Field` component to make the input accessible.
+In this case label will not be visible, but screen reader will announce it:
+
+```tsx
+import { PillsInput } from '@mantine/core';
+
+// Accessible input – it has aria-label
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
+
+If `label` prop is set, the input will be accessible it is not required to set `aria-label`:
+
+```tsx
+import { PillsInput } from '@mantine/core';
+
+// Accessible input – it has associated label element
+function Demo() {
+ return (
+
+
+
+ );
+}
+```
diff --git a/docs/pages/core/pin-input.mdx b/docs/pages/core/pin-input.mdx
new file mode 100644
index 00000000000..9abe9556884
--- /dev/null
+++ b/docs/pages/core/pin-input.mdx
@@ -0,0 +1,47 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PinInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.PinInput);
+
+## Usage
+
+
+
+## Regex type
+
+You can use regular expression to validate user input. Characters that do not match given expression
+will be disregarded. For example, to create a `PinInput` that will accept only numbers from `0` to `3`,
+set `type={/^[0-3]+/}`:
+
+
+
+## One time code
+
+Some operating systems expose the last received SMS code to be used by applications like your keyboard.
+If the current form input asks for this code, your keyboard adapts and proposes the code as keyboard-suggestion.
+Prop `oneTimeCode` makes your input setting `autocomplete="one-time-code"` which allows using that feature.
+
+```tsx
+import { PinInput } from '@mantine/core';
+
+function OneTimeCodeInput() {
+ return ;
+}
+```
+
+## Accessibility
+
+Inputs do not have associated labels, set `aria-label` to make component visible to the screen reader:
+
+```tsx
+import { PinInput } from '@mantine/core';
+
+function Accessibility() {
+ return ;
+}
+```
+
+
+
+
diff --git a/docs/pages/core/popover.mdx b/docs/pages/core/popover.mdx
new file mode 100644
index 00000000000..067c7a41ca7
--- /dev/null
+++ b/docs/pages/core/popover.mdx
@@ -0,0 +1,146 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { PopoverDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Popover);
+
+## Usage
+
+
+
+## Controlled
+
+You can control Popover state with `opened` and `onChange` props:
+
+```tsx
+import { useState } from 'react';
+import { Popover, Button } from '@mantine/core';
+
+function Demo() {
+ const [opened, setOpened] = useState(false);
+ return (
+
+
+ setOpened((o) => !o)}>Toggle popover
+
+
+ Dropdown
+
+ );
+}
+```
+
+Controlled example with mouse events:
+
+
+
+## Focus trap
+
+If you need to use interactive elements (inputs, buttons, etc.) inside `Popover.Dropdown`, set `trapFocus` prop:
+
+
+
+## Inline elements
+
+Enable `inline` middleware to use `Popover` with inline elements:
+
+
+
+## Same width
+
+Set `width="target"` prop to make Popover dropdown take the same width as target element:
+
+
+
+## offset
+
+Set `offset` prop to a number to change dropdown position relative to the target element.
+This way you can control dropdown offset on main axis only.
+
+
+
+To control offset on both axis, pass object with `mainAxis` and `crossAxis` properties:
+
+
+
+## Dropdown arrow
+
+Set `withArrow` prop to add an arrow to the dropdown. Arrow is a `div` element rotated with `transform: rotate(45deg)`.
+
+`arrowPosition` prop determines how arrow is position relative to the target element when `position` is set to `*-start` and `*-end` values on `Popover` component.
+By default, the value is `center` – the arrow is positioned in the center of the target element if it is possible.
+
+If you change `arrowPosition` to `side`, then the arrow will be positioned on the side of the target element,
+and you will be able to control arrow offset with `arrowOffset` prop. Note that when `arrowPosition` is set to `center`,
+`arrowOffset` prop is ignored.
+
+
+
+## Disabled
+
+Set `disabled` prop to prevent `Popover.Dropdown` from rendering:
+
+
+
+## Click outside
+
+By default, `Popover` closes when you click outside of the dropdown. To disable this behavior, set `closeOnClickOutside={false}`.
+
+You can configure events that are used for click outside detection with `clickOutsideEvents` prop.
+By default, `Popover` listens to `mousedown` and `touchstart` events. You can change it to any other
+events, for example, `mouseup` and `touchend`:
+
+
+
+## Initial focus
+
+Popover uses [FocusTrap](/core/focus-trap/) component to manage focus.
+Add `data-autofocus` attribute to element that should receive initial focus:
+
+```tsx
+import { Popover } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ Target
+
+
+
+
+
+
+
+ );
+}
+```
+
+
+
+## Accessibility
+
+Popover follows [WAI-ARIA recommendations](https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal):
+
+- Dropdown element has `role="dialog"` and `aria-labelledby="target-id"` attributes
+- Target element has `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls="dropdown-id"` attributes
+
+Uncontrolled Popover will be accessible only when used with `button` element or component that renders it ([Button](/core/button/), [ActionIcon](/core/action-icon/), etc.).
+Other elements will not support `Space` and `Enter` key presses.
+
+## Keyboard interactions
+
+
diff --git a/docs/pages/core/portal.mdx b/docs/pages/core/portal.mdx
new file mode 100644
index 00000000000..261c3746d6a
--- /dev/null
+++ b/docs/pages/core/portal.mdx
@@ -0,0 +1,90 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+
+export default Layout(MDX_DATA.Portal);
+
+## Usage
+
+Portal is a wrapper component for [ReactDOM.createPortal](https://reactjs.org/docs/portals.html) API.
+Render any component or element at the end of `document.body` or at a given element. [Modal](/core/modal/) and [Drawer](/core/drawer/) components are wrapped in Portal by default.
+
+Use Portal to render a component or an element at a different place (defaults to the end of `document.body`).
+Portal is useful when you want to prevent parent styles from interfering with children,
+usually all these styles are related to `position` and `z-index` properties
+and portals are used for components with fixed position, for example, modals.
+
+```tsx
+import { useState } from 'react';
+import { Portal } from '@mantine/core';
+
+function Demo() {
+ const [opened, setOpened] = useState(false);
+
+ return (
+
+ {opened && (
+
+ Your modal content
+
+ )}
+
+ setOpened(true)} type="button">
+ Open modal
+
+
+ );
+}
+```
+
+In the example above, the div element is rendered outside of parent main (before closing body tag),
+but still receives `opened` and `onClose` props. The element will not be affected by parent z-index.
+
+## Specify target dom node
+
+You can specify dom node where portal will be rendered by passing `target` prop:
+
+```tsx
+import { Portal } from '@mantine/core';
+
+const container = document.createElement('div');
+document.body.appendChild(container);
+
+function Demo() {
+ return My portal ;
+}
+```
+
+Alternatively, you can specify selector to render portal in existing element:
+
+```tsx
+import { Portal } from '@mantine/core';
+
+function Demo() {
+ return My portal ;
+}
+```
+
+If you don't specify the target element, new one will be created and appended to the `document.body` for each Portal component.
+
+## Server side rendering
+
+`createPortal` is not supported during server side rendering.
+All components inside Portal are rendered only after the application was mounted to the dom.
+
+## OptionalPortal component
+
+`OptionalPortal` component lets you configure whether children should be rendered in `Portal`.
+It accepts the same props as the `Portal` component:
+
+```tsx
+import { OptionalPortal } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+ This text is rendered in Portal
+ This text is rendered as regular child
+ >
+ );
+}
+```
diff --git a/docs/pages/core/progress.mdx b/docs/pages/core/progress.mdx
new file mode 100644
index 00000000000..579787b01f2
--- /dev/null
+++ b/docs/pages/core/progress.mdx
@@ -0,0 +1,45 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ProgressDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Progress);
+
+## Usage
+
+
+
+## Compound components
+
+
+
+## With tooltips
+
+
+
+
+
+
+
+## Accessibility
+
+- Progress section has `role="progressbar"` attribute
+- Progress section has `aria-valuenow` attribute with current value
+- `aria-valuemin` and `aria-valuemax` attributes are always set to `0` and `100` as component does not support other values
+
+Set `aria-label` attribute to label progress:
+
+```tsx
+import { Progress } from '@mantine/core';
+
+function Demo() {
+ return ;
+}
+
+function DemoCompound() {
+ return (
+
+
+
+ );
+}
+```
diff --git a/docs/pages/core/radio.mdx b/docs/pages/core/radio.mdx
new file mode 100644
index 00000000000..e0a58064e92
--- /dev/null
+++ b/docs/pages/core/radio.mdx
@@ -0,0 +1,62 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { RadioDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Radio);
+
+## Usage
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Radio } from '@mantine/core';
+
+function Demo() {
+ const [checked, setChecked] = useState(false);
+ return setChecked(event.currentTarget.checked)} />;
+}
+```
+
+## Change icon
+
+
+
+## Disabled state
+
+
+
+## Radio.Group component
+
+
+
+## Controlled Radio.Group
+
+```tsx
+import { useState } from 'react';
+import { Radio } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('react');
+
+ return (
+
+
+
+
+
+
+ );
+}
+```
+
+
diff --git a/docs/pages/core/rating.mdx b/docs/pages/core/rating.mdx
new file mode 100644
index 00000000000..e3489d4ce3b
--- /dev/null
+++ b/docs/pages/core/rating.mdx
@@ -0,0 +1,37 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { RatingDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Rating);
+
+## Usage
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Rating } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState(0);
+ return ;
+}
+```
+
+## Read only
+
+
+
+## Fractions
+
+
+
+## Custom symbol
+
+
+
+## Symbols for each item
+
+
diff --git a/docs/pages/core/ring-progress.mdx b/docs/pages/core/ring-progress.mdx
new file mode 100644
index 00000000000..a290764a01e
--- /dev/null
+++ b/docs/pages/core/ring-progress.mdx
@@ -0,0 +1,45 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { RingProgressDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.RingProgress);
+
+## Usage
+
+Set `sections` prop to an array of:
+
+- `value` – number between 0 and 100 – amount of space filled by segment
+- `color` – segment color from theme or any other css color value
+
+
+
+## Size, thickness & rounded caps
+
+Use `size`, `thickness` & `roundCaps` props to configure RingProgress, size and thickness values:
+
+
+
+## Sections tooltips
+
+Add `tooltip` property to section to display floating [Tooltip](/core/tooltip/) when user hovers over it:
+
+
+
+## Root color
+
+Use `rootColor` property to change the root color:
+
+
+
+## Sections props
+
+You can add any additional props to sections:
+
+
+
+## Customize label
+
+You can add any React node as label, e.g. [Text](/core/text/) component with some additional styles
+or [ThemeIcon](/core/theme-icon/):
+
+
diff --git a/docs/pages/core/scroll-area.mdx b/docs/pages/core/scroll-area.mdx
new file mode 100644
index 00000000000..e1419930402
--- /dev/null
+++ b/docs/pages/core/scroll-area.mdx
@@ -0,0 +1,57 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { ScrollAreaDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.ScrollArea);
+
+## Usage
+
+`ScrollArea` component supports the following props:
+
+- `type` defines scrollbars behavior:
+ - `hover` – scrollbars are visible on hover
+ - `scroll` – scrollbars are visible on scroll
+ - `auto` – similar to `overflow: auto` – scrollbars are always visible when the content is overflowing
+ - `always` – same as `auto`, but scrollbars are always visible regardless of whether the content is overflowing
+ - `never` – scrollbars are always hidden
+- `offsetScrollbars` – offset scrollbars with padding
+- `scrollbarSize` – scrollbar size, controls scrollbar and thumb width/height
+- `scrollHideDelay` – delay in ms to hide scrollbars, applicable only when type is `hover` or `scroll`
+
+
+
+## Horizontal scrollbars
+
+
+
+## Subscribe to scroll position changes
+
+Set `onScrollPositionChange` function to subscribe to scroll position changes,
+it will be called each time user scrolls with x and y coordinates:
+
+
+
+## Scroll to position
+
+To programmatically scroll to any position,
+get viewport element ref with `viewportRef` prop and call `scrollTo` method:
+
+
+
+## Styles API
+
+
+
+## Scroll element into view
+
+
+
+## ScrollArea.Autosize
+
+`ScrollArea.Autosize` component allows to create scrollable containers when given max-height is reached:
+
+
+
+## ScrollArea.Autosize with Popover
+
+
diff --git a/docs/pages/core/segmented-control.mdx b/docs/pages/core/segmented-control.mdx
new file mode 100644
index 00000000000..c151882c6a1
--- /dev/null
+++ b/docs/pages/core/segmented-control.mdx
@@ -0,0 +1,110 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SegmentedControlDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.SegmentedControl);
+
+## Usage
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { SegmentedControl } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('react');
+
+ return (
+
+ );
+}
+```
+
+## Data prop
+
+`SegmentedControl` support two different data formats:
+
+1. An array of strings – used when `value` and `label` are the same
+2. An array of objects – used when `value` and `label` are different
+
+```tsx
+import { SegmentedControl } from '@mantine/core';
+
+function ArrayOfStrings() {
+ return ;
+}
+
+function ArrayOfObjects() {
+ return (
+
+ );
+}
+```
+
+## Disabled
+
+To disable `SegmentedControl` item, use array of objects `data` format and set `disabled: true`
+on the item that you want to disable. To disable the entire component, use `disabled` prop.
+
+
+
+## React node as label
+
+You can use any React node as label:
+
+
+
+## Color
+
+By default, `SegmentedControl` uses `theme.white` with shadow in light color scheme and `var(--mantine-color-dark-6)` background color for indicator.
+Set `color` prop to change indicator `background-color`:
+
+
+
+## Transitions
+
+Change transition properties with:
+
+- `transitionDuration` – all transitions duration in ms, `200` by default
+- `transitionTimingFunction` – all transitions timing function, `ease` by default
+
+
+
+## readOnly
+
+Set `readOnly` prop to prevent value from being changed:
+
+
+
+
+
+
+
+## Accessibility and usability
+
+`SegmentedControl` uses radio inputs under the hood, it is accessible by default with no extra steps required if you have text in labels.
+Component support the same keyboard events as a regular radio group.
+
+In case you do not have text in labels (for example, when you want to use `SegmentedControl` with icons only),
+use [VisuallyHidden](/core/visually-hidden) to make component accessible:
+
+
diff --git a/docs/pages/core/select.mdx b/docs/pages/core/select.mdx
new file mode 100644
index 00000000000..098ac6c3792
--- /dev/null
+++ b/docs/pages/core/select.mdx
@@ -0,0 +1,128 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SelectDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Select);
+
+
+
+## Usage
+
+`Select` allows capturing user input based on suggestions from the list.
+Unlike [Autocomplete](/core/autocomplete/), `Select` does not allow entering custom values.
+
+
+
+## Controlled
+
+`Select` value must be a string, other types are not supported.
+`onChange` function is called with a string value as a single argument.
+
+```tsx
+import { useState } from 'react';
+import { Select } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return ;
+}
+```
+
+## Allow deselect
+
+`allowDeselect` prop determines whether the value should be deselected when user clicks on the selected option.
+By default, `allowDeselect` is `true`:
+
+
+
+## Searchable
+
+Set `searchable` prop to allow filtering options by user input:
+
+
+
+## Nothing found
+
+Set `nothingFoundMessage` prop to display given message when no options match search query.
+If `nothingFoundMessage` is not set, `Select` dropdown will be hidden when no options match search query.
+The message is not displayed when trimmed search query is empty.
+
+
+
+## Checked option icon
+
+Set `checkIconPosition` prop to `left` or `right` to control position of check icon in active option.
+To remove the check icon, set `withCheckIcon={false}`.
+
+
+
+
+
+
+
+
+
+## Sort options
+
+By default, options are sorted by their position in the data array. You can change this behavior
+with `filter` function:
+
+
+
+
+
+
+
+## Scrollable dropdown
+
+By default, the options list is wrapped with [ScrollArea.Autosize](/core/scroll-area).
+You can control dropdown max-height with `maxDropdownHeight` prop if you do not change the default settings.
+
+If you want to use native scrollbars, set `withScrollArea={false}`. Note that in this case,
+you will need to change dropdown styles with [Styles API](/styles/styles-api).
+
+
+
+## Group options
+
+
+
+## Disabled options
+
+When option is disabled, it cannot be selected and is ignored in keyboard navigation.
+
+
+
+
+
+## Input props
+
+
+
+
+
+## Read only
+
+Set `readOnly` to make the input read only. When `readOnly` is set,
+`Select` will not show suggestions and will not call `onChange` function.
+
+
+
+## Disabled
+
+Set `disabled` to disable the input. When `disabled` is set,
+user cannot interact with the input and `Select` will not show suggestions.
+
+
+
+## Error state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/simple-grid.mdx b/docs/pages/core/simple-grid.mdx
new file mode 100644
index 00000000000..0d083bbdb68
--- /dev/null
+++ b/docs/pages/core/simple-grid.mdx
@@ -0,0 +1,30 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SimpleGridDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.SimpleGrid);
+
+## Usage
+
+`SimpleGrid` is a responsive grid system with equal width columns.
+It use CSS grid layout. If you need to set different widths for columns, use
+[Grid](/core/grid) component instead.
+
+
+
+## Responsive props
+
+`cols`, `spacing` and `verticalSpacing` props support object notation for responsive values,
+it works the same way as [style props](/styles/style-props): the object may have `base`, `xs`,
+`sm`, `md`, `lg` and `xl` key, and values from those keys will be applied according to current
+viewport width.
+
+In the following example, `cols={{ base: 1, sm: 2, lg: 5 }}` means:
+
+- 1 column if viewport width is less than `sm` breakpoint
+- 2 columns if viewport width is between `sm` and `lg` breakpoints
+- 5 columns if viewport width is greater than `lg` breakpoint
+
+Same logic applies to `spacing` and `verticalSpacing` props.
+
+
diff --git a/docs/pages/core/skeleton.mdx b/docs/pages/core/skeleton.mdx
new file mode 100644
index 00000000000..6f2fbbc656a
--- /dev/null
+++ b/docs/pages/core/skeleton.mdx
@@ -0,0 +1,24 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SkeletonDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Skeleton);
+
+## Usage
+
+Use `Skeleton` to create a placeholder for loading content. `Skeleton` support the following props:
+
+- `height` – height – any valid CSS value
+- `width` – width - any valid CSS value
+- `radius` – key of `theme.radius` or any valid CSS value to set border-radius
+- `circle` – if true width, height and border-radius will equal to value specified in `height` prop
+- `animate` – true by default, controls animation
+
+
+
+## With content
+
+If you want to indicate the loading state of content that is already on page, wrap it with Skeleton
+and control loading overlay visibility with `visible` prop:
+
+
diff --git a/docs/pages/core/slider.mdx b/docs/pages/core/slider.mdx
new file mode 100644
index 00000000000..291155adbd0
--- /dev/null
+++ b/docs/pages/core/slider.mdx
@@ -0,0 +1,150 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SliderDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Slider);
+
+## Usage
+
+
+
+## Controlled
+
+Controlled `Slider`:
+
+```tsx
+import { useState } from 'react';
+import { Slider } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState(40);
+ return ;
+}
+```
+
+Controlled `RangeSlider`:
+
+```tsx
+import { useState } from 'react';
+import { RangeSlider } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState<[number, number]>([20, 80]);
+ return ;
+}
+```
+
+## Disabled
+
+
+
+## onChangeEnd
+
+`onChangeEnd` callback is called when user the slider is stopped from being dragged or value is changed with keyboard.
+You can use it as a debounced callback to avoid too frequent updates.
+
+
+
+## Control label
+
+To change label behavior and appearance, set the following props:
+
+- `label` – formatter function, accepts value as an argument, set null to disable label, defaults to `f => f`
+- `labelAlwaysOn` – if true – label will always be displayed, by default label is visible only when user is dragging
+- `labelTransitionProps` – props passed down to the [Transition](/core/transition) component, can be used to customize label animation
+
+
+
+## Min, max and step
+
+
+
+## Marks
+
+Add any number of marks to slider by setting `marks` prop to an array of objects:
+
+```tsx
+const marks = [
+ { value: 20 }, // -> displays mark on slider track
+ { value: 40, label: '40%' }, // -> adds mark label below slider track
+];
+```
+
+Note that mark value is relative to slider value, not width:
+
+
+
+## Thumb size
+
+
+
+## Thumb children
+
+
+
+## Scale
+
+You can use the `scale` prop to represent the value on a different scale.
+
+In the following demo, the value `x` represents the value `2^x`. Increasing `x` by one increases the represented value by 2 to the power of `x`.
+
+
+
+## Inverted
+
+You can invert the track with the `inverted` prop:
+
+
+
+
+
+
+
+Example of using [Styles API](/styles/styles-api/) to change `Slider` styles:
+
+
+
+## Vertical slider
+
+`Slider` and `RangeSlider` do not provide vertical orientation as it is very rarely used.
+If you need this feature you can build it yourself with [use-move](/hooks/use-move/) hook.
+
+## Accessibility
+
+`Slider` and `RangeSlider` components are accessible by default:
+
+- Thumbs are focusable
+- When the user uses mouse to interact with the slider, focus is moved to the slider track, when the user presses arrows focus is moved to the thumb
+- Value can be changed with arrows with step increment/decrement
+
+To label component for screen readers, add labels to thumbs:
+
+```tsx
+import { Slider, RangeSlider } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+## Keyboard interactions
+
+
diff --git a/docs/pages/core/space.mdx b/docs/pages/core/space.mdx
new file mode 100644
index 00000000000..2117d415bd9
--- /dev/null
+++ b/docs/pages/core/space.mdx
@@ -0,0 +1,35 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SpaceDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Space);
+
+## Usage
+
+Use `Space` component to add horizontal or vertical spacing between elements:
+
+
+
+
+
+
+
+## Where to use
+
+In most cases, you would want to use margin props instead of `Space` when working with Mantine components:
+
+```tsx
+First line
+// is not required as the same can be achieved with margin
+Second line
+```
+
+But when you work with regular HTML elements you do not have access to `theme.spacing` and you may want to use
+`Space` component to skip direct theme subscription:
+
+```tsx
+First line
+
+// Margin props are not available on div, use Space to add spacing from theme
+Second line
+```
diff --git a/docs/pages/core/spoiler.mdx b/docs/pages/core/spoiler.mdx
new file mode 100644
index 00000000000..ffe43f3f80a
--- /dev/null
+++ b/docs/pages/core/spoiler.mdx
@@ -0,0 +1,34 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SpoilerDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Spoiler);
+
+## Usage
+
+Use `Spoiler` to hide long section of content.
+Set `maxHeight` prop to control point at which content will be hidden under spoiler and show/hide control appears.
+If the content height is less than `maxHeight`, the spoiler will just render children.
+
+`hideLabel` and `showLabel` props are required – they are used as spoiler toggle button label in corresponding state.
+
+
+
+## Transition duration
+
+Control transition duration by setting `transitionDuration` prop (transition-duration CSS property in ms).
+To disable animations, set `transitionDuration={0}`:
+
+
+
+## Get control ref
+
+```tsx
+import { useRef } from 'react';
+import { Spoiler } from '@mantine/core';
+
+function Demo() {
+ const spoilerControlRef = useRef(null);
+ return ;
+}
+```
diff --git a/docs/pages/core/stack.mdx b/docs/pages/core/stack.mdx
new file mode 100644
index 00000000000..98f60dda04e
--- /dev/null
+++ b/docs/pages/core/stack.mdx
@@ -0,0 +1,14 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { StackDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Stack);
+
+## Usage
+
+`Stack` is a vertical flex container. If you need a horizontal flex container, use [Group](/core/group)
+component instead. If you need to have full control over flex container properties, use [Flex](/core/flex) component.
+
+
+
+
diff --git a/docs/pages/core/stepper.mdx b/docs/pages/core/stepper.mdx
new file mode 100644
index 00000000000..5fb19e5a769
--- /dev/null
+++ b/docs/pages/core/stepper.mdx
@@ -0,0 +1,149 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { StepperDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Stepper);
+
+## Usage
+
+
+
+## Allow step select
+
+To disable step selection, set `allowStepSelect` prop on `Stepper.Step` component.
+It can be used to prevent the user from reaching next steps while letting them go back and forth between steps they've already reached before:
+
+
+
+## Disable next steps selection
+
+Another way to disable selection of upcoming steps is to use the `allowNextStepsSelect` directly on the `Stepper` component.
+This is useful when you don't need to control the behavior specifically for each step.
+
+
+
+## Color, radius and size
+
+
+
+Component size is controlled by two props: `size` and `iconSize`.
+`size` prop controls icon size, label and description font size.
+`iconSize` allows to overwrite icon size separately from other size values:
+
+
+
+## With custom icons
+
+You can replace the step icon by setting `icon` prop on `Stepper.Step` component.
+To change completed check icon set `completedIcon` on `Stepper` component.
+You can use any React node as an icon: component, string, number:
+
+
+
+You can use `Stepper` with icons only. Note that in this case, you will have to
+set `aria-label` or `title` on `Stepper.Step` component to make it accessible:
+
+
+
+You can also change the completed icon for each step, for example, to indicate error state:
+
+
+
+## Vertical orientation
+
+
+
+## Icon position
+
+To change step icon and body arrangement, set `iconPosition="right"`:
+
+
+
+## Loading state
+
+To indicate loading state set `loading` prop on Step component, `Loader` will replace step icon.
+You can configure the default loader in the [theme](/theming/theme-object/).
+
+
+
+
+
+
+
+Examples of styles customization with Styles API:
+
+
+
+
+
+
+
+## Get step ref
+
+You can get refs of step button and stepper root element (div):
+
+```tsx
+import { useRef } from 'react';
+import { Stepper } from '@mantine/core';
+
+function MyStepper() {
+ const firstStep = useRef(null);
+ const stepper = useRef(null);
+
+ return (
+
+
+
+
+ );
+}
+```
+
+## Wrap Stepper.Step
+
+`Stepper` component relies on `Stepper.Step` order. Wrapping `Stepper.Step` is not supported,
+Instead you will need to use different approaches:
+
+```tsx
+import { Stepper } from '@mantine/core';
+
+// This will not work, step children will not render
+function WillNotWork() {
+ return (
+
+ This part will not render
+
+ );
+}
+
+// Create a separate component for children
+function WillWork() {
+ return This will work as expected!
;
+}
+
+function Demo() {
+ return (
+
+ First step
+ {/* Wrapped Stepper.Step will not render children, do not do that */}
+
+
+
+
+ Third step
+
+ );
+}
+```
+
+## Accessibility
+
+` ` components render button element, set `aria-label` or `title` props
+to make component visible for screen readers in case you do not specify `label` or `description`:
+
+```tsx
+ // -> not ok, empty labels for screen reader
+ // -> ok
+ // -> ok
+ // -> ok
+```
diff --git a/docs/pages/core/switch.mdx b/docs/pages/core/switch.mdx
new file mode 100644
index 00000000000..c66391e44e4
--- /dev/null
+++ b/docs/pages/core/switch.mdx
@@ -0,0 +1,94 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { SwitchDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Switch);
+
+## Usage
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Switch } from '@mantine/core';
+
+function Demo() {
+ const [checked, setChecked] = useState(false);
+ return setChecked(event.currentTarget.checked)} />;
+}
+```
+
+## Inner Labels
+
+
+
+## Icon labels
+
+
+
+## Thumb icon
+
+
+
+## Switch.Group
+
+
+
+## Controlled Switch.Group
+
+```tsx
+import { useState } from 'react';
+import { Switch } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+
+ return (
+
+
+
+
+ );
+}
+```
+
+
+
+
+
+## Get input ref
+
+```tsx
+import { useRef } from 'react';
+import { Switch } from '@mantine/core';
+
+function Demo() {
+ const ref = useRef(null);
+ return ;
+}
+```
+
+## Accessibility
+
+`Switch` is a regular `input[type="checkbox"]`. Set `aria-label` if the `Switch` is used without `label` prop:
+
+```tsx
+import { Switch } from '@mantine/core';
+
+// -> not ok, input is not labeled
+function Bad() {
+ return ;
+}
+
+// -> ok, input has aria-label
+function Good() {
+ return ;
+}
+
+// -> ok, input has associated label
+function AlsoGood() {
+ return ;
+}
+```
diff --git a/docs/pages/core/table.mdx b/docs/pages/core/table.mdx
new file mode 100644
index 00000000000..f93dca4be0a
--- /dev/null
+++ b/docs/pages/core/table.mdx
@@ -0,0 +1,49 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TableDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Table);
+
+## Usage
+
+Table data for all examples:
+
+```tsx
+const elements = [
+ { position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' },
+ { position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' },
+ { position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' },
+ { position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' },
+ { position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' },
+];
+```
+
+
+
+## Spacing
+
+To control spacing use `horizontalSpacing` and `verticalSpacing` props. Both props support spacing from `theme.spacing` and any valid CSS value to set cell padding:
+
+
+
+## Caption and tfoot
+
+Table support tfoot and caption elements. Set `captionSide` prop (top or bottom) to change caption position.
+
+
+
+## Striped and rows hover
+
+
+
+## Scroll container
+
+To prevent viewport overflow wrap `Table` with `Table.ScrollContainer`.
+The component accepts `minWidth` prop which sets minimum width below which table will be scrollable.
+
+
+
+By default, `Table.ScrollContainer` uses [ScrollArea](/core/scroll-area), you can change it
+to native scrollbars by setting `type="native"`:
+
+
diff --git a/docs/pages/core/tabs.mdx b/docs/pages/core/tabs.mdx
new file mode 100644
index 00000000000..65209c436ca
--- /dev/null
+++ b/docs/pages/core/tabs.mdx
@@ -0,0 +1,287 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TabsDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Tabs);
+
+## Usage
+
+
+
+## Controlled Tabs
+
+To control Tabs state, use `value` and `onChange` props:
+
+```tsx
+import { useState } from 'react';
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ const [activeTab, setActiveTab] = useState('first');
+
+ return (
+
+
+ First tab
+ Second tab
+
+
+ First panel
+ Second panel
+
+ );
+}
+```
+
+## Uncontrolled Tabs
+
+If you do not need to subscribe to Tabs state changes, use `defaultValue`:
+
+```tsx
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ return (
+
+
+ First tab
+ Second tab
+
+
+ First panel
+ Second panel
+
+ );
+}
+```
+
+## Change colors
+
+To change colors of all tabs, set `color` on `Tabs` component, to change color of the individual tab,
+set `color` on `Tabs.Tab`.
+
+
+
+## Tabs position
+
+
+
+To display tab on the opposite side, set `margin-left: auto` with `ml="auto"` prop ot with `className`:
+
+
+
+## Inverted tabs
+
+To make tabs inverted, place `Tabs.Panel` components before `Tabs.List` and add `inverted` prop to `Tabs` component:
+
+
+
+## Vertical tabs placement
+
+To change placement of `Tabs.List` in vertical orientation set `placement` prop:
+
+
+
+## Disabled tabs
+
+Set `disabled` prop on `Tabs.Tab` component to disable tab.
+Disabled tab cannot be activated with mouse or keyboard, and they will be skipped when user navigates with arrow keys:
+
+
+
+## Activation mode
+
+By default, tabs are activated when user presses arrows keys or Home/End keys.
+To disable that set `activateTabWithKeyboard={false}` on `Tabs` component:
+
+
+
+## Tab deactivation
+
+By default, active tab cannot be deactivated. To allow that set `allowTabDeactivation` on `Tabs` component:
+
+
+
+## Unmount inactive tabs
+
+By default, inactive `Tabs.Panel` will stay mounted, to unmount inactive tabs, set `keepMounted={false}` on Tabs.
+This is useful when you want to render components that impact performance inside `Tabs.Panel`. Note that
+components that are rendered inside `Tabs.Panel` will reset their state on each mount (tab change).
+
+```tsx
+import { Tabs } from '@mantine/core';
+
+// Second tab panel will be mounted only when user activates second tab
+function Demo() {
+ return (
+
+
+ First tab
+ Second tab
+
+
+ First panel
+ Second panel
+
+ );
+}
+```
+
+## Get tab control ref
+
+```tsx
+import { useRef } from 'react';
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ const secondTabRef = useRef(null);
+
+ return (
+
+
+ First tab
+
+ Second tab
+
+ Third tab
+
+
+ );
+}
+```
+
+## Usage with react-router
+
+```tsx
+ } />
+```
+
+```tsx
+import { useNavigate, useParams } from 'react-router-dom';
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ const navigate = useNavigate();
+ const { tabValue } = useParams();
+
+ return (
+ navigate(`/tabs/${value}`)}>
+
+ First tab
+ Second tab
+
+
+ );
+}
+```
+
+## Usage with Next.js router
+
+```tsx
+// For file /tabs/[activeTab].tsx
+import { useRouter } from 'next/router';
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ const router = useRouter();
+
+ return (
+ router.push(`/tabs/${value}`)}
+ >
+
+ First tab
+ Second tab
+
+
+ );
+}
+```
+
+
+
+
+
+Example of Styles API usage to customize tab styles:
+
+
+
+## Accessibility
+
+Tabs component follows [WAI-ARIA recommendations](https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html) on accessibility.
+
+If you use `Tabs.Tab` without text content, for example, only with icon, then set `aria-label`
+or use [VisuallyHidden](/core/visually-hidden) component:
+
+```tsx
+import { Tabs, VisuallyHidden } from '@mantine/core';
+import { IconCoin } from '@tabler/icons-react';
+
+function Demo() {
+ return (
+
+
+ {/* aria-label is not required, tab is labelled by children */}
+ Chat
+
+ {/* aria-label is required, tab is not labelled by children */}
+ } />
+
+ {/* You can use VisuallyHidden instead of aria-label */}
+ }>
+ Get money
+
+
+
+ );
+}
+```
+
+To set tabs list label, set `aria-label` on `Tabs.List` component, it will be announced by screen reader:
+
+```tsx
+import { Tabs } from '@mantine/core';
+
+function Demo() {
+ return (
+
+ {/* Tabs.List aria-label will be announced when tab is focused for the first time */}
+
+ Most recent
+ Unanswered
+ Archived
+
+
+ );
+}
+```
+
+## Keyboard interactions
+
+
diff --git a/docs/pages/core/tags-input.mdx b/docs/pages/core/tags-input.mdx
new file mode 100644
index 00000000000..4cfa783bdd4
--- /dev/null
+++ b/docs/pages/core/tags-input.mdx
@@ -0,0 +1,135 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TagsInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.TagsInput);
+
+
+
+## Usage
+
+`TagsInput` provides a way to enter multiple values. It can be used with suggestions or without them.
+`TagsInput` is similar to [MultiSelect](/core/multi-select), but it allows entering custom values.
+
+
+
+## Controlled
+
+`TagsInput` value must be an array of strings, other types are not supported.
+`onChange` function is called with an array of strings as a single argument.
+
+```tsx
+import { useState } from 'react';
+import { TagsInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState([]);
+ return ;
+}
+```
+
+## Max selected values
+
+You can limit the number of selected values with `maxTags` prop. This will not allow adding more values
+once the limit is reached.
+
+
+
+## Allow duplicates
+
+By default `TagsInput` does not allow to add duplicate values, but you can change this behavior by
+setting `allowDuplicates` prop. Value is considered duplicate if it is already present in the `value` array,
+regardless of the case and trailing whitespace.
+
+
+
+## Split chars
+
+By default, `TagsInput` splits values by comma (`,`), you can change this behavior by setting
+`splitChars` prop to an array of strings. All values from `splitChars` cannot be included in the final value.
+Values are also splitted on paste.
+
+Example of splitting by `,`, `|` and space:
+
+
+
+## With suggestions
+
+`TagsInput` can be used with suggestions, it will render suggestions list under input and allow to select
+suggestions with keyboard or mouse. Note that user is not limited to suggestions, it is still possible to
+enter custom values. If you want to allow values only from suggestions, use [MultiSelect](/core/multi-select) component instead.
+
+
+
+
+
+
+
+
+
+## Sort options
+
+By default, options are sorted by their position in the data array. You can change this behavior
+with `filter` function:
+
+
+
+
+
+
+
+## Scrollable dropdown
+
+By default, the options list is wrapped with [ScrollArea.Autosize](/core/scroll-area).
+You can control dropdown max-height with `maxDropdownHeight` prop if you do not change the default settings.
+
+If you want to use native scrollbars, set `withScrollArea={false}`. Note that in this case,
+you will need to change dropdown styles with [Styles API](/styles/styles-api).
+
+
+
+## Group options
+
+
+
+## Disabled options
+
+When option is disabled, it cannot be selected and is ignored in keyboard navigation.
+Note that user can still enter disabled option as a value. If you want to prohibit certain values,
+use controlled component and filter them out in `onChange` function.
+
+
+
+
+
+## Input props
+
+
+
+
+
+## Read only
+
+Set `readOnly` to make the input read only. When `readOnly` is set,
+`TagsInput` will not show suggestions and will not call `onChange` function.
+
+
+
+## Disabled
+
+Set `disabled` to disable the input. When `disabled` is set,
+user cannot interact with the input and `TagsInput` will not show suggestions.
+
+
+
+## Error state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/text-input.mdx b/docs/pages/core/text-input.mdx
new file mode 100644
index 00000000000..b035675c116
--- /dev/null
+++ b/docs/pages/core/text-input.mdx
@@ -0,0 +1,43 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TextInputDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.TextInput);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { TextInput } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return setValue(event.currentTarget.value)} />;
+}
+```
+
+## Sections
+
+
+
+## Error state
+
+
+
+## Disabled state
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/pages/core/text.mdx b/docs/pages/core/text.mdx
new file mode 100644
index 00000000000..a187ef48883
--- /dev/null
+++ b/docs/pages/core/text.mdx
@@ -0,0 +1,57 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TextDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Text);
+
+## Usage
+
+
+
+
+
+
+
+## Truncate
+
+Set `truncate` prop to add `text-overflow: ellipsis` styles:
+
+
+
+## Line clamp
+
+Specify maximum number of lines with `lineClamp` prop. This option uses [-webkit-line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp)
+CSS property ([caniuse](https://caniuse.com/css-line-clamp)). Note that `padding-bottom` cannot be set on text element:
+
+
+
+Line clamp can also be used with any children (not only strings), for example with [TypographyStylesProvider](/core/typography-styles-provider/):
+
+
+
+## Inherit styles
+
+Text always applies font-size, font-family and line-height styles,
+but in some cases this is not a desired behavior. To force Text to inherit parent
+styles set `inherit` prop. For example, highlight part of [Title](/core/title/):
+
+
+
+
+
+## span prop
+
+Use `span` prop as a shorthand for `component="span"`:
+
+```tsx
+import { Text } from '@mantine/core';
+
+function Demo() {
+ return (
+ <>
+ Same as below
+ Same as above
+ >
+ );
+}
+```
diff --git a/docs/pages/core/textarea.mdx b/docs/pages/core/textarea.mdx
new file mode 100644
index 00000000000..21a78712338
--- /dev/null
+++ b/docs/pages/core/textarea.mdx
@@ -0,0 +1,39 @@
+import { Layout } from '@/layout';
+import { MDX_DATA } from '@/mdx';
+import { TextareaDemos } from '@mantine/demos';
+
+export default Layout(MDX_DATA.Textarea);
+
+## Usage
+
+
+
+
+
+## Controlled
+
+```tsx
+import { useState } from 'react';
+import { Textarea } from '@mantine/core';
+
+function Demo() {
+ const [value, setValue] = useState('');
+ return