Vite · Part 6 — CSS Handling
CSS in Vite with zero config: plain side-effect imports, scoped CSS Modules, built-in Sass/Less/Stylus, the ?inline query for Shadow DOM, automatic PostCSS, Lightning CSS, and code-split CSS in the build. With a CSS pipeline explorer.
CSS is where Vite’s “it just works” reputation is most earned {CSS là nơi danh tiếng “chạy là được” của Vite xứng đáng nhất}. No loaders, no MiniCssExtractPlugin, no config for the common cases — you import a stylesheet and it works in dev (hot-swapped) and in build (extracted and hashed) {Không loader, không MiniCssExtractPlugin, không config cho trường hợp phổ biến — bạn import một stylesheet và nó chạy khi dev (hot-swap) và khi build (tách và hash)}.
Switch between CSS styles to see what you import and what Vite produces {Chuyển giữa các kiểu CSS để xem bạn import gì và Vite tạo ra gì}:
1. Plain CSS — a side-effect import {CSS thường — import tác dụng phụ}
import "./button.css";
That’s it {Vậy thôi}. The import has no binding — it’s a side effect that adds the styles to the page {Import không có binding — nó là tác dụng phụ thêm style vào trang}. In dev, Vite injects them into a <style> tag and hot-swaps on edit {Khi dev, Vite chèn vào thẻ <style> và hot-swap khi sửa}. In the build, they’re extracted into a hashed .css file and linked {Khi build, chúng được tách vào file .css đã hash và link}.
The catch: class names are global, so collisions are your problem {Vướng mắc: tên class là toàn cục, nên va chạm là vấn đề của bạn}.
2. CSS Modules — scoped by default {CSS Modules — scope mặc định}
Name a file *.module.css and Vite turns it into a CSS Module {Đặt tên file *.module.css và Vite biến nó thành CSS Module}:
import styles from "./Button.module.css";
// styles = { btn: "btn_a1b2c3" } ← hashed, scoped
<button className={styles.btn}>Go</button>;
Each class name is hashed and locally scoped, so .btn in one file never clashes with .btn in another {Mỗi tên class được hash và scope cục bộ, nên .btn ở file này không bao giờ đụng .btn ở file khác}. You import a JS object mapping your names to the generated ones {Bạn import một object JS ánh xạ tên của bạn sang tên được sinh}. Configure naming via css.modules {Cấu hình đặt tên qua css.modules}:
export default defineConfig({
css: { modules: { localsConvention: "camelCase" } }, // styles.myClass
});
3. Preprocessors — just install {Preprocessor — chỉ cần cài}
Sass, Less, and Stylus work with no config — install the compiler and import the file {Sass, Less, Stylus chạy không config — cài trình biên dịch và import file}:
npm install -D sass-embedded
import "./styles.scss"; // compiled automatically
Combine with Modules: Component.module.scss gives you nesting and scoping {Kết hợp với Modules: Component.module.scss cho bạn lồng và scope}. Vite uses Sass’s modern API by default (the legacy API was removed in Vite 7) {Vite dùng API hiện đại của Sass mặc định (API cũ bị bỏ ở Vite 7)}.
4. Query suffixes — ?inline and ?raw {Hậu tố query — ?inline và ?raw}
By default importing CSS injects it {Mặc định import CSS sẽ chèn nó}. Sometimes you want the text instead {Đôi khi bạn muốn văn bản thay thế}:
import css from "./theme.css?inline"; // css = the stylesheet as a string
?inline returns the compiled CSS as a string without injecting it, so you can attach it where you want — most commonly a Shadow DOM root for a web component {?inline trả về CSS đã biên dịch dạng chuỗi mà không chèn, để bạn gắn nơi mình muốn — phổ biến nhất là root Shadow DOM cho web component}. (?raw gives the untransformed file contents — covered with assets in Part 7.) {(?raw cho nội dung file chưa biến đổi — bàn cùng asset ở Phần 7.)}
5. PostCSS & Lightning CSS {PostCSS & Lightning CSS}
Drop a postcss.config.js at the root and Vite runs PostCSS on every stylesheet automatically {Đặt postcss.config.js ở gốc và Vite chạy PostCSS trên mọi stylesheet tự động}:
// postcss.config.js
export default { plugins: [autoprefixer()] };
This is how Tailwind, autoprefixer, and nesting plugins integrate — no Vite-specific wiring {Đây là cách Tailwind, autoprefixer, và plugin nesting tích hợp — không cần dây nối riêng cho Vite}. Vite also supports Lightning CSS (a fast Rust transformer/minifier) as an opt-in alternative for transforms and minification {Vite cũng hỗ trợ Lightning CSS (trình biến đổi/minify Rust nhanh) như lựa chọn opt-in cho biến đổi và minify}:
export default defineConfig({
css: { transformer: "lightningcss" },
build: { cssMinify: "lightningcss" },
});
6. CSS is code-split too {CSS cũng được tách code}
In the production build, CSS imported by an async chunk is split into its own file and loaded only when that chunk loads {Trong build production, CSS được import bởi chunk async sẽ tách thành file riêng và chỉ tải khi chunk đó tải}. So a lazy-loaded route’s styles don’t bloat your initial CSS {Nên style của route lazy không phình CSS ban đầu}. This is automatic — the same code-splitting you saw with JS in the Webpack series, applied to CSS {Cái này tự động — cùng việc tách code bạn thấy với JS trong series Webpack, áp cho CSS}. You can force everything into one file with build.cssCodeSplit: false if you prefer {Bạn có thể ép tất cả vào một file với build.cssCodeSplit: false nếu thích}.
7. Exercises {Bài tập}
1. Two components each define a global .title class with different styles and they clash. What’s the idiomatic Vite fix? {Hai component đều định nghĩa class toàn cục .title với style khác nhau và đụng nhau. Cách sửa kiểu Vite?}
Solution {Lời giải}
Use CSS Modules: rename to *.module.css and reference styles.title — names become hashed and locally scoped {Dùng CSS Modules: đổi tên thành *.module.css và tham chiếu styles.title — tên được hash và scope cục bộ}.
2. You’re building a web component and need the CSS as a string to attach to its Shadow DOM, not injected into the page. How? {Bạn xây web component và cần CSS dạng chuỗi để gắn vào Shadow DOM của nó, không chèn vào trang. Sao?}
Solution {Lời giải}
Import with the ?inline query: import css from "./theme.css?inline" returns the stylesheet text without injecting {Import với query ?inline: import css from "./theme.css?inline" trả về văn bản stylesheet mà không chèn}.
3. How do you add Sass and autoprefixer to a Vite project? {Làm sao thêm Sass và autoprefixer vào dự án Vite?}
Solution {Lời giải}
npm i -D sass-embedded then import .scss directly; add postcss.config.js with autoprefixer() — Vite runs both automatically, no loader config {npm i -D sass-embedded rồi import .scss trực tiếp; thêm postcss.config.js với autoprefixer() — Vite chạy cả hai tự động, không cần config loader}.
Stretch {Nâng cao}: in the explorer, compare the Plain vs CSS Modules tabs and note exactly what differs in the output — the global selector vs the hashed, scoped one {trong explorer, so tab Plain vs CSS Modules và để ý đúng cái khác trong output — selector toàn cục vs cái đã hash, scope}.
Key takeaways {Điểm chính}
- Plain CSS imports are side effects; class names are global {Import CSS thường là tác dụng phụ; tên class toàn cục}.
*.module.css= scoped, hashed class names with zero config {*.module.css= tên class scope, hash, không config}.- Preprocessors (Sass/Less/Stylus) work after a single install {Preprocessor chạy sau một lần cài}.
?inlinereturns CSS as a string for Shadow DOM and the like {?inlinetrả CSS dạng chuỗi cho Shadow DOM v.v}.- PostCSS auto-runs via
postcss.config.js; CSS is code-split in the build {PostCSS tự chạy quapostcss.config.js; CSS được tách code khi build}.
Next up {Tiếp theo}
Part 7 — Static assets & glob import: importing images and files as URLs, the ?url / ?raw / ?worker queries, the public/ directory, and import.meta.glob for bulk imports {Phần 7 — Asset tĩnh & glob import: import ảnh và file dạng URL, query ?url / ?raw / ?worker, thư mục public/, và import.meta.glob để import hàng loạt}.