Selectors for Humans, Hashes for Machines
One aspect of CSS modules that I truly appreciate is its ability to compress class names into very short hashes. This feature allows me to keep my CSS selectors as long and descriptive as needed, while still compressing them into concise three or four character hashes. It aligns with my rule for CSS: selectors should be written for human readability, but compressed for machine efficiency.
However, setting the hash length too short can lead to hidden hash collision issues which are not easy to detect until you encounter a bug. To address this, I’ve developed a solution in Vite that utilizes the getJSON
callback function provided by postcss-modules. By storing all used selector hashes in a map, we can easily check for any duplicates. If a duplicate hash is detected, an error is immediately raised, and the build process is halted. In such cases, the issue can be resolved by simply increasing the hash length and rebuilding the project.
// vite.config.js
let modulesConfig = {
generateScopedName: "[local]-[hash:base64:4]"
};
if (process.env.NODE_ENV === "production") {
const fileSet = new Set();
const hashSet = new Set();
modulesConfig = {
getJSON(file, json) {
console.log(json);
if (fileSet.has(file)) return;
fileSet.add(file);
Object.values(json).forEach(hash => {
if (hashSet.has(hash)) {
throw new Error("CSS MODULES HASH COLLISION ERROR");
}
hashSet.add(hash);
});
},
generateScopedName: "[hash:base64:2]"
};
}
export default defineConfig({
css: {
modules: modulesConfig,
}
});
Here is the TypeScript version:
// vite.config.ts
interface ModulesConfig {
generateScopedName: string;
getJSON?: (file: string, json: Record<string, string>) => void;
}
let modulesConfig: ModulesConfig = {
generateScopedName: "[local]-[hash:base64:4]"
};
if (process.env.NODE_ENV === "production") {
const fileSet = new Set<string>();
const hashSet = new Set<string>();
modulesConfig = {
getJSON(file: string, json: Record<string, string>): void {
console.log(json);
if (fileSet.has(file)) return;
fileSet.add(file);
Object.values(json).forEach((hash: string) => {
if (hashSet.has(hash)) {
throw new Error("CSS MODULES HASH COLLISION ERROR");
}
hashSet.add(hash);
});
},
generateScopedName: "[hash:base64:2]"
};
}
export default defineConfig({
css: {
modules: modulesConfig,
}
});
I have implemented this code snippet in my react-template, and I am extremely satisfied with the results. It strikes a perfect balance between readability and efficiency, ensuring that my CSS is both human-friendly and optimized for production.