Parcel 09 ๐ฆ CSS Support โ Plain CSS, imports & bundling
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
| Method | Syntax | Output |
|---|---|---|
| JavaScript | import './app.css' | Extracted CSS + <link> in HTML |
| HTML | <link rel="stylesheet" href="./app.css"> | Processed CSS file |
Dependency types
| Syntax | Behavior |
|---|---|
@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)
| Feature | Behavior |
|---|---|
| Vendor prefixing | Adds/removes prefixes for targets |
| Color Level 4/5 | Downleveled with fallbacks |
| Logical properties | Converted to physical properties |
| Media query range | Converted to legacy syntax |
CSS Modules
| Item | Value |
|---|---|
| Filename | *.module.css |
| Import | import styles from './file.module.css' |
| Generated name | Hash-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
| Pitfall | Why It Happens | Fix |
|---|---|---|
CSS not applied from a .tsx import | Inline <script> in HTML does not follow the module graph | Use <script type="module" src="./app.tsx"> |
url() not resolved | Path relative to the wrong file | Use paths relative to the file where the url() appears |
| Vendor prefixes missing | No browserslist field | Add the field to package.json |
CSS Module class is undefined | Filename missing .module | Rename to *.module.css |
@import not inlined | Imported file is outside the project root | Check 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
| Item | Value |
|---|---|
| CSS import from JS | import './app.css' |
| CSS import from HTML | <link rel="stylesheet" href="..."> |
@import behavior | Inlined into the same bundle |
url() behavior | Resolved, hashed, rewritten |
| Transpilation trigger | browserslist field |
| Vendor prefixing | Automatic with targets |
| CSS Modules | *.module.css suffix |
| Scoped class names | Hash-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
@importandurl()dependencies, and bundles the result . @importfiles are inlined, not preserved. The output stylesheet contains the rules from every imported file. Theurl()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.csssuffix 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.
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!