mirror of
https://github.com/withastro/astro.git
synced 2025-02-17 22:44:24 -05:00
Introduce <style global>
(#824)
* Adding support for multiple <style> blocks * Adding support for `<style global>` * scoping @keyframes should also be skipped for <style global> * Adding test coverage for muliple style blocks, global blocks, and scoped keyframes * docs: Updating docs for `<style global>` support * Adding yarn changeset * Punctuation fix in styling docs * docs: Clarifying example use cases given in the docs Co-authored-by: Tony Sullivan <tony.f.sullivan@gmail.com>
This commit is contained in:
parent
041788878d
commit
294a656ed9
10 changed files with 103 additions and 28 deletions
8
.changeset/empty-trainers-chew.md
Normal file
8
.changeset/empty-trainers-chew.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
'@astrojs/parser': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds support for global style blocks via `<style global>`
|
||||||
|
|
||||||
|
Be careful with this escape hatch! This is best reserved for uses like importing styling libraries like Tailwind, or changing global CSS variables.
|
|
@ -63,6 +63,8 @@ For best results, you should only have one `<style>` tag per-Astro component. Th
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Using `<style global>` will skip automatic scoping for every CSS rule in the `<style>` block. This escape hatch should be avoided if possible but can be useful if, for example, you need to modify styling for HTML elements added by an external library.
|
||||||
|
|
||||||
Sass (an alternative to CSS) is also available via `<style lang="scss">`.
|
Sass (an alternative to CSS) is also available via `<style lang="scss">`.
|
||||||
|
|
||||||
📚 Read our full guide on [Component Styling](/guides/styling) to learn more.
|
📚 Read our full guide on [Component Styling](/guides/styling) to learn more.
|
||||||
|
|
|
@ -30,6 +30,32 @@ To create global styles, add a `:global()` wrapper around a selector (the same a
|
||||||
<h1>I have both scoped and global styles</h1>
|
<h1>I have both scoped and global styles</h1>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To include every selector in a `<style>` as global styles, use `<style global>`. It's best to avoid using this escape hatch if possible, but it can be useful if you find yourself repeating `:global()` multiple times in the same `<style>`.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- src/components/MyComponent.astro -->
|
||||||
|
<style>
|
||||||
|
/* Scoped class selector within the component */
|
||||||
|
.scoped {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
/* Scoped element selector within the component */
|
||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style global>
|
||||||
|
/* Global style */
|
||||||
|
h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="scoped">I’m a scoped style and only apply to this component</div>
|
||||||
|
<h1>I have both scoped and global styles</h1>
|
||||||
|
```
|
||||||
|
|
||||||
📚 Read our full guide on [Astro component syntax](/core-concepts/astro-components#css-styles) to learn more about using the `<style>` tag.
|
📚 Read our full guide on [Astro component syntax](/core-concepts/astro-components#css-styles) to learn more about using the `<style>` tag.
|
||||||
|
|
||||||
## Cross-Browser Compatibility
|
## Cross-Browser Compatibility
|
||||||
|
@ -198,7 +224,7 @@ _Note: all the examples here use `lang="scss"` which is a great convenience for
|
||||||
|
|
||||||
That `.btn` class is scoped within that component, and won’t leak out. It means that you can **focus on styling and not naming.** Local-first approach fits in very well with Astro’s ESM-powered design, favoring encapsulation and reusability over global scope. While this is a simple example, it should be noted that **this scales incredibly well.** And if you need to share common values between components, [Sass’ module system][sass-use] also gets our recommendation for being easy to use, and a great fit with component-first design.
|
That `.btn` class is scoped within that component, and won’t leak out. It means that you can **focus on styling and not naming.** Local-first approach fits in very well with Astro’s ESM-powered design, favoring encapsulation and reusability over global scope. While this is a simple example, it should be noted that **this scales incredibly well.** And if you need to share common values between components, [Sass’ module system][sass-use] also gets our recommendation for being easy to use, and a great fit with component-first design.
|
||||||
|
|
||||||
By contrast, Astro does allow global styles via the `:global()` escape hatch, however, this should be avoided if possible. To illustrate this: say you used your button in a `<Nav />` component, and you wanted to style it differently there. You might be tempted to have something like:
|
By contrast, Astro does allow global styles via the `:global()` and `<style global>` escape hatches. However, this should be avoided if possible. To illustrate this: say you used your button in a `<Nav />` component, and you wanted to style it differently there. You might be tempted to have something like:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
---
|
---
|
||||||
|
|
|
@ -103,7 +103,7 @@ export interface Style extends BaseNode {
|
||||||
|
|
||||||
export interface Ast {
|
export interface Ast {
|
||||||
html: TemplateNode;
|
html: TemplateNode;
|
||||||
css: Style;
|
css: Style[];
|
||||||
module: Script;
|
module: Script;
|
||||||
// instance: Script;
|
// instance: Script;
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -226,18 +226,6 @@ export class Parser {
|
||||||
export default function parse(template: string, options: ParserOptions = {}): Ast {
|
export default function parse(template: string, options: ParserOptions = {}): Ast {
|
||||||
const parser = new Parser(template, options);
|
const parser = new Parser(template, options);
|
||||||
|
|
||||||
// TODO we may want to allow multiple <style> tags —
|
|
||||||
// one scoped, one global. for now, only allow one
|
|
||||||
if (parser.css.length > 1) {
|
|
||||||
parser.error(
|
|
||||||
{
|
|
||||||
code: 'duplicate-style',
|
|
||||||
message: 'You can only have one <style> tag per Astro file',
|
|
||||||
},
|
|
||||||
parser.css[1].start
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const instance_scripts = parser.js.filter((script) => script.context === 'default');
|
// const instance_scripts = parser.js.filter((script) => script.context === 'default');
|
||||||
// const module_scripts = parser.js.filter((script) => script.context === 'module');
|
// const module_scripts = parser.js.filter((script) => script.context === 'module');
|
||||||
const astro_scripts = parser.js.filter((script) => script.context === 'setup');
|
const astro_scripts = parser.js.filter((script) => script.context === 'setup');
|
||||||
|
@ -264,7 +252,7 @@ export default function parse(template: string, options: ParserOptions = {}): As
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: parser.html,
|
html: parser.html,
|
||||||
css: parser.css[0],
|
css: parser.css,
|
||||||
// instance: instance_scripts[0],
|
// instance: instance_scripts[0],
|
||||||
module: astro_scripts[0],
|
module: astro_scripts[0],
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -880,7 +880,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
|
||||||
|
|
||||||
const { script, createCollection } = compileModule(ast, ast.module, state, compileOptions);
|
const { script, createCollection } = compileModule(ast, ast.module, state, compileOptions);
|
||||||
|
|
||||||
compileCss(ast.css, state);
|
(ast.css || []).map(css => compileCss(css, state));
|
||||||
|
|
||||||
const html = await compileHtml(ast.html, state, compileOptions);
|
const html = await compileHtml(ast.html, state, compileOptions);
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
|
||||||
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
||||||
}
|
}
|
||||||
|
|
||||||
walkAstWithVisitors(ast.css, cssVisitors);
|
(ast.css || []).map(css => walkAstWithVisitors(css, cssVisitors));
|
||||||
walkAstWithVisitors(ast.html, htmlVisitors);
|
walkAstWithVisitors(ast.html, htmlVisitors);
|
||||||
|
|
||||||
// Run all of the finalizer functions in parallel because why not.
|
// Run all of the finalizer functions in parallel because why not.
|
||||||
|
|
|
@ -66,6 +66,7 @@ export interface TransformStyleOptions {
|
||||||
filename: string;
|
filename: string;
|
||||||
scopedClass: string;
|
scopedClass: string;
|
||||||
tailwindConfig?: string;
|
tailwindConfig?: string;
|
||||||
|
global?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** given a class="" string, does it contain a given class? */
|
/** given a class="" string, does it contain a given class? */
|
||||||
|
@ -78,7 +79,7 @@ function hasClass(classList: string, className: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert styles to scoped CSS */
|
/** Convert styles to scoped CSS */
|
||||||
async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig }: TransformStyleOptions): Promise<StyleTransformResult> {
|
async function transformStyle(code: string, { logging, type, filename, scopedClass, tailwindConfig, global }: TransformStyleOptions): Promise<StyleTransformResult> {
|
||||||
let styleType: StyleType = 'css'; // important: assume CSS as default
|
let styleType: StyleType = 'css'; // important: assume CSS as default
|
||||||
if (type) {
|
if (type) {
|
||||||
styleType = getStyleType.get(type) || styleType;
|
styleType = getStyleType.get(type) || styleType;
|
||||||
|
@ -131,17 +132,19 @@ async function transformStyle(code: string, { logging, type, filename, scopedCla
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Astro scoped styles (always on)
|
if (!global) {
|
||||||
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
|
// 2b. Astro scoped styles (skip for global style blocks)
|
||||||
|
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
|
||||||
|
|
||||||
// 2c. Scoped @keyframes
|
// 2c. Scoped @keyframes
|
||||||
postcssPlugins.push(
|
postcssPlugins.push(
|
||||||
postcssKeyframes({
|
postcssKeyframes({
|
||||||
generateScopedName(keyframesName) {
|
generateScopedName(keyframesName) {
|
||||||
return `${keyframesName}-${scopedClass}`;
|
return `${keyframesName}-${scopedClass}`;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 2d. Autoprefixer (always on)
|
// 2d. Autoprefixer (always on)
|
||||||
postcssPlugins.push(autoprefixer());
|
postcssPlugins.push(autoprefixer());
|
||||||
|
@ -215,6 +218,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
|
||||||
const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : '';
|
const code = Array.isArray(node.children) ? node.children.map(({ data }: any) => data).join('\n') : '';
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
|
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
|
||||||
|
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
|
||||||
styleNodes.push(node);
|
styleNodes.push(node);
|
||||||
styleTransformPromises.push(
|
styleTransformPromises.push(
|
||||||
transformStyle(code, {
|
transformStyle(code, {
|
||||||
|
@ -223,6 +227,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
|
||||||
filename,
|
filename,
|
||||||
scopedClass,
|
scopedClass,
|
||||||
tailwindConfig: compileOptions.astroConfig.devOptions.tailwindConfig,
|
tailwindConfig: compileOptions.astroConfig.devOptions.tailwindConfig,
|
||||||
|
global: globalAttr && globalAttr.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -246,6 +251,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
|
||||||
if (!node.content || !node.content.styles) return;
|
if (!node.content || !node.content.styles) return;
|
||||||
const code = node.content.styles;
|
const code = node.content.styles;
|
||||||
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
|
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
|
||||||
|
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
|
||||||
styleNodes.push(node);
|
styleNodes.push(node);
|
||||||
styleTransformPromises.push(
|
styleTransformPromises.push(
|
||||||
transformStyle(code, {
|
transformStyle(code, {
|
||||||
|
@ -253,6 +259,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
|
||||||
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
|
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
|
||||||
filename,
|
filename,
|
||||||
scopedClass,
|
scopedClass,
|
||||||
|
global: globalAttr && globalAttr.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,6 +52,20 @@ CSSBundling('Bundles CSS', async (context) => {
|
||||||
const typographyIndex = bundledContents.indexOf('body{');
|
const typographyIndex = bundledContents.indexOf('body{');
|
||||||
const colorsIndex = bundledContents.indexOf(':root{');
|
const colorsIndex = bundledContents.indexOf(':root{');
|
||||||
assert.ok(typographyIndex < colorsIndex);
|
assert.ok(typographyIndex < colorsIndex);
|
||||||
|
|
||||||
|
// test 5: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks)
|
||||||
|
const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')];
|
||||||
|
assert.is(scopedNavStyles.length, 2);
|
||||||
|
|
||||||
|
// test 6: assert <style global> was not scoped (in Nav.astro)
|
||||||
|
const globalStyles = [...bundledContents.matchAll('html{')];
|
||||||
|
assert.is(globalStyles.length, 1);
|
||||||
|
|
||||||
|
// test 7: assert keyframes are only scoped for non-global styles (from Nav.astro)
|
||||||
|
const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')];
|
||||||
|
const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')];
|
||||||
|
assert.ok(scopedKeyframes.length > 0);
|
||||||
|
assert.ok(globalKeyframes.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
CSSBundling.run();
|
CSSBundling.run();
|
||||||
|
|
|
@ -2,6 +2,36 @@
|
||||||
.nav {
|
.nav {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes nav-scoped-fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style global>
|
||||||
|
html {
|
||||||
|
--primary: aquamarine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nav-global-fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<nav class=".nav">
|
<nav class=".nav">
|
||||||
|
|
Loading…
Add table
Reference in a new issue