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:
Tony Sullivan 2021-07-23 19:51:27 +02:00 committed by GitHub
parent 041788878d
commit 294a656ed9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 103 additions and 28 deletions

View 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.

View file

@ -63,6 +63,8 @@ For best results, you should only have one `<style>` tag per-Astro component. Th
</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">`.
📚 Read our full guide on [Component Styling](/guides/styling) to learn more.

View file

@ -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>
```
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">Im 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.
## 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 wont leak out. It means that you can **focus on styling and not naming.** Local-first approach fits in very well with Astros 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
---

View file

@ -103,7 +103,7 @@ export interface Style extends BaseNode {
export interface Ast {
html: TemplateNode;
css: Style;
css: Style[];
module: Script;
// instance: Script;
meta: {

View file

@ -226,18 +226,6 @@ export class Parser {
export default function parse(template: string, options: ParserOptions = {}): Ast {
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 module_scripts = parser.js.filter((script) => script.context === 'module');
const astro_scripts = parser.js.filter((script) => script.context === 'setup');
@ -264,7 +252,7 @@ export default function parse(template: string, options: ParserOptions = {}): As
return {
html: parser.html,
css: parser.css[0],
css: parser.css,
// instance: instance_scripts[0],
module: astro_scripts[0],
meta: {

View file

@ -880,7 +880,7 @@ export async function codegen(ast: Ast, { compileOptions, filename, fileID }: Co
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);

View file

@ -91,7 +91,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
}
walkAstWithVisitors(ast.css, cssVisitors);
(ast.css || []).map(css => walkAstWithVisitors(css, cssVisitors));
walkAstWithVisitors(ast.html, htmlVisitors);
// Run all of the finalizer functions in parallel because why not.

View file

@ -66,6 +66,7 @@ export interface TransformStyleOptions {
filename: string;
scopedClass: string;
tailwindConfig?: string;
global?: boolean;
}
/** 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 */
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
if (type) {
styleType = getStyleType.get(type) || styleType;
@ -131,17 +132,19 @@ async function transformStyle(code: string, { logging, type, filename, scopedCla
}
}
// 2b. Astro scoped styles (always on)
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
if (!global) {
// 2b. Astro scoped styles (skip for global style blocks)
postcssPlugins.push(astroScopedStyles({ className: scopedClass }));
// 2c. Scoped @keyframes
postcssPlugins.push(
postcssKeyframes({
generateScopedName(keyframesName) {
return `${keyframesName}-${scopedClass}`;
},
})
);
// 2c. Scoped @keyframes
postcssPlugins.push(
postcssKeyframes({
generateScopedName(keyframesName) {
return `${keyframesName}-${scopedClass}`;
},
})
);
}
// 2d. Autoprefixer (always on)
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') : '';
if (!code) return;
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
styleNodes.push(node);
styleTransformPromises.push(
transformStyle(code, {
@ -223,6 +227,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
filename,
scopedClass,
tailwindConfig: compileOptions.astroConfig.devOptions.tailwindConfig,
global: globalAttr && globalAttr.value,
})
);
return;
@ -246,6 +251,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
if (!node.content || !node.content.styles) return;
const code = node.content.styles;
const langAttr = (node.attributes || []).find(({ name }: any) => name === 'lang');
const globalAttr = (node.attributes || []).find(({ name }: any) => name === 'global');
styleNodes.push(node);
styleTransformPromises.push(
transformStyle(code, {
@ -253,6 +259,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
type: (langAttr && langAttr.value[0] && langAttr.value[0].data) || undefined,
filename,
scopedClass,
global: globalAttr && globalAttr.value,
})
);
},

View file

@ -52,6 +52,20 @@ CSSBundling('Bundles CSS', async (context) => {
const typographyIndex = bundledContents.indexOf('body{');
const colorsIndex = bundledContents.indexOf(':root{');
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();

View file

@ -2,6 +2,36 @@
.nav {
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>
<nav class=".nav">