feat(theme): new CSS cascade layers plugin + built-in v4.useCssCascadeLayers future flag (#11142)

Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
Sébastien Lorber 2025-05-22 19:55:02 +02:00 committed by GitHub
parent a301b24d64
commit abd04a2b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 894 additions and 0 deletions

View file

@ -0,0 +1,3 @@
.tsbuildinfo*
tsconfig*
__tests__

View file

@ -0,0 +1,7 @@
# `@docusaurus/plugin-css-cascade-layers`
CSS Cascade Layer plugin for Docusaurus
## Usage
See [plugin-css-cascade-layers documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-css-cascade-layers).

View file

@ -0,0 +1,29 @@
{
"name": "@docusaurus/plugin-css-cascade-layers",
"version": "3.7.0",
"description": "CSS Cascade Layer plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "tsc --build",
"watch": "tsc --build --watch"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/docusaurus.git",
"directory": "packages/docusaurus-plugin-css-cascade-layers"
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/types": "3.7.0",
"@docusaurus/utils-validation": "3.7.0",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=18.0"
}
}

View file

@ -0,0 +1,96 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
generateLayersDeclaration,
findLayer,
isValidLayerName,
} from '../layers';
import type {PluginOptions} from '../options';
describe('isValidLayerName', () => {
it('accepts valid names', () => {
expect(isValidLayerName('layer1')).toBe(true);
expect(isValidLayerName('layer1.layer2')).toBe(true);
expect(isValidLayerName('layer-1.layer_2.layer3')).toBe(true);
});
it('rejects layer with coma', () => {
expect(isValidLayerName('lay,er1')).toBe(false);
});
it('rejects layer with space', () => {
expect(isValidLayerName('lay er1')).toBe(false);
});
});
describe('generateLayersDeclaration', () => {
it('for list of layers', () => {
expect(generateLayersDeclaration(['layer1', 'layer2'])).toBe(
'@layer layer1, layer2;',
);
});
it('for empty list of layers', () => {
// Not useful to generate it, but still valid CSS anyway
expect(generateLayersDeclaration([])).toBe('@layer ;');
});
});
describe('findLayer', () => {
const inputFilePath = 'filePath';
function testFor(layers: PluginOptions['layers']) {
return findLayer(inputFilePath, Object.entries(layers));
}
it('for empty layers', () => {
expect(testFor({})).toBeUndefined();
});
it('for single matching layer', () => {
expect(testFor({layer: (filePath) => filePath === inputFilePath})).toBe(
'layer',
);
});
it('for single non-matching layer', () => {
expect(
testFor({layer: (filePath) => filePath !== inputFilePath}),
).toBeUndefined();
});
it('for multiple matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath === inputFilePath,
layer2: (filePath) => filePath === inputFilePath,
layer3: (filePath) => filePath === inputFilePath,
}),
).toBe('layer1');
});
it('for multiple non-matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath !== inputFilePath,
layer2: (filePath) => filePath !== inputFilePath,
layer3: (filePath) => filePath !== inputFilePath,
}),
).toBeUndefined();
});
it('for multiple mixed matching layers', () => {
expect(
testFor({
layer1: (filePath) => filePath !== inputFilePath,
layer2: (filePath) => filePath === inputFilePath,
layer3: (filePath) => filePath !== inputFilePath,
layer4: (filePath) => filePath === inputFilePath,
}),
).toBe('layer2');
});
});

View file

@ -0,0 +1,105 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {
validateOptions,
type PluginOptions,
type Options,
DEFAULT_OPTIONS,
} from '../options';
import type {Validate} from '@docusaurus/types';
function testValidateOptions(options: Options) {
return validateOptions({
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
options,
});
}
describe('validateOptions', () => {
it('accepts undefined options', () => {
// @ts-expect-error: should error
expect(testValidateOptions(undefined)).toEqual(DEFAULT_OPTIONS);
});
it('accepts empty options', () => {
expect(testValidateOptions({})).toEqual(DEFAULT_OPTIONS);
});
describe('layers', () => {
it('accepts empty layers', () => {
expect(testValidateOptions({layers: {}})).toEqual({
...DEFAULT_OPTIONS,
layers: {},
});
});
it('accepts undefined layers', () => {
const config: Options = {
layers: undefined,
};
expect(testValidateOptions(config)).toEqual(DEFAULT_OPTIONS);
});
it('accepts custom layers', () => {
const config: Options = {
layers: {
layer1: (filePath: string) => {
return !!filePath;
},
layer2: (filePath: string) => {
return !!filePath;
},
},
};
expect(testValidateOptions(config)).toEqual({
...DEFAULT_OPTIONS,
layers: config.layers,
});
});
it('rejects layer with bad name', () => {
const config: Options = {
layers: {
'layer 1': (filePath) => !!filePath,
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(`""layers.layer 1" is not allowed"`);
});
it('rejects layer with bad value', () => {
const config: Options = {
layers: {
// @ts-expect-error: should error
layer1: 'bad value',
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(
`""layers.layer1" must be of type function"`,
);
});
it('rejects layer with bad function arity', () => {
const config: Options = {
layers: {
// @ts-expect-error: should error
layer1: () => {},
},
};
expect(() =>
testValidateOptions(config),
).toThrowErrorMatchingInlineSnapshot(
`""layers.layer1" must have an arity of 1"`,
);
});
});
});

View file

@ -0,0 +1,68 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import {PostCssPluginWrapInLayer} from './postCssPlugin';
import {generateLayersDeclaration} from './layers';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {PluginOptions, Options} from './options';
const PluginName = 'docusaurus-plugin-css-cascade-layers';
const LayersDeclarationModule = 'layers.css';
function getLayersDeclarationPath(
context: LoadContext,
options: PluginOptions,
) {
const {generatedFilesDir} = context;
const pluginId = options.id;
if (pluginId !== 'default') {
// Since it's only possible to declare a single layer order
// using this plugin twice doesn't really make sense
throw new Error(
'The CSS Cascade Layers plugin does not support multiple instances.',
);
}
return path.join(
generatedFilesDir,
PluginName,
pluginId,
LayersDeclarationModule,
);
}
export default function pluginCssCascadeLayers(
context: LoadContext,
options: PluginOptions,
): Plugin | null {
const layersDeclarationPath = getLayersDeclarationPath(context, options);
return {
name: PluginName,
getClientModules() {
return [layersDeclarationPath];
},
async contentLoaded({actions}) {
await actions.createData(
LayersDeclarationModule,
generateLayersDeclaration(Object.keys(options.layers)),
);
},
configurePostCss(postCssOptions) {
postCssOptions.plugins.push(PostCssPluginWrapInLayer(options));
return postCssOptions;
},
};
}
export {validateOptions} from './options';
export type {PluginOptions, Options};

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export type LayerEntry = [string, (filePath: string) => boolean];
export function isValidLayerName(layer: string): boolean {
// TODO improve validation rule to match spec, not high priority
return !layer.includes(',') && !layer.includes(' ');
}
export function generateLayersDeclaration(layers: string[]): string {
return `@layer ${layers.join(', ')};`;
}
export function findLayer(
filePath: string,
layers: LayerEntry[],
): string | undefined {
// Using find() => layers order matter
// The first layer that matches is used in priority even if others match too
const layerEntry = layers.find((layer) => layer[1](filePath));
return layerEntry?.[0]; // return layer name
}

View file

@ -0,0 +1,87 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Joi} from '@docusaurus/utils-validation';
import {isValidLayerName} from './layers';
import type {OptionValidationContext} from '@docusaurus/types';
export type PluginOptions = {
id: string; // plugin id
layers: Record<string, (filePath: string) => boolean>;
};
export type Options = {
layers?: PluginOptions['layers'];
};
// Not ideal to compute layers using "filePath.includes()"
// But this is mostly temporary until we add first-class layers everywhere
function layerFor(...params: string[]) {
return (filePath: string) => params.some((p) => filePath.includes(p));
}
// Object order matters, it defines the layer order
export const DEFAULT_LAYERS: PluginOptions['layers'] = {
'docusaurus.infima': layerFor('node_modules/infima/dist'),
'docusaurus.theme-common': layerFor(
'packages/docusaurus-theme-common/lib',
'node_modules/@docusaurus/theme-common/lib',
),
'docusaurus.theme-classic': layerFor(
'packages/docusaurus-theme-classic/lib',
'node_modules/@docusaurus/theme-classic/lib',
),
'docusaurus.core': layerFor(
'packages/docusaurus/lib',
'node_modules/@docusaurus/core/lib',
),
'docusaurus.plugin-debug': layerFor(
'packages/docusaurus-plugin-debug/lib',
'node_modules/@docusaurus/plugin-debug/lib',
),
'docusaurus.theme-mermaid': layerFor(
'packages/docusaurus-theme-mermaid/lib',
'node_modules/@docusaurus/theme-mermaid/lib',
),
'docusaurus.theme-live-codeblock': layerFor(
'packages/docusaurus-theme-live-codeblock/lib',
'node_modules/@docusaurus/theme-live-codeblock/lib',
),
'docusaurus.theme-search-algolia.docsearch': layerFor(
'node_modules/@docsearch/css/dist',
),
'docusaurus.theme-search-algolia': layerFor(
'packages/docusaurus-theme-search-algolia/lib',
'node_modules/@docusaurus/theme-search-algolia/lib',
),
// docusaurus.website layer ? (declare it, even if empty?)
};
export const DEFAULT_OPTIONS: Partial<PluginOptions> = {
id: 'default',
layers: DEFAULT_LAYERS,
};
const pluginOptionsSchema = Joi.object<PluginOptions>({
layers: Joi.object()
.pattern(
Joi.custom((val, helpers) => {
if (!isValidLayerName(val)) {
return helpers.error('any.invalid');
}
return val;
}),
Joi.function().arity(1).required(),
)
.default(DEFAULT_LAYERS),
});
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options);
}

View file

@ -0,0 +1,45 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {findLayer} from './layers';
import type {Root, PluginCreator} from 'postcss';
import type {PluginOptions} from './options';
function wrapCssRootInLayer(root: Root, layer: string): void {
const rootBefore = root.clone();
root.removeAll();
root.append({
type: 'atrule',
name: 'layer',
params: layer,
nodes: rootBefore.nodes,
});
}
export const PostCssPluginWrapInLayer: PluginCreator<{
layers: PluginOptions['layers'];
}> = (options) => {
if (!options) {
throw new Error('PostCssPluginWrapInLayer options are mandatory');
}
const layers = Object.entries(options.layers);
return {
postcssPlugin: 'postcss-wrap-in-layer',
Once(root) {
const filePath = root.source?.input.file;
if (!filePath) {
return;
}
const layer = findLayer(filePath, layers);
if (layer) {
wrapCssRootInLayer(root, layer);
}
},
};
};
PostCssPluginWrapInLayer.postcss = true;

View file

@ -0,0 +1,8 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="@docusaurus/module-type-aliases" />

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": false
},
"include": ["src"],
"exclude": ["**/__tests__/**"]
}

View file

@ -19,6 +19,7 @@
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/plugin-css-cascade-layers": "3.7.0",
"@docusaurus/plugin-content-blog": "3.7.0",
"@docusaurus/plugin-content-docs": "3.7.0",
"@docusaurus/plugin-content-pages": "3.7.0",

View file

@ -62,6 +62,13 @@ export default function preset(
}
const plugins: PluginConfig[] = [];
// TODO Docusaurus v4: temporary due to the opt-in flag
// In v4 we'd like to use layers everywhere natively
if (siteConfig.future.v4.useCssCascadeLayers) {
plugins.push(makePluginConfig('@docusaurus/plugin-css-cascade-layers'));
}
if (docs !== false) {
plugins.push(makePluginConfig('@docusaurus/plugin-content-docs', docs));
}

View file

@ -13,3 +13,11 @@ Hide color mode toggle in small viewports
display: none;
}
}
/*
Restore some Infima style that broke with CSS Cascade Layers
See https://github.com/facebook/docusaurus/pull/11142
*/
:global(.navbar__items--right) > :last-child {
padding-right: 0;
}

View file

@ -136,6 +136,7 @@ export type FasterConfig = {
export type FutureV4Config = {
removeLegacyPostBuildHeadAttribute: boolean;
useCssCascadeLayers: boolean;
};
export type FutureConfig = {

View file

@ -25,6 +25,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -99,6 +100,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -173,6 +175,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -247,6 +250,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -321,6 +325,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -395,6 +400,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -469,6 +475,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -545,6 +552,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -621,6 +629,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],
@ -700,6 +709,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],

View file

@ -99,6 +99,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
},
"v4": {
"removeLegacyPostBuildHeadAttribute": false,
"useCssCascadeLayers": false,
},
},
"headTags": [],

View file

@ -50,6 +50,7 @@ describe('normalizeConfig', () => {
future: {
v4: {
removeLegacyPostBuildHeadAttribute: true,
useCssCascadeLayers: true,
},
experimental_faster: {
swcJsLoader: true,
@ -754,6 +755,7 @@ describe('future', () => {
const future: DocusaurusConfig['future'] = {
v4: {
removeLegacyPostBuildHeadAttribute: true,
useCssCascadeLayers: true,
},
experimental_faster: {
swcJsLoader: true,
@ -1861,6 +1863,7 @@ describe('future', () => {
it('accepts v4 - full', () => {
const v4: FutureV4Config = {
removeLegacyPostBuildHeadAttribute: true,
useCssCascadeLayers: true,
};
expect(
normalizeConfig({
@ -1976,5 +1979,80 @@ describe('future', () => {
`);
});
});
describe('useCssCascadeLayers', () => {
it('accepts - undefined', () => {
const v4: Partial<FutureV4Config> = {
useCssCascadeLayers: undefined,
};
expect(
normalizeConfig({
future: {
v4,
},
}),
).toEqual(v4Containing({useCssCascadeLayers: false}));
});
it('accepts - true', () => {
const v4: Partial<FutureV4Config> = {
useCssCascadeLayers: true,
};
expect(
normalizeConfig({
future: {
v4,
},
}),
).toEqual(v4Containing({useCssCascadeLayers: true}));
});
it('accepts - false', () => {
const v4: Partial<FutureV4Config> = {
useCssCascadeLayers: false,
};
expect(
normalizeConfig({
future: {
v4,
},
}),
).toEqual(v4Containing({useCssCascadeLayers: false}));
});
it('rejects - null', () => {
const v4: Partial<FutureV4Config> = {
// @ts-expect-error: invalid
useCssCascadeLayers: 42,
};
expect(() =>
normalizeConfig({
future: {
v4,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.v4.useCssCascadeLayers" must be a boolean
"
`);
});
it('rejects - number', () => {
const v4: Partial<FutureV4Config> = {
// @ts-expect-error: invalid
useCssCascadeLayers: 42,
};
expect(() =>
normalizeConfig({
future: {
v4,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.v4.useCssCascadeLayers" must be a boolean
"
`);
});
});
});
});

View file

@ -68,11 +68,13 @@ export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = {
export const DEFAULT_FUTURE_V4_CONFIG: FutureV4Config = {
removeLegacyPostBuildHeadAttribute: false,
useCssCascadeLayers: false,
};
// When using the "v4: true" shortcut
export const DEFAULT_FUTURE_V4_CONFIG_TRUE: FutureV4Config = {
removeLegacyPostBuildHeadAttribute: true,
useCssCascadeLayers: true,
};
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
@ -270,6 +272,9 @@ const FUTURE_V4_SCHEMA = Joi.alternatives()
removeLegacyPostBuildHeadAttribute: Joi.boolean().default(
DEFAULT_FUTURE_V4_CONFIG.removeLegacyPostBuildHeadAttribute,
),
useCssCascadeLayers: Joi.boolean().default(
DEFAULT_FUTURE_V4_CONFIG.useCssCascadeLayers,
),
}),
Joi.boolean()
.required()

View file

@ -109,6 +109,7 @@ Héctor
héllô
IANAD
Infima
infima
inlines
interactiveness
Interpolatable

View file

@ -41,3 +41,4 @@ import Readme from "../README.mdx"
- [Analytics](/tests/pages/analytics)
- [History tests](/tests/pages/history-tests)
- [Embeds](/tests/pages/embeds)
- [Style Isolation tests](/tests/pages/style-isolation)

View file

@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.exampleContainer {
border: solid thin;
}
.isolated:not(#a#b) {
&,
* {
@layer docusaurus {
all: revert-layer;
}
/*
Yes, unfortunately we need to revert sub-layers one by one
See https://bsky.app/profile/sebastienlorber.com/post/3lpqzuxat6s2v
*/
@layer docusaurus.infima {
all: revert-layer;
}
}
}

View file

@ -0,0 +1,174 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import styles from './index.module.css';
/* eslint-disable @docusaurus/prefer-docusaurus-heading */
function ExampleContainer({
isolated,
children,
}: {
isolated?: boolean;
children: ReactNode;
}) {
return (
<div
className={clsx(
styles.exampleContainer,
isolated ? styles.isolated : undefined,
)}>
{children}
</div>
);
}
function ExampleRow({name, children}: {name: string; children: ReactNode}) {
return (
<tr>
<td>{name}</td>
<td>
<ExampleContainer>{children}</ExampleContainer>
</td>
<td>
<ExampleContainer isolated>{children}</ExampleContainer>
</td>
</tr>
);
}
function ExamplesTable() {
return (
<table className="table-auto border-collapse border border-gray-300">
<thead>
<tr>
<th>Example</th>
<th>Normal</th>
<th>Isolated</th>
</tr>
</thead>
<tbody>
<ExampleRow name="h1">
<h1>title</h1>
</ExampleRow>
<ExampleRow name="p">
<p>text</p>
</ExampleRow>
<ExampleRow name="a">
{/* eslint-disable-next-line */}
<a href="https://example.com">link</a>
</ExampleRow>
<ExampleRow name="code">
<code>code</code>
</ExampleRow>
<ExampleRow name="pre > code">
<pre>
<code>code</code>
</pre>
</ExampleRow>
<ExampleRow name="blockquote">
<blockquote>some text</blockquote>
</ExampleRow>
<ExampleRow name="button">
{/* eslint-disable-next-line */}
<button>button</button>
</ExampleRow>
<ExampleRow name="ul">
<ul>
<li>item1</li>
<li>item2</li>
</ul>
</ExampleRow>
<ExampleRow name="ol">
<ol>
<li>item1</li>
<li>item2</li>
</ol>
</ExampleRow>
<ExampleRow name="kbd">
<kbd>kbd</kbd>
</ExampleRow>
<ExampleRow name="shadow">
<div className="shadow--tl">shadow (KO)</div>
</ExampleRow>
<ExampleRow name="table">
<table>
<thead>
<tr>
<th>Col1</th>
<th>Col2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell1</td>
<td>Cell2</td>
</tr>
<tr>
<td>Cell3</td>
<td>Cell3</td>
</tr>
</tbody>
</table>
</ExampleRow>
<ExampleRow name="Infima button primary">
{/* eslint-disable-next-line */}
<button className="button button--primary">button</button>
</ExampleRow>
<ExampleRow name="Infima alert danger">
<div className="alert alert--danger">danger</div>
</ExampleRow>
<ExampleRow name="Infima badge success">
<div className="badge badge--success">success</div>
</ExampleRow>
</tbody>
</table>
);
}
export default function StyleIsolation(): ReactNode {
return (
<Layout>
<main
style={{padding: 30}}
className="markdown" // class added on purpose, creates extra pollution
>
<Heading as="h1">Style Isolation tests</Heading>
<p>
This shows how to isolate your components from Docusaurus global
styles. A workaround for{' '}
<Link
target="_blank"
href="https://github.com/facebook/docusaurus/issues/6032">
this issue
</Link>
.
</p>
<ExamplesTable />
</main>
</Layout>
);
}

View file

@ -199,6 +199,7 @@ export default {
future: {
v4: {
removeLegacyPostBuildHeadAttribute: true,
useCssCascadeLayers: true,
},
experimental_faster: {
swcJsLoader: true,
@ -221,6 +222,7 @@ export default {
- `v4`: Permits to opt-in for upcoming Docusaurus v4 breaking changes and features, to prepare your site in advance for this new version. Use `true` as a shorthand to enable all the flags.
- [`removeLegacyPostBuildHeadAttribute`](https://github.com/facebook/docusaurus/pull/10435): Removes the legacy `plugin.postBuild({head})` API that prevents us from applying useful SSG optimizations ([explanations](https://github.com/facebook/docusaurus/pull/10850)).
- [`useCssCascadeLayers`](https://github.com/facebook/docusaurus/pull/11142): This enables the [Docusaurus CSS Cascade Layers plugin](./plugins/plugin-css-cascade-layers.mdx) with pre-configured layers that we plan to apply by default for Docusaurus v4.
- `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags. Read more on the [Docusaurus Faster](https://github.com/facebook/docusaurus/issues/10556) issue. Available feature flags:
- [`swcJsLoader`](https://github.com/facebook/docusaurus/pull/10435): Use [SWC](https://swc.rs/) to transpile JS (instead of [Babel](https://babeljs.io/)).
- [`swcJsMinimizer`](https://github.com/facebook/docusaurus/pull/10441): Use [SWC](https://swc.rs/) to minify JS (instead of [Terser](https://github.com/terser/terser)).

View file

@ -31,3 +31,4 @@ These plugins will add a useful behavior to your Docusaurus site.
- [@docusaurus/plugin-google-analytics](./plugin-google-analytics.mdx)
- [@docusaurus/plugin-google-gtag](./plugin-google-gtag.mdx)
- [@docusaurus/plugin-google-tag-manager](./plugin-google-tag-manager.mdx)
- [@docusaurus/plugin-css-cascade-layers](./plugin-css-cascade-layers.mdx) u

View file

@ -0,0 +1,95 @@
---
sidebar_position: 9
slug: /api/plugins/@docusaurus/plugin-css-cascade-layers
---
# 📦 plugin-css-cascade-layers
import APITable from '@site/src/components/APITable';
:::caution Experimental
This plugin is mostly designed to be used internally by the classic preset through the [Docusaurus `future.v4.useCssCascadeLayers` flag](../docusaurus.config.js.mdx#future), although it can also be used as a standalone plugin. Please [let us know here](https://github.com/facebook/docusaurus/pull/11142) if you have a use case for it and help us design an API that makes sense for the future of Docusaurus.
:::
A plugin for wrapping CSS modules of your Docusaurus site in [CSS Cascade Layers](https://css-tricks.com/css-cascade-layers/). This modern CSS feature is widely supported by all browsers. It allows grouping CSS rules in layers of specificity and gives you more control over the CSS cascade.
Use this plugin to:
- apply a top-level `@layer myLayer { ... }` block rule around any CSS module, including un-layered third-party CSS.
- define an explicit layer ordering
:::caution
To use this plugin properly, it's recommended to have a solid understanding of [CSS Cascade Layers](https://css-tricks.com/css-cascade-layers/), the [CSS Cascade](https://developer.mozilla.org/docs/Web/CSS/CSS_cascade/Cascade) and [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity).
:::
## Installation {#installation}
```bash npm2yarn
npm install --save @docusaurus/plugin-css-cascade-layers
```
:::tip
If you use the preset `@docusaurus/preset-classic`, this plugin is automatically configured for you with the [`siteConfig.future.v4.useCssCascadeLayers`](../docusaurus.config.js.mdx#future) flag.
:::
## Configuration {#configuration}
Accepted fields:
```mdx-code-block
<APITable>
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `layers` | `Layers` | **Built-in layers** | An object representing all the CSS cascade layers you want to use, and whether the layer should be applied to a given file path. See examples and types below. |
```mdx-code-block
</APITable>
```
### Types {#types}
#### `Layers` {#EditUrlFunction}
```ts
type Layers = Record<
string, // layer name
(filePath: string) => boolean // layer matcher
>;
```
The `layers` object is defined by:
- key: the name of a layer
- value: a function to define if a given CSS module file should be in that layer
:::caution Order matters
The object order matters:
- the keys order defines an explicit CSS layer order
- when multiple layers match a file path, only the first layer will apply
:::
### Example configuration {#ex-config}
You can configure this plugin through plugin options.
```js
const options = {
layers: {
'docusaurus.infima': (filePath) =>
filePath.includes('/node_modules/infima/dist'),
'docusaurus.theme-classic': (filePath) =>
filePath.includes('/node_modules/@docusaurus/theme-classic/lib'),
},
};
```