CSS Property Reordering: PostCSS and Fallback Risks
Using PostCSS to parse CSS AST (Abstract Syntax Tree) enables automatic property reordering under rules like Concentric or Recess, improving team formatting consistency. However, physical declaration order inside a CSS rule often represents browser fallbacks and vendor prefix precedence. This article explains PostCSS AST reordering safety.
1. Problem: declaration order destruction during AST reordering
PostCSS parses CSS source code into Root, Rule, and Declaration nodes. Reordering a Declaration array alphabetically or by naive dictionary lookup can corrupt browser fallback declarations and vendor prefix override sequences.
2. Minimal reproduction: fallback and prefix order corrupted by reordering
The CSS below uses duplicate property declarations to provide browser fallbacks. If property reordering removes duplicate declarations or swaps their order, fallback mechanisms in older browsers fail.
/* Source: display: block serves as fallback for non-flex browsers */
.box {
display: block;
display: flex;
-webkit-transform: scale(1);
transform: scale(1);
}
/* Incorrect reorder: duplicate display declaration deduplicated, losing fallback */
.box {
display: flex;
transform: scale(1);
}3. Root cause: declaration cascade inside rules
Inside a CSS rule, when duplicate declarations (e.g., multiple display or color properties) or vendor prefixes exist, browsers process them sequentially, ignoring unsupported values and retaining the last valid declaration. Swapping order invalidates fallback logic.
4. Recommendation: AST sorting with fallback and vendor prefix preservation
1. Strictly forbid deduplication or order swapping for duplicate property declarations. 2. Vendor prefixes (e.g., -webkit-) must precede standard unprefixed properties so standard properties take priority. 3. Group CSS variables (--*) at the top while maintaining relative order.
5. Complete code: AST declaration sorting with fallback protection
This TypeScript code demonstrates PostCSS AST Declaration sorting, preserving relative positions for duplicate fallbacks and vendor prefixes.
interface CSSDeclaration {
prop: string;
value: string;
}
const PROPERTY_GROUPS = [
["position", "top", "right", "bottom", "left", "z-index"],
["display", "flex", "grid", "width", "height", "margin", "padding"],
["color", "background", "font-size", "line-height"],
];
function getPropertyOrder(prop: string): number {
const cleanProp = prop.replace(/^-(webkit|moz|ms|o)-/, "");
for (let groupIndex = 0; groupIndex < PROPERTY_GROUPS.length; groupIndex++) {
const itemIndex = PROPERTY_GROUPS[groupIndex].indexOf(cleanProp);
if (itemIndex !== -1) {
return groupIndex * 100 + itemIndex;
}
}
return 9999;
}
function safeReorderDeclarations(decls: CSSDeclaration[]): CSSDeclaration[] {
const indexed = decls.map((d, index) => ({ decl: d, originalIndex: index }));
indexed.sort((a, b) => {
const propA = a.decl.prop;
const propB = b.decl.prop;
// 1. Same property: strictly preserve original relative order for fallbacks
if (propA === propB) {
return a.originalIndex - b.originalIndex;
}
// 2. Vendor prefix vs standard: prefix comes first
const baseA = propA.replace(/^-(webkit|moz|ms|o)-/, "");
const baseB = propB.replace(/^-(webkit|moz|ms|o)-/, "");
if (baseA === baseB) {
const isPrefixA = propA.startsWith("-");
const isPrefixB = propB.startsWith("-");
if (isPrefixA && !isPrefixB) return -1;
if (!isPrefixA && isPrefixB) return 1;
return a.originalIndex - b.originalIndex;
}
// 3. Different properties: sort by defined property groups
const orderA = getPropertyOrder(propA);
const orderB = getPropertyOrder(propB);
if (orderA !== orderB) {
return orderA - orderB;
}
return a.originalIndex - b.originalIndex;
});
return indexed.map((item) => item.decl);
}
const sampleDecls: CSSDeclaration[] = [
{ prop: "display", value: "block" },
{ prop: "display", value: "flex" },
{ prop: "-webkit-transform", value: "scale(1)" },
{ prop: "transform", value: "scale(1)" },
{ prop: "color", value: "red" },
];
console.log(safeReorderDeclarations(sampleDecls));6. Incorrect approaches
Converting Declarations to a JavaScript Object.fromEntries() dictionary, which silently overwrites duplicate fallback properties. Placing standard properties before vendor prefixes, causing outdated prefix rules to override standard CSS.
7. Boundaries: @keyframes declarations and CSS Grid namespaces
Inside @keyframes blocks, declaration order directly impacts keyframe interpolation. CSS Grid area lines also require contextual continuity and should not be forcibly reordered.
8. Verification
Configure stable sorting rules in Stylelint order/properties-order. Use headless browsers to compare computed styles before and after AST reordering.
9. FAQ
Does PostCSS property reordering improve Gzip compression? Slightly, because consistent ordering increases repeated string patterns for LZ77 compression.
What is the most common bug during property sorting? Storing AST declarations in a Key-Value Map, which discards duplicate display or color fallback declarations.
Why is PostCSS safer than regex replacement? PostCSS constructs a full AST that isolates selectors, comments, media queries, and declarations accurately.
10. Summary
When reordering CSS properties via PostCSS AST, preserve relative order for duplicate fallback declarations and vendor prefixes. Never convert AST nodes into overwriting key-value dictionaries.
Sources and further reading
Specifications and references for checking the article’s technical details.
Related articles
CSS Selector Merging: Cascade and Specificity Risks
Distinguish CSS formatting from minification, analyzing how selector merging and reordering break source cascade order and specificity, with safe optimization patterns.
Best practicesPurgeCSS: Unused Styles, Safelists and Minification
Explain PurgeCSS unused style removal principles, prevent dynamic class name stripping, and combine Tree-shaking with Minification and CDN caching.
How it worksJavaScript Minification: ASTs and Source Maps
Deconstruct the JS compression lifecycle: building ASTs via Parsers, mangling variables and stripping dead branches in Transform, and emitting Source Maps.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool