CSS Formatting, Minification and Property Order: What Is Safe

Which CSS whitespace is significant, why shorthand properties make alphabetical sorting risky, the value rewrites that break minified CSS, and how to format SCSS and Less.

ยท 10 min read

Illustration for the CSS Beautifier and Minifier guide

CSS looks forgiving about layout. You can write a rule on one line or twenty, indent with tabs or spaces, and the browser does not care. That is mostly true, which is why CSS formatters and minifiers exist. But a few spaces in CSS carry meaning, the order of declarations sometimes decides what you see, and several "optimisations" that minifiers like to apply quietly break valid stylesheets. This guide covers what is safe to change, using the CSS Beautifier and Minifier for the examples.

The whitespace that matters

The CSS Syntax specification turns a stylesheet into tokens, and most whitespace between tokens is ignored. Spaces around {, }, ;, : in a declaration, and , in a selector list or value can all go. Three kinds of whitespace cannot.

The descendant combinator. In a selector, a space is an operator. .card p means "a p inside .card", while .cardp is a different class. The subtle case is a pseudo-class: .menu :hover matches any hovered element inside .menu, but .menu:hover matches .menu itself when hovered. A minifier must keep that space.

Operators inside calc(). The + and - operators in calc(), min(), max() and clamp() must have whitespace on both sides. calc(100% - 2px) works; calc(100%-2px) is invalid because -2px reads as a negative number. Multiplication and division do not need spaces, but keeping the author's spacing is the safe choice.

Spaces inside values. margin: 0 auto has two values, font: 600 1rem/1.3 system-ui has four. Those spaces separate components and must remain, although runs of spaces can shrink to one.

WhitespaceExampleRemovable?
Around braces, semicolons, colonsa { color : red ; }Yes: a{color:red}
After commasrgb(0, 0, 0), h1, h2Yes
Around >, +, ~ combinatorsul > liYes: ul>li
Descendant combinator.menu :hoverNo
Around + and - in calc()calc(100% - 2px)No
Between value componentsmargin: 0 autoCollapse to one space
Inside strings and url()content: "a b"Never touch

Why property order matters, sometimes

Inside one rule, when two declarations set the same property, the later one wins. Usually you do not have duplicates, so order does not matter: color and padding can appear in any order with the same result. The exception is shorthand properties.

A shorthand sets every longhand it covers, including the ones you did not mention, which it resets to their initial value. border: 0 sets border-top-width, border-top-style, border-top-color and the same for the other three sides, plus border-image. So these two rules differ:

.a { border-top: 4px solid red; border: 0; }   /* no border at all */
.b { border: 0; border-top: 4px solid red; }   /* red top border */

Some shorthands reset longhands whose names give no hint. font resets line-height, inset sets top, right, bottom and left, gap sets row-gap and column-gap, flex-flow sets flex-direction and flex-wrap, and place-items sets align-items and justify-items. all resets nearly everything.

Two rules side by side: border-top then border gives a box with no border, while border then border-top gives a red top border; a naive alphabetical sort turns the second rule into the first
Two rules side by side: border-top then border gives a box with no border, while border then border-top gives a red top border; a naive alphabetical sort turns the second rule into the first

Sorting properties without breaking the cascade

Alphabetical property order has real benefits: declarations are easy to find, duplicates sit next to each other, and two people editing the same rule produce smaller diffs. The danger is that a plain alphabetical sort puts border before border-top every time, which is fine for rule .b above and wrong for rule .a.

A cascade-safe sort treats overlapping properties as a dependency. Our sorter does this:

  1. Two properties conflict if they are the same property (including vendor-prefixed copies such as -webkit-transition and transition), if one is a shorthand of the other by name (border covers border-top and border-top-color, border-color covers border-top-color), or if they appear in a list of shorthands with unrelated longhand names (font and line-height, inset and top, and so on).
  2. It repeatedly picks the alphabetically first declaration that has no unplaced conflicting declaration before it in the original rule.

The result is as alphabetical as possible while every conflicting pair keeps its original order, so the computed style is identical. Vendor prefixes sort next to the standard property, prefixed first, which is the order autoprefixer writes.

Some declarations are fixed points that sorting must not move across: custom properties (--brand: red), Sass $variables, @include and @extend lines, and nested rules. A mixin can set any property, so moving a declaration from below an @include to above it changes which value wins. The sorter only reorders runs of plain declarations between those points.

There are other conventions. Some teams group properties by purpose (positioning, box model, typography, visual), often enforced with Stylelint's order plugins. Grouping is a matter of taste that a tool cannot infer, while alphabetical order is mechanical, which is its appeal.

The minifier rewrites that cause bugs

Whitespace and comment removal is where most of the savings in a hand-written stylesheet come from. Aggressive minifiers also rewrite values, and each rewrite has a failure mode:

  • Dropping units from zero. 0px and 0 are equivalent for most lengths, but not everywhere. calc(0px + 5%) becomes calc(0 + 5%), which is invalid because a unitless number cannot be added to a percentage. In a custom property, --gap: 0px used later in calc(var(--gap) + 1rem) has the same problem. And some older browsers mishandled a unitless flex-basis in flex: 1 1 0.
  • Shortening colours. #ffffff to #fff is safe; turning named colours into hex or rgba into hex with alpha can break support in old email clients.
  • Merging rules. Combining two rules with identical declarations moves one of them, which can change the cascade when a third rule sits between them.
  • Removing "duplicate" declarations. display: -webkit-box; display: flex; looks like a duplicate but is a deliberate fallback: browsers ignore the value they do not understand.

The minifier in our tool does none of these. It removes comments (keeping /*! licence comments if you ask), collapses whitespace, drops the last semicolon in each block and removes rules with no declarations. Every value is left exactly as written. On three real stylesheets the savings were:

StylesheetOriginalMinifiedSaved
pdfjs-dist 6.4.299, pdf_viewer.css168,569 bytes122,962 bytes27.1%
react-day-picker 8.10.2, style.css6,265 bytes4,590 bytes26.7%
@xyflow/react 12.12.0, style.css18,596 bytes15,709 bytes15.5%

Sorting the same three files moved thousands of declarations and the beautified output still matched the input token for token. The chips under the output show the exact figures for your file.

Build-time tools such as Lightning CSS and cssnano are well tested and do more, and they are the right choice in a pipeline where you check the output in the browsers you support. A whitespace-only minifier is for the cases without a pipeline: critical CSS pasted into a page head, styles in an HTML email, a CMS field with a length limit.

SCSS and Less

Preprocessor syntax adds a few things a formatter must understand:

  • // line comments, which plain CSS does not have (in CSS, // is not a comment and url(http://...) must survive).
  • Nesting, including the parent selector & as in &:hover and &__title.
  • Interpolation, #{$name}, which contains braces that are not blocks.
  • Variables ($color in Sass, @color in Less), maps such as (key: value), mixins (@include, .mixin();), and control directives like @if and @each.

Choosing SCSS or Less in the tool turns on // comments; the rest is handled in all modes. Formatting is done by js-beautify's CSS engine, which handles nesting well. Very unusual syntax, such as Less detached rulesets with complex guards, may come out on fewer lines than you like but keeps its tokens.

A worked example

Start with a minified rule:

.btn{padding:8px 16px;border:1px solid #ccc;border-top:0;color:#fff;background:#dc2626}

Beautify with sorting on:

.btn {
  background: #dc2626;
  border: 1px solid #ccc;
  border-top: 0;
  color: #fff;
  padding: 8px 16px;
}

border still comes before border-top, so the button has no top border, as before. Now swap the two in the input (border-top:0;border:1px solid #ccc) and sort again: the sorter keeps border-top first this time, because in that rule border overrides it and moving it would add a top border that was not there.

Checklist

  • Never remove the space in a descendant selector or around + and - in calc().
  • Treat shorthand and longhand pairs as ordered; sort everything else freely.
  • Keep fallbacks that look like duplicates.
  • Leave value rewriting to a build tool you test, or skip it: after gzip it saves little.
  • Keep licence comments in distributed CSS.

For JavaScript, see the companion guide on beautifying and minifying JavaScript. Related tools: CSS Gradient Generator, Box Shadow Generator and PX to REM Converter.

Related searches

  • css beautifier
  • sort css properties alphabetically
  • does css property order matter
  • css shorthand overrides longhand
  • minify css safely
  • descendant combinator whitespace
  • calc requires spaces around plus
  • css property order conventions

Open the CSS Beautifier and Minifier