Setting Up CSS Preprocessors for Website Project
Choosing a preprocessor and configuring it correctly from day one is the foundation of a maintainable CSS architecture. Incorrect configuration leads to duplicate imports, slow builds, tool conflicts. This work is done once and determines development convenience for the project's lifetime.
Choosing a Tool
| Criterion | SCSS | LESS | PostCSS | Tailwind |
|---|---|---|---|---|
| Syntax | CSS+ | CSS+ | CSS (native) | Utilities |
| Mixins | Powerful | Basic | Via plugins | No |
| Functions | Extended | Basic | Via plugins | No |
| Build Performance | Good | Good | Excellent | Excellent |
| Ecosystem | Large | Medium | Huge | Growing |
| New Projects | Yes | Legacy/AntD | Always | React/Vue |
Recommendation: SCSS for classic projects, Tailwind for React/Vue, PostCSS as a mandatory layer on top of any.
Setup in Vite
# SCSS
npm install -D sass
# LESS
npm install -D less
# PostCSS and plugins
npm install -D postcss autoprefixer postcss-preset-env cssnano postcss-import
// vite.config.ts — full configuration
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@styles': path.resolve(__dirname, 'src/styles'),
},
},
css: {
preprocessorOptions: {
scss: {
// Automatically inject variables and mixins
additionalData: `
@use "@styles/abstracts/variables" as v;
@use "@styles/abstracts/mixins" as m;
@use "sass:math";
@use "sass:color";
@use "sass:map";
`,
// Use modern Dart Sass API
api: 'modern-compiler',
// Suppress legacy warnings
silenceDeprecations: ['legacy-js-api'],
},
},
// CSS Modules
modules: {
localsConvention: 'camelCase',
generateScopedName:
process.env.NODE_ENV === 'production'
? '[hash:base64:8]'
: '[name]__[local]__[hash:base64:4]',
},
// PostCSS applies to all CSS after preprocessor compilation
postcss: './postcss.config.js',
// Source maps in development
devSourcemap: true,
},
});
// postcss.config.js
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-nested'),
require('postcss-custom-media'),
require('autoprefixer'),
require('postcss-preset-env')({
stage: 2,
features: {
'nesting-rules': true,
'custom-properties': false,
},
}),
...(isProd
? [require('cssnano')({ preset: ['default', { discardComments: { removeAll: true } }] })]
: []),
],
};
Setup in Webpack
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(sa|sc)ss$/,
use: [
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: isProd
? '[hash:base64:8]'
: '[name]__[local]--[hash:base64:4]',
auto: /\.module\.(sa|sc)ss$/, // Only *.module.scss
},
importLoaders: 2,
sourceMap: !isProd,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: './postcss.config.js',
},
},
},
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: { outputStyle: 'compressed' },
additionalData: `@use "@/styles/abstracts" as *;`,
},
},
],
},
],
},
plugins: [
...(isProd ? [new MiniCssExtractPlugin({
filename: 'assets/[name].[contenthash:8].css',
chunkFilename: 'assets/[id].[contenthash:8].css',
})] : []),
],
};
Preprocessor File Structure
src/styles/
abstracts/
_variables.scss
_functions.scss
_mixins.scss
_placeholders.scss
_index.scss ← @forward everything
base/
_reset.scss ← modern-normalize or custom
_typography.scss
_root.scss ← :root { --variables }
components/ ← styles for each component
layout/ ← header, footer, grid, sidebar
themes/
_light.scss
_dark.scss
vendors/ ← overrides of third-party libraries
_swiper.scss
_leaflet.scss
main.scss ← entry point
Linting: Stylelint
npm install -D stylelint stylelint-config-standard-scss stylelint-order
// .stylelintrc.js
module.exports = {
extends: [
'stylelint-config-standard-scss',
],
plugins: ['stylelint-order'],
rules: {
// Property order
'order/properties-order': [
'content',
'position', 'top', 'right', 'bottom', 'left', 'z-index',
'display', 'flex', 'flex-direction', 'flex-wrap', 'gap',
'align-items', 'justify-content',
'grid', 'grid-template', 'grid-area',
'width', 'min-width', 'max-width',
'height', 'min-height', 'max-height',
'margin', 'padding',
'background', 'color', 'font', 'font-size', 'font-weight',
'border', 'border-radius',
'box-shadow', 'opacity', 'transform', 'transition', 'animation',
],
// SCSS-specific
'scss/at-rule-no-unknown': true,
'scss/no-duplicate-mixins': true,
'scss/no-global-function-names': true,
// Forbid nesting deeper than 3 levels
'max-nesting-depth': 3,
// Forbid !important except exceptions
'declaration-no-important': [true, { severity: 'warning' }],
},
};
// package.json scripts
{
"scripts": {
"lint:css": "stylelint \"src/**/*.{css,scss}\"",
"lint:css:fix": "stylelint \"src/**/*.{css,scss}\" --fix"
}
}
Browserslist
Autoprefixer and postcss-preset-env read the list of target browsers:
// .browserslistrc
[production]
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 11
[development]
last 1 chrome version
last 1 firefox version
last 1 safari version
Or in package.json:
{
"browserslist": {
"production": ["> 0.5%", "last 2 versions", "not dead"],
"development": ["last 1 chrome version", "last 1 firefox version"]
}
}
Timeline
Full initial CSS infrastructure setup (preprocessor + PostCSS + CSS Modules + Stylelint + Browserslist): 4–8 hours. Setup for existing project with migration: 1–2 days. This work pays for itself in the first month of development through code consistency and elimination of typical errors.







