summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Tony Sullivan <tony.f.sullivan@outlook.com> 2021-07-23 19:51:27 +0200
committerGravatar GitHub <noreply@github.com> 2021-07-23 13:51:27 -0400
commit294a656ed92707939dbd3b771c31a5d6a8a05024 (patch)
tree8ec79481b8813cbc7061170eb3f895e3c66b2e07
parent041788878d6091a9b5a064068cbacdb4761700ca (diff)
downloadastro-294a656ed92707939dbd3b771c31a5d6a8a05024.tar.gz
astro-294a656ed92707939dbd3b771c31a5d6a8a05024.tar.zst
astro-294a656ed92707939dbd3b771c31a5d6a8a05024.zip
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>
-rw-r--r--.changeset/empty-trainers-chew.md8
-rw-r--r--docs/src/pages/core-concepts/astro-components.md2
-rw-r--r--docs/src/pages/guides/styling.md28
-rw-r--r--packages/astro-parser/src/interfaces.ts2
-rw-r--r--packages/astro-parser/src/parse/index.ts14
-rw-r--r--packages/astro/src/compiler/codegen/index.ts2
-rw-r--r--packages/astro/src/compiler/transform/index.ts2
-rw-r--r--packages/astro/src/compiler/transform/styles.ts29
-rw-r--r--packages/astro/test/astro-css-bundling.test.js14
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro30
10 files changed, 103 insertions, 28 deletions
diff --git a/.changeset/empty-trainers-chew.md b/.changeset/empty-trainers-chew.md
new file mode 100644
index 000000000..8e35c3aea
--- /dev/null
+++ b/.changeset/empty-trainers-chew.md
@@ -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.
diff --git a/docs/src/pages/core-concepts/astro-components.md b/docs/src/pages/core-concepts/astro-components.md
index 5c20169da..a2a476a37 100644
--- a/docs/src/pages/core-concepts/astro-components.md
+++ b/docs/src/pages/core-concepts/astro-components.md
@@ -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.
diff --git a/docs/src/pages/guides/styling.md b/docs/src/pages/guides/styling.md
index d6a3691a2..43dc4f206 100644
--- a/docs/src/pages/guides/styling.md
+++ b/docs/src/pages/guides/styling.md
@@ -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">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.
## 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.
-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
---
diff --git a/packages/astro-parser/src/interfaces.ts b/packages/astro-parser/src/interfaces.ts
index 1e996027c..335643aa5 100644
--- a/packages/astro-parser/src/interfaces.ts
+++ b/packages/astro-parser/src/interfaces.ts
@@ -103,7 +103,7 @@ export interface Style extends BaseNode {
export interface Ast {
html: TemplateNode;
- css: Style;
+ css: Style[];
module: Script;
// instance: Script;
meta: {
diff --git a/packages/astro-parser/src/parse/index.ts b/packages/astro-parser/src/parse/index.ts
index ef33bfaca..776d46e2b 100644
--- a/packages/astro-parser/src/parse/index.ts
+++ b/packages/astro-parser/src/parse/index.ts
@@ -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: {
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index f2a32e30f..85e6349db 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -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);
diff --git a/packages/astro/src/compiler/transform/index.ts b/packages/astro/src/compiler/transform/index.ts
index c9943cc5f..3e0a971cd 100644
--- a/packages/astro/src/compiler/transform/index.ts
+++ b/packages/astro/src/compiler/transform/index.ts
@@ -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.
diff --git a/packages/astro/src/compiler/transform/styles.ts b/packages/astro/src/compiler/transform/styles.ts
index ed163132c..715dfb942 100644
--- a/packages/astro/src/compiler/transform/styles.ts
+++ b/packages/astro/src/compiler/transform/styles.ts
@@ -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,
})
);
},
diff --git a/packages/astro/test/astro-css-bundling.test.js b/packages/astro/test/astro-css-bundling.test.js
index 8b5ab993d..d674c6657 100644
--- a/packages/astro/test/astro-css-bundling.test.js
+++ b/packages/astro/test/astro-css-bundling.test.js
@@ -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();
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro
index deafe668c..e54a600f4 100644
--- a/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro
@@ -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">