| | |

Parcel 09 ๐Ÿ“ฆ CSS Support โ€” Plain CSS, imports & bundling

โฑ๏ธ Estimated read: 11 min | |

Parcel treats CSS as a first-class asset type. A .css file can be imported from JavaScript, referenced from HTML, or used as a build entry point. When Parcel encounters a CSS file, it parses the stylesheet, discovers its dependenciesโ€”@import statements, url() references, and @font-face sourcesโ€”and bundles the result into a single output stylesheet per entry. The CSS that ships is not the source file; it is a transformed and minified stylesheet whose URLs point at the hashed assets Parcel produced.

This chapter covers the plain CSS pipeline: importing CSS from JavaScript and HTML, the @import inlining behavior, the url() dependency rewriting, the automatic vendor prefixing and syntax lowering when browserslist is declared, and the CSS Modules opt-in for scoped class names.

Key point: Parcel inlines @imported CSS files into the same bundle as the importing file. The url() references inside those imported files are resolved relative to the file they appear in, not the file that imported them.


Why CSS handling is built into the asset graph

The dependency discovery problem. A CSS file contains references to other files: @import './other.css', url('./background.png'), and @font-face { src: url('./font.woff2') }. Parcel parses these references, adds each one to the asset graph, and rewrites the URL in the output to point at the hashed output file. The CSS file is not copied as-is; it is transformed into a document whose references resolve to the actual build artifacts.

The bundling problem. A JavaScript file that imports CSS produces a side effect: the styles must be injected into the page. Parcel handles this by extracting the CSS into a separate bundle and inserting a <link> element into the HTML output, or by injecting the styles at runtime depending on the build configuration. The CSS is not inlined into the JavaScript bundle by default; it is extracted so the browser can load it in parallel with the script .

The transpilation problem. Modern CSS syntaxโ€”lab() colors, logical properties, media query range syntax, nestingโ€”is not supported by every browser. Parcel’s CSS transformer, written in Rust, downlevels the syntax and adds vendor prefixes when a browserslist field is present. The transpilation is opt-in, just like the JavaScript transpilation: without browserslist, the CSS is emitted as written .


a. Importing CSS from JavaScript and HTML

A CSS file can be imported from JavaScript as a side-effect import. The import returns nothing; the styles are included in the build.

// app.js
import './app.css'

When Parcel builds an HTML entry that references app.js, the imported CSS is extracted into a separate file, and a <link rel="stylesheet"> element is inserted into the HTML <head> so the browser can load the styles in parallel with the script . The output HTML contains both the stylesheet link and the script tag.

A CSS file can also be referenced directly from HTML with a <link rel="stylesheet"> element. This is the more direct path: the CSS is the entry, and Parcel processes it without going through JavaScript.

<link rel="stylesheet" href="./app.css">

Both paths produce the same result: a hashed CSS bundle in the output, referenced from the HTML. The JavaScript path adds the link element automatically; the HTML path requires the developer to write the link element in the source.


b. @import inlining and url() rewriting

An @import statement inside a CSS file is not preserved in the output. Parcel inlines the imported file’s contents into the same bundle as the importing file. The @import is resolved, the imported CSS is parsed, and its rules are concatenated into the output stylesheet .

/* app.css */
@import './reset.css';
@import './layout.css';

.button {
  background: url('./images/button.png');
}

The output contains the rules from reset.css and layout.css followed by the .button rule. The url('./images/button.png') is resolved relative to app.css, the file where the url() appears. An url() inside layout.css is resolved relative to layout.css, not app.css. This is the standard CSS resolution rule, and Parcel follows it.

The url() reference to an image, font, or other asset is processed like any other dependency. The image is hashed, the URL is rewritten to the output path, and the browser requests the correct file.


c. Transpilation and vendor prefixing

By default, Parcel does not transpile CSS. The output preserves the source syntax. When a browserslist field is declared in package.json, Parcel enables its CSS transpilation pipeline, which is equivalent to Autoprefixer and PostCSS Preset Env for the most common cases .

The transformer adds vendor-prefixed fallbacks for features that require them. For example, image-set() gets a -webkit-image-set() fallback for Chrome. It also removes unnecessary prefixes that the target browsers do not need, reducing bundle size. The syntax lowering covers Color Level 4 and 5 (lab(), oklab(), color-mix()), logical properties, media query range syntax, clamp(), and multi-value display .

The transpilation is the Rust-based Parcel CSS transformer, which became the default in Parcel v2.4.0. It replaced the previous PostCSS-based pipeline for the built-in transforms, and it is significantly faster . PostCSS is still supported for custom plugins and for frameworks like Tailwind that require it .


d. CSS Modules

A file named with the .module.css suffix is treated as a CSS Module. The default import returns an object that maps the original class names to the generated, scoped class names.

import styles from './button.module.css'

element.className = styles.button

The generated class name includes a hash of the file path by default, which prevents collisions between modules. The naming pattern can be customized through the @parcel/transformer-css configuration in package.json . CSS Modules are enabled per file by the filename convention, not globally. To enable them for all CSS files, the global option in the transformer configuration must be set .


Complete Example Session

/* ============================================
   PART 1: THE SOURCE CSS
   ============================================ */
/* app.css */
@import './reset.css';

.button {
  background: url('./images/button.png');
  display: flex;
}
/* ============================================
   PART 2: THE IMPORTED FILE
   ============================================ */
/* reset.css */
* {
  margin: 0;
  padding: 0;
}
// ============================================
// PART 3: THE JAVASCRIPT IMPORT
// ============================================
// app.js
import './app.css'

// The CSS is extracted into a separate bundle.
// A <link> is inserted into the HTML output.
# ============================================
# PART 4: THE BUILD
# ============================================
parcel build index.html

# Output:
# dist/index.html
# dist/app.a1b2c3.css
# dist/button.d4e5f6.png
<!-- ============================================
     PART 5: THE OUTPUT HTML
     ============================================ -->
<link rel="stylesheet" href="/app.a1b2c3.css">
<script type="module" src="/app.j1k2l3.js"></script>

<!-- The CSS is loaded in parallel with the script. -->
/* ============================================
   PART 6: THE OUTPUT CSS
   ============================================ */
/* reset.css is inlined */
* {
  margin: 0;
  padding: 0;
}

.button {
  background: url("/button.d4e5f6.png");
  display: flex;
}

/* The url() is rewritten to the hashed asset. */
/* ============================================
   PART 7: VENDOR PREFIXING (WITH BROWSERSLIST)
   ============================================ */
/* Source */
.logo {
  background: image-set(url(logo.png) 2x, url(logo.png) 1x);
}

/* Output (Chrome target) */
.logo {
  background: -webkit-image-set(url(logo.png) 2x, url(logo.png) 1x);
  background: image-set(url("logo.a1b2c3.png") 2x, url("logo.a1b2c3.png") 1x);
}
/* ============================================
   PART 8: SYNTAX LOWERING
   ============================================ */
/* Source */
.card {
  color: lab(50% 40 59.5);
  margin-inline-start: 16px;
}

/* Output for older browsers */
.card {
  color: #e54b4b;
  margin-left: 16px;
}
/* ============================================
   PART 9: CSS MODULES
   ============================================ */
/* button.module.css */
.button {
  background: blue;
}
// ============================================
// PART 10: CSS MODULE IMPORT
// ============================================
import styles from './button.module.css'

// styles.button is the scoped class name.
element.className = styles.button

The ten parts covered the source CSS, the imported file, the JavaScript import, the build output, the output HTML, the output CSS with rewritten URLs, vendor prefixing, syntax lowering, the CSS Module file, and the module import.


Quick Reference

Import methods

MethodSyntaxOutput
JavaScriptimport './app.css'Extracted CSS + <link> in HTML
HTML<link rel="stylesheet" href="./app.css">Processed CSS file

Dependency types

SyntaxBehavior
@import './other.css'Inlined into the same bundle
url('./image.png')Resolved, hashed, URL rewritten
@font-face { src: url(...) }Font asset processed

Transpilation (with browserslist)

FeatureBehavior
Vendor prefixingAdds/removes prefixes for targets
Color Level 4/5Downleveled with fallbacks
Logical propertiesConverted to physical properties
Media query rangeConverted to legacy syntax

CSS Modules

ItemValue
Filename*.module.css
Importimport styles from './file.module.css'
Generated nameHash-prefixed by default
Global opt-in@parcel/transformer-css config

Best Practices

โœ… Do This:

// Import global CSS from JavaScript
import './app.css'                                          // โœ…
<!-- Reference CSS from HTML for the entry stylesheet -->
<link rel="stylesheet" href="./app.css">                     // โœ…
/* Use relative paths for url() references */
background: url('./images/bg.png');                          // โœ…
// Declare browserslist for CSS transpilation
"browserslist": ["> 0.25%", "not dead"]                     // โœ…

โŒ Don’t Do This:

/* Use absolute paths for url() in custom properties */
--bg: url('/images/bg.png');  // may fail resolution          // โŒ
// Import CSS Modules without the .module suffix
import styles from './button.css'  // styles is undefined     // โŒ
/* Assume @import is preserved in the output */
@import './other.css';  // inlined, not preserved              // โŒ

Common Pitfalls

PitfallWhy It HappensFix
CSS not applied from a .tsx importInline <script> in HTML does not follow the module graphUse <script type="module" src="./app.tsx">
url() not resolvedPath relative to the wrong fileUse paths relative to the file where the url() appears
Vendor prefixes missingNo browserslist fieldAdd the field to package.json
CSS Module class is undefinedFilename missing .moduleRename to *.module.css
@import not inlinedImported file is outside the project rootCheck the resolution path

Real-World Examples

1. JavaScript CSS import

import './app.css'

2. HTML stylesheet link

<link rel="stylesheet" href="./app.css">

3. @import inlining

@import './reset.css';

4. url() image reference

background: url('./bg.png');

5. Vendor prefix target

{ "browserslist": ["last 2 Chrome versions"] }

6. Syntax lowering

/* Source */ color: lab(50% 40 59.5);
/* Output */ color: #e54b4b;

7. CSS Module file

/* button.module.css */
.button { background: blue; }

8. CSS Module import

import styles from './button.module.css'

9. Custom naming pattern

{ "@parcel/transformer-css": { "pattern": "[name]-[local]" } }

10. Global CSS Modules

{ "@parcel/transformer-css": { "global": true } }

Visual

The CSS dependency graph

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  app.css                                                     โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ @import './reset.css' โ”€โ”€โ–บ inlined                 โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ url('./bg.png') โ”€โ”€โ–บ hashed asset                  โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ .button { ... } โ”€โ”€โ–บ output rule                   โ”‚
โ”‚                                                              โ”‚
โ”‚  Output: one CSS bundle with all imports inlined.            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

JavaScript import extraction

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  app.js imports './app.css'                                  โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Parcel extracts the CSS                                     โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ dist/app.a1b2c3.css                               โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ dist/index.html gets:                              โ”‚
โ”‚             <link rel="stylesheet" href="/app.a1b2c3.css">  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Transpilation decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  browserslist?                                                โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ”œโ”€โ”€ No โ”€โ”€โ–บ source syntax preserved                    โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ””โ”€โ”€ Yes โ”€โ”€โ–บ transpile for targets                     โ”‚
โ”‚             โ”œโ”€โ”€ vendor prefixes                              โ”‚
โ”‚             โ”œโ”€โ”€ syntax lowering                              โ”‚
โ”‚             โ””โ”€โ”€ unnecessary prefixes removed                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

CSS Modules scoping

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  button.module.css                                           โ”‚
โ”‚    .button { background: blue; }                             โ”‚
โ”‚       โ”‚                                                      โ”‚
โ”‚       โ–ผ                                                      โ”‚
โ”‚  Output: .button_a1b2c3 { background: blue; }               โ”‚
โ”‚                                                              โ”‚
โ”‚  import styles from './button.module.css'                    โ”‚
โ”‚  styles.button === 'button_a1b2c3'                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
CSS import from JSimport './app.css'
CSS import from HTML<link rel="stylesheet" href="...">
@import behaviorInlined into the same bundle
url() behaviorResolved, hashed, rewritten
Transpilation triggerbrowserslist field
Vendor prefixingAutomatic with targets
CSS Modules*.module.css suffix
Scoped class namesHash-prefixed by default

Key takeaways:

  • CSS is an asset in the dependency graph. Importing it from JavaScript or referencing it from HTML adds it to the build. Parcel parses the file, discovers its @import and url() dependencies, and bundles the result .
  • @import files are inlined, not preserved. The output stylesheet contains the rules from every imported file. The url() references are resolved relative to the file they appear in, not the file that imported them .
  • CSS imported from JavaScript is extracted into a separate bundle. A <link> element is inserted into the HTML output so the browser can load the CSS in parallel with the script. The CSS is not inlined into the JavaScript bundle .
  • Transpilation is opt-in. Without browserslist, the CSS is emitted as written. With it, Parcel adds vendor prefixes, lowers modern syntax, and removes unnecessary prefixes for the target browsers .
  • CSS Modules are enabled by filename. The .module.css suffix triggers scoping. The default import returns a class-name map. The generated class names are hash-prefixed by default .
  • Parcel CSS is the default transformer. Since v2.4.0, the Rust-based transformer handles the built-in transforms, and PostCSS is still supported for custom plugins .

Remember: CSS handling in Parcel is the same asset-graph model as JavaScript. The file is parsed, its dependencies are discovered, and the output is a transformed document with rewritten URLs. @import is inlined. url() is hashed. Transpilation waits for browserslist. CSS Modules wait for the .module.css suffix. The one mistake that causes the most trouble is importing CSS from an inline <script> in HTML, which does not follow the module graph. Use <script type="module" src="./app.js"> so the JavaScript import chain is traced correctly.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!