diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3b8aa86..6e98150 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,12 +10,12 @@ jobs:
       fail-fast: false
       matrix:
         node-version:
-          - 16
-          - 14
-          - 12
+          - 22
+          - 20
+          - 18
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
       - run: npm install
diff --git a/index.js b/index.js
index cd07b89..38a36a5 100644
--- a/index.js
+++ b/index.js
@@ -1,218 +1,209 @@
-'use strict';
-module.exports = {
-	parserOptions: {
-		ecmaFeatures: {
-			jsx: true
-		}
-	},
-	plugins: [
-		'react',
-		'react-hooks'
-	],
-	settings: {
-		react: {
-			version: 'detect'
-		}
-	},
-	rules: {
-		'react/boolean-prop-naming': [
-			'error',
-			{
-				validateNested: true
-			}
-		],
-		'react/button-has-type': 'error',
-		'react/jsx-child-element-spacing': 'error',
-		'react/default-props-match-prop-types': 'error',
-		'react/function-component-definition': [
-			'error',
-			{
-				namedComponents: 'function-declaration',
-				unnamedComponents: 'arrow-function'
-			}
-		],
-		'react/hook-use-state': 'error',
-		'react/iframe-missing-sandbox': 'error',
-		'react/no-access-state-in-setstate': 'error',
-		'react/no-array-index-key': 'error',
-		'react/no-arrow-function-lifecycle': 'error',
-		'react/no-children-prop': 'error',
-		'react/no-danger': 'error',
-		'react/no-danger-with-children': 'error',
-		'react/no-deprecated': 'error',
-		'react/no-did-update-set-state': 'error',
-		'react/no-direct-mutation-state': 'error',
-		'react/no-find-dom-node': 'error',
-		'react/no-invalid-html-attribute': 'error',
-		'react/no-is-mounted': 'error',
-		'react/no-namespace': 'error',
-		'react/no-redundant-should-component-update': 'error',
-		'react/no-render-return-value': 'error',
-		'react/no-typos': 'error',
-		'react/no-string-refs': [
-			'error',
-			{
-				noTemplateLiterals: true
-			}
-		],
-		'react/no-this-in-sfc': 'error',
-		'react/no-unescaped-entities': 'error',
-		'react/no-unknown-property': 'error',
-		'react/no-unsafe': 'error',
-		'react/no-unused-prop-types': 'error',
-		'react/no-unused-state': 'error',
-		'react/prefer-read-only-props': 'error',
-		'react/prop-types': 'error',
-		'react/react-in-jsx-scope': 'error',
-		'react/require-default-props': [
-			'error',
-			{
-				forbidDefaultForRequired: true,
-				ignoreFunctionalComponents: true
-			}
-		],
-		'react/self-closing-comp': 'error',
-		'react/state-in-constructor': [
-			'error',
-			'never'
-		],
-		'react/static-property-placement': 'error',
-		'react/style-prop-object': [
-			'error',
-			{
-				allow: [
-					// This allows react-intl’s `<FormattedNumber value={0.42} style='percent'/>`.
-					'FormattedNumber'
-				]
-			}
-		],
-		'react/void-dom-elements-no-children': 'error',
-		'react/jsx-boolean-value': 'error',
-		'react/jsx-closing-bracket-location': [
-			'error',
-			{
-				nonEmpty: 'tag-aligned',
-				selfClosing: false
-			}
-		],
-		'react/jsx-closing-tag-location': 'error',
-		'react/jsx-curly-newline': [
-			'error',
-			{
-				multiline: 'consistent',
-				singleline: 'forbid'
-			}
-		],
-		'react/jsx-curly-spacing': [
-			'error',
-			'never'
-		],
-		'react/jsx-equals-spacing': [
-			'error',
-			'never'
-		],
-		'react/jsx-first-prop-new-line': 'error',
-		'react/jsx-indent': [
-			'error',
-			'tab',
-			{
-				checkAttributes: true,
-				indentLogicalExpressions: true
-			}
-		],
-		'react/jsx-indent-props': [
-			'error',
-			'tab'
-		],
-		'react/jsx-key': [
-			'error',
-			{
-				checkFragmentShorthand: true,
-				checkKeyMustBeforeSpread: true,
-				warnOnDuplicates: true
-			}
-		],
-		'react/jsx-max-props-per-line': [
-			'error',
-			{
-				maximum: 3,
-				when: 'multiline'
-			}
-		],
-		'react/jsx-no-bind': [
-			'error',
-			{
-				allowArrowFunctions: true
-			}
-		],
-		'react/jsx-no-comment-textnodes': 'error',
-		'react/jsx-no-constructed-context-values': 'error',
-		'react/jsx-no-duplicate-props': [
-			'error',
-			{
-				ignoreCase: true
-			}
-		],
-		'react/jsx-no-script-url': 'error',
-		'react/jsx-no-target-blank': [
-			'error',
-			{
-				warnOnSpreadAttributes: true,
-				forms: true
-			}
-		],
-		'react/jsx-no-undef': 'error',
-		'react/jsx-no-useless-fragment': 'error',
-		// Disabled for now as it produces too many errors
-		// 'react/jsx-one-expression-per-line': ['error', {allow: 'single-child'}],
-		'react/jsx-curly-brace-presence': [
-			'error',
-			{
-				props: 'never',
-				children: 'never',
-				propElementValues: 'always'
-			}
-		],
-		'react/jsx-fragments': [
-			'error',
-			'syntax'
-		],
-		'react/jsx-pascal-case': 'error',
-		'react/jsx-props-no-multi-spaces': 'error',
-		'react/jsx-sort-props': [
-			'error',
-			{
-				callbacksLast: true,
-				shorthandFirst: true,
-				noSortAlphabetically: true,
-				reservedFirst: true
-			}
-		],
-		'react/jsx-tag-spacing': [
-			'error',
-			{
-				closingSlash: 'never',
-				beforeSelfClosing: 'never',
-				afterOpening: 'never',
-				beforeClosing: 'never'
-			}
-		],
-		'react/jsx-uses-react': 'error',
-		'react/jsx-uses-vars': 'error',
-		'react/jsx-wrap-multilines': [
-			'error',
-			{
-				declaration: 'parens-new-line',
-				assignment: 'parens-new-line',
-				return: 'parens-new-line',
-				arrow: 'parens-new-line',
-				condition: 'ignore',
-				logical: 'ignore',
-				prop: 'ignore'
-			}
-		],
+import react from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
 
-		'react-hooks/rules-of-hooks': 'error',
-		'react-hooks/exhaustive-deps': 'warn'
-	}
-};
+export default [
+	{
+		plugins: {
+			react,
+			'react-hooks': reactHooks,
+		},
+		languageOptions: {
+			parserOptions: {
+				ecmaFeatures: {
+					jsx: true,
+				},
+			},
+		},
+		settings: {
+			react: {
+				version: 'detect',
+			},
+		},
+		rules: {
+			'react/boolean-prop-naming': [
+				'error',
+				{
+					validateNested: true,
+				},
+			],
+			'react/button-has-type': 'error',
+			'react/jsx-child-element-spacing': 'error',
+			'react/default-props-match-prop-types': 'error',
+			'react/function-component-definition': [
+				'error',
+				{
+					namedComponents: 'function-declaration',
+					unnamedComponents: 'arrow-function',
+				},
+			],
+			'react/hook-use-state': 'error',
+			'react/iframe-missing-sandbox': 'error',
+			'react/no-access-state-in-setstate': 'error',
+			'react/no-array-index-key': 'error',
+			'react/no-arrow-function-lifecycle': 'error',
+			'react/no-children-prop': 'error',
+			'react/no-danger': 'error',
+			'react/no-danger-with-children': 'error',
+			'react/no-deprecated': 'error',
+			'react/no-did-update-set-state': 'error',
+			'react/no-direct-mutation-state': 'error',
+			'react/no-find-dom-node': 'error',
+			'react/no-invalid-html-attribute': 'error',
+			'react/no-is-mounted': 'error',
+			'react/no-namespace': 'error',
+			'react/no-redundant-should-component-update': 'error',
+			'react/no-render-return-value': 'error',
+			'react/no-typos': 'error',
+			'react/no-string-refs': [
+				'error',
+				{
+					noTemplateLiterals: true,
+				},
+			],
+			'react/no-this-in-sfc': 'error',
+			'react/no-unescaped-entities': 'error',
+			'react/no-unknown-property': 'error',
+			'react/no-unsafe': 'error',
+			'react/no-unused-prop-types': 'error',
+			'react/no-unused-state': 'error',
+			'react/prefer-read-only-props': 'error',
+			'react/prop-types': 'error',
+			'react/react-in-jsx-scope': 'error',
+			'react/require-default-props': [
+				'error',
+				{
+					forbidDefaultForRequired: true,
+					ignoreFunctionalComponents: true,
+				},
+			],
+			'react/self-closing-comp': 'error',
+			'react/state-in-constructor': ['error', 'never'],
+			'react/static-property-placement': 'error',
+			'react/style-prop-object': [
+				'error',
+				{
+					allow: [
+						// This allows react-intl’s `<FormattedNumber value={0.42} style='percent'/>`.
+						'FormattedNumber',
+					],
+				},
+			],
+			'react/void-dom-elements-no-children': 'error',
+			'react/jsx-boolean-value': 'error',
+			'react/jsx-closing-bracket-location': [
+				'error',
+				{
+					nonEmpty: 'tag-aligned',
+					selfClosing: false,
+				},
+			],
+			'react/jsx-closing-tag-location': 'error',
+			'react/jsx-curly-newline': [
+				'error',
+				{
+					multiline: 'consistent',
+					singleline: 'forbid',
+				},
+			],
+			'react/jsx-curly-spacing': ['error', 'never'],
+			'react/jsx-equals-spacing': ['error', 'never'],
+			'react/jsx-first-prop-new-line': 'error',
+			'react/jsx-indent': [
+				'error',
+				'tab',
+				{
+					checkAttributes: true,
+					indentLogicalExpressions: true,
+				},
+			],
+			'react/jsx-indent-props': ['error', 'tab'],
+			'react/jsx-key': [
+				'error',
+				{
+					checkFragmentShorthand: true,
+					checkKeyMustBeforeSpread: true,
+					warnOnDuplicates: true,
+				},
+			],
+			'react/jsx-max-props-per-line': [
+				'error',
+				{
+					maximum: 3,
+					when: 'multiline',
+				},
+			],
+			'react/jsx-no-bind': [
+				'error',
+				{
+					allowArrowFunctions: true,
+				},
+			],
+			'react/jsx-no-comment-textnodes': 'error',
+			'react/jsx-no-constructed-context-values': 'error',
+			'react/jsx-no-duplicate-props': [
+				'error',
+				{
+					ignoreCase: true,
+				},
+			],
+			'react/jsx-no-script-url': 'error',
+			'react/jsx-no-target-blank': [
+				'error',
+				{
+					warnOnSpreadAttributes: true,
+					forms: true,
+				},
+			],
+			'react/jsx-no-undef': 'error',
+			'react/jsx-no-useless-fragment': 'error',
+			// Disabled for now as it produces too many errors
+			// 'react/jsx-one-expression-per-line': ['error', {allow: 'single-child'}],
+			'react/jsx-curly-brace-presence': [
+				'error',
+				{
+					props: 'never',
+					children: 'never',
+					propElementValues: 'always',
+				},
+			],
+			'react/jsx-fragments': ['error', 'syntax'],
+			'react/jsx-pascal-case': 'error',
+			'react/jsx-props-no-multi-spaces': 'error',
+			'react/jsx-sort-props': [
+				'error',
+				{
+					callbacksLast: true,
+					shorthandFirst: true,
+					noSortAlphabetically: true,
+					reservedFirst: true,
+				},
+			],
+			'react/jsx-tag-spacing': [
+				'error',
+				{
+					closingSlash: 'never',
+					beforeSelfClosing: 'never',
+					afterOpening: 'never',
+					beforeClosing: 'never',
+				},
+			],
+			'react/jsx-uses-react': 'error',
+			'react/jsx-uses-vars': 'error',
+			'react/jsx-wrap-multilines': [
+				'error',
+				{
+					declaration: 'parens-new-line',
+					assignment: 'parens-new-line',
+					return: 'parens-new-line',
+					arrow: 'parens-new-line',
+					condition: 'ignore',
+					logical: 'ignore',
+					prop: 'ignore',
+				},
+			],
+
+			'react-hooks/rules-of-hooks': 'error',
+			'react-hooks/exhaustive-deps': 'warn',
+		},
+	},
+];
diff --git a/package.json b/package.json
index 19d32c3..079d3dc 100644
--- a/package.json
+++ b/package.json
@@ -10,12 +10,17 @@
 		"email": "sindresorhus@gmail.com",
 		"url": "https://sindresorhus.com"
 	},
+	"type": "module",
 	"engines": {
 		"node": ">=12"
 	},
 	"scripts": {
 		"test": "ava"
 	},
+	"exports": {
+		".": "./index.js",
+		"./space": "./space.js"
+	},
 	"files": [
 		"index.js",
 		"space.js"
@@ -51,16 +56,15 @@
 		"simple"
 	],
 	"devDependencies": {
-		"ava": "^2.4.0",
-		"eslint": "^8.6.0",
+		"ava": "^6.2.0",
+		"eslint": "^9.18.0",
 		"eslint-plugin-react": "^7.29.0",
-		"eslint-plugin-react-hooks": "^4.3.0",
-		"is-plain-obj": "^3.0.0",
+		"eslint-plugin-react-hooks": "^5.1.0",
 		"react": "^17.0.2"
 	},
 	"peerDependencies": {
-		"eslint": ">=8.6.0",
+		"eslint": ">=9.18.0",
 		"eslint-plugin-react": ">=7.29.0",
-		"eslint-plugin-react-hooks": ">=4.3.0"
+		"eslint-plugin-react-hooks": ">=5.1.0"
 	}
 }
diff --git a/readme.md b/readme.md
index a86ba71..22df646 100644
--- a/readme.md
+++ b/readme.md
@@ -10,51 +10,37 @@ npm install --save-dev eslint-config-xo eslint-config-xo-react eslint-plugin-rea
 
 ## Usage
 
-Add some ESLint config to your package.json:
+Add some ESLint config to your `eslint.config.js`:
 
-```json
-{
-	"name": "my-awesome-project",
-	"eslintConfig": {
-		"extends": [
-			"xo",
-			"xo-react"
-		]
-	}
-}
-```
-
-Or to .eslintrc:
+```js
+// eslint.config.js
+import xo from 'eslint-config-xo';
+import xoReact from 'eslint-config-xo-react';
 
-```json
-{
-	"extends": [
-		"xo",
-		"xo-react"
-	]
-}
+export default [...xo, ...xoReact];
 ```
 
 Use the `space` sub-config if you want 2 space indentation instead of tabs:
 
-```json
-{
-	"extends": [
-		"xo",
-		"xo-react/space"
-	]
-}
+```js
+// eslint.config.js
+import xo from 'eslint-config-xo';
+import xoReactSpace from 'eslint-config-xo-react/space';
+
+export default [...xo, ...xoReactSpace];
 ```
 
-You can also mix it with a [XO](https://github.com/xojs/xo) sub-config:
+You can also mix it with a [eslint-config-xo](https://github.com/xojs/eslint-config-xo) sub-config:
 
-```json
-{
-	"extends": [
-		"xo/esnext",
-		"xo-react"
-	]
-}
+```js
+// eslint.config.js
+import xoSpace from 'eslint-config-xo/space';
+import xoReactSpace from 'eslint-config-xo-react/space';
+
+export default [
+	...xoSpace,
+	...xoReactSpace
+];
 ```
 
 ## Tip
diff --git a/space.js b/space.js
index 8f9d4b6..31aa43e 100644
--- a/space.js
+++ b/space.js
@@ -1,16 +1,14 @@
-'use strict';
-const path = require('path');
+import configs from './index.js';
 
-module.exports = {
-	extends: path.join(__dirname, 'index.js'),
-	rules: {
-		'react/jsx-indent-props': [
-			'error',
-			2
-		],
-		'react/jsx-indent': [
-			'error',
-			2
-		]
-	}
-};
+const [config] = configs;
+
+export default [
+	{
+		...config,
+		rules: {
+			...config.rules,
+			'react/jsx-indent-props': ['error', 2],
+			'react/jsx-indent': ['error', 2],
+		},
+	},
+];
diff --git a/test/test.js b/test/test.js
index e7bbf05..85d8916 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,12 +1,13 @@
 import test from 'ava';
-import isPlainObj from 'is-plain-obj';
+import eslintConfigXoReact from '../index.js';
+import eslintConfigXoReactSpace from '../space.js';
 import {ESLint} from 'eslint';
 
 const hasRule = (errors, ruleId) => errors.some(error => error.ruleId === ruleId);
 
 async function runEslint(string, config) {
 	const eslint = new ESLint({
-		useEslintrc: false,
+		overrideConfigFile: true,
 		overrideConfig: config,
 	});
 
@@ -16,28 +17,20 @@ async function runEslint(string, config) {
 }
 
 test('main', async t => {
-	const config = require('../space.js');
+	t.true(Array.isArray(eslintConfigXoReact));
 
-	t.true(isPlainObj(config));
-	t.true(isPlainObj(config.rules));
-
-	const errors = await runEslint('var app = <div className="foo">Unicorn</div>', config);
+	const errors = await runEslint('var app = <div className="foo">Unicorn</div>', eslintConfigXoReact);
 	t.true(hasRule(errors, 'react/react-in-jsx-scope'));
 });
 
 test('space', async t => {
-	const config = require('../space.js');
-
-	t.true(isPlainObj(config));
-	t.true(isPlainObj(config.rules));
+	t.true(Array.isArray(eslintConfigXoReactSpace));
 
-	const errors = await runEslint('<App>\n\t<Hello/>\n</App>', config);
+	const errors = await runEslint('<App>\n\t<Hello/>\n</App>', eslintConfigXoReactSpace);
 	t.true(hasRule(errors, 'react/jsx-indent'));
 });
 
 test('no errors', async t => {
-	const config = require('../index.js');
-
-	const errors = await runEslint('var React = require(\'react\');\nvar el = <div/>;', config);
+	const errors = await runEslint('var React = require(\'react\');\nvar el = <div/>;', eslintConfigXoReact);
 	t.deepEqual(errors, []);
 });