mrkeyoor.com_
Thu 06 Aug 10:55 UTC
npmWeb Frontendupdated 06 Aug 2026

css-declaration-sorter

css-declaration-sorter is a PostCSS 8 plugin that reorders the declarations inside each CSS rule. It changes nothing else: no values are rewritten, no prefixes added, no bytes minified. You pick one of four built-in orders (alphabetical, smacss, concentric-css, frakto) or pass your own comparator that receives two property names and returns -1, 0 or 1. Each built-in order is a hand-arranged list of 476 properties generated from MDN's browser-compat-data, so the coverage tracks real CSS rather than someone's memory. It runs in PostCSS's OnceExit hook, walks every rule and at-rule including nested ones, pulls comments out and reattaches them to the declaration they were next to, and sorts with a bubble sort so equally ranked declarations keep their original relative order. Two things it deliberately does not do: there is no CLI of its own, and with keepOverrides turned on it refuses to reorder a shorthand past its own longhands.

Verdict

A small, well-scoped fixer that does one job from real MDN property data, and at 2.7 KB gzipped it costs nothing in a build. Check two things first: that no custom property is sitting mid-rule quietly blocking the sort, and that you are not relying on cssnano to install it, because preset-default 8 dropped the dependency.

API stability4/5The whole surface is two options, order and keepOverrides, and neither has changed since the 7.0 rewrite; the dual ESM and CJS exports map has held across the 7.x line and 7.4.0 only added the frakto order. The point off is the CommonJS build exporting the function itself while the shipped .d.cts advertises a named export, a mismatch that turns correct-looking TypeScript into a runtime undefined
Docs3/5The readme covers install, all four orders with links to their sources, both options and an examples directory, which is enough to get running in ten minutes; it never mentions that a built-in order forces the PostCSS run async, that properties outside the bundled list act as sorting barriers, or how the CommonJS export is actually shaped, and those three are what people hit in practice
Maintenance4/5Pushed 2026-08-03, 7.4.0 published April 2026 adding a new order, and only 1 genuinely open issue out of 9 open issues and PRs, so the backlog is real rather than abandoned-looking; against that it is one maintainer and the biggest downstream consumer, cssnano-preset-default, removed the dependency in 8.0.0
Ecosystem3/5About 16.4M downloads a week, but that number was inherited from cssnano preset-default 5 through 7 rather than earned by direct adoption, and preset-default 8.0.0 dropped it in May 2026 so the figure should fall; direct users are a far smaller group, and the niche is already split between postcss-sorting, stylelint order rules and the author's own Prettier plugin

Use it if

  • You already run PostCSS in your build and want declaration order settled by a tool instead of by code review: one plugin, one order option, zero developer discipline required
  • You want the ordering rules grounded in real property data: all four built-in orders are 476-entry lists generated from MDN browser-compat-data, so new properties arrive through package updates rather than pull requests to a hand-kept list
  • Compressed CSS size is a target: grouping the same properties into the same position across rules gives gzip longer repeated runs, which is the stated goal of the project and the one benefit you can measure
  • You write SCSS or Less: swap the PostCSS syntax to postcss-scss or postcss-less and nested rules get sorted too, with no change to the plugin configuration
  • You have legacy CSS that leans on source order: keepOverrides leaves a shorthand and its longhands in place, including vendor-prefixed pairs, so turning the plugin on does not quietly change computed styles
Skip it if

Setup reality

npm install --save-dev postcss css-declaration-sorter. PostCSS is a peer dependency at ^8.0.9 and is not pulled in for you, so a fresh project gets an unmet peer warning followed by a confusing runtime error later. The package is type: module and its exports map has no './package.json' entry, so any tool that reads the manifest by path fails with ERR_PACKAGE_PATH_NOT_EXPORTED. Under ESM you write import { cssDeclarationSorter } from 'css-declaration-sorter'; under CommonJS the built file ends with module.exports = cssDeclarationSorter, so you require the module itself and destructuring the same name the type definitions show hands you undefined. The next surprise is asynchrony: picking one of the built-in orders makes the plugin fetch its property list with a dynamic import inside OnceExit, which turns the whole PostCSS run async and breaks any synchronous .process(css).css call, while a custom comparator stays sync. An unknown order string does not throw either, it rejects the returned promise with 'Invalid built-in order', so a typo in a config file surfaces as an unhandled rejection rather than a startup error. Declared engines are node ^14 || ^16 || >=18, there is no bundled CLI, and the usual driver is either postcss.config.js or the separate postcss-cli package.

Patterns

Wire it into postcss.config.jspostcss-config

// postcss.config.mjs
import { cssDeclarationSorter } from 'css-declaration-sorter';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';

export default {
  plugins: [
    autoprefixer(),
    cssDeclarationSorter({ order: 'smacss', keepOverrides: true }),
    cssnano(),
  ],
};

Put it after autoprefixer so the prefixed declarations it generates get sorted too, and before the minifier so the compressor sees the grouped output. Order within the plugin array is the only sequencing control you have; the plugin itself always runs in OnceExit, which means it sees the tree after every other plugin's node visitors have finished.

Import it correctly in ESM and CommonJSimport-shapes

// ESM: named export, matches the type definitions
import { cssDeclarationSorter } from 'css-declaration-sorter';

// CommonJS: the module IS the function
const cssDeclarationSorter = require('css-declaration-sorter');

// CommonJS, wrong, typechecks and then crashes
const { cssDeclarationSorter } = require('css-declaration-sorter');
// TypeError: cssDeclarationSorter is not a function

dist/main.cjs assigns exports.cssDeclarationSorter and exports.default and then overwrites the whole object with module.exports = cssDeclarationSorter, so both named properties are undefined at require time. The shipped main.d.cts still declares them, so TypeScript will not warn you. Reading the manifest also fails: require('css-declaration-sorter/package.json') throws ERR_PACKAGE_PATH_NOT_EXPORTED.

Pick one of the four bundled ordersbuilt-in-orders

cssDeclarationSorter({ order: 'alphabetical' })    // default, a to z
cssDeclarationSorter({ order: 'smacss' })          // box, border, background, text, other
cssDeclarationSorter({ order: 'concentric-css' })  // outside the box model, inward
cssDeclarationSorter({ order: 'frakto' })          // positioning, box, layout, type, visual

// input:  display: block; animation: none; color: #C55; border: 0;
// smacss: display: block; border: 0; color: #C55; animation: none;

// a typo rejects the promise instead of throwing at startup
cssDeclarationSorter({ order: 'smacs' })
// Error: Invalid built-in order 'smacs' provided.

All four are 476-property lists, not comparison rules, so they behave identically toward anything not on the list. Validate the order string yourself if it comes from user config: an invalid value produces a rejected promise from OnceExit, which in a bare postcss-cli run reads as an unhandled rejection rather than a clear configuration error.

Built-in orders force an async PostCSS runsync-vs-async

import postcss from 'postcss';
import { cssDeclarationSorter } from 'css-declaration-sorter';

const css = 'a { color: red; background: blue; }';

// throws: Use process(css).then(cb) to work with async plugins
postcss([cssDeclarationSorter({ order: 'alphabetical' })]).process(css, { from: undefined }).css;

// fine
const result = await postcss([cssDeclarationSorter({ order: 'alphabetical' })])
  .process(css, { from: undefined });
console.log(result.css);

The built-in orders are loaded with import('../orders/<name>.mjs') inside OnceExit, so the plugin returns a promise and PostCSS refuses to run synchronously. Anything that calls .process(css).css without awaiting, including some older Gulp and webpack loaders and most quick scripts, will fail. Passing a comparator function instead keeps the whole run synchronous.

Write your own order, and stay synchronouscustom-comparator

const byLength = (a, b) => (a.length < b.length ? -1 : a.length > b.length ? 1 : 0);

cssDeclarationSorter({ order: byLength });

// a real one: your team's list first, everything else alphabetical
const FIRST = ['position', 'top', 'right', 'bottom', 'left', 'z-index'];
const myOrder = (a, b) => {
  const ia = FIRST.indexOf(a);
  const ib = FIRST.indexOf(b);
  if (ia !== -1 || ib !== -1) return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib);
  return a < b ? -1 : a > b ? 1 : 0;
};

The comparator is handed two property names as plain strings and must return -1, 0 or 1, exactly like Array.sort. Unlike the built-in lists it also sees custom properties, so a string comparison sorts --brand along with everything else instead of stopping at it. This is also the only configuration that keeps a synchronous PostCSS pipeline working.

One unknown property can stop the whole rule sortingunknown-property-barrier

// with order: 'alphabetical'

'a { z-index: 1; color: blue; }'
// -> a { color: blue; z-index: 1; }        sorted

'a { z-index: 1; --brand: red; color: blue; }'
// -> a { z-index: 1; --brand: red; color: blue; }   untouched

'a { --brand: red; z-index: 1; color: blue; }'
// -> a { --brand: red; color: blue; z-index: 1; }   sorted around it

The comparator returns 0 for any property missing from the 476-entry list, and the bubble sort only swaps adjacent pairs, so an unknown declaration in the middle of a rule is a wall the sort cannot cross. Custom properties are the common case, but so is any property newer than the version of the MDN data bundled in your installed release. Move custom properties to the top of the rule, or use a comparator function.

Do not reorder shorthands past their longhandskeep-overrides

cssDeclarationSorter({ order: 'alphabetical', keepOverrides: true });

// input
'a { animation-name: some; animation: greeting; color: red; }'

// keepOverrides: false  (default) -> the override is destroyed
'a { animation: greeting; animation-name: some; color: red; }'

// keepOverrides: true -> left alone
'a { animation-name: some; animation: greeting; color: red; }'

Declaration order is part of the cascade, so this flag is the difference between a formatting change and a behaviour change. It works from the plugin's own shorthand table and strips vendor prefixes before comparing, so -webkit-animation-name and animation are treated as a pair. Turn it on for any codebase you did not write, and audit the diff of the first run rather than trusting it.

Sort SCSS or Less sourcesscss-and-less

import postcss from 'postcss';
import scssSyntax from 'postcss-scss';
import { cssDeclarationSorter } from 'css-declaration-sorter';

const result = await postcss([cssDeclarationSorter({ order: 'concentric-css' })])
  .process(scssSource, { from: 'src/app.scss', syntax: scssSyntax });

// package.json config for postcss-cli
// "postcss": {
//   "syntax": "postcss-scss",
//   "map": false,
//   "plugins": { "css-declaration-sorter": { "order": "smacss" } }
// }

postcss-scss and postcss-less are separate installs and neither is a dependency here. Sorting source files means rewriting the files developers edit, so run it as a formatting step with the result committed, not as part of the same build that produces dist CSS. Nested rules are sorted, but SCSS @include and @extend lines are at-rules rather than declarations and stay where they are.

Run it from the command linecli-usage

npm install --save-dev postcss postcss-cli css-declaration-sorter

# pipe one file
npx postcss input.css --use css-declaration-sorter

# overwrite a whole tree, no source maps
npx postcss 'src/**/*.css' --use css-declaration-sorter --replace --no-map

# SCSS with a named order, config read from package.json
npx postcss 'src/**/*.scss' --syntax postcss-scss --replace --config package.json

There is no bin in this package, so postcss-cli is a required extra install. Use --replace only against a clean working tree: the sort rewrites files in place, and with keepOverrides off on legacy CSS the diff can include real behaviour changes hidden among hundreds of harmless line moves.

Get the same sorting through Prettierprettier-instead

npm install --save-dev prettier prettier-plugin-css-order

// .prettierrc
{
  "plugins": ["prettier-plugin-css-order"],
  "cssDeclarationSorterOrder": "smacss",
  "cssDeclarationSorterKeepOverrides": true
}

Same author, and it depends on css-declaration-sorter ^7.3.0 underneath, so the ordering is identical. It covers CSS, SCSS and Less through Prettier's own parsers and runs on save rather than only at build time, which is usually the better place for a formatting change. Do not run both: two tools rewriting the same files is how you get a format-on-save loop.

Alternatives

PackageRegistryPick it when
postcss-sortingnpmYou want to control the order of at-rules, comments and custom properties as well as plain declarations, with a config you write yourself
stylelint-config-recess-ordernpmYou would rather lint and autofix order inside the editor so developers see it, instead of silently rewriting during the build
prettier-plugin-css-ordernpmYour team already formats with Prettier and you want sorting in the same pass across CSS, SCSS, Less and styled files