CSS Minify Tests

Correctness tests for CSS minification tools. Each test provides a CSS input and a single canonical minified output. A tool passes if its output matches exactly.

Contribute tests on GitHub

Minifiers tested:

Summary

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
v5.3.3v0.0.34v0.0.41v9.3.1v5.0.5v0.28.2v1.33.0v1.105.1
anchor 2 / 5 1 / 5 5 / 5 2 / 5 2 / 5 1 / 5 0 / 5 1 / 5
calc 2 / 34 2 / 34 34 / 34 8 / 34 2 / 34 22 / 34 28 / 34 17 / 34
charset 5 / 13 12 / 13 13 / 13 6 / 13 2 / 13 0 / 13 8 / 13 7 / 13
color-rounding 0 / 13 13 / 13 13 / 13 1 / 13 0 / 13 1 / 13 1 / 13 0 / 13
colors 26 / 70 58 / 70 70 / 70 42 / 70 29 / 70 38 / 70 46 / 70 25 / 70
combined 0 / 6 0 / 6 6 / 6 0 / 6 0 / 6 0 / 6 0 / 6 0 / 6
comments 5 / 7 3 / 7 7 / 7 4 / 7 4 / 7 4 / 7 3 / 7 4 / 7
container 2 / 6 2 / 6 6 / 6 1 / 6 1 / 6 1 / 6 4 / 6 0 / 6
counter-style 1 / 3 3 / 3 3 / 3 1 / 3 3 / 3 0 / 3 1 / 3 1 / 3
duplicates 18 / 24 6 / 24 24 / 24 14 / 24 20 / 24 12 / 24 13 / 24 6 / 24
empty-rules 7 / 7 7 / 7 7 / 7 7 / 7 7 / 7 6 / 7 6 / 7 6 / 7
escaping 3 / 15 13 / 15 15 / 15 3 / 15 4 / 15 12 / 15 14 / 15 11 / 15
font-face 3 / 13 2 / 13 13 / 13 4 / 13 3 / 13 4 / 13 5 / 13 2 / 13
gradients 0 / 11 0 / 11 11 / 11 2 / 11 0 / 11 1 / 11 4 / 11 0 / 11
idempotency 0 / 1 1 / 1 1 / 1 1 / 1 0 / 1 0 / 1 0 / 1 0 / 1
import 0 / 5 4 / 5 5 / 5 0 / 5 0 / 5 4 / 5 0 / 5 0 / 5
initial 0 / 8 0 / 8 8 / 8 7 / 8 0 / 8 0 / 8 0 / 8 0 / 8
keyframes 0 / 2 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2
lang 0 / 7 0 / 7 7 / 7 1 / 7 0 / 7 0 / 7 2 / 7 0 / 7
layer 1 / 4 1 / 4 4 / 4 0 / 4 1 / 4 1 / 4 3 / 4 1 / 4
media 1 / 13 4 / 13 13 / 13 6 / 13 1 / 13 6 / 13 2 / 13 1 / 13
merging 18 / 24 10 / 24 24 / 24 18 / 24 15 / 24 10 / 24 15 / 24 6 / 24
nesting 0 / 37 12 / 37 37 / 37 12 / 37 0 / 37 15 / 37 6 / 37 8 / 37
page 3 / 4 4 / 4 4 / 4 3 / 4 3 / 4 3 / 4 3 / 4 2 / 4
property 3 / 8 3 / 8 8 / 8 4 / 8 3 / 8 0 / 8 4 / 8 2 / 8
scope 1 / 3 3 / 3 3 / 3 1 / 3 1 / 3 2 / 3 1 / 3 0 / 3
selectors 11 / 20 5 / 20 20 / 20 13 / 20 8 / 20 11 / 20 18 / 20 5 / 20
selectors-advanced 5 / 26 6 / 26 26 / 26 8 / 26 7 / 26 8 / 26 6 / 26 6 / 26
shorthand-values 15 / 25 18 / 25 25 / 25 19 / 25 13 / 25 13 / 25 23 / 25 2 / 25
shorthands 9 / 57 11 / 57 57 / 57 12 / 57 7 / 57 5 / 57 26 / 57 0 / 57
shorthands-unsafe 7 / 11 8 / 11 11 / 11 8 / 11 5 / 11 4 / 11 6 / 11 3 / 11
starting-style 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 2 / 2 1 / 2 2 / 2
supports 3 / 5 2 / 5 5 / 5 2 / 5 3 / 5 1 / 5 3 / 5 1 / 5
transforms 0 / 11 0 / 11 11 / 11 6 / 11 0 / 11 8 / 11 11 / 11 0 / 11
values 14 / 57 18 / 57 57 / 57 41 / 57 19 / 57 19 / 57 36 / 57 16 / 57
vendors 0 / 3 0 / 3 3 / 3 2 / 3 0 / 3 0 / 3 2 / 3 0 / 3
whitespace 13 / 21 19 / 21 21 / 21 13 / 21 16 / 21 13 / 21 17 / 21 13 / 21
zero-units 10 / 13 12 / 13 13 / 13 10 / 13 10 / 13 9 / 13 13 / 13 8 / 13
Total 188 / 594 265 / 594 594 / 594 286 / 594 193 / 594 238 / 594 333 / 594 156 / 594
Total (%) 31.64% 44.61% 100% 48.14% 32.49% 40.06% 56.06% 26.26%
Total Duration 66ms 202ms 152ms 436ms 92ms 400ms 9ms 392ms

Historical Trends

Pass rate over time across 80 recorded runs.

anchor #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

position-area: center center shortens to center

position-area: center center is equivalent to position-area: center. When both axes are center, the second value can be omitted.

a {
  position-area: center center;
}

Expected

a{position-area:center}

Outputs

clean-css ✗

a{position-area:center center}

csskit ✗

a{position-area:center center}

csslop ✓

a{position-area:center}

cssnano ✗

a{position-area:center center}

csso ✗

a{position-area:center center}

esbuild ✗

a{position-area:center center}

lightningcss ✗

a{position-area:center center}

sass ✗

a{position-area:center center}
4ms
29ms
12ms
44ms
3ms
19ms
1ms
3ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

position-area drops default center axis

position-area: top center shortens to position-area: top. When one axis is a directional keyword and the other is center, the center can be omitted as it is the default for the unspecified axis.

a {
  position-area: top center;
}

Expected

a{position-area:top}

Outputs

clean-css ✗

a{position-area:top center}

csskit ✗

a{position-area:center top}

csslop ✓

a{position-area:top}

cssnano ✗

a{position-area:top center}

csso ✗

a{position-area:top center}

esbuild ✗

a{position-area:top center}

lightningcss ✗

a{position-area:top center}

sass ✗

a{position-area:top center}
1ms
1ms
1ms
1ms
1ms
2ms
1ms
2ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

@position-try equivalent to flip-block tactic

A @position-try that only flips the block axis (bottom -> top) is equivalent to the built-in flip-block try tactic. The at-rule can be removed and the reference replaced with the keyword, saving the entire block.

@position-try --flip {
  position-area: top;
}

a {
  position-area: bottom;
  position-try-fallbacks: --flip;
}

Expected

a{position-area:bottom;position-try-fallbacks:flip-block}

Outputs

clean-css ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

csskit ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

csslop ✓

a{position-area:bottom;position-try-fallbacks:flip-block}

cssnano ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

csso ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

esbuild ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

lightningcss ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}

sass ✗

@position-try --flip{position-area:top}a{position-area:bottom;position-try-fallbacks:--flip}
1ms
1ms
1ms
2ms
1ms
1ms
1ms
5ms
0004 ✓ ✗ ✓ ✓ ✓ ✗ ✗ ✗
Details

Empty @position-try removal

An empty @position-try block with no declarations can be removed entirely.

@position-try --empty {
}

Expected

Outputs

clean-css ✓

csskit ✗

@position-try --empty{}

csslop ✓

cssnano ✓

csso ✓

esbuild ✗

@position-try --empty{}

lightningcss ✗

@position-try --empty{}

sass ✗

@position-try --empty{}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

Unresolved position-try-fallbacks must be retained

position-try-fallbacks: --left, --undefined must keep --undefined even if no matching @position-try --undefined exists in the current stylesheet. Unresolved dashed-ident fallbacks are skipped at runtime and have no effect. However, it may be defined in other stylesheets ran on the same page, resulting in an incorrect outcome compared to the unminified code.

@position-try --left {
  position-area: left;
}
a {
  position-area: left;
  position-try-fallbacks: --left, --undefined;
}

Expected

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

Validate

Outputs

clean-css ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

csskit ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

csslop ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

cssnano ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

csso ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

esbuild ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}

lightningcss ✗

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left, --undefined}

sass ✓

@position-try --left{position-area:left}a{position-area:left;position-try-fallbacks:--left,--undefined}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
Subtotal 2 / 5 1 / 5 5 / 5 2 / 5 2 / 5 1 / 5 0 / 5 1 / 5
Percent 40% 20% 100% 40% 40% 20% 0% 20%
Duration 5ms 29ms 15ms 48ms 5ms 25ms 1ms 12ms

calc #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

Resolve calcs and merge them in margin

Resolve any static calculations, then apply merging of identical values for properties like margin, padding, or border.

a {
  margin: calc(1px + 2px) calc(4px - 1px);
}

Expected

a{margin:3px}

Outputs

clean-css ✗

a{margin:calc(1px + 2px) calc(4px - 1px)}

csskit ✗

a{margin:calc(1px + 2px)calc(4px - 1px)}

csslop ✓

a{margin:3px}

cssnano ✗

a{margin:calc(3px)}

csso ✗

a{margin:calc(1px + 2px) calc(4px - 1px)}

esbuild ✓

a{margin:3px}

lightningcss ✓

a{margin:3px}

sass ✗

a{margin:3px 3px}
1ms
7ms
2ms
4ms
1ms
1ms
1ms
4ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Smart separator preservation in calc()

Spaces around + and - operators inside calc() must be preserved. Removing them would cause 50vh+10px to be parsed as a single dimension token, or 100%-20px to change meaning. This tests that minifiers do not over-strip.

a {
  width: calc(100% - 20px);
  height: calc(50vh + 10px);
}

Expected

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

Outputs

clean-css ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

csskit ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

csslop ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

cssnano ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

csso ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

esbuild ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

lightningcss ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}

sass ✓

a{width:calc(100% - 20px);height:calc(50vh + 10px)}
1ms
1ms
2ms
1ms
1ms
1ms
1ms
3ms
0003 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Nested calc() flattening and reduction

calc(calc(100% - 20px) + 10px) should flatten the inner calc() and reduce compatible units: -20px + 10px = -10px, yielding calc(100% - 10px).

a {
  width: calc(calc(100% - 20px) + 10px);
}

Expected

a{width:calc(100% - 10px)}

Outputs

clean-css ✗

a{width:calc(calc(100% - 20px) + 10px)}

csskit ✗

a{width:calc(calc(100% - 20px) + 10px)}

csslop ✓

a{width:calc(100% - 10px)}

cssnano ✓

a{width:calc(100% - 10px)}

csso ✗

a{width:calc(calc(100% - 20px) + 10px)}

esbuild ✓

a{width:calc(100% - 10px)}

lightningcss ✓

a{width:calc(100% - 10px)}

sass ✗

a{width:calc(100% - 20px + 10px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc constant folding

calc(100px * 2) can be fully resolved to 200px at compile time since all values are known.

a {
  width: calc(100px * 2);
}

Expected

a{width:200px}

Outputs

clean-css ✗

a{width:calc(100px * 2)}

csskit ✗

a{width:calc(100px*2)}

csslop ✓

a{width:200px}

cssnano ✗

a{width:calc(200px)}

csso ✗

a{width:calc(100px*2)}

esbuild ✓

a{width:200px}

lightningcss ✓

a{width:200px}

sass ✓

a{width:200px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc addition folding

calc(50px + 50px) can be resolved to 100px since both operands share the same unit.

a {
  width: calc(50px + 50px);
}

Expected

a{width:100px}

Outputs

clean-css ✗

a{width:calc(50px + 50px)}

csskit ✗

a{width:calc(50px + 50px)}

csslop ✓

a{width:100px}

cssnano ✗

a{width:calc(100px)}

csso ✗

a{width:calc(50px + 50px)}

esbuild ✓

a{width:100px}

lightningcss ✓

a{width:100px}

sass ✓

a{width:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Calc partial reduction

Complex calc expressions with compatible units are partially reduced: px terms are combined while percentage terms are preserved.

a {
  width: calc(((75.37% - 63.5px) - 900px) + (2 * 100px));
}

Expected

a{width:calc(75.37% - 763.5px)}

Outputs

clean-css ✗

a{width:calc(((75.37% - 63.5px) - 900px) + (2 * 100px))}

csskit ✗

a{width:calc(((75.37% - 63.5px) - 900px) + (2*100px))}

csslop ✓

a{width:calc(75.37% - 763.5px)}

cssnano ✓

a{width:calc(75.37% - 763.5px)}

csso ✗

a{width:calc(((75.37% - 63.5px) - 900px) + (2*100px))}

esbuild ✓

a{width:calc(75.37% - 763.5px)}

lightningcss ✓

a{width:calc(75.37% - 763.5px)}

sass ✗

a{width:calc(75.37% - 63.5px - 900px + 200px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Resolve calculations with long decimal points rounded

Replace the calculation with the result to reduce character count. To balanace accuracy and decimal point precision with character reduction, take the total number of characters in the resolved number (including .), and ensures it is 7 or less, and it correctly rounds the final digit if needed.

See the following post for decimal precision reasoning:

.a { width: calc(100% / 3); }
.b { width: calc(100% / 6); }
.c { width: calc(100% / 7); }
.d { width: calc(100% / 9); }
.e { width: calc(100% / 11); }
.f { width: calc(100% / 13); }
.g { width: calc(100% / 14); }

Expected

.a{width:33.3333%}.b{width:16.6667%}.c{width:14.2857%}.d{width:11.1111%}.e{width:9.09091%}.f{width:7.69231%}.g{width:7.14286%}

Outputs

clean-css ✗

.a{width:calc(100% / 3)}.b{width:calc(100% / 6)}.c{width:calc(100% / 7)}.d{width:calc(100% / 9)}.e{width:calc(100% / 11)}.f{width:calc(100% / 13)}.g{width:calc(100% / 14)}

csskit ✗

.a{width:calc(100%/3)}.b{width:calc(100%/6)}.c{width:calc(100%/7)}.d{width:calc(100%/9)}.e{width:calc(100%/11)}.f{width:calc(100%/13)}.g{width:calc(100%/14)}

csslop ✓

.a{width:33.3333%}.b{width:16.6667%}.c{width:14.2857%}.d{width:11.1111%}.e{width:9.09091%}.f{width:7.69231%}.g{width:7.14286%}

cssnano ✗

.a{width:calc(100% / 3)}.b{width:calc(50% / 3)}.c{width:calc(100% / 7)}.d{width:calc(100% / 9)}.e{width:calc(100% / 11)}.f{width:calc(100% / 13)}.g{width:calc(50% / 7)}

csso ✗

.a{width:calc(100%/3)}.b{width:calc(100%/6)}.c{width:calc(100%/7)}.d{width:calc(100%/9)}.e{width:calc(100%/11)}.f{width:calc(100%/13)}.g{width:calc(100%/14)}

esbuild ✗

.a{width:calc(100% / 3)}.b{width:calc(100% / 6)}.c{width:calc(100% / 7)}.d{width:calc(100% / 9)}.e{width:calc(100% / 11)}.f{width:calc(100% / 13)}.g{width:calc(100% / 14)}

lightningcss ✓

.a{width:33.3333%}.b{width:16.6667%}.c{width:14.2857%}.d{width:11.1111%}.e{width:9.09091%}.f{width:7.69231%}.g{width:7.14286%}

sass ✗

.a{width:33.3333333333%}.b{width:16.6666666667%}.c{width:14.2857142857%}.d{width:11.1111111111%}.e{width:9.0909090909%}.f{width:7.6923076923%}.g{width:7.1428571429%}
1ms
1ms
2ms
2ms
1ms
1ms
1ms
2ms
0008 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc multiply by 1 (identity)

Multiplying a value by 1 has no effect. calc(100px * 1) simplifies to 100px, removing the calc() wrapper entirely.

a {
  width: calc(100px * 1);
}

Expected

a{width:100px}

Outputs

clean-css ✗

a{width:calc(100px * 1)}

csskit ✗

a{width:calc(100px*1)}

csslop ✓

a{width:100px}

cssnano ✗

a{width:calc(100px)}

csso ✗

a{width:calc(100px*1)}

esbuild ✓

a{width:100px}

lightningcss ✓

a{width:100px}

sass ✓

a{width:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc add zero same unit (identity)

Adding zero of the same unit has no effect. calc(100px + 0px) simplifies to 100px, removing the calc() wrapper.

Note: A zero term can only be combined with a term of the same unit, not removed outright.

a {
  width: calc(100px + 0px);
}

Expected

a{width:100px}

Outputs

clean-css ✗

a{width:calc(100px + 0px)}

csskit ✗

a{width:calc(100px + 0px)}

csslop ✓

a{width:100px}

cssnano ✗

a{width:calc(100px)}

csso ✗

a{width:calc(100px + 0px)}

esbuild ✓

a{width:100px}

lightningcss ✓

a{width:100px}

sass ✓

a{width:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc double negation elimination

calc(-1 * (-10px)) negates a negative value. The product -1 * -10px = 10px, fully resolving to a plain length value.

a {
  margin-left: calc(-1 * (-10px));
  margin-right: calc(-10 * (-10px));
}

Expected

a{margin-left:10px;margin-right:100px}

Outputs

clean-css ✗

a{margin-left:calc(-1 * (-10px));margin-right:calc(-10 * (-10px))}

csskit ✗

a{margin-left:calc(-1*(-10px));margin-right:calc(-10*(-10px))}

csslop ✓

a{margin-left:10px;margin-right:100px}

cssnano ✗

a{margin-left:calc(10px);margin-right:calc(100px)}

csso ✗

a{margin-left:calc(-1*(-10px));margin-right:calc(-10*(-10px))}

esbuild ✓

a{margin-left:10px;margin-right:100px}

lightningcss ✓

a{margin-left:10px;margin-right:100px}

sass ✓

a{margin-left:10px;margin-right:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Calc double inversion elimination

Dividing by a reciprocal cancels out. calc(1 / (1 / 50px)) simplifies back to 50px because the two inversions undo each other.

a {
  width: calc(1 / (1 / 50px));
}

Expected

a{width:50px}

Outputs

clean-css ✗

a{width:calc(1 / (1 / 50px))}

csskit ✗

a{width:calc(1/(1/50px))}

csslop ✓

a{width:50px}

cssnano ✗

a{width:calc(50px)}

csso ✗

a{width:calc(1/(1/50px))}

esbuild ✗

a{width:calc(1/(1/50px))}

lightningcss ✗

a{width:calc(1 / (1 / 50px))}

sass ✓

a{width:50px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc sum flattening across nested calc()

Nested additions inside calc() are flattened into a single addition. Two nested calc() sums are merged and all px terms are combined: 10 + 20 + 30 + 40 = 100px.

a {
  width: calc(calc(10px + 20px) + calc(30px + 40px));
}

Expected

a{width:100px}

Validate

Outputs

clean-css ✗

a{width:calc(calc(10px + 20px) + calc(30px + 40px))}

csskit ✗

a{width:calc(calc(10px + 20px) + calc(30px + 40px))}

csslop ✓

a{width:100px}

cssnano ✗

a{width:calc(100px)}

csso ✗

a{width:calc(calc(10px + 20px) + calc(30px + 40px))}

esbuild ✓

a{width:100px}

lightningcss ✓

a{width:100px}

sass ✓

a{width:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc product flattening with multiple numbers

When a multiplication chain contains multiple plain numbers, they are multiplied together first. 2 * 3 becomes 6, then 6 * 10px = 60px.

a {
  width: calc(2 * 3 * 10px);
}

Expected

a{width:60px}

Outputs

clean-css ✗

a{width:calc(2 * 3 * 10px)}

csskit ✗

a{width:calc(2*3*10px)}

csslop ✓

a{width:60px}

cssnano ✗

a{width:calc(60px)}

csso ✗

a{width:calc(2*3*10px)}

esbuild ✓

a{width:60px}

lightningcss ✓

a{width:60px}

sass ✓

a{width:60px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc distribute multiplication over sum

Multiplication distributes over addition. 2 * (50px + 25px) expands to 100px + 50px, then the like terms combine to 150px.

a {
  width: calc(2 * (50px + 25px));
}

Expected

a{width:150px}

Validate

Outputs

clean-css ✗

a{width:calc(2 * (50px + 25px))}

csskit ✗

a{width:calc(2*(50px + 25px))}

csslop ✓

a{width:150px}

cssnano ✗

a{width:calc(150px)}

csso ✗

a{width:calc(2*(50px + 25px))}

esbuild ✓

a{width:150px}

lightningcss ✓

a{width:150px}

sass ✓

a{width:150px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc combine like percentage terms

Terms with the same unit are combined by adding their values. calc(50% + 25%) becomes 75%, removing the calc() wrapper.

a {
  width: calc(50% + 25%);
}

Expected

a{width:75%}

Outputs

clean-css ✗

a{width:calc(50% + 25%)}

csskit ✗

a{width:calc(50% + 25%)}

csslop ✓

a{width:75%}

cssnano ✗

a{width:calc(75%)}

csso ✗

a{width:calc(50% + 25%)}

esbuild ✓

a{width:75%}

lightningcss ✓

a{width:75%}

sass ✓

a{width:75%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc division of pure numbers

calc(10 / 2) divides two unitless numbers, yielding 5. Since line-height accepts unitless numbers, the result is a plain 5 with the calc() wrapper removed.

a {
  line-height: calc(10 / 2);
}

Expected

a{line-height:5}

Outputs

clean-css ✗

a{line-height:calc(10 / 2)}

csskit ✗

a{line-height:calc(10/2)}

csslop ✓

a{line-height:5}

cssnano ✗

a{line-height:calc(5)}

csso ✗

a{line-height:calc(10/2)}

esbuild ✓

a{line-height:5}

lightningcss ✓

a{line-height:5}

sass ✓

a{line-height:5}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Calc self-subtraction resolves to zero

calc(50px - 50px) subtracts identical values. The same-unit terms cancel out: 50 - 50 = 0. The result is 0 (unitless zero is valid for lengths).

a {
  width: calc(50px - 50px);
}

Expected

a{width:0}

Outputs

clean-css ✗

a{width:calc(50px - 50px)}

csskit ✗

a{width:calc(50px - 50px)}

csslop ✓

a{width:0}

cssnano ✗

a{width:calc(0px)}

csso ✗

a{width:calc(50px - 50px)}

esbuild ✗

a{width:0px}

lightningcss ✓

a{width:0}

sass ✗

a{width:0px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Calc multiply by zero

calc(100px * 0) multiplies a dimension by zero. The result is 0px, which serializes as 0.

a {
  width: calc(100px * 0);
}

Expected

a{width:0}

Outputs

clean-css ✗

a{width:calc(100px * 0)}

csskit ✗

a{width:calc(100px*0)}

csslop ✓

a{width:0}

cssnano ✗

a{width:calc(0px)}

csso ✗

a{width:calc(100px*0)}

esbuild ✗

a{width:0px}

lightningcss ✓

a{width:0}

sass ✗

a{width:0px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Calc constant pi resolution

pi is a numeric constant (~3.14159). calc(pi * 10px) resolves the constant then multiplies, yielding 31.4159px.

a {
  width: calc(pi * 100px);
}

Expected

a{width:314.159px}

Validate

Outputs

clean-css ✗

a{width:calc(pi * 100px)}

csskit ✗

a{width:calc(pi*100px)}

csslop ✓

a{width:314.159px}

cssnano ✗

a{width:calc(314.15927px)}

csso ✗

a{width:calc(pi*100px)}

esbuild ✗

a{width:calc(pi*100px)}

lightningcss ✓

a{width:314.159px}

sass ✗

a{width:314.159265359px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Calc constant e resolution

e is Euler's number (~2.71828). calc(e * 10px) resolves the constant then multiplies, yielding 27.1828px.

a {
  width: calc(e * 100px);
}

Expected

a{width:271.828px}

Validate

Outputs

clean-css ✗

a{width:calc(e * 100px)}

csskit ✗

a{width:calc(e*100px)}

csslop ✓

a{width:271.828px}

cssnano ✗

a{width:calc(271.82818px)}

csso ✗

a{width:calc(e*100px)}

esbuild ✗

a{width:calc(e*100px)}

lightningcss ✓

a{width:271.828px}

sass ✗

a{width:271.8281828459px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0021 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Calc canonicalize compatible length units

Compatible units convert to canonical units: 1in = 96px. The sum combines 96px + 0px = 96px, resolving to a plain length value.

a {
  width: calc(1in + 0px);
}

Expected

a{width:96px}

Validate

Outputs

clean-css ✗

a{width:calc(1in + 0px)}

csskit ✗

a{width:calc(1in + 0px)}

csslop ✓

a{width:96px}

cssnano ✗

a{width:calc(1in)}

csso ✗

a{width:calc(1in + 0px)}

esbuild ✗

a{width:calc(1in + 0px)}

lightningcss ✓

a{width:96px}

sass ✗

a{width:1in}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc single term unwrap

calc(100px) contains a single numeric value with no operations. The calc() wrapper is unnecessary and should be removed, yielding 100px.

a {
  width: calc(100px);
}

Expected

a{width:100px}

Outputs

clean-css ✗

a{width:calc(100px)}

csskit ✗

a{width:calc(100px)}

csslop ✓

a{width:100px}

cssnano ✗

a{width:calc(100px)}

csso ✗

a{width:calc(100px)}

esbuild ✓

a{width:100px}

lightningcss ✓

a{width:100px}

sass ✓

a{width:100px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Calc division folding dimension by number

calc(100px / 4) creates a Product of 100px and Invert(4). The Invert resolves 4 to 0.25, then the Product yields 25px. Fully resolved, no calc() needed.

a {
  width: calc(100px / 4);
}

Expected

a{width:25px}

Validate

Outputs

clean-css ✗

a{width:calc(100px / 4)}

csskit ✗

a{width:calc(100px/4)}

csslop ✓

a{width:25px}

cssnano ✗

a{width:calc(25px)}

csso ✗

a{width:calc(100px/4)}

esbuild ✓

a{width:25px}

lightningcss ✓

a{width:25px}

sass ✓

a{width:25px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Calc combine like terms preserving incompatible units

calc(100% - 50px + 0%) has both percentage and px terms. Like units are combined: 100% + 0% = 100%, while -50px remains unchanged. The result is calc(100% - 50px) -- calc() is preserved because % and px cannot be resolved together at specified-value time.

a {
  width: calc(100% - 50px + 0%);
}

Expected

a{width:calc(100% - 50px)}

Validate

Outputs

clean-css ✗

a{width:calc(100% - 50px + 0%)}

csskit ✗

a{width:calc(100% - 50px + 0%)}

csslop ✓

a{width:calc(100% - 50px)}

cssnano ✓

a{width:calc(100% - 50px)}

csso ✗

a{width:calc(100% - 50px + 0%)}

esbuild ✓

a{width:calc(100% - 50px)}

lightningcss ✓

a{width:calc(100% - 50px)}

sass ✗

a{width:calc(100% - 50px + 0%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

calc() zero with different unit must not be dropped

calc(100px + 0%) must not simplify to 100px. Although 0% is numerically zero, it has a different unit type. The browser resolves % against the containing block at computed value time, and future animations or transitions could interpolate through non-zero percentage values.

a {
  width: calc(100px + 0%);
}

Expected

a{width:calc(100px + 0%)}

Outputs

clean-css ✓

a{width:calc(100px + 0%)}

csskit ✓

a{width:calc(100px + 0%)}

csslop ✓

a{width:calc(100px + 0%)}

cssnano ✓

a{width:calc(100px + 0%)}

csso ✓

a{width:calc(100px + 0%)}

esbuild ✓

a{width:calc(100px + 0%)}

lightningcss ✗

a{width:100px}

sass ✓

a{width:calc(100px + 0%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

Convert reciprocal multiplication to division

Convert multiplication by decimal to divide by whole number in calc.

When a decimal is used for multiplication and it is between 0 and 1, take it and put it in a fraction over 1.

0.25 = 0.25 1

Then multiply the top and bottom by 10 to the power of however many decimal places there are. For .25 you'd use 102 (100), for .0625 you'd use 104 (10000).

0.25 1 × 100 100 = 25 100

Find the Greatest Common Factor (GCF) of the top and bottom, if it exists, and reduce the fraction by dividing both numerator and denominator by the GCF.

25 ÷ 25 100 ÷ 25 = 1 4

After division, if the top value is 1, then you can use the bottom (denominator) as a replacement in your CSS calc, and convert from multiplication to division.

Examples:

  • calc(var(--x)*.5) => calc(var(--x)/2)
  • calc(var(--x)*.25) => calc(var(--x)/4)
  • calc(var(--x)*.05) => calc(var(--x)/20)
  • calc(var(--x)*.03125) => calc(var(--x)/32)

li {
  transition-delay: calc(sibling-index() * 0.25s);
}

Expected

li{transition-delay:calc(sibling-index()/4s)}

Outputs

clean-css ✗

li{transition-delay:calc(sibling-index() * 0.25s)}

csskit ✗

li{transition-delay:calc(sibling-index()*.25s)}

csslop ✓

li{transition-delay:calc(sibling-index()/4s)}

cssnano ✗

li{transition-delay:calc(sibling-index() * .25s)}

csso ✗

li{transition-delay:calc(sibling-index()*.25s)}

esbuild ✓

li{transition-delay:calc(sibling-index()/4s)}

lightningcss ✗

li{transition-delay:calc(sibling-index() * .25s)}

sass ✗

li{transition-delay:calc(sibling-index()*.25s)}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0027 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Flatten negative width pixel calculations to 0

In the CSS spec, widths cannot have negative values, so any calculation that would return a negative value is interpreted as 0.

a {
  width: calc(5px - 10px);
  margin: calc(5px - 10px);
}

Expected

a{width:0;margin:-5px}

Outputs

clean-css ✗

a{width:calc(5px - 10px);margin:calc(5px - 10px)}

csskit ✗

a{width:calc(5px - 10px);margin:calc(5px - 10px)}

csslop ✓

a{width:0;margin:-5px}

cssnano ✗

a{width:calc(-5px);margin:calc(-5px)}

csso ✗

a{width:calc(5px - 10px);margin:calc(5px - 10px)}

esbuild ✗

a{width:-5px;margin:-5px}

lightningcss ✗

a{width:-5px;margin:-5px}

sass ✗

a{width:-5px;margin:-5px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0028 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Flatten negative width rem calculations to 0

In the CSS spec, widths cannot have negative values, so any calculation that would return a negative value is interpreted as 0.

a {
  width: calc(-0.5rem);
  margin: calc(-0.5rem);
}

Expected

a{width:0;margin:-.5rem}

Outputs

clean-css ✗

a{width:calc(-.5rem);margin:calc(-.5rem)}

csskit ✗

a{width:calc(-.5rem);margin:calc(-.5rem)}

csslop ✓

a{width:0;margin:-.5rem}

cssnano ✗

a{width:calc(-.5rem);margin:calc(-.5rem)}

csso ✗

a{width:calc(-.5rem);margin:calc(-.5rem)}

esbuild ✗

a{width:-.5rem;margin:-.5rem}

lightningcss ✗

a{width:-.5rem;margin:-.5rem}

sass ✗

a{width:-0.5rem;margin:-0.5rem}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0029 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge rules after flattening negative width calculations to 0

In the CSS spec, widths cannot have negative values, so any calculation that would return a negative value is interpretted as 0.

a {
  width: calc(5px - 10px);
}
b {
  width: calc(-0.5rem);
}

Expected

a,b{width:0}

Outputs

clean-css ✗

a{width:calc(5px - 10px)}b{width:calc(-.5rem)}

csskit ✗

a{width:calc(5px - 10px)}b{width:calc(-.5rem)}

csslop ✓

a,b{width:0}

cssnano ✗

a{width:calc(-5px)}b{width:calc(-.5rem)}

csso ✗

a{width:calc(5px - 10px)}b{width:calc(-.5rem)}

esbuild ✗

a{width:-5px}b{width:-.5rem}

lightningcss ✗

a{width:-5px}b{width:-.5rem}

sass ✗

a{width:-5px}b{width:-0.5rem}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0030 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Resolve simple percent division in calc

Replace a calculation with the result when it is a percentage divided by a number that results in a non-repeating positive interger that is shorter than the minified version of the calculation.

Example:

  • calc(100%/16) is 13 characters
  • 6.25% is 5 characters

.a { width: calc(100% / 1); }
.b { width: calc(100% / 2); }
.c { width: calc(100% / 4); }
.d { width: calc(100% / 5); }
.e { width: calc(100% / 8); }
.f { width: calc(100% / 10); }
.g { width: calc(100% / 16); }

Expected

.a{width:100%}.b{width:50%}.c{width:25%}.d{width:20%}.e{width:12.5%}.f{width:10%}.g{width:6.25%}

Outputs

clean-css ✗

.a{width:calc(100% / 1)}.b{width:calc(100% / 2)}.c{width:calc(100% / 4)}.d{width:calc(100% / 5)}.e{width:calc(100% / 8)}.f{width:calc(100% / 10)}.g{width:calc(100% / 16)}

csskit ✗

.a{width:calc(100%/1)}.b{width:calc(100%/2)}.c{width:calc(100%/4)}.d{width:calc(100%/5)}.e{width:calc(100%/8)}.f{width:calc(100%/10)}.g{width:calc(100%/16)}

csslop ✓

.a{width:100%}.b{width:50%}.c{width:25%}.d{width:20%}.e{width:12.5%}.f{width:10%}.g{width:6.25%}

cssnano ✗

.a{width:calc(100%)}.b{width:calc(50%)}.c{width:calc(25%)}.d{width:calc(20%)}.e{width:calc(12.5%)}.f{width:calc(10%)}.g{width:calc(6.25%)}

csso ✗

.a{width:calc(100%/1)}.b{width:calc(100%/2)}.c{width:calc(100%/4)}.d{width:calc(100%/5)}.e{width:calc(100%/8)}.f{width:calc(100%/10)}.g{width:calc(100%/16)}

esbuild ✓

.a{width:100%}.b{width:50%}.c{width:25%}.d{width:20%}.e{width:12.5%}.f{width:10%}.g{width:6.25%}

lightningcss ✓

.a{width:100%}.b{width:50%}.c{width:25%}.d{width:20%}.e{width:12.5%}.f{width:10%}.g{width:6.25%}

sass ✓

.a{width:100%}.b{width:50%}.c{width:25%}.d{width:20%}.e{width:12.5%}.f{width:10%}.g{width:6.25%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0031 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✓
Details

Resolve mixed time units in calc

Resolve both inner values in the calculation to the same units, perform the resolve the calculation. In this case .8s is shorter than 800ms.

a {
  transition-duration: calc(1s - 200ms);
}

Expected

a{transition-duration:.8s}

Outputs

clean-css ✗

a{transition-duration:calc(1s - 200ms)}

csskit ✗

a{transition-duration:calc(1s - .2s)}

csslop ✓

a{transition-duration:.8s}

cssnano ✗

a{transition-duration:calc(.8s)}

csso ✗

a{transition-duration:calc(1s - 200ms)}

esbuild ✓

a{transition-duration:.8s}

lightningcss ✓

a{transition-duration:.8s}

sass ✓

a{transition-duration:.8s}
1ms
2ms
1ms
1ms
1ms
1ms
1ms
1ms
0032 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Resolve with correct order of operations in subtraction with parentheses

You must first resolve the inner parentheses (25px - 4px) => 21px. Resulting in 100% - 21px - 2px or 100% + -21px + -2px which can be resolved to 100% + -23px or 100% - 23px.

a {
  margin: calc(100% - (25px - 4px) - 2px);
}

Expected

a{margin:calc(100% - 23px)}

Outputs

clean-css ✗

a{margin:calc(100% - (25px - 4px) - 2px)}

csskit ✗

a{margin:calc(100% - (25px - 4px) - 2px)}

csslop ✓

a{margin:calc(100% - 23px)}

cssnano ✓

a{margin:calc(100% - 23px)}

csso ✗

a{margin:calc(100% - (25px - 4px) - 2px)}

esbuild ✓

a{margin:calc(100% - 23px)}

lightningcss ✓

a{margin:calc(100% - 23px)}

sass ✗

a{margin:calc(100% - 21px - 2px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0033 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Resolve with correct order of operations in division with parentheses

In this example (100% - 5em) / 2 can be represented as this fraction:

100% − 5em 2

Which allows dividing each part of the numerator by the denominator, like so: (100% / 2) - (5em / 2). Which can be resolved to 50% - 2.5em.

a {
  margin: calc((100% - 5em) / 2);
}

Expected

a{margin:calc(50% - 2.5em)}

Outputs

clean-css ✗

a{margin:calc((100% - 5em)/ 2)}

csskit ✗

a{margin:calc((100% - 5em)/2)}

csslop ✓

a{margin:calc(50% - 2.5em)}

cssnano ✓

a{margin:calc(50% - 2.5em)}

csso ✗

a{margin:calc((100% - 5em)/2)}

esbuild ✗

a{margin:calc((100% - 5em)/2)}

lightningcss ✓

a{margin:calc(50% - 2.5em)}

sass ✗

a{margin:calc((100% - 5em)/2)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0034 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Resolve with correct PEMDAS order of operations

In this example -1 * (1rem + 16px) / 2, the parentheses go first, but cannot be resolved any further because of differing relative vs absolute units. Then the multiplication is applied, creating (-1rem + -16px) / 2. This can be represented as a fraction:

-1rem − 16px 2

Which allows dividing each part of the numerator by the denominator, like so: (-1rem / 2) - (16px / 2). Which can be resolved to -.5rem - 8px.

a {
  margin: calc(-1 * (1rem + 16px) / 2);
}

Expected

a{margin:calc(-.5rem - 8px)}

Outputs

clean-css ✗

a{margin:calc(-1 * (1rem + 16px)/ 2)}

csskit ✗

a{margin:calc(-1*(1rem + 16px)/2)}

csslop ✓

a{margin:calc(-.5rem - 8px)}

cssnano ✓

a{margin:calc(-.5rem - 8px)}

csso ✗

a{margin:calc(-1*(1rem + 16px)/2)}

esbuild ✗

a{margin:calc(-.5*(1rem + 16px))}

lightningcss ✓

a{margin:calc(-.5rem - 8px)}

sass ✗

a{margin:calc(-1*(1rem + 16px)/2)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 2 / 34 2 / 34 34 / 34 8 / 34 2 / 34 22 / 34 28 / 34 17 / 34
Percent 5.88% 5.88% 100% 23.52% 5.88% 64.7% 82.35% 50%
Duration 5ms 18ms 16ms 30ms 10ms 20ms 1ms 36ms

charset #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

@charset removal

@charset "UTF-8" is redundant in modern workflows since UTF-8 is the default encoding. It should be stripped.

@charset "UTF-8";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset "UTF-8";a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "UTF-8";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
2ms
7ms
1ms
2ms
4ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✗ ✓ ✗ ✗ ✗
Details

Duplicate @charset removal

Only the first @charset declaration is meaningful; subsequent @charset rules are dropped. Uses two non-UTF-8 charsets to avoid special-casing UTF-8 removal.

@charset "gbk";
@charset "big5";
a {
  color: red;
}

Expected

@charset "gbk";a{color:red}

Outputs

clean-css ✓

@charset "gbk";a{color:red}

csskit ✓

@charset "gbk";a{color:red}

csslop ✓

@charset "gbk";a{color:red}

cssnano ✗

a{color:red}

csso ✓

@charset "gbk";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✗

a{color:red}

sass ✗

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Move @charset to the top of documents

If a @charset is used and set to anything besides the default UTF-8, it must be retained and placed at the start of the minified CSS file. If the @charset is not moved to the top of the document, the CSS file will be considered invalid.

"It is a specific byte sequence that can only be placed at the very beginning of a stylesheet." - https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@charset

a {
  color: red;
}
@charset "gbk";

Expected

@charset "gbk";a{color:red}

Outputs

clean-css ✓

@charset "gbk";a{color:red}

csskit ✓

@charset "gbk";a{color:red}

csslop ✓

@charset "gbk";a{color:red}

cssnano ✗

a{color:red}

csso ✗

a{color:red}

esbuild ✗

a{color:red}@charset "gbk";

lightningcss ✗

a{color:red}

sass ✗

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✓ ✓ ✗ ✓ ✗ ✓ ✓
Details

Replace unicode escapes with actual character

If the @charset is set to UTF-16 or UTF-8 (the default if @charset is not present), you can safely replace the escape codes for UTF values with their actual character value.

.foo:before {
  content: "\e61d";
}

Expected

.foo:before{content:""}

Outputs

clean-css ✗

.foo:before{content:"\e61d"}

csskit ✓

.foo:before{content:""}

csslop ✓

.foo:before{content:""}

cssnano ✗

.foo:before{content:"\e61d"}

csso ✓

.foo:before{content:""}

esbuild ✗

.foo:before{content:"\e61d"}

lightningcss ✓

.foo:before{content:""}

sass ✓

.foo:before{content:""}
1ms
1ms
4ms
5ms
1ms
1ms
1ms
1ms
0005 ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Do not replace unicode escapes in non-unicode charsets

If the @charset is not set to UTF-16 or UTF-8 (the default if @charset is not present), then it may not be safe to convert escaped unicode values with real unicode characters.

@charset "gbk";
.foo:before {
  content: "\e61d";
}

Expected

@charset "gbk";.foo:before{content:"\e61d"}

Outputs

clean-css ✓

@charset "gbk";.foo:before{content:"\e61d"}

csskit ✗

@charset "gbk";.foo:before{content:""}

csslop ✓

@charset "gbk";.foo:before{content:"\e61d"}

cssnano ✗

.foo:before{content:"\e61d"}

csso ✗

@charset "gbk";.foo:before{content:""}

esbuild ✗

@charset "UTF-8";.foo:before{content:"\e61d"}

lightningcss ✗

.foo:before{content:""}

sass ✗

.foo:before{content:""}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Handle multiple charsets

A @charset must be the first part of a CSS or it is ignored. Assume any declarations of a @charset are intentional, and if not at the top of a CSS file, it is likely a result of concatenating files but it is meant to be used. Move the first seen @charset to the top of the document. Remove all subsequent @charset's as they are ignored by the browser. If the first seen @charset uses UTF-8, it can be removed too, as it is the default encoding and does not need to be explicitly defined.

.foo {
  color: red;
}

@charset "gbk";

.bar {
  color: tan;
}

@charset "big5";

.baz {
  background: red;
}

Expected

@charset "gbk";.foo{color:red}.bar{color:tan}.baz{background:red}

Outputs

clean-css ✓

@charset "gbk";.foo{color:red}.bar{color:tan}.baz{background:red}

csskit ✓

@charset "gbk";.foo{color:red}.bar{color:tan}.baz{background:red}

csslop ✓

@charset "gbk";.foo{color:red}.bar{color:tan}.baz{background:red}

cssnano ✗

.foo{color:red}.bar{color:tan}.baz{background:red}

csso ✗

.foo{color:red}.bar{color:tan}.baz{background:red}

esbuild ✗

.foo{color:red}@charset "gbk";.bar{color:tan}@charset "big5";.baz{background:red}

lightningcss ✗

.foo{color:red}.bar{color:tan}.baz{background:red}

sass ✗

.foo{color:red}.bar{color:tan}.baz{background:red}
2ms
2ms
2ms
1ms
2ms
1ms
1ms
1ms
0007 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

A UTF-16 @charset label means UTF-8

Determining the fallback encoding says that if the @charset label resolves to utf-16be or utf-16le, the encoding used is utf-8. The declaration bytes are ASCII, so the file cannot really be UTF-16. @charset "utf-16be"; therefore means @charset "utf-8";, which is the default and can be dropped (see charset/0001).

@charset "utf-16be";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset "utf-16be";a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "utf-16be";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
4ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

utf-16 is a UTF-16LE label, so it also means UTF-8

The Encoding Standard maps utf-16, utf-16le, ucs-2, unicode, unicodefeff and iso-10646-ucs-2 to UTF-16LE, and utf-16be / unicodefffe to UTF-16BE. Every one of them becomes utf-8 when determining the fallback encoding, so the rule is redundant and removable.

A minifier that string-matches only utf-16be/utf-16le keeps this rule and misses the optimisation.

@charset "utf-16";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset "utf-16";a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "utf-16";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Shorten the @charset label to its shortest alias

Labels are looked up in the Encoding Standard, which gives every encoding several aliases. iso-8859-1, latin1, us-ascii, ascii, cp819, and l1 all resolve to the same windows-1252 encoding, so @charset "ISO-8859-1"; can be written @charset "l1";, saving 8 bytes.

@charset "ISO-8859-1";
a {
  color: red;
}

Expected

@charset "l1";a{color:red}

Validate

Outputs

clean-css ✗

@charset "ISO-8859-1";a{color:red}

csskit ✓

@charset "l1";a{color:red}

csslop ✓

@charset "l1";a{color:red}

cssnano ✗

a{color:red}

csso ✗

@charset "ISO-8859-1";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✗

a{color:red}

sass ✗

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

An unrecognised @charset label has no effect

If the label does not match an Encoding Standard label the lookup fails and the stylesheet keeps the encoding it would have had anyway, so the rule is inert and removable. Gecko DetermineNonBOMEncoding only uses the label when Encoding::ForLabel returns non-null; WebKit/Blink SetEncoding returns early when !encoding.IsValid().

@charset "not-a-charset";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset "not-a-charset";a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "not-a-charset";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

An uppercased @charset is not an encoding declaration

CSS at-keywords are ASCII case-insensitive, but the @charset rule, and its value are not. Engines compare raw bytes against @charset ". So @CHARSET "gbk"; declares nothing and the parser drops the at-rule; it can be removed.

@CHARSET "gbk";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

@CHARSET "gbk";a{color:red}

csso ✗

@CHARSET "gbk";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✗

@CHARSET "gbk";a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

Never resolve escapes inside a @charset label

Engines read @charset rules as raw bytes, not CSS strings, therefore escaping isn't taken into account. Everything between @charset " and "; is the label. The literal bytes of \67 bk might resolve in CSS syntax as "gbk" - a valid encoding label, but the pre-escaped label not an Encoding Standard label, so the rule is inert and removable.

A minifier that unescapes strings would emit @charset "gbk"; and change how the browser decodes the whole file. This is incorrect.

@charset "\67 bk";
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset "\67 bk";a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "gbk";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

A single-quoted @charset is not an encoding declaration

@charset must use single quotes, as the byte sequence - not "tokenized" syntax is detected. @charset 'gbk'; is therefore inert, and will be discarded in engines. Minifiers can therefore safely remove this.

Normalising the quotes instead - which minifiers do to ordinary strings - turns a rule the browser ignored into a live encoding declaration. This is incorrect and may cause bugs.

@charset 'gbk';
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✗

@charset 'gbk';a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

@charset "gbk";a{color:red}

esbuild ✗

@charset "UTF-8";a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 5 / 13 12 / 13 13 / 13 6 / 13 2 / 13 0 / 13 8 / 13 7 / 13
Percent 38.46% 92.3% 100% 46.15% 15.38% 0% 61.53% 53.84%
Duration 4ms 10ms 12ms 14ms 10ms 9ms 1ms 9ms

color-rounding #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

oklab() rounding: out-of-gamut colours stay in native space

oklab() values with excess precision are rounded to 3 decimal places. Out-of-gamut oklab colours (chroma too high to fit in sRGB) stay in oklab space; they cannot be safely represented as a hex value.

In-gamut oklab colours may be minified to hex if the hex form is shorter.

a {
  color: oklab(0.50000 0.28456 -0.14876);
}

Expected

a{color:oklab(.5 .285 -.149)}

Outputs

clean-css ✗

a{color:oklab(.5 .28456 -.14876)}

csskit ✓

a{color:oklab(.5 .285 -.149)}

csslop ✓

a{color:oklab(.5 .285 -.149)}

cssnano ✗

a{color:oklab(.5 .28456 -.14876)}

csso ✗

a{color:oklab(.5 .28456-.14876)}

esbuild ✗

a{color:oklab(.5 .28456 -.14876)}

lightningcss ✗

a{color:oklab(50% .28456 -.14876)}

sass ✗

a{color:oklab(.5 .28456 -0.14876)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0002 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color-mix in oklch resolved to oklch value

color-mix(in oklch, red 50%, blue 50%) can be resolved at build time to oklch(.54 .285 326.6). A minifier that resolves the mix must do so in oklch space -- mixing in srgb produces a different color. Keeping the result in oklch avoids lossy gamut mapping to sRGB hex. Precision is rounded to 3dp (L/C) and 1dp (hue) -- well below JND. Lightness uses the number form (.54) rather than percent (54%) -- same length but consistent with always preferring numbers over percentages since percentages are longer for other components.

For a lot more detail on rounding, read Too Much Color.

a {
  color: color-mix(in oklch, red 50%, blue 50%);
}

Expected

a{color:oklch(.54 .285 326.6)}

Validate

Outputs

clean-css ✗

a{color:color-mix(in oklch,red 50%,#00f 50%)}

csskit ✓

a{color:oklch(.54 .285 326.6)}

csslop ✓

a{color:oklch(.54 .285 326.6)}

cssnano ✗

a{color:color-mix(in oklch,red 50%,blue 50%)}

csso ✗

a{color:color-mix(in oklch,red 50%,blue 50%)}

esbuild ✗

a{color:color-mix(in oklch,red 50%,blue 50%)}

lightningcss ✗

a{color:oklch(53.9985% .285449 326.643)}

sass ✗

a{color:color-mix(in oklch, red 50%, blue 50%)}
1ms
2ms
1ms
1ms
2ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color-mix() in oklch resolved to oklch() when out of sRGB gamut

color-mix(in oklch, lime, blue) produces a high-chroma teal in oklch space that is outside the sRGB gamut. A minifier must not convert this to a hex color (which would clamp and change the color). Instead, the mix should be resolved to oklch functional notation: oklch(.659 .304 203.3), which is both shorter than the color-mix and preserves the intended color. L/C at 3dp and hue at 1dp - well below dEOk JND of 0.02.

a {
  color: color-mix(in oklch, lime, blue);
}

Expected

a{color:oklch(.659 .304 203.3)}

Outputs

clean-css ✗

a{color:color-mix(in oklch,#0f0,#00f)}

csskit ✓

a{color:oklch(.659 .304 203.3)}

csslop ✓

a{color:oklch(.659 .304 203.3)}

cssnano ✗

a{color:color-mix(in oklch,lime,blue)}

csso ✗

a{color:color-mix(in oklch,lime,blue)}

esbuild ✗

a{color:color-mix(in oklch,lime,blue)}

lightningcss ✗

a{color:oklch(65.9227% .304021 203.274)}

sass ✗

a{color:color-mix(in oklch, lime, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Fractional sRGB channels round to integers

rgb(127.6 64.4 191.5) has fractional channel values that are meaningless in 8-bit sRGB. Browsers round to the nearest integer (128, 64, 192) before display. A minifier should round-then-convert to hex #8040c0, not truncate (which would give #7f40bf).

a {
  color: rgb(127.6 64.4 191.5);
}

Expected

a{color:#8040c0}

Outputs

clean-css ✗

a{color:rgb(127.6 64.4 191.5)}

csskit ✓

a{color:#8040c0}

csslop ✓

a{color:#8040c0}

cssnano ✓

a{color:#8040c0}

csso ✗

a{color:rgb(127.6 64.4 191.5)}

esbuild ✓

a{color:#8040c0}

lightningcss ✓

a{color:#8040c0}

sass ✗

a{color:rgb(50.0392156863%,25.2549019608%,75.0980392157%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
3ms
0005 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

oklch/oklab excess precision rounds to 3 decimal places

oklch and oklab use a 0-1 lightness scale and small chroma/a/b ranges. dEOk JND is 0.02, so 3dp (0.001) on L/C/a/b gives two orders of magnitude headroom. Hue at 1dp (0.1 deg) contributes ~0.0003 dEOk - effectively zero. A minifier can round e.g. oklch(0.55432 0.12276 180.4567 / 0.74567) to oklch(.554 .123 180.5/.746) with no perceptible change. Alpha is 0-1, so 3dp applies there too.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: oklch(0.55432 0.12276 180.4567 / 0.74567);
  background-color: oklab(0.65432 -0.23456 0.18901);
}

Expected

a{color:oklch(.554 .123 180.5/.746);background-color:oklab(.654 -.235 .189)}

Outputs

clean-css ✗

a{color:oklch(.55432 .12276 180.4567 / .74567);background-color:oklab(.65432 -.23456 .18901)}

csskit ✓

a{color:oklch(.554 .123 180.5/.746);background-color:oklab(.654 -.235 .189)}

csslop ✓

a{color:oklch(.554 .123 180.5/.746);background-color:oklab(.654 -.235 .189)}

cssnano ✗

a{color:oklch(.55432 .12276 180.4567/.74567);background-color:oklab(.65432 -.23456 .18901)}

csso ✗

a{color:oklch(.55432 .12276 180.4567/.74567);background-color:oklab(.65432-.23456 .18901)}

esbuild ✗

a{color:oklch(.55432 .12276 180.4567 / .74567);background-color:oklab(.65432 -.23456 .18901)}

lightningcss ✗

a{color:oklch(55.432% .12276 180.457/.74567);background-color:oklab(65.432% -.23456 .18901)}

sass ✗

a{color:oklch(.55432 .12276 180.4567/.74567);background-color:oklab(.65432 -0.23456 .18901)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0006 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

lch/lab excess precision rounds to 1 decimal place

CIE LCH and Lab use a 0-100 lightness scale, chroma up to ~150, and a/b axes of roughly -128 to 128. dE00 JND is 2.0. At 0dp (integers) the worst-case error is already sub-JND for L/C/a/b, but lch chroma at 0dp can hit ~0.5 dE00. 1dp gives two orders of magnitude headroom (worst-case ~0.05 dE00 for all channels). A minifier can round e.g. lch(54.321 43.765 274.456 / 0.74567) to lch(54.3 43.8 274.5/.746). Alpha is 0-1, so 3dp applies there.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: lch(54.321 100.456 274.456 / 0.74567);
  background-color: lab(54.321 -60.456 70.789);
}

Expected

a{color:lch(54.3 100.5 274.5/.746);background-color:lab(54.3 -60.5 70.8)}

Outputs

clean-css ✗

a{color:lch(54.321 100.456 274.456 / .74567);background-color:lab(54.321 -60.456 70.789)}

csskit ✓

a{color:lch(54.3 100.5 274.5/.746);background-color:lab(54.3 -60.5 70.8)}

csslop ✓

a{color:lch(54.3 100.5 274.5/.746);background-color:lab(54.3 -60.5 70.8)}

cssnano ✗

a{color:lch(54.321 100.456 274.456/.74567);background-color:lab(54.321 -60.456 70.789)}

csso ✗

a{color:lch(54.321 100.456 274.456/.74567);background-color:lab(54.321-60.456 70.789)}

esbuild ✗

a{color:lch(54.321 100.456 274.456 / .74567);background-color:lab(54.321 -60.456 70.789)}

lightningcss ✗

a{color:lch(54.321% 100.456 274.456/.74567);background-color:lab(54.321% -60.456 70.789)}

sass ✗

a{color:lch(54.321 100.456 274.456/.74567);background-color:lab(54.321 -60.456 70.789)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(srgb-linear) excess precision rounds to 4 decimal places

sRGB-linear channels are in the 0–1 range. Unlike other 0–1 colour spaces which use 3dp, srgb-linear needs 4dp because its linear transfer function amplifies rounding errors for near-black values at 3dp. Alpha is 0–1, so 3dp applies.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(srgb-linear 0.21586 0.04511 1.53123 / 0.8765);
}

Expected

a{color:color(srgb-linear .2159 .0451 1.5312/.877)}

Outputs

clean-css ✗

a{color:color(srgb-linear .21586 .04511 1.53123 / .8765)}

csskit ✓

a{color:color(srgb-linear .2159 .0451 1.5312/.877)}

csslop ✓

a{color:color(srgb-linear .2159 .0451 1.5312/.877)}

cssnano ✗

a{color:color(srgb-linear .21586 .04511 1.53123/.8765)}

csso ✗

a{color:color(srgb-linear .21586 .04511 1.53123/.8765)}

esbuild ✗

a{color:color(srgb-linear .21586 .04511 1.53123 / .8765)}

lightningcss ✗

a{color:color(srgb-linear .21586 .04511 1.53123/.8765)}

sass ✗

a{color:color(srgb-linear .21586 .04511 1.53123/.8765)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(display-p3) excess precision rounds to 3 decimal places

Display P3 channels are in the 0–1 range. 3dp is well below the perceptual JND and provides sufficient headroom for chained colour operations. Alpha is 0–1, so 3dp applies there too.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(display-p3 0.97654 0.12345 0.01789 / 0.8765);
}

Expected

a{color:color(display-p3 .977 .123 .018/.877)}

Outputs

clean-css ✗

a{color:color(display-p3 .97654 .12345 .01789 / .8765)}

csskit ✓

a{color:color(display-p3 .977 .123 .018/.877)}

csslop ✓

a{color:color(display-p3 .977 .123 .018/.877)}

cssnano ✗

a{color:color(display-p3 .97654 .12345 .01789/.8765)}

csso ✗

a{color:color(display-p3 .97654 .12345 .01789/.8765)}

esbuild ✗

a{color:color(display-p3 .97654 .12345 .01789 / .8765)}

lightningcss ✗

a{color:color(display-p3 .97654 .12345 .01789/.8765)}

sass ✗

a{color:color(display-p3 .97654 .12345 .01789/.8765)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(a98-rgb) excess precision rounds to 3 decimal places

A98 RGB channels are in the 0–1 range. 3dp is well below the perceptual JND.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(a98-rgb 1.05123 0.03456 0.07891);
}

Expected

a{color:color(a98-rgb 1.051 .035 .079)}

Outputs

clean-css ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}

csskit ✓

a{color:color(a98-rgb 1.051 .035 .079)}

csslop ✓

a{color:color(a98-rgb 1.051 .035 .079)}

cssnano ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}

csso ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}

esbuild ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}

lightningcss ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}

sass ✗

a{color:color(a98-rgb 1.05123 .03456 .07891)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(prophoto-rgb) excess precision rounds to 3 decimal places

ProPhoto RGB channels are in the 0–1 range. 3dp is well below the perceptual JND.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(prophoto-rgb 0.88765 0.01234 0.12345);
}

Expected

a{color:color(prophoto-rgb .888 .012 .123)}

Outputs

clean-css ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}

csskit ✓

a{color:color(prophoto-rgb .888 .012 .123)}

csslop ✓

a{color:color(prophoto-rgb .888 .012 .123)}

cssnano ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}

csso ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}

esbuild ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}

lightningcss ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}

sass ✗

a{color:color(prophoto-rgb .88765 .01234 .12345)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(rec2020) excess precision rounds to 3 decimal places

Rec. 2020 channels are in the 0–1 range. 3dp is well below the perceptual JND.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(rec2020 0.93456 0.07891 0.02345);
}

Expected

a{color:color(rec2020 .935 .079 .023)}

Outputs

clean-css ✗

a{color:color(rec2020 .93456 .07891 .02345)}

csskit ✓

a{color:color(rec2020 .935 .079 .023)}

csslop ✓

a{color:color(rec2020 .935 .079 .023)}

cssnano ✗

a{color:color(rec2020 .93456 .07891 .02345)}

csso ✗

a{color:color(rec2020 .93456 .07891 .02345)}

esbuild ✗

a{color:color(rec2020 .93456 .07891 .02345)}

lightningcss ✗

a{color:color(rec2020 .93456 .07891 .02345)}

sass ✗

a{color:color(rec2020 .93456 .07891 .02345)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(xyz-d65) excess precision rounds to 4 decimal places

XYZ D65 channels in CSS color() cover a range beyond 0–1 (D65 white point channels can exceed 1.0 for wide-gamut spaces). Like srgb-linear, this wider range means 3dp can introduce visible rounding errors, so 4dp is required.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(xyz-d65 0.53456 0.28768 0.06789);
}

Expected

a{color:color(xyz-d65 .5346 .2877 .0679)}

Outputs

clean-css ✗

a{color:color(xyz-d65 .53456 .28768 .06789)}

csskit ✓

a{color:color(xyz-d65 .5346 .2877 .0679)}

csslop ✓

a{color:color(xyz-d65 .5346 .2877 .0679)}

cssnano ✗

a{color:color(xyz-d65 .53456 .28768 .06789)}

csso ✗

a{color:color(xyz-d65 .53456 .28768 .06789)}

esbuild ✗

a{color:color(xyz-d65 .53456 .28768 .06789)}

lightningcss ✗

a{color:color(xyz .53456 .28768 .06789)}

sass ✗

a{color:color(xyz .53456 .28768 .06789)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color(xyz-d50) excess precision rounds to 4 decimal places

XYZ D50 channels in CSS color() can exceed the 0–1 range (D50 white point values go above 1.0 for some channels). Like srgb-linear and xyz-d65, the wider numeric range means 3dp can introduce visible rounding errors, so 4dp is required.

Both colours here are out-of-gamut for sRGB, so they stay in their native colour space. In-gamut colours may be minified to hex.

For a lot more detail on this, read Too Much Color.

a {
  color: color(xyz-d50 0.54567 0.31098 0.04876);
}

Expected

a{color:color(xyz-d50 .5457 .311 .0488)}

Outputs

clean-css ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}

csskit ✓

a{color:color(xyz-d50 .5457 .311 .0488)}

csslop ✓

a{color:color(xyz-d50 .5457 .311 .0488)}

cssnano ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}

csso ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}

esbuild ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}

lightningcss ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}

sass ✗

a{color:color(xyz-d50 .54567 .31098 .04876)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 13 13 / 13 13 / 13 1 / 13 0 / 13 1 / 13 1 / 13 0 / 13
Percent 0% 100% 100% 7.69% 0% 7.69% 7.69% 0%
Duration 1ms 4ms 4ms 8ms 5ms 7ms 1ms 14ms

colors #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Hex shortening

#ffffff can be collapsed to #fff when all digit pairs match.

a {
  color: #ffffff;
}

Expected

a{color:#fff}

Outputs

clean-css ✓

a{color:#fff}

csskit ✓

a{color:#fff}

csslop ✓

a{color:#fff}

cssnano ✓

a{color:#fff}

csso ✓

a{color:#fff}

esbuild ✓

a{color:#fff}

lightningcss ✓

a{color:#fff}

sass ✓

a{color:#fff}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Hex to named color

#ff0000 is longer than the named keyword red. The shorter form should be used.

a {
  color: #ff0000;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
6ms
0003 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

rgb() to named color

rgb(255, 0, 0) is equivalent to red and shorter.

a {
  color: rgb(255, 0, 0);
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

rgba() with alpha=1

Fully opaque rgba(0, 0, 0, 1) can drop the alpha channel and convert to a shorter hex form.

a {
  color: rgba(0, 0, 0, 1);
}

Expected

a{color:#000}

Outputs

clean-css ✓

a{color:#000}

csskit ✓

a{color:#000}

csslop ✓

a{color:#000}

cssnano ✓

a{color:#000}

csso ✓

a{color:#000}

esbuild ✓

a{color:#000}

lightningcss ✓

a{color:#000}

sass ✓

a{color:#000}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

rgb() to named color (non-trivial)

rgb(210, 180, 140) equals the named color tan. Tests awareness of the full CSS named-color table, not just the obvious primaries.

a {
  color: rgb(210, 180, 140);
}

Expected

a{color:tan}

Outputs

clean-css ✓

a{color:tan}

csskit ✓

a{color:tan}

csslop ✓

a{color:tan}

cssnano ✓

a{color:tan}

csso ✓

a{color:tan}

esbuild ✓

a{color:tan}

lightningcss ✓

a{color:tan}

sass ✓

a{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Hex shortening (non-white)

#aabbcc can be collapsed to #abc. Same rule as #ffffff -> #fff but with a non-trivial color.

a {
  color: #aabbcc;
}

Expected

a{color:#abc}

Outputs

clean-css ✓

a{color:#abc}

csskit ✓

a{color:#abc}

csslop ✓

a{color:#abc}

cssnano ✓

a{color:#abc}

csso ✓

a{color:#abc}

esbuild ✓

a{color:#abc}

lightningcss ✓

a{color:#abc}

sass ✓

a{color:#abc}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

RGBA to hex alpha notation

rgba(255, 0, 0, 0.5) is shorter as #ff000080 using 8-digit hex with alpha channel.

a {
  color: rgba(255, 0, 0, 0.5);
}

Expected

a{color:#ff000080}

Outputs

clean-css ✗

a{color:rgba(255,0,0,.5)}

csskit ✓

a{color:#ff000080}

csslop ✓

a{color:#ff000080}

cssnano ✓

a{color:#ff000080}

csso ✗

a{color:rgba(255,0,0,.5)}

esbuild ✓

a{color:#ff000080}

lightningcss ✓

a{color:#ff000080}

sass ✗

a{color:rgba(255,0,0,.5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

hsl() to named color

hsl(0, 100%, 50%) is pure red. The named keyword red is shorter. Conversion applies from hsl color space, not just rgb/hex.

a {
  color: hsl(0, 100%, 50%);
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

rgba() zero alpha to short hex

rgba(0, 0, 0, 0) is fully transparent. The 4-digit hex #0000 is the shortest equivalent representation (5 chars vs 11 for transparent). Note that 4-digit hex (CSS Color Level 4) may not be emitted by all minifiers -- some produce transparent instead, which is correct but not minimal.

a {
  color: rgba(0, 0, 0, 0);
}

Expected

a{color:#0000}

Outputs

clean-css ✗

a{color:transparent}

csskit ✓

a{color:#0000}

csslop ✓

a{color:#0000}

cssnano ✓

a{color:#0000}

csso ✗

a{color:transparent}

esbuild ✓

a{color:#0000}

lightningcss ✓

a{color:#0000}

sass ✗

a{color:rgba(0,0,0,0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

HWB to shorter representation

hwb(0 0% 0%) resolves to pure red (#ff0000) which is shorter as the named color red.

a {
  color: hwb(0 0% 0%);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:hwb(0 0% 0%)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

a{color:hwb(0 0% 0%)}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Named color to shorter hex

rebeccapurple (#663399) is shorter as the 3-digit hex #639.

a {
  color: rebeccapurple;
}

Expected

a{color:#639}

Outputs

clean-css ✗

a{color:#663399}

csskit ✓

a{color:#639}

csslop ✓

a{color:#639}

cssnano ✓

a{color:#639}

csso ✓

a{color:#639}

esbuild ✓

a{color:#639}

lightningcss ✓

a{color:#639}

sass ✓

a{color:#639}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

RGB to named color (olive)

rgb(128, 128, 0) is #808000, which is shorter as the named color olive.

a {
  color: rgb(128, 128, 0);
}

Expected

a{color:olive}

Outputs

clean-css ✓

a{color:olive}

csskit ✓

a{color:olive}

csslop ✓

a{color:olive}

cssnano ✓

a{color:olive}

csso ✓

a{color:olive}

esbuild ✓

a{color:olive}

lightningcss ✓

a{color:olive}

sass ✓

a{color:olive}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

HSL to named color (green)

hsl(120, 100%, 25%) resolves to #008000 which is shorter as the named color green.

a {
  color: hsl(120, 100%, 25%);
}

Expected

a{color:green}

Outputs

clean-css ✗

a{color:#007f00}

csskit ✓

a{color:green}

csslop ✓

a{color:green}

cssnano ✓

a{color:green}

csso ✓

a{color:green}

esbuild ✓

a{color:green}

lightningcss ✓

a{color:green}

sass ✗

a{color:rgb(0%,50%,0%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Uppercase hex to lowercase shorthand

#AABB11 should be lowercased and collapsed to #ab1.

a {
  color: #AABB11;
}

Expected

a{color:#ab1}

Outputs

clean-css ✓

a{color:#ab1}

csskit ✓

a{color:#ab1}

csslop ✓

a{color:#ab1}

cssnano ✓

a{color:#ab1}

csso ✓

a{color:#ab1}

esbuild ✓

a{color:#ab1}

lightningcss ✓

a{color:#ab1}

sass ✓

a{color:#ab1}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Modern rgb() with alpha to 8-digit hex

rgb(143 101 98 / 43%) can be converted to the shorter 8-digit hex #8f65626e. The alpha 43% maps to byte 0x6E (110). This is the standard 8-bit quantization browsers apply internally, making the conversion safe.

a {
  background-color: rgb(143 101 98 / 43%);
}

Expected

a{background-color:#8f65626e}

Validate

Outputs

clean-css ✗

a{background-color:rgb(143 101 98 / 43%)}

csskit ✓

a{background-color:#8f65626e}

csslop ✓

a{background-color:#8f65626e}

cssnano ✓

a{background-color:#8f65626e}

csso ✗

a{background-color:rgb(143 101 98/43%)}

esbuild ✓

a{background-color:#8f65626e}

lightningcss ✓

a{background-color:#8f65626e}

sass ✗

a{background-color:rgba(143,101,98,.43)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

Modern rgb() with alpha 1 to named color

The modern space-separated rgb(255 0 0 / 1) syntax with full opacity should minify to the shortest equivalent named color red, dropping the alpha channel.

a {
  color: rgb(255 0 0 / 1);
}

Expected

a{color:red}

Validate

Outputs

clean-css ✗

a{color:rgb(255 0 0 / 1)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

a{color:rgb(255 0 0/1)}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Modern hsl() with alpha to 8-digit hex

hsl(120 100% 50% / 0.5) resolves to fully saturated green at 50% opacity. This can be converted to the shorter 8-digit hex #00ff0080. The alpha 0.5 maps to byte 0x80 (128) under standard 8-bit quantization.

a {
  color: hsl(120 100% 50% / 0.5);
}

Expected

a{color:#00ff0080}

Validate

Outputs

clean-css ✗

a{color:hsl(120 100% 50% / .5)}

csskit ✓

a{color:#00ff0080}

csslop ✓

a{color:#00ff0080}

cssnano ✓

a{color:#00ff0080}

csso ✗

a{color:hsl(120 100% 50%/.5)}

esbuild ✓

a{color:#00ff0080}

lightningcss ✓

a{color:#00ff0080}

sass ✗

a{color:rgba(0,255,0,.5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() with known colors flattened to named color

color-mix(in srgb, red 50%, blue 50%) can be resolved at build time. The 50/50 sRGB mix of red (255,0,0) and blue (0,0,255) is (128,0,128) = purple. Minifiers should flatten color-mix() with static arguments to the shortest form.

a {
  color: color-mix(in srgb, red 50%, blue 50%);
}

Expected

a{color:purple}

Validate

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 50%,#00f 50%)}

csskit ✓

a{color:purple}

csslop ✓

a{color:purple}

cssnano ✗

a{color:color-mix(in srgb,red 50%,blue 50%)}

csso ✗

a{color:color-mix(in srgb,red 50%,blue 50%)}

esbuild ✗

a{color:color-mix(in srgb,red 50%,blue 50%)}

lightningcss ✓

a{color:purple}

sass ✗

a{color:color-mix(in srgb, red 50%, blue 50%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() default percentages flattened to named color

When color-mix() omits percentages (defaulting to 50%/50%) with known color arguments, minifiers can resolve the mix at build time. color-mix(in srgb, red, blue) is the same 50/50 mix as the explicit form, yielding (128,0,128), or purple.

a {
  color: color-mix(in srgb, red, blue);
}

Expected

a{color:purple}

Validate

Outputs

clean-css ✗

a{color:color-mix(in srgb,red,#00f)}

csskit ✓

a{color:purple}

csslop ✓

a{color:purple}

cssnano ✗

a{color:color-mix(in srgb,red,blue)}

csso ✗

a{color:color-mix(in srgb,red,blue)}

esbuild ✗

a{color:color-mix(in srgb,red,blue)}

lightningcss ✓

a{color:purple}

sass ✗

a{color:color-mix(in srgb, red, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Static relative color syntax resolves to plain color

color(from red srgb r g b / 0.5) passes all channels through from red unchanged, so it resolves to srgb(1 0 0 / 0.5) which is #ff000080. A minifier that can evaluate static relative color expressions should fold this.

a {
  color: color(from red srgb r g b / 0.5);
}

Expected

a{color:#ff000080}

Outputs

clean-css ✗

a{color:color(from red srgb r g b / .5)}

csskit ✓

a{color:#ff000080}

csslop ✓

a{color:#ff000080}

cssnano ✗

a{color:color(from red srgb r g b/.5)}

csso ✗

a{color:color(from red srgb r g b/.5)}

esbuild ✗

a{color:color(from red srgb r g b / .5)}

lightningcss ✗

a{color:color(srgb 1 0 0/.5)}

sass ✗

a{color:color(from red srgb r g b/0.5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
10ms
0021 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

rgb() none keyword resolves to 0 outside interpolation

none in rgb(none 128 0) means "missing channel". Outside an interpolation context (like color-mix), missing channels resolve to 0. So this is rgb(0 128 0) = #008000 = green.

a {
  color: rgb(none 128 0);
}

Expected

a{color:green}

Validate

Outputs

clean-css ✗

a{color:rgb(none 128 0)}

csskit ✓

a{color:green}

csslop ✓

a{color:green}

cssnano ✓

a{color:green}

csso ✗

a{color:rgb(none 128 0)}

esbuild ✗

a{color:rgb(none 128 0)}

lightningcss ✓

a{color:green}

sass ✗

a{color:rgb(none 128 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

8-digit hex with non-repeating alpha must not shorten

#ff00007a has alpha 7a (not 77), so it cannot be shortened to 4-digit hex #f007. A minifier that naively pairs digits would produce the wrong alpha.

a {
  color: #ff00007a;
}

Expected

a{color:#ff00007a}

Outputs

clean-css ✓

a{color:#ff00007a}

csskit ✓

a{color:#ff00007a}

csslop ✓

a{color:#ff00007a}

cssnano ✓

a{color:#ff00007a}

csso ✓

a{color:#ff00007a}

esbuild ✓

a{color:#ff00007a}

lightningcss ✓

a{color:#ff00007a}

sass ✗

a{color:rgba(255,0,0,.4784313725)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Named color shorter than any hex equivalent

purple is #800080 -- 6 chars vs 7 for hex, and the digits don't pair so it can't shorten to 3-digit hex either. A minifier must not convert to hex here.

a {
  color: purple;
}

Expected

a{color:purple}

Outputs

clean-css ✓

a{color:purple}

csskit ✓

a{color:purple}

csslop ✓

a{color:purple}

cssnano ✓

a{color:purple}

csso ✓

a{color:purple}

esbuild ✓

a{color:purple}

lightningcss ✓

a{color:purple}

sass ✓

a{color:purple}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix with none channel uses the other color's value

In interpolation contexts like color-mix, a none (missing) channel is replaced by the corresponding channel from the other color before mixing. rgb(none 0 0) mixed 50/50 with rgb(200 0 0) yields rgb(200 0 0) = #c80000 because the missing R channel adopts 200 from the other side.

a {
  color: color-mix(in srgb, rgb(none 0 0) 50%, rgb(200 0 0) 50%);
}

Expected

a{color:#c80000}

Validate

Outputs

clean-css ✗

a{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(200 0 0) 50%)}

csskit ✓

a{color:#c80000}

csslop ✓

a{color:#c80000}

cssnano ✗

a{color:color-mix(in srgb,#000 50%,#c80000 50%)}

csso ✗

a{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(200 0 0) 50%)}

esbuild ✗

a{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(200 0 0) 50%)}

lightningcss ✓

a{color:#c80000}

sass ✗

a{color:color-mix(in srgb, rgb(none 0 0) 50%, rgb(200, 0, 0) 50%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

currentColor must not be resolved

currentColor is a runtime keyword that inherits the computed color value. It cannot be replaced with a static color at build time because the inherited value depends on the DOM context.

a {
  color: currentColor;
}

Expected

a{color:currentColor}

Outputs

clean-css ✓

a{color:currentColor}

csskit ✓

a{color:currentColor}

csslop ✓

a{color:currentColor}

cssnano ✓

a{color:currentColor}

csso ✓

a{color:currentColor}

esbuild ✓

a{color:currentColor}

lightningcss ✓

a{color:currentColor}

sass ✓

a{color:currentColor}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

oklch() numeric minification

oklch() values can have their numbers minified (leading zero removal) but must not be converted to sRGB hex since the oklch gamut exceeds sRGB.

a {
  color: oklch(0.7 0.15 180);
}

Expected

a{color:oklch(.7 .15 180)}

Outputs

clean-css ✓

a{color:oklch(.7 .15 180)}

csskit ✓

a{color:oklch(.7 .15 180)}

csslop ✓

a{color:oklch(.7 .15 180)}

cssnano ✓

a{color:oklch(.7 .15 180)}

csso ✓

a{color:oklch(.7 .15 180)}

esbuild ✓

a{color:oklch(.7 .15 180)}

lightningcss ✗

a{color:oklch(70% .15 180)}

sass ✓

a{color:oklch(.7 .15 180)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0027 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix identity resolves to the color itself

color-mix(in srgb, red, red) mixes a color with itself at equal proportions. The result is always the original color regardless of the interpolation space.

a {
  color: color-mix(in srgb, red, red);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red,red)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{color:color-mix(in srgb,red,red)}

csso ✗

a{color:color-mix(in srgb,red,red)}

esbuild ✗

a{color:color-mix(in srgb,red,red)}

lightningcss ✓

a{color:red}

sass ✗

a{color:color-mix(in srgb, red, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0028 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

display-p3 color must not be converted to sRGB

color(display-p3 1 0 0) is a P3 red that cannot be represented in sRGB. Converting to hex or rgb would clamp the value and lose the wider gamut intent. The color function and space must be preserved.

a {
  color: color(display-p3 1 0 0);
}

Expected

a{color:color(display-p3 1 0 0)}

Outputs

clean-css ✓

a{color:color(display-p3 1 0 0)}

csskit ✓

a{color:color(display-p3 1 0 0)}

csslop ✓

a{color:color(display-p3 1 0 0)}

cssnano ✓

a{color:color(display-p3 1 0 0)}

csso ✓

a{color:color(display-p3 1 0 0)}

esbuild ✓

a{color:color(display-p3 1 0 0)}

lightningcss ✓

a{color:color(display-p3 1 0 0)}

sass ✓

a{color:color(display-p3 1 0 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0029 ✓ ✗ ✓ ✓ ✓ ✗ ✗ ✗
Details

color-mix with var(): minify literal color argument

rgba(255, 255, 255, 1.0) inside a color-mix() with a var() second argument can still be shortened to #fff. The var() prevents resolving the mix, but the literal color is independently minifiable.

a {
  color: color-mix(in oklch, rgba(255, 255, 255, 1.0), var(--foo));
}

Expected

a{color:color-mix(in oklch,#fff,var(--foo))}

Validate

Outputs

clean-css ✓

a{color:color-mix(in oklch,#fff,var(--foo))}

csskit ✗

a{color:color-mix(in oklch,rgba(255,255,255,1),var(--foo))}

csslop ✓

a{color:color-mix(in oklch,#fff,var(--foo))}

cssnano ✓

a{color:color-mix(in oklch,#fff,var(--foo))}

csso ✓

a{color:color-mix(in oklch,#fff,var(--foo))}

esbuild ✗

a{color:color-mix(in oklch,rgba(255,255,255,1),var(--foo))}

lightningcss ✗

a{color:color-mix(in oklch, #fff, var(--foo))}

sass ✗

a{color:color-mix(in oklch, rgb(255, 255, 255), var(--foo))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0030 ✓ ✗ ✓ ✓ ✓ ✗ ✗ ✗
Details

color-mix with var(): minify hsl to named color

hsl(0, 100%, 50%) inside a color-mix() with a var() second argument can still be shortened to red. The var() prevents resolving the mix, but each literal color argument is independently minifiable.

a {
  color: color-mix(in srgb, hsl(0, 100%, 50%), var(--foo));
}

Expected

a{color:color-mix(in srgb,red,var(--foo))}

Validate

Outputs

clean-css ✓

a{color:color-mix(in srgb,red,var(--foo))}

csskit ✗

a{color:color-mix(in srgb,hsl(0,100%,50%),var(--foo))}

csslop ✓

a{color:color-mix(in srgb,red,var(--foo))}

cssnano ✓

a{color:color-mix(in srgb,red,var(--foo))}

csso ✓

a{color:color-mix(in srgb,red,var(--foo))}

esbuild ✗

a{color:color-mix(in srgb,hsl(0,100%,50%),var(--foo))}

lightningcss ✗

a{color:color-mix(in srgb, red, var(--foo))}

sass ✗

a{color:color-mix(in srgb, hsl(0, 100%, 50%), var(--foo))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0031 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

color-mix: elide default oklab interpolation space

Per CSS Color 5, oklab is the default interpolation method for color-mix() when none is specified. in oklab can be elided, saving 9 bytes. Browsers already support the omitted form.

a {
  color: color-mix(in oklab, var(--a), var(--b));
}

Expected

a{color:color-mix(var(--a),var(--b))}

Validate

Outputs

clean-css ✗

a{color:color-mix(in oklab,var(--a),var(--b))}

csskit ✗

a{color:color-mix(in oklab,var(--a),var(--b))}

csslop ✓

a{color:color-mix(var(--a),var(--b))}

cssnano ✗

a{color:color-mix(in oklab,var(--a),var(--b))}

csso ✗

a{color:color-mix(in oklab,var(--a),var(--b))}

esbuild ✗

a{color:color-mix(in oklab,var(--a),var(--b))}

lightningcss ✗

a{color:color-mix(in oklab, var(--a), var(--b))}

sass ✗

a{color:color-mix(in oklab, var(--a), var(--b))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0032 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Named color to hex enables space elision

blue (4 chars) converts to #00f (4 chars) which looks like a no-op, but # is an unambiguous token start so the preceding space can be dropped: 1px solid#00f saves 1 byte over 1px solid blue.

a {
  border: 1px solid blue;
}

Expected

a{border:1px solid#00f}

Outputs

clean-css ✗

a{border:1px solid #00f}

csskit ✗

a{border:1px solid blue}

csslop ✓

a{border:1px solid#00f}

cssnano ✗

a{border:1px solid blue}

csso ✗

a{border:1px solid #00f}

esbuild ✗

a{border:1px solid blue}

lightningcss ✗

a{border:1px solid #00f}

sass ✗

a{border:1px solid blue}
1ms
2ms
1ms
2ms
1ms
1ms
1ms
1ms
0033 ✓ ✗ ✓ ✓ ✓ ✓ ✗ ERROR
Details

Out-of-gamut display-p3 color must not be clamped

color(display-p3 1.2 -0.3 0.5) has channels outside [0,1]. These are valid per CSS Color 4 and represent colors outside the display-p3 gamut. A minifier must not clamp these values or convert to sRGB, as that would destroy the author's intent. Only numeric minification (leading zero removal) is safe.

Additionally 1.2 -0.3 value can have whitespace elided as the leading dash is unambiguous: 1.2-0.3 is two number tokens.

a {
  color: color(display-p3 1.2-0.3 0.5);
}

Expected

a{color:color(display-p3 1.2-.3 .5)}

Outputs

clean-css ✓

a{color:color(display-p3 1.2-.3 .5)}

csskit ✗

a{color:color(display-p3 1.2 -.3 .5)}

csslop ✓

a{color:color(display-p3 1.2-.3 .5)}

cssnano ✓

a{color:color(display-p3 1.2-.3 .5)}

csso ✓

a{color:color(display-p3 1.2-.3 .5)}

esbuild ✓

a{color:color(display-p3 1.2-.3 .5)}

lightningcss ✗

a{color:color(display-p3 1.2 -.3 .5)}

sass ✗

$description: The display-p3 color space has 3 channels but (display-p3 0.8999999999999999 0.5) has 2.
  ╷
2 │   color: color(display-p3 1.2-0.3 0.5);
  │          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  ╵
  - 2:10  root stylesheet
1ms
1ms
1ms
1ms
1ms
1ms
1ms
5ms
0034 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() with 100% first color eliminates to that color

color-mix(in srgb, red 100%, blue) means 100% of the first color and 0% of the second. The result is just red. Minifiers should detect this and replace the entire expression with the shortest form of the first color.

a {
  color: color-mix(in srgb, red 100%, blue);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 100%,#00f)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{color:color-mix(in srgb,red 100%,blue)}

csso ✗

a{color:color-mix(in srgb,red 100%,blue)}

esbuild ✗

a{color:color-mix(in srgb,red 100%,blue)}

lightningcss ✓

a{color:red}

sass ✗

a{color:color-mix(in srgb, red 100%, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0035 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() with 0% first color eliminates to the second color

color-mix(in srgb, red 0%, blue) means 0% of the first color and 100% of the second. The result is just blue, whose shortest form is #00f. Minifiers should detect this and replace the entire expression with the second color.

a {
  color: color-mix(in srgb, red 0%, blue);
}

Expected

a{color:#00f}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 0%,#00f)}

csskit ✓

a{color:#00f}

csslop ✓

a{color:#00f}

cssnano ✗

a{color:color-mix(in srgb,red 0%,blue)}

csso ✗

a{color:color-mix(in srgb,red 0%,blue)}

esbuild ✗

a{color:color-mix(in srgb,red 0%,blue)}

lightningcss ✓

a{color:#00f}

sass ✗

a{color:color-mix(in srgb, red 0%, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0036 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Redundant 50% percentages removed from color-mix()

color-mix(in srgb, currentcolor 50%, red 50%) — 50%/50% is the default mix ratio, so both percentage arguments are redundant and can be removed. The result is color-mix(in srgb, currentcolor, red).

a {
  color: color-mix(in srgb, currentcolor 50%, red 50%);
}

Expected

a{color:color-mix(in srgb,currentcolor,red)}

Outputs

clean-css ✗

a{color:color-mix(in srgb,currentcolor 50%,red 50%)}

csskit ✓

a{color:color-mix(in srgb,currentcolor,red)}

csslop ✓

a{color:color-mix(in srgb,currentcolor,red)}

cssnano ✗

a{color:color-mix(in srgb,currentcolor 50%,red 50%)}

csso ✗

a{color:color-mix(in srgb,currentcolor 50%,red 50%)}

esbuild ✗

a{color:color-mix(in srgb,currentcolor 50%,red 50%)}

lightningcss ✗

a{color:color-mix(in srgb, currentcolor 50%, red 50%)}

sass ✗

a{color:color-mix(in srgb, currentcolor 50%, red 50%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0037 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Redundant "shorter hue" removed from color-mix()

color-mix(in oklch shorter hue, currentcolor, red) — shorter is the default hue interpolation method for polar color spaces, so shorter hue is redundant and can be removed. The result is color-mix(in oklch, currentcolor, red).

a {
  color: color-mix(in oklch shorter hue, currentcolor, red);
}

Expected

a{color:color-mix(in oklch,currentcolor,red)}

Outputs

clean-css ✗

a{color:color-mix(in oklch shorter hue,currentcolor,red)}

csskit ✓

a{color:color-mix(in oklch,currentcolor,red)}

csslop ✓

a{color:color-mix(in oklch,currentcolor,red)}

cssnano ✗

a{color:color-mix(in oklch shorter hue,currentcolor,red)}

csso ✗

a{color:color-mix(in oklch shorter hue,currentcolor,red)}

esbuild ✗

a{color:color-mix(in oklch shorter hue,currentcolor,red)}

lightningcss ✗

a{color:color-mix(in oklch shorter hue, currentcolor, red)}

sass ✗

a{color:color-mix(in oklch shorter hue, currentcolor, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0038 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗
Details

Inner colors minified inside color-mix()

color-mix(in oklch, rgba(255, 255, 255, 1), currentcolor) — the color-mix cannot be statically resolved because currentcolor is dynamic, but the inner rgba(255, 255, 255, 1) can be shortened to #fff. Minifiers should optimize individual color arguments even when the overall mix must be preserved.

a {
  color: color-mix(in oklch, rgba(255, 255, 255, 1), currentcolor);
}

Expected

a{color:color-mix(in oklch,#fff,currentcolor)}

Outputs

clean-css ✓

a{color:color-mix(in oklch,#fff,currentcolor)}

csskit ✓

a{color:color-mix(in oklch,#fff,currentcolor)}

csslop ✓

a{color:color-mix(in oklch,#fff,currentcolor)}

cssnano ✓

a{color:color-mix(in oklch,#fff,currentcolor)}

csso ✓

a{color:color-mix(in oklch,#fff,currentcolor)}

esbuild ✗

a{color:color-mix(in oklch,rgba(255,255,255,1),currentcolor)}

lightningcss ✗

a{color:color-mix(in oklch, #fff, currentcolor)}

sass ✗

a{color:color-mix(in oklch, rgb(255, 255, 255), currentcolor)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0039 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() with 0% second color eliminates to the first color

color-mix(in srgb, red, blue 0%) means the second color contributes nothing. The result is just red. Minifiers should detect this and replace the entire expression with the shortest form of the first color.

a {
  color: color-mix(in srgb, red, blue 0%);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red,#00f 0%)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{color:color-mix(in srgb,red,blue 0%)}

csso ✗

a{color:color-mix(in srgb,red,blue 0%)}

esbuild ✗

a{color:color-mix(in srgb,red,blue 0%)}

lightningcss ✓

a{color:red}

sass ✗

a{color:color-mix(in srgb, red, blue 0%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0040 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() percentages below 100% produce semi-transparent result

color-mix(in srgb, red 30%, blue 30%) — the percentages sum to 60%, not 100%. Per CSS Color 5, the mix is normalized to 50%/50% but the alpha of the result is multiplied by sum / 100 = 0.6. The result is rgba(128, 0, 128, 0.6), i.e. a semi-transparent purple. A minifier that resolves this to opaque purple would be incorrect: the alpha multiplier would be lost.

a {
  color: color-mix(in srgb, red 30%, blue 30%);
}

Expected

a{color:#80008099}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 30%,#00f 30%)}

csskit ✓

a{color:#80008099}

csslop ✓

a{color:#80008099}

cssnano ✗

a{color:color-mix(in srgb,red 30%,blue 30%)}

csso ✗

a{color:color-mix(in srgb,red 30%,blue 30%)}

esbuild ✗

a{color:color-mix(in srgb,red 30%,blue 30%)}

lightningcss ✓

a{color:#80008099}

sass ✗

a{color:color-mix(in srgb, red 30%, blue 30%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0041 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

color-mix() with percentages summing over 100% must normalize

color-mix(in srgb, red 80%, blue 40%) — the percentages sum to 120%. Per CSS Color 5 percentage normalization, they scale to 66.67%/33.33%. The result is rgb(170, 0, 85) = #a05. A minifier that treats the percentages at face value without normalizing would produce an incorrect color.

a {
  color: color-mix(in srgb, red 80%, blue 40%);
}

Expected

a{color:#a05}

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 80%,#00f 40%)}

csskit ✓

a{color:#a05}

csslop ✓

a{color:#a05}

cssnano ✗

a{color:color-mix(in srgb,red 80%,blue 40%)}

csso ✗

a{color:color-mix(in srgb,red 80%,blue 40%)}

esbuild ✗

a{color:color-mix(in srgb,red 80%,blue 40%)}

lightningcss ✓

a{color:#a05}

sass ✗

a{color:color-mix(in srgb, red 80%, blue 40%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0042 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

In-gamut oklab colour is minified to hex

oklab(0.5 -0.1 0.1) is within the sRGB gamut. A minifier should convert it to the shortest sRGB representation (#3c740a) rather than keeping the longer oklab form.

a {
  color: oklab(0.5 -0.1 0.1);
}

Expected

a{color:#3c740a}

Outputs

clean-css ✗

a{color:oklab(.5 -.1 .1)}

csskit ✓

a{color:#3c740a}

csslop ✓

a{color:#3c740a}

cssnano ✗

a{color:oklab(.5 -.1 .1)}

csso ✗

a{color:oklab(.5-.1 .1)}

esbuild ✓

a{color:#3c740a}

lightningcss ✗

a{color:oklab(50% -.1 .1)}

sass ✗

a{color:oklab(.5 -0.1 .1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0043 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

In-gamut display-p3 colour is minified to shortest sRGB form

color(display-p3 0.5 0.5 0.5) is a neutral grey that is within the sRGB gamut. A minifier should convert it to the shortest sRGB representation (the named colour gray) rather than keeping the longer display-p3 form.

a {
  color: color(display-p3 0.5 0.5 0.5);
}

Expected

a{color:gray}

Outputs

clean-css ✗

a{color:color(display-p3 .5 .5 .5)}

csskit ✓

a{color:gray}

csslop ✓

a{color:gray}

cssnano ✗

a{color:color(display-p3 .5 .5 .5)}

csso ✗

a{color:color(display-p3 .5 .5 .5)}

esbuild ✗

a{color:#7f8080}

lightningcss ✗

a{color:color(display-p3 .5 .5 .5)}

sass ✗

a{color:color(display-p3 .5 .5 .5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0044 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Named colors as quoted strings

When a quoted string contains a named color (green, rebeccapurple, salmon, etc) it should not be minified to hex.

a {
  content: "green";
}

Expected

a{content:"green"}

Outputs

clean-css ✓

a{content:"green"}

csskit ✓

a{content:"green"}

csslop ✓

a{content:"green"}

cssnano ✓

a{content:"green"}

csso ✓

a{content:"green"}

esbuild ✓

a{content:"green"}

lightningcss ✓

a{content:"green"}

sass ✓

a{content:"green"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0045 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Hex colors as quoted strings

When a quoted string contains a long form hex value (#FF0000, #00FF00FF, etc.) it should not be minified (red, #0F0, etc).

a {
  content: "#FF0000";
}

Expected

a{content:"#FF0000"}

Outputs

clean-css ✓

a{content:"#FF0000"}

csskit ✓

a{content:"#FF0000"}

csslop ✓

a{content:"#FF0000"}

cssnano ✓

a{content:"#FF0000"}

csso ✓

a{content:"#FF0000"}

esbuild ✓

a{content:"#FF0000"}

lightningcss ✓

a{content:"#FF0000"}

sass ✓

a{content:"#FF0000"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0046 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

URL strings containing color names

When a URL path string contains a color name, it should not be minified to a hex value.

a {
  background-image: url(green.png);
}

Expected

a{background-image:url(green.png)}

Outputs

clean-css ✓

a{background-image:url(green.png)}

csskit ✓

a{background-image:url(green.png)}

csslop ✓

a{background-image:url(green.png)}

cssnano ✓

a{background-image:url(green.png)}

csso ✓

a{background-image:url(green.png)}

esbuild ✓

a{background-image:url(green.png)}

lightningcss ✓

a{background-image:url(green.png)}

sass ✓

a{background-image:url(green.png)}
1ms
3ms
1ms
1ms
1ms
1ms
1ms
1ms
0047 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color-mix() with 3 colors (N-color support)

color-mix(in oklab, red, orange, purple) mixes three equal-weight colors. CSS Color 5 allows N colors in color-mix(); no percentages means each gets 1/3.

These colours have been carefully chosen so that the mix remains in-gamut, meaning the result can safely be reduced to #d45456.

a {
  color: color-mix(in oklab, red, orange, purple);
}

Expected

a{color:#d45456}

Validate

Outputs

clean-css ✗

a{color:color-mix(in oklab,red,orange,purple)}

csskit ✓

a{color:#d45456}

csslop ✓

a{color:#d45456}

cssnano ✗

a{color:color-mix(in oklab,red,orange,purple)}

csso ✗

a{color:color-mix(in oklab,red,orange,purple)}

esbuild ✗

a{color:color-mix(in oklab,red,orange,purple)}

lightningcss ✗

a{color:color-mix(in oklab, red, orange, purple)}

sass ✗

a{color:color-mix(in oklab, red, orange, purple)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0048 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

color-mix() all-zero percentages become transparent black

color-mix(in srgb, red 0%, green 0%, blue 0%) has all percentages at 0%, so the sum is 0% and leftover is 100%. Per CSS Color 5, when leftover equals 100% the result is transparent black. The shortest representation is #0000.

a {
  color: color-mix(in srgb, red 0%, green 0%, blue 0%);
}

Expected

a{color:#0000}

Validate

Outputs

clean-css ✗

a{color:color-mix(in srgb,red 0%,green 0%,#00f 0%)}

csskit ✓

a{color:#0000}

csslop ✓

a{color:#0000}

cssnano ✗

a{color:color-mix(in srgb,red 0%,green 0%,blue 0%)}

csso ✗

a{color:color-mix(in srgb,red 0%,green 0%,blue 0%)}

esbuild ✗

a{color:color-mix(in srgb,red 0%,green 0%,blue 0%)}

lightningcss ✗

a{color:color-mix(in srgb, red 0%, green 0%, blue 0%)}

sass ✗

a{color:color-mix(in srgb, red 0%, green 0%, blue 0%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0049 ✗ ✓ ✓ ✓ ✓ ✓ ✗ ✗
Details

color-mix() N-color with var() cannot be resolved

color-mix(in srgb, blue 50%, var(--foo), red 50%) contains a var() mid-list. Even though the known percentages sum to 100%, the var() may carry its own percentage (e.g. green 20%), which would change normalization. A minifier must not reduce this; the result is whitespace-stripped only.

a {
  color: color-mix(in srgb, blue 50%, var(--foo), red 50%);
}

Expected

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

Outputs

clean-css ✗

a{color:color-mix(in srgb,#00f 50%,var(--foo),red 50%)}

csskit ✓

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

csslop ✓

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

cssnano ✓

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

csso ✓

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

esbuild ✓

a{color:color-mix(in srgb,blue 50%,var(--foo),red 50%)}

lightningcss ✗

a{color:color-mix(in srgb, blue 50%, var(--foo), red 50%)}

sass ✗

a{color:color-mix(in srgb, blue 50%, var(--foo), red 50%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0050 ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗
Details

Always shorten the color keyword transparent to hex

According to the CSS3 color specification, the color keyword transparent is equivalent to rbga(0, 0, 0, 0), which can also be written as the shortform hex, #0000, which is shorter than the keyword. As such, all uses of the word "transparent" as a color keyword should be shortend to the hex equivalent.

p {
  background: transparent;
}

Expected

p{background:#0000}

Outputs

clean-css ✗

p{background:0 0}

csskit ✓

p{background:#0000}

csslop ✓

p{background:#0000}

cssnano ✓

p{background:#0000}

csso ✗

p{background:0 0}

esbuild ✗

p{background:transparent}

lightningcss ✗

p{background:0 0}

sass ✗

p{background:rgba(0,0,0,0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0051 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Do not convert effectively transparent colors to transparent

All colors with an alpha of 0, are equivalent to the keyword transparent, which according to the spec is technically #0000. However, a minifier should not convert non-black colors with an alpha of 0 to #0000, as that will effect the blending of the color when used in transitions.

p {
  color: #5987FC00;
  background: rgba(133, 40, 55, 0);
  border-color: hsla(180, 100%, 25.1%, 0);
}

Expected

p{color:#5987fc00;background:#85283700;border-color:#00808000}

Outputs

clean-css ✗

p{color:#5987FC00;background:rgba(133,40,55,0);border-color:hsla(180,100%,25.1%,0)}

csskit ✗

p{color:#5987FC00;background:#85283700;border-color:#00808000}

csslop ✓

p{color:#5987fc00;background:#85283700;border-color:#00808000}

cssnano ✓

p{color:#5987fc00;background:#85283700;border-color:#00808000}

csso ✗

p{color:#5987fc00;background:0 0;border-color:transparent}

esbuild ✓

p{color:#5987fc00;background:#85283700;border-color:#00808000}

lightningcss ✓

p{color:#5987fc00;background:#85283700;border-color:#00808000}

sass ✗

p{color:rgba(89,135,252,0);background:rgba(133,40,55,0);border-color:rgba(0%,50.2%,50.2%,0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0052 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Simplify useless light-dark functions

The light-dark(a, b) CSS functions allows defining two possible colors based on the color-scheme setting. These will automatically switch when the color-scheme changes. However, if both values supplied are identical, then the entire function can be replaced with the value.

a {
  background: light-dark(var(--foo), var(--foo));
  color: light-dark(red, red);
}

Expected

a{background:var(--foo);color:red}

Outputs

clean-css ✗

a{background:light-dark(var(--foo),var(--foo));color:light-dark(red,red)}

csskit ✗

a{background:light-dark(var(--foo),var(--foo));color:light-dark(red,red)}

csslop ✓

a{background:var(--foo);color:red}

cssnano ✗

a{background:light-dark(var(--foo),var(--foo));color:light-dark(red,red)}

csso ✗

a{background:light-dark(var(--foo),var(--foo));color:light-dark(red,red)}

esbuild ✗

a{background:light-dark(var(--foo),var(--foo));color:light-dark(red,red)}

lightningcss ✗

a{background:light-dark(var(--foo),var(--foo));color:red}

sass ✗

a{background:light-dark(var(--foo), var(--foo));color:light-dark(red, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0053 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

Shorten colors in light-dark functions

The light-dark(a, b) CSS functions allows defining two possible colors based on the color-scheme setting. These will automatically switch when the color-scheme changes. Both values should be minified to their shortest correct representation.

a {
  color: light-dark(#FF0000, #D2B48C);
}

Expected

a{color:light-dark(red,tan)}

Outputs

clean-css ✓

a{color:light-dark(red,tan)}

csskit ✓

a{color:light-dark(red,tan)}

csslop ✓

a{color:light-dark(red,tan)}

cssnano ✓

a{color:light-dark(red,tan)}

csso ✓

a{color:light-dark(red,tan)}

esbuild ✗

a{color:light-dark(#FF0000,#D2B48C)}

lightningcss ✓

a{color:light-dark(red,tan)}

sass ✗

a{color:light-dark(#FF0000, #D2B48C)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0054 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Shorten colors in light-dark functions and simplify if useless

After converting both colors in a light-dark function to their shortest representation, if the results are identical values, then the entire function can be replaced by just the value.

a {
  color: light-dark(#FF0000, rgb(255, 0, 0));
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:light-dark(red,red)}

csskit ✗

a{color:light-dark(red,red)}

csslop ✓

a{color:red}

cssnano ✗

a{color:light-dark(red,red)}

csso ✗

a{color:light-dark(red,red)}

esbuild ✗

a{color:light-dark(#FF0000,rgb(255,0,0))}

lightningcss ✓

a{color:red}

sass ✗

a{color:light-dark(#FF0000, rgb(255, 0, 0))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0055 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove useless calc's in hsl(from)

The calculations used here can all be resolved and simplified.

  • a{color:hsl(from rebeccapurple calc(h)calc(s*1)calc(l + 0)/calc(1 / 1))}
  • a{color:hsl(from rebeccapurple calc(h)calc(s*1)calc(l + 0)/1)}
  • a{color:hsl(from rebeccapurple calc(h)calc(s*1)l/1)}
  • a{color:hsl(from rebeccapurple calc(h)s l/1)}
  • a{color:hsl(from rebeccapurple h s l/1)}
  • a{color:rebeccapurple}
  • a{color:#639}

a {
  color: hsl(from rebeccapurple calc(h) calc(s * 1) calc(l + 0) / calc(1 / 1));
}

Expected

a{color:#639}

Outputs

clean-css ✗

a{color:hsl(from #663399 calc(h) calc(s * 1) calc(l + 0)/ calc(1 / 1))}

csskit ✗

a{color:hsl(from rebeccapurple calc(h) calc(s * 1) calc(l + 0) / calc(1 / 1))}

csslop ✓

a{color:#639}

cssnano ✗

a{color:hsl(from rebeccapurple calc(h) calc(s) calc(0 + l)/calc(1))}

csso ✗

a{color:hsl(from rebeccapurple calc(h) calc(s*1) calc(l + 0)/calc(1/1))}

esbuild ✗

a{color:hsl(from rebeccapurple h calc(s*1) calc(l + 0) / 1)}

lightningcss ✓

a{color:#639}

sass ✗

a{color:hsl(from rebeccapurple calc(h) calc(s * 1) calc(l + 0)/1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0056 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Collapse whitesparound around calc in an hsl(from)

Whitespace around adjacent calc's in an hsl(from) can be removed.

a {
  --red:#f01;
  --h:10;
  color: hsl(from var(--red) calc(var(--h)) calc(s * 2) calc(l + 1) / 0.6);
}

Expected

a{--red:#f01;--h:10;color:hsl(from var(--red)calc(var(--h))calc(s*2)calc(l + 1)/.6)}

Outputs

clean-css ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s * 2) calc(l + 1)/ .6)}

csskit ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s * 2) calc(l + 1) / .6)}

csslop ✓

a{--red:#f01;--h:10;color:hsl(from var(--red)calc(var(--h))calc(s*2)calc(l + 1)/.6)}

cssnano ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(2 * s) calc(1 + l)/.6)}

csso ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s*2) calc(l + 1)/.6)}

esbuild ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s*2) calc(l + 1) / .6)}

lightningcss ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s * 2) calc(l + 1) / .6)}

sass ✗

a{--red:#f01;--h:10;color:hsl(from var(--red) calc(var(--h)) calc(s * 2) calc(l + 1)/0.6)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0057 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Convert LAB color to keyword name when shorter

LAB color spaces with percents that exactly produce an in-gamut sRGB color value should be converted to hex or color keyword, whichever is shortest.

a {
  color: lab(54.291% 64.644% 55.913%);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:lab(54.291% 64.644% 55.913%)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{color:lab(54.291% 64.644% 55.913%)}

csso ✗

a{color:lab(54.291% 64.644% 55.913%)}

esbuild ✓

a{color:red}

lightningcss ✗

a{color:lab(54.291% 80.805 69.8913)}

sass ✗

a{color:lab(54.291 80.805 69.89125)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
9ms
0058 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Convert numeric LAB color to keyword name when shorter

LAB color spaces with numbers that exactly produce an in-gamut sRGB color value should be converted to hex or color keyword, whichever is shortest.

a {
  color: lab(54.291 80.805 69.8913);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:lab(54.291 80.805 69.8913)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{color:lab(54.291 80.805 69.8913)}

csso ✗

a{color:lab(54.291 80.805 69.8913)}

esbuild ✓

a{color:red}

lightningcss ✗

a{color:lab(54.291% 80.805 69.8913)}

sass ✗

a{color:lab(54.291 80.805 69.8913)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0059 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Remove .0 decimal from display-p3

The .0 in the number values of a display-p3 give no additional information and can be removed. Importantly, color(display-p3 1 0 0), should not be converted to a more narrow gamut like sRGB (rgb(255, 0, 0), #F00, red, etc).

a {
  color: color(display-p3 1.0 0.0 0.0);
}

Expected

a{color:color(display-p3 1 0 0)}

Outputs

clean-css ✓

a{color:color(display-p3 1 0 0)}

csskit ✓

a{color:color(display-p3 1 0 0)}

csslop ✓

a{color:color(display-p3 1 0 0)}

cssnano ✓

a{color:color(display-p3 1 0 0)}

csso ✓

a{color:color(display-p3 1 0 0)}

esbuild ✓

a{color:color(display-p3 1 0 0)}

lightningcss ✓

a{color:color(display-p3 1 0 0)}

sass ✓

a{color:color(display-p3 1 0 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0060 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Short-hex to color keyword

If a short hex representation is longer than the color keyword representation, use the keyword.

a {
  color: #f00;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0061 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

RRGGBBAA Hex to color keyword

If a longform hex can be represented with a shortform hex or color keyword, use the shortest representation.

a {
  color: #ff0000ff;
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:#ff0000ff}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

a{color:#ff0000ff}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0062 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✓
Details

Numeric HSL values to color keyword

If an HSL color uses numeric values that could be represented with a shorter hex or color keyword representation, use the shortest option.

a {
  color: hsl(0 100 50);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:hsl(0 100 50)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

a{color:hsl(0 100 50)}

esbuild ✗

a{color:hsl(0 100 50)}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0063 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

RGBA to color keyword

Convert all RGBA color representations to hex or color keywords, whichever is shortest.

a {
  color: rgba(255, 0, 0, 1);
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0064 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ERROR
Details

Numeric HWB values to color keyword

If an HWB color uses numeric values that could be represented with a shorter hex or color keyword representation, use the shortest option.

a {
  color: hwb(0 0 0);
}

Expected

a{color:red}

Outputs

clean-css ✗

a{color:hwb(0 0 0)}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✗

a{color:hwb(0 0 0)}

esbuild ✗

a{color:hwb(0 0 0)}

lightningcss ✓

a{color:red}

sass ✗

$whiteness: Expected 0 to have unit "%".
  ╷
2 │   color: hwb(0 0 0);
  │          ^^^^^^^^^^
  ╵
  - 2:10  root stylesheet
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0065 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Do not convert outline:transparent to outline:none

A transparent outline does not display anything on the screen in most situations, making it equivalent to setting the outline to none. Though this is fewer characters it may break accessibility. Users with their OS in "High Contrast" mode will actually see an outline when it is set to transparent, but will not when it is set to none. You can however convert transparent to #0000, which is functionally equivalent according to the CSS spec.

button {
  outline: transparent;
}

Expected

button{outline:#0000}

Outputs

clean-css ✗

button{outline:transparent}

csskit ✓

button{outline:#0000}

csslop ✓

button{outline:#0000}

cssnano ✓

button{outline:#0000}

csso ✗

button{outline:transparent}

esbuild ✗

button{outline:transparent}

lightningcss ✓

button{outline:#0000}

sass ✗

button{outline:rgba(0,0,0,0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0066 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Color-mix in OKLCH to color keyword

If all values in the color mix are the same, they can be merged into a single color and reduced to its shortest representation.

.a {
  color: color-mix(in oklch, #f00, red);
  background: color-mix(in oklch, #d2b48c, tan)
}

Expected

.a{color:red;background:tan}

Outputs

clean-css ✗

.a{color:color-mix(in oklch,red,red);background:color-mix(in oklch,tan,tan)}

csskit ✓

.a{color:red;background:tan}

csslop ✓

.a{color:red;background:tan}

cssnano ✗

.a{color:color-mix(in oklch,red,red);background:color-mix(in oklch,tan,tan)}

csso ✗

.a{color:color-mix(in oklch,red,red);background:color-mix(in oklch,tan,tan)}

esbuild ✗

.a{color:color-mix(in oklch,#f00,red);background:color-mix(in oklch,#d2b48c,tan)}

lightningcss ✗

.a{color:oklch(62.7955% .257683 29.2339);background:oklch(78.6187% .063821 74.619)}

sass ✗

.a{color:color-mix(in oklch, #f00, red);background:color-mix(in oklch, #d2b48c, tan)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0067 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

High precision OKLCH to color keyword

Given a high precision OKLCH color representation that is equivalent to an RGB color value, in this case rgb(255, 0, 0), convert it to its shortest hex or named color keyword representation.

.b {
  color: oklch(62.7955% .257683 29.2339);
  background: oklch(78.6187% .063821 74.619);
}

Expected

.b{color:red;background:tan}

Outputs

clean-css ✗

.b{color:oklch(62.7955% .257683 29.2339);background:oklch(78.6187% .063821 74.619)}

csskit ✗

.b{color:oklch(62.796 .258 29.2);background:oklch(78.619 .064 74.6)}

csslop ✓

.b{color:red;background:tan}

cssnano ✗

.b{color:oklch(62.7955% .257683 29.2339);background:oklch(78.6187% .063821 74.619)}

csso ✗

.b{color:oklch(62.7955%.257683 29.2339);background:oklch(78.6187%.063821 74.619)}

esbuild ✓

.b{color:red;background:tan}

lightningcss ✗

.b{color:oklch(62.7955% .257683 29.2339);background:oklch(78.6187% .063821 74.619)}

sass ✗

.b{color:oklch(.627955 .257683 29.2339);background:oklch(.786187 .063821 74.619)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0068 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

Low precision OKLCH to color keyword

Lower decimal point precision (DPP) OKLCH representations are perceptually identical to their higher precision versions. Given a lower precision OKLCH color representation that is equivalent to an RGB color value, in this case rgb(255, 0, 0), convert it to its shortest hex or named color keyword representation.

.c {
  color: oklch(.628 .2577 29.23);
  background: oklch(.786 .064 74.6);
}

Expected

.c{color:red;background:tan}

Outputs

clean-css ✗

.c{color:oklch(.628 .2577 29.23);background:oklch(.786 .064 74.6)}

csskit ✗

.c{color:oklch(.628 .2577 29.23);background:tan}

csslop ✓

.c{color:red;background:tan}

cssnano ✗

.c{color:oklch(.628 .2577 29.23);background:oklch(.786 .064 74.6)}

csso ✗

.c{color:oklch(.628 .2577 29.23);background:oklch(.786 .064 74.6)}

esbuild ✓

.c{color:red;background:tan}

lightningcss ✗

.c{color:oklch(62.8% .2577 29.23);background:oklch(78.6% .064 74.6)}

sass ✗

.c{color:oklch(.628 .2577 29.23);background:oklch(.786 .064 74.6)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0069 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove space in border shorthand before hex with alpha

The # in a hex color is an unambiguous token start, allowing the preceding space to be dropped.

See also: colors/0034

a {
  border: 3px solid #0006;
}

Expected

a{border:3px solid#0006}

Outputs

clean-css ✗

a{border:3px solid #0006}

csskit ✓

a{border:3px solid#0006}

csslop ✓

a{border:3px solid#0006}

cssnano ✗

a{border:3px solid #0006}

csso ✗

a{border:3px solid #0006}

esbuild ✗

a{border:3px solid #0006}

lightningcss ✗

a{border:3px solid #0006}

sass ✗

a{border:3px solid rgba(0,0,0,.4)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0070 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Do not rename grey/gray when used in a custom-property name

References to custom property names must not be changed.

div {
  background: var(--gray);
  color: var(--grey);
}

Expected

div{background:var(--gray);color:var(--grey)}

Outputs

clean-css ✓

div{background:var(--gray);color:var(--grey)}

csskit ✓

div{background:var(--gray);color:var(--grey)}

csslop ✓

div{background:var(--gray);color:var(--grey)}

cssnano ✓

div{background:var(--gray);color:var(--grey)}

csso ✓

div{background:var(--gray);color:var(--grey)}

esbuild ✓

div{background:var(--gray);color:var(--grey)}

lightningcss ✓

div{background:var(--gray);color:var(--grey)}

sass ✓

div{background:var(--gray);color:var(--grey)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
Subtotal 26 / 70 58 / 70 70 / 70 42 / 70 29 / 70 38 / 70 46 / 70 25 / 70
Percent 37.14% 82.85% 100% 60% 41.42% 54.28% 65.71% 35.71%
Duration 5ms 12ms 15ms 41ms 10ms 46ms 1ms 79ms

combined #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge rules after property and selector optimizations

This test validates correct order of operations in a minifier. If the merging or de-duplication of rules occurs prior to selector and/or property optimizations, then it will miss out on opportunities for merging identical rules that are only revealed after they've had those optimizations.

  1. All property optimizations should be done
    • Colors converted to smallest representations
    • Border shorthand applied
    • etc.
  2. Selectors on individual rules are optimized
    • :where(td):where(.error) => :where(td.error)
    • :is(span, div) => span,div
    • etc.
  3. Merging/deduping of rules is applied
    • div,span,:where(td.error),span,div => div,span,:where(td.error)

Before attempting to pass this test, first pass the tests it combines:

  • colors/0002 - #FF0000 -> red
  • colors/0016 - rgba(255 0 0 / 1) -> red
  • colors/0075 - hsl(0 100 50) -> red
  • duplicates/0007 - a{color:red}a{color:red} -> a{color:red}
  • selectors-advanced/0001 - :is(a,b) -> a,b
  • selectors-advanced/0018 - :where(.foo):where([bar]) -> :where(.foo[bar])
  • shorthands/0041 - (border-(color/style/width) = border)

div,
span,
:where(td):where(.error) {
  border: 1px dashed hsl(0 100 50);
  color: #FF0000;
}

:is(div, span) {
  border-color: #F00;
  border-style: dashed;
  border-width: 1px;
  color: rgba(255 0 0 / 1.0);
}

Expected

div,span,:where(td.error){border:1px dashed red;color:red}

Outputs

clean-css ✗

:where(td):where(.error),div,span{border:1px dashed;color:red}:is(div,span){border:1px dashed red;color:rgba(255 0 0 / 1)}

csskit ✗

div,span,:where(td):where(.error){border:1px dashed red;color:red}:is(div,span){border-color:red;border-style:dashed;border-width:1px;color:red}

csslop ✓

div,span,:where(td.error){border:1px dashed red;color:red}

cssnano ✗

:where(td):where(.error),div,span{border:1px dashed red;color:red}:is(div,span){border-color:red;border-style:dashed;border-width:1px;color:red}

csso ✗

:where(td):where(.error),div,span{border:1px dashed hsl(0 100 50);color:red}:is(div,span){border-color:red;border-style:dashed;border-width:1px;color:rgba(255 0 0/1)}

esbuild ✗

div,span,:where(td):where(.error){border:1px dashed hsl(0 100 50);color:red}:is(div,span){border-color:red;border-style:dashed;border-width:1px;color:red}

lightningcss ✗

div,span,:where(td):where(.error),:is(div,span){color:red;border:1px dashed red}

sass ✗

div,span,:where(td):where(.error){border:1px dashed red;color:red}:is(div,span){border-color:red;border-style:dashed;border-width:1px;color:red}
2ms
1ms
1ms
8ms
1ms
1ms
1ms
2ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ERROR
Details

Merge identical adjacent rules after color minification

After other optimizations are complete, like minifying color representations, merge any identical adjacent rules.

Combines:

  • colors/0002 - #FF0000
  • colors/0003 - rgb(255, 0, 0)
  • colors/0008 - hsl(0, 100%, 50%)
  • colors/0016 - rgb(255 0 0 / 1)
  • colors/0010 - hwb(0 0% 0%)
  • colors/0021 - rgb(255 none none)
  • colors/0023 - red
  • colors/0024 - color-mix(in srgb, rgb(none 0 0) 50%, rgb(255 0 0) 50%)
  • colors/0028 - color-mix(in srgb, red, red)
  • colors/0036 - color-mix(in srgb, red 100%, blue)
  • colors/0037 - color-mix(in srgb, blue 0%, red)
  • colors/0041 - color-mix(in srgb, red, blue 0%)
  • colors/0067 - light-dark(red, red)
  • colors/0070 - lab(54.291% 64.644% 55.913%)
  • colors/0071 - lab(54.291 80.805 69.8913)
  • colors/0073 - #F00
  • colors/0074 - #FF0000FF
  • colors/0075 - hsl(0 100 50)
  • colors/0076 - rgba(255, 0, 0, 1)
  • colors/0077 - hwb(0 0 0)
  • comments/0001 - a{color:red}/* comment */ -> a{color:red}
  • merging/0002 - .a {color:red}.b{color:red} -> .a,.b{color:red}
  • whitespace/0001 - a {\n color: red;\n} -> a{color:red}

.a { color: #F00; } /* colors/0073 */
.b { color: rgba(255, 0, 0, 1); } /* colors/0076 */
.c { color: red; } /* colors/0023 */
.d { color: #FF0000; } /* colors/0002 */
.e { color: #FF0000FF; } /* colors/0074 */
.f { color: RGBA(255, 0, 0, 1.0); } /* colors/0076 */
.g { color: rgb(255, 0, 0); } /* `colors/0003` */
.h { color: RGB(255 0 0); } /* colors/0016 */
.i { color: rgba(255 0 0 / 1); } /* colors/0016 */
.j { color: rgb(255 none none); } /* colors/0021 */
.k { color: hsl(0 100 50); } /* colors/0075 */
.l { color: HSL(0, 100%, 50%); } /* colors/0008 */
.m { color: hwb(0 0 0); } /* colors/0077 */
.n { color: HWB(0 0% 0%); } /* colors/0010 */
.o { color: light-dark(#F00, red); } /* colors/0067 */
.p { color: lab(54.291% 64.644% 55.913%); } /* colors/0070 */
.q { color: lab(54.291 80.805 69.8913); } /* colors/0071 */
.r { color: color-mix(in srgb, red, red); } /* colors/0028 */
.s { color: color-mix(in srgb, red 100%, blue); } /* colors/0036 */
.t { color: color-mix(in srgb, blue 0%, red); } /* colors/0037 */
.u { color: color-mix(in srgb, red, blue 0%); } /* colors/0041 */
.v { color: color-mix(in srgb, rgb(none 0 0) 50%, rgb(255 0 0) 50%); } /* colors/0024 */

Expected

.a,.b,.c,.d,.e,.f,.g,.h,.i,.j,.k,.l,.m,.n,.o,.p,.q,.r,.s,.t,.u,.v{color:red}

Outputs

clean-css ✗

.a,.b,.c,.d{color:red}.e{color:#ff0000FF}.f,.g{color:red}.h{color:RGB(255 0 0)}.i{color:rgba(255 0 0 / 1)}.j{color:rgb(255 none none)}.k{color:hsl(0 100 50)}.l{color:red}.m{color:hwb(0 0 0)}.n{color:HWB(0 0% 0%)}.o{color:light-dark(red,red)}.p{color:lab(54.291% 64.644% 55.913%)}.q{color:lab(54.291 80.805 69.8913)}.r{color:color-mix(in srgb,red,red)}.s{color:color-mix(in srgb,red 100%,#00f)}.t{color:color-mix(in srgb,#00f 0%,red)}.u{color:color-mix(in srgb,red,#00f 0%)}.v{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(255 0 0) 50%)}

csskit ✗

.a{color:red}.b{color:red}.c{color:red}.d{color:red}.e{color:red}.f{color:red}.g{color:red}.h{color:red}.i{color:red}.j{color:red}.k{color:red}.l{color:red}.m{color:red}.n{color:red}.o{color:light-dark(red,red)}.p{color:red}.q{color:red}.r{color:red}.s{color:red}.t{color:red}.u{color:red}.v{color:red}

csslop ✓

.a,.b,.c,.d,.e,.f,.g,.h,.i,.j,.k,.l,.m,.n,.o,.p,.q,.r,.s,.t,.u,.v{color:red}

cssnano ✗

.a,.b,.c,.d,.e,.f,.g,.h,.i,.j,.k,.l,.m,.n{color:red}.o{color:light-dark(red,red)}.p{color:lab(54.291% 64.644% 55.913%)}.q{color:lab(54.291 80.805 69.8913)}.r{color:color-mix(in srgb,red,red)}.s{color:color-mix(in srgb,red 100%,blue)}.t{color:color-mix(in srgb,blue 0%,red)}.u{color:color-mix(in srgb,red,blue 0%)}.v{color:color-mix(in srgb,#000 50%,red 50%)}

csso ✗

.a,.b,.c,.d{color:red}.e{color:#ff0000ff}.f{color:RGBA(255,0,0,1)}.g{color:red}.h{color:RGB(255 0 0)}.i{color:rgba(255 0 0/1)}.j{color:rgb(255 none none)}.k{color:hsl(0 100 50)}.l{color:HSL(0,100%,50%)}.m{color:hwb(0 0 0)}.n{color:HWB(0 0% 0%)}.o{color:light-dark(red,red)}.p{color:lab(54.291% 64.644% 55.913%)}.q{color:lab(54.291 80.805 69.8913)}.r{color:color-mix(in srgb,red,red)}.s{color:color-mix(in srgb,red 100%,blue)}.t{color:color-mix(in srgb,blue 0%,red)}.u{color:color-mix(in srgb,red,blue 0%)}.v{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(255 0 0) 50%)}

esbuild ✗

.a,.b,.c,.d,.e,.f,.g,.h,.i{color:red}.j{color:rgb(255 none none)}.k{color:hsl(0 100 50)}.l{color:red}.m{color:hwb(0 0 0)}.n{color:red}.o{color:light-dark(#F00,red)}.p,.q{color:red}.r{color:color-mix(in srgb,red,red)}.s{color:color-mix(in srgb,red 100%,blue)}.t{color:color-mix(in srgb,blue 0%,red)}.u{color:color-mix(in srgb,red,blue 0%)}.v{color:color-mix(in srgb,rgb(none 0 0) 50%,rgb(255 0 0) 50%)}

lightningcss ✗

.a,.b,.c,.d,.e,.f,.g,.h,.i{color:red}.j{color:red}.k,.l,.m,.n,.o{color:red}.p{color:lab(54.291% 80.805 69.8913)}.q{color:lab(54.291% 80.805 69.8913)}.r,.s,.t,.u,.v{color:red}

sass ✗

$whiteness: Expected 0 to have unit "%".
   ╷
13 │ .m { color: hwb(0 0 0); } /* colors/0077 */
   │             ^^^^^^^^^^
   ╵
  - 13:13  root stylesheet
2ms
1ms
3ms
3ms
2ms
1ms
1ms
4ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Introduce nesting to repeated selectors and deduplicate overridden properties

Nest adjacent rules where the selectors all start with the same prefix. Then, if any selectors are used in multiple nested rules with the same properties, remove the earlier defined property. If this results in any empty rules, remove those as well.

Combines:

  • duplicates/0020
    • h1,h2{color:#001}h1{color:#002}h2{color:#003} -> h1{color:#002}h2{color:#003}
  • empty-rules/0004
    • a{color:red;b{}} -> a{color:red}
  • nesting/0020
    • .foo .a{color:red}.foo .b{color:tan} -> .foo{.a{color:red}.b{color:tan}}

.foo {
  color: #001;
}
.foo h1,
.foo h2 {
  color: #002;
}
.foo h1 {
  color: #003;
}
.foo h2 {
  color: #004;
}

Expected

.foo{color:#001;h1{color:#003}h2{color:#004}}

Outputs

clean-css ✗

.foo{color:#001}.foo h1{color:#003}.foo h2{color:#004}

csskit ✗

.foo{color:#001}.foo h1,.foo h2{color:#002}.foo h1{color:#003}.foo h2{color:#004}

csslop ✓

.foo{color:#001;h1{color:#003}h2{color:#004}}

cssnano ✗

.foo{color:#001}.foo h1,.foo h2{color:#002}.foo h1{color:#003}.foo h2{color:#004}

csso ✗

.foo{color:#001}.foo h1{color:#003}.foo h2{color:#004}

esbuild ✗

.foo{color:#001}.foo h1,.foo h2{color:#002}.foo h1{color:#003}.foo h2{color:#004}

lightningcss ✗

.foo{color:#001}.foo h1,.foo h2{color:#002}.foo h1{color:#003}.foo h2{color:#004}

sass ✗

.foo{color:#001}.foo h1,.foo h2{color:#002}.foo h1{color:#003}.foo h2{color:#004}
1ms
1ms
1ms
2ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Linear gradient optimizations and background property shorthand

Convert all colors to their minified representation first, then apply advanced linear-gradient optimizations and combine all background properties to use the background property shorthand.

Combines:

  • colors/0001 - Short hex
    • #ffffff -> #fff
  • colors/0002 - Hex to color keyword
    • #ff0000 -> red
  • colors/0003 - RGB to color keyword
    • rgb(255, 0, 0) -> red
  • colors/0008 - HSL to color keyword
    • hsl(0, 100%, 50%) -> red
  • colors/0011 - Color keyword to hex
    • blue -> #00f
  • gradients/0001 - Remove to bottom.
    • linear-gradient(to bottom, red, tan) -> linear-gradient(red,tan)
  • gradients/0006 - Combine colors in gradient and remove 0%/100%
    • linear-gradient(red 0%,red 50%,tan 50%,tan 100%) -> linear-gradient(red 50%,tan 0)
  • shorthands/0076 - Background properties shorthand
  • values/0072 - keyword to percent
    • background-position:right bottom; -> background-position:100% 100%;

p {
  background: linear-gradient(
    to bottom,
    #ff0000 0%,
    hsl(0, 100%, 50%) 25%,
    rgb(255, 0, 0) 50%,
    blue 50%,
    #0000ff 100%
  );
  background-position: right bottom;
}

Expected

p{background:linear-gradient(red 50%,#00f 0) 100% 100%}

Outputs

clean-css ✗

p{background:linear-gradient(to bottom,red 0,red 25%,red 50%,#00f 50%,#00f 100%);background-position:right bottom}

csskit ✗

p{background:linear-gradient(to bottom,#ff0000 0%,hsl(0,100%,50%)25%,rgb(255,0,0)50%,blue 50%,#0000ff 100%);background-position:right bottom}

csslop ✓

p{background:linear-gradient(red 50%,#00f 0) 100% 100%}

cssnano ✗

p{background:linear-gradient(180deg,red,red 25%,red 50%,blue 0,#00f);background-position:100% 100%}

csso ✗

p{background:linear-gradient(to bottom,red 0,red 25%,red 50%,#00f 50%,#00f 100%);background-position:right bottom}

esbuild ✗

p{background:linear-gradient(to bottom,red 0% 25%,red 50%,#00f 50% 100%);background-position:right bottom}

lightningcss ✗

p{background:linear-gradient(red 0% 25%,red 50%,#00f 50% 100%) 100% 100%}

sass ✗

p{background:linear-gradient(to bottom, #ff0000 0%, hsl(0, 100%, 50%) 25%, rgb(255, 0, 0) 50%, blue 50%, #0000ff 100%);background-position:right bottom}
1ms
2ms
2ms
2ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge selectors with comma and perform advanced property shorthand

Remove the comment, merge rules with identical selectors, apply font shorthand.

Combines:

  • comments/0001 - Basic comment removal
  • merging/0019 - Merge identical selector separated by non-overlapping rules
  • shorthands/0017 - Font longhand merge

body {
  line-height: 1.5;
}
/* Comment */
html {
  color: red;
}
body {
  font-size: 75%;
  font-family: sans-serif;
}

Expected

html{color:red}body{font:75%/1.5 sans-serif}

Also accepted

body{font:75%/1.5 sans-serif}html{color:red}

Outputs

clean-css ✗

body{line-height:1.5;font-size:75%;font-family:sans-serif}html{color:red}

csskit ✗

body{line-height:1.5}html{color:red}body{font-size:75%;font-family:sans-serif}

csslop ✓

html{color:red}body{font:75%/1.5 sans-serif}

cssnano ✗

body{line-height:1.5;font-size:75%;font-family:sans-serif}html{color:red}

csso ✗

body{line-height:1.5;font-size:75%;font-family:sans-serif}html{color:red}

esbuild ✗

body{line-height:1.5}html{color:red}body{font-size:75%;font-family:sans-serif}

lightningcss ✗

body{line-height:1.5}html{color:red}body{font-family:sans-serif;font-size:75%}

sass ✗

body{line-height:1.5}html{color:red}body{font-size:75%;font-family:sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove useless :is() and useless nesting

If there is no value in nesting selectors, un-nest them. If there is no value in wrapping a selector with :is(), unwrap it.

Combines:

  • nesting/0013 - Unnest empty parent - a{b{color:red}} =>a b{color:red}
  • selectors/0018 - Remove usesless :is() - :is(.x) => .x

:is(.x) {
  :is(.y) {
    color: red;
  }
}

Expected

.x .y{color:red}

Outputs

clean-css ✗

csskit ✗

:is(.x){:is(.y){color:red}}

csslop ✓

.x .y{color:red}

cssnano ✗

:is(.x){:is(.y){color:red}}

csso ✗

esbuild ✗

:is(.x){:is(.y){color:red}}

lightningcss ✗

.x{& .y{color:red}}

sass ✗

:is(.x) :is(.y){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
Subtotal 0 / 6 0 / 6 6 / 6 0 / 6 0 / 6 0 / 6 0 / 6 0 / 6
Percent 0% 0% 100% 0% 0% 0% 0% 0%
Duration 6ms 5ms 8ms 18ms 4ms 4ms 1ms 10ms

comments #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Basic comment removal

Standard /* ... */ comments should be stripped entirely.

/* comment */
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Inline comment removal

A comment embedded within a declaration value (color: /* inline */ red) must be removed without breaking the surrounding value.

a {
  color: /* inline */ red;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✗ ✓ ✓ ✗ ✓ ✗ ✓
Details

Important comment preservation

Comments starting with /*! are license/legal markers and must be preserved. Stripping them can cause legal compliance issues.

/*! important */
a {
  color: red;
}

Expected

/*! important */a{color:red}

Outputs

clean-css ✓

/*! important */a{color:red}

csskit ✗

a{color:red}

csslop ✓

/*! important */a{color:red}

cssnano ✓

/*! important */a{color:red}

csso ✗

/*! important */
a{color:red}

esbuild ✓

/*! important */a{color:red}

lightningcss ✗

/*! important */
a{color:red}

sass ✓

/*! important */a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Comment in custom property value must be preserved

--bar: a/**/b tokenizes as ident:a ident:b with no whitespace between them. Replacing the comment with a space produces a b which is a different token sequence. This matters for @container style() queries matching against custom property values.

a {
  --bar: a/**/b;
}

Expected

a{--bar:a/**/b}

Outputs

clean-css ✓

a{--bar:a/**/b}

csskit ✗

a{--bar:a b}

csslop ✓

a{--bar:a/**/b}

cssnano ✗

a{--bar:a b}

csso ✓

a{--bar:a/**/b}

esbuild ✗

a{--bar: ab}

lightningcss ✗

a{--bar:ab}

sass ✗

a{--bar: a/**/b}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Comment in @container style query custom property must be preserved

@container style(--bar: a/**/b) uses a comment as a zero-width token separator. Replacing it with a space or removing it entirely changes the token sequence the query matches against, breaking the correspondence with the declaration --bar: a/**/b.

@container style(--bar: a/**/b) {
  a {
    color: red;
  }
}

Expected

@container style(--bar:a/**/b){a{color:red}}

Outputs

clean-css ✗

@container style(--bar:ab){a{color:red}}

csskit ✗

@container style(--bar:a b){a{color:red}}

csslop ✓

@container style(--bar:a/**/b){a{color:red}}

cssnano ✗

@container style(--bar: a b){a{color:red}}

csso ✗

@container style(--bar: a/**/b){a{color:red}}

esbuild ✗

@container style(--bar: ab){a{color:red}}

lightningcss ✗

@container style(--bar:ab){a{color:red}}

sass ✗

@container style(--bar: a/**/b){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Remove comments from inside keyframes percents

Comments inside of keyframes should be removed.

@keyframes fade {
  0% {
    /* A comment */
    opacity: 1;
  }
  to {
    /* A comment */
    opacity: 0;
  }
}

Expected

@keyframes fade{0%{opacity:1}to{opacity:0}}

Outputs

clean-css ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

csskit ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

csslop ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

cssnano ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

csso ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

esbuild ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

lightningcss ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}

sass ✓

@keyframes fade{0%{opacity:1}to{opacity:0}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove comments from custom-property values

If a custom-property is assigned to only a CSS comment, the entire comment can safely be removed. However, the custom-property declaration must remain. If a custom-property contains values with a comment between them, then the comment token must remain, though it's contents can be removed.

:root {
  --foo:/*base-color*/;
  --bar:12/* some text */12;
  --black:#000;
}

Expected

:root{--foo:;--bar:12/**/12;--black:#000}

Outputs

clean-css ✗

:root{--foo:/*base-color*/;--bar:12/* some text */12;--black:#000}

csskit ✗

:root{--foo:;--bar:12 12;--black:#000}

csslop ✓

:root{--foo:;--bar:12/**/12;--black:#000}

cssnano ✗

:root{--foo: ;--bar:12 12;--black:#000}

csso ✗

:root{--foo:/*base-color*/;--bar:12/* some text */12;--black:#000}

esbuild ✗

:root{--foo:;--bar:1212;--black:#000}

lightningcss ✗

:root{--foo:;--bar:1212;--black:#000}

sass ✗

:root{--foo:/*base-color*/;--bar:12/* some text */12;--black:#000}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 5 / 7 3 / 7 7 / 7 4 / 7 4 / 7 4 / 7 3 / 7 4 / 7
Percent 71.42% 42.85% 100% 57.14% 57.14% 57.14% 42.85% 57.14%
Duration 1ms 2ms 2ms 5ms 1ms 5ms 1ms 5ms

container #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

@container whitespace removal

Remove whitespace inside @container query while preserving the container name and condition. The space between name and ( is required.

@container sidebar (width > 700px) {
  a {
    color: red;
  }
}

Expected

@container sidebar (width>700px){a{color:red}}

Outputs

clean-css ✗

@container sidebar (width > 700px){a{color:red}}

csskit ✓

@container sidebar (width>700px){a{color:red}}

csslop ✓

@container sidebar (width>700px){a{color:red}}

cssnano ✗

@container sidebar (width > 700px){a{color:red}}

csso ✗

@container sidebar (width > 700px){a{color:red}}

esbuild ✗

@container sidebar (width > 700px){a{color:red}}

lightningcss ✓

@container sidebar (width>700px){a{color:red}}

sass ✗

@container sidebar (width > 700px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Empty @container removal

An @container rule with no child rules has no effect and can be removed.

@container (min-width: 700px) {
}

Expected

Outputs

clean-css ✓

csskit ✗

@container(min-width:700px){}

csslop ✓

cssnano ✓

csso ✓

esbuild ✓

lightningcss ✓

sass ✗

@container (min-width: 700px){}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

Container style query whitespace

style() container queries must be preserved. Whitespace around : inside the condition can be removed but the style() function itself must not be stripped as unknown.

@container style(--theme: dark) {
  a {
    color: red;
  }
}

Expected

@container style(--theme:dark){a{color:red}}

Outputs

clean-css ✓

@container style(--theme:dark){a{color:red}}

csskit ✓

@container style(--theme:dark){a{color:red}}

csslop ✓

@container style(--theme:dark){a{color:red}}

cssnano ✗

@container style(--theme: dark){a{color:red}}

csso ✗

@container style(--theme: dark){a{color:red}}

esbuild ✗

@container style(--theme: dark){a{color:red}}

lightningcss ✓

@container style(--theme:dark){a{color:red}}

sass ✗

@container style(--theme: dark){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert min-width to range syntax in @container

@container (min-width: 700px) can be shortened to @container(width>=700px) using range syntax. Container queries support the same range forms as media queries. In addition the white space can be elided as an at-keyword cannot contain (.

@container (min-width: 700px) {
  a {
    color: red;
  }
}

Expected

@container(width>=700px){a{color:red}}

Outputs

clean-css ✗

@container (min-width:700px){a{color:red}}

csskit ✗

@container(min-width:700px){a{color:red}}

csslop ✓

@container(width>=700px){a{color:red}}

cssnano ✗

@container (min-width: 700px){a{color:red}}

csso ✗

@container (min-width: 700px){a{color:red}}

esbuild ✗

@container (min-width: 700px){a{color:red}}

lightningcss ✗

@container (width>=700px){a{color:red}}

sass ✗

@container (min-width: 700px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert max-width to range syntax in @container

@container (max-width: 500px) can be shortened to @container(width<=500px) using range syntax. In addition the white space can be elided as an at-keyword cannot contain (.

@container (max-width: 500px) {
  a {
    color: red;
  }
}

Expected

@container(width<=500px){a{color:red}}

Outputs

clean-css ✗

@container (max-width:500px){a{color:red}}

csskit ✗

@container(max-width:500px){a{color:red}}

csslop ✓

@container(width<=500px){a{color:red}}

cssnano ✗

@container (max-width: 500px){a{color:red}}

csso ✗

@container (max-width: 500px){a{color:red}}

esbuild ✗

@container (max-width: 500px){a{color:red}}

lightningcss ✗

@container (width<=500px){a{color:red}}

sass ✗

@container (max-width: 500px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Named container with min-width to range syntax

@container sidebar (min-width: 700px) can use range syntax for the condition while preserving the container name: @container sidebar (width>=700px).

@container sidebar (min-width: 700px) {
  a {
    color: red;
  }
}

Expected

@container sidebar (width>=700px){a{color:red}}

Outputs

clean-css ✗

@container sidebar (min-width:700px){a{color:red}}

csskit ✗

@container sidebar (min-width:700px){a{color:red}}

csslop ✓

@container sidebar (width>=700px){a{color:red}}

cssnano ✗

@container sidebar (min-width: 700px){a{color:red}}

csso ✗

@container sidebar (min-width: 700px){a{color:red}}

esbuild ✗

@container sidebar (min-width: 700px){a{color:red}}

lightningcss ✓

@container sidebar (width>=700px){a{color:red}}

sass ✗

@container sidebar (min-width: 700px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 2 / 6 2 / 6 6 / 6 1 / 6 1 / 6 1 / 6 4 / 6 0 / 6
Percent 33.33% 33.33% 100% 16.66% 16.66% 16.66% 66.66% 0%
Duration 1ms 1ms 1ms 4ms 1ms 4ms 1ms 3ms

counter-style #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✓ ✗ ✓ ✓
Details

@counter-style whitespace removal

Basic whitespace minification inside a @counter-style rule. The space between the at-keyword and the name must be preserved (unlike @page), but all other internal whitespace is removable. The default encoding is UTF-8, therefore the \1F44D codepoint can be replaced with 👍.

@counter-style thumbs {
  system: cyclic;
  symbols: "\1F44D";
  suffix: " ";
}

Expected

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}

Outputs

clean-css ✗

@counter-style thumbs{system:cyclic;symbols:"\1F44D";suffix:" "}

csskit ✓

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}

csslop ✓

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}

cssnano ✗

@counter-style thumbs{system:cyclic;symbols:"\1F44D";suffix:" "}

csso ✓

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}

esbuild ✗

@counter-style thumbs{system:cyclic;symbols:"\1f44d";suffix:" "}

lightningcss ✓

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}

sass ✓

@counter-style thumbs{system:cyclic;symbols:"👍";suffix:" "}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗
Details

Empty @counter-style removal

An empty @counter-style with no descriptors can be removed entirely.

@counter-style empty {
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✗

@counter-style empty{}

lightningcss ✗

@counter-style empty{}

sass ✗

@counter-style empty{}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✗ ✓ ✗ ✗ ✗
Details

Quoted symbols must preserve quotes

symbols: "(" ")" must keep quotes. Parentheses are not valid unquoted ident tokens and stripping quotes would produce a parse error.

The whitespace between quotes can be removed for a very slight improvement in compression.

@counter-style paren {
  system: fixed;
  symbols: "(" ")";
}

Expected

@counter-style paren{system:fixed;symbols:"("")"}

Outputs

clean-css ✗

@counter-style paren{system:fixed;symbols:"(" ")"}

csskit ✓

@counter-style paren{system:fixed;symbols:"("")"}

csslop ✓

@counter-style paren{system:fixed;symbols:"("")"}

cssnano ✗

@counter-style paren{system:fixed;symbols:"(" ")"}

csso ✓

@counter-style paren{system:fixed;symbols:"("")"}

esbuild ✗

@counter-style paren{system:fixed;symbols:"(" ")"}

lightningcss ✗

@counter-style paren{system:fixed;symbols:"(" ")"}

sass ✗

@counter-style paren{system:fixed;symbols:"(" ")"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 1 / 3 3 / 3 3 / 3 1 / 3 3 / 3 0 / 3 1 / 3 1 / 3
Percent 33.33% 100% 100% 33.33% 100% 0% 33.33% 33.33%
Duration 1ms 1ms 1ms 2ms 1ms 2ms 1ms 1ms

duplicates #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Duplicate declaration removal

When a property is declared twice in the same rule, the last value wins. The earlier duplicate should be removed.

a {
  color: red;
  color: tan;
}

Expected

a{color:tan}

Outputs

clean-css ✓

a{color:tan}

csskit ✗

a{color:red;color:tan}

csslop ✓

a{color:tan}

cssnano ✗

a{color:red;color:tan}

csso ✓

a{color:tan}

esbuild ✗

a{color:red;color:tan}

lightningcss ✓

a{color:tan}

sass ✗

a{color:red;color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Duplicate rule removal

When two adjacent rules share the same selector and the later one completely overrides the earlier, the earlier rule can be removed.

a {
  color: red;
}
a {
  color: tan;
}

Expected

a{color:tan}

Outputs

clean-css ✓

a{color:tan}

csskit ✗

a{color:red}a{color:tan}

csslop ✓

a{color:tan}

cssnano ✗

a{color:red;color:tan}

csso ✓

a{color:tan}

esbuild ✗

a{color:red}a{color:tan}

lightningcss ✓

a{color:tan}

sass ✗

a{color:red}a{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Duplicate rule removal with intervening rule

When a later rule completely overrides an earlier rule with the same selector, the earlier rule should be removed even with other rules between them.

a {
  color: red;
  font-size: 16px;
}
b {
  margin: 0;
}
a {
  color: tan;
  font-size: 16px;
}

Expected

b{margin:0}a{color:tan;font-size:16px}

Also accepted

a{color:tan;font-size:16px}b{margin:0}
b{margin:0}a{font-size:16px;color:tan}
a{font-size:16px;color:tan}b{margin:0}

Outputs

clean-css ✓

b{margin:0}a{color:tan;font-size:16px}

csskit ✗

a{color:red;font-size:16px}b{margin:0}a{color:tan;font-size:16px}

csslop ✓

b{margin:0}a{color:tan;font-size:16px}

cssnano ✗

a{color:red;color:tan;font-size:16px}b{margin:0}

csso ✓

a{color:tan;font-size:16px}b{margin:0}

esbuild ✗

a{color:red;font-size:16px}b{margin:0}a{color:tan;font-size:16px}

lightningcss ✓

b{margin:0}a{color:tan;font-size:16px}

sass ✗

a{color:red;font-size:16px}b{margin:0}a{color:tan;font-size:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✗ ✓ ✓ ✓ ✓ ✗ ✗
Details

Duplicate selector removal within a rule

Duplicate selectors in a selector list should be deduplicated.

h1, h2, h3, h4, h5, h5 {
  color: red;
}

Expected

h1,h2,h3,h4,h5{color:red}

Outputs

clean-css ✓

h1,h2,h3,h4,h5{color:red}

csskit ✗

h1,h2,h3,h4,h5,h5{color:red}

csslop ✓

h1,h2,h3,h4,h5{color:red}

cssnano ✓

h1,h2,h3,h4,h5{color:red}

csso ✓

h1,h2,h3,h4,h5{color:red}

esbuild ✓

h1,h2,h3,h4,h5{color:red}

lightningcss ✗

h1,h2,h3,h4,h5,h5{color:red}

sass ✗

h1,h2,h3,h4,h5,h5{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Duplicate properties with env() fallback must be preserved

The plain 1rem fallback and env() declaration are progressive enhancement. Removing the "duplicate" breaks browsers that don't support env().

a {
  padding-top: 1rem;
  padding-top: calc(1rem + env(safe-area-inset-top));
}

Expected

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

Outputs

clean-css ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

csskit ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

csslop ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

cssnano ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

csso ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

esbuild ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

lightningcss ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}

sass ✓

a{padding-top:1rem;padding-top:calc(1rem + env(safe-area-inset-top))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Duplicate declaration removal with !important

When a property is declared with !important and then redeclared without it, the non-important declaration loses and should be removed. The !important declaration always wins regardless of order.

a {
  color: red !important;
  color: blue;
}

Expected

a{color:red!important}

Validate

Outputs

clean-css ✓

a{color:red!important}

csskit ✗

a{color:red!important;color:blue}

csslop ✓

a{color:red!important}

cssnano ✗

a{color:red!important;color:blue}

csso ✓

a{color:red!important}

esbuild ✗

a{color:red!important;color:#00f}

lightningcss ✗

a{color:#00f;color:red!important}

sass ✗

a{color:red !important;color:blue}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Entirely identical rule removal

When two rules have the same selector and identical declarations, the duplicate rule is fully redundant and should be removed.

a {
  color: red;
  font-size: 16px;
}
a {
  color: red;
  font-size: 16px;
}

Expected

a{color:red;font-size:16px}

Also accepted

a{font-size:16px;color:red}

Outputs

clean-css ✓

a{color:red;font-size:16px}

csskit ✗

a{color:red;font-size:16px}a{color:red;font-size:16px}

csslop ✓

a{color:red;font-size:16px}

cssnano ✓

a{color:red;font-size:16px}

csso ✓

a{color:red;font-size:16px}

esbuild ✓

a{color:red;font-size:16px}

lightningcss ✓

a{color:red;font-size:16px}

sass ✗

a{color:red;font-size:16px}a{color:red;font-size:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Remove duplicate scrollbar-gutter

If multiple scrollbar-gutters are applied in the same rule, the last one will be used and the earlier declarations will be ignored.

p {
  scrollbar-gutter: auto;
  scrollbar-gutter: stable;
  scrollbar-gutter: stable both-edges;
}

Expected

p{scrollbar-gutter:stable both-edges}

Outputs

clean-css ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}

csskit ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}

csslop ✓

p{scrollbar-gutter:stable both-edges}

cssnano ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}

csso ✓

p{scrollbar-gutter:stable both-edges}

esbuild ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}

lightningcss ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}

sass ✗

p{scrollbar-gutter:auto;scrollbar-gutter:stable;scrollbar-gutter:stable both-edges}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✗ ✓ ✓ ✓ ✓ ✗ ✗
Details

Duplicate complex selectors in a selector list

Duplicate compound/complex selectors within a selector list should be deduplicated, including selectors beyond simple type selectors.

.nav .item,
.footer,
.nav .item {
  color: red;
}

Expected

.nav .item,.footer{color:red}

Also accepted

.footer,.nav .item{color:red}

Outputs

clean-css ✓

.footer,.nav .item{color:red}

csskit ✗

.nav .item,.footer,.nav .item{color:red}

csslop ✓

.nav .item,.footer{color:red}

cssnano ✓

.footer,.nav .item{color:red}

csso ✓

.footer,.nav .item{color:red}

esbuild ✓

.nav .item,.footer{color:red}

lightningcss ✗

.nav .item,.footer,.nav .item{color:red}

sass ✗

.nav .item,.footer,.nav .item{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✓ ✗ ✓ ✓ ✓ ✓ ✗ ✗
Details

Duplicate rules with reordered selectors

Two rules with the same selectors in different order and identical declarations are equivalent. Remove the duplicate by normalizing selector order.

h1, h2 {
  color: red;
}
h2, h1 {
  color: red;
}

Expected

h1,h2{color:red}

Also accepted

h2,h1{color:red}

Outputs

clean-css ✓

h1,h2{color:red}

csskit ✗

h1,h2{color:red}h2,h1{color:red}

csslop ✓

h1,h2{color:red}

cssnano ✓

h1,h2{color:red}

csso ✓

h1,h2{color:red}

esbuild ✓

h1,h2{color:red}

lightningcss ✗

h1,h2,h2,h1{color:red}

sass ✗

h1,h2{color:red}h2,h1{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Flat and nested equivalent rules should dedup

.foo .bar{...} and .foo { .bar{...} } resolve to the same selector. When the declarations are identical, the duplicate should be removed.

.foo .bar {
  color: red;
}
.foo {
  .bar {
    color: red;
  }
}

Expected

.foo .bar{color:red}

Outputs

clean-css ✓

.foo .bar{color:red}

csskit ✗

.foo .bar{color:red}.foo{.bar{color:red}}

csslop ✓

.foo .bar{color:red}

cssnano ✗

.foo .bar{color:red}.foo{.bar{color:red}}

csso ✓

.foo .bar{color:red}

esbuild ✗

.foo .bar{color:red}.foo{.bar{color:red}}

lightningcss ✗

.foo .bar{color:red}.foo{& .bar{color:red}}

sass ✗

.foo .bar{color:red}.foo .bar{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✓ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Partial duplicate rule absorbed by superset

When a later rule for the same selector contains all declarations of an earlier rule plus additional ones, the earlier rule is redundant and can be removed.

a {
  color: red;
}
a {
  color: red;
  font-size: 1rem;
}

Expected

a{color:red;font-size:1rem}

Also accepted

a{font-size:1rem;color:red}

Outputs

clean-css ✓

a{color:red;font-size:1rem}

csskit ✗

a{color:red}a{color:red;font-size:1rem}

csslop ✓

a{color:red;font-size:1rem}

cssnano ✓

a{color:red;font-size:1rem}

csso ✓

a{color:red;font-size:1rem}

esbuild ✗

a{color:red}a{color:red;font-size:1rem}

lightningcss ✓

a{color:red;font-size:1rem}

sass ✗

a{color:red}a{color:red;font-size:1rem}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

!important wins on custom properties

When a custom property has both a normal and !important declaration, the !important one wins regardless of order. The normal declaration is dead code. Note the space before !important is part of the syntax, not the value.

a {
  --color: blue;
  --color: red !important;
}

Expected

a{--color:red !important}

Outputs

clean-css ✗

a{--color:blue;--color:red!important}

csskit ✗

a{--color:blue;--color:red !important}

csslop ✓

a{--color:red !important}

cssnano ✗

a{--color:blue;--color:red !important}

csso ✗

a{--color:blue;--color:red!important}

esbuild ✗

a{--color: blue;--color: red !important}

lightningcss ✗

a{--color:blue;--color:red!important}

sass ✗

a{--color: blue;--color: red !important}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✓ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Duplicate shorthand property override

border-top: 1px solid blue followed by border-top: 2px solid red is a duplicate declaration. The last one wins and the first can be removed.

a {
  border-top: 1px solid blue;
  border-top: 2px solid red;
}

Expected

a{border-top:2px solid red}

Outputs

clean-css ✓

a{border-top:2px solid red}

csskit ✗

a{border-top:1px solid blue;border-top:2px solid red}

csslop ✓

a{border-top:2px solid red}

cssnano ✓

a{border-top:2px solid red}

csso ✓

a{border-top:2px solid red}

esbuild ✗

a{border-top:1px solid blue;border-top:2px solid red}

lightningcss ✓

a{border-top:2px solid red}

sass ✗

a{border-top:1px solid blue;border-top:2px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

UNSAFE: border-top before border-block must not be removed

border-top is physical and border-block is logical. In horizontal-tb writing mode border-block maps to top/bottom, but in vertical writing modes it maps to left/right. Removing border-top would change rendering in some writing modes.

a {
  border-top: 1px solid red;
  border-block: 2px solid red;
}

Expected

a{border-top:1px solid red;border-block:2px solid red}

Also accepted

a{border-block:2px solid red;border-top:1px solid red}

Outputs

clean-css ✓

a{border-top:1px solid red;border-block:2px solid red}

csskit ✓

a{border-top:1px solid red;border-block:2px solid red}

csslop ✓

a{border-top:1px solid red;border-block:2px solid red}

cssnano ✓

a{border-top:1px solid red;border-block:2px solid red}

csso ✓

a{border-top:1px solid red;border-block:2px solid red}

esbuild ✓

a{border-top:1px solid red;border-block:2px solid red}

lightningcss ✓

a{border-top:1px solid red;border-block:2px solid red}

sass ✓

a{border-top:1px solid red;border-block:2px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

UNSAFE: margin-left before margin-inline-start must not be removed

margin-left is physical and margin-inline-start is logical. In LTR margin-inline-start maps to margin-left, but in RTL it maps to margin-right. Both declarations interact via cascade order and writing mode, so removing either changes rendering in some configurations.

a {
  margin-left: 10px;
  margin-inline-start: 20px;
}

Expected

a{margin-left:10px;margin-inline-start:20px}

Also accepted

a{margin-inline-start:20px;margin-left:10px}

Outputs

clean-css ✓

a{margin-left:10px;margin-inline-start:20px}

csskit ✓

a{margin-left:10px;margin-inline-start:20px}

csslop ✓

a{margin-left:10px;margin-inline-start:20px}

cssnano ✓

a{margin-left:10px;margin-inline-start:20px}

csso ✓

a{margin-left:10px;margin-inline-start:20px}

esbuild ✓

a{margin-left:10px;margin-inline-start:20px}

lightningcss ✓

a{margin-left:10px;margin-inline-start:20px}

sass ✓

a{margin-left:10px;margin-inline-start:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

UNSAFE: padding-top before padding-block-start must not be removed

padding-top is physical and padding-block-start is logical. In horizontal-tb they resolve to the same edge, but in vertical writing modes padding-block-start maps to padding-left or padding-right. Removing either declaration changes rendering in some writing modes.

a {
  padding-top: 10px;
  padding-block-start: 20px;
}

Expected

a{padding-top:10px;padding-block-start:20px}

Also accepted

a{padding-block-start:20px;padding-top:10px}

Outputs

clean-css ✓

a{padding-top:10px;padding-block-start:20px}

csskit ✓

a{padding-top:10px;padding-block-start:20px}

csslop ✓

a{padding-top:10px;padding-block-start:20px}

cssnano ✓

a{padding-top:10px;padding-block-start:20px}

csso ✓

a{padding-top:10px;padding-block-start:20px}

esbuild ✓

a{padding-top:10px;padding-block-start:20px}

lightningcss ✓

a{padding-top:10px;padding-block-start:20px}

sass ✓

a{padding-top:10px;padding-block-start:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
8ms
0018 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

UNSAFE: inset-block-start before top must not be removed

inset-block-start is logical and top is physical. In horizontal-tb they resolve to the same edge, but in vertical writing modes inset-block-start maps to left or right. Removing either changes rendering in some writing modes.

a {
  inset-block-start: 10px;
  top: 20px;
}

Expected

a{inset-block-start:10px;top:20px}

Also accepted

a{top:20px;inset-block-start:10px}

Outputs

clean-css ✓

a{inset-block-start:10px;top:20px}

csskit ✓

a{inset-block-start:10px;top:20px}

csslop ✓

a{inset-block-start:10px;top:20px}

cssnano ✓

a{inset-block-start:10px;top:20px}

csso ✓

a{inset-block-start:10px;top:20px}

esbuild ✓

a{inset-block-start:10px;top:20px}

lightningcss ✓

a{inset-block-start:10px;top:20px}

sass ✓

a{inset-block-start:10px;top:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Duplicate custom property removal

When a property is declared twice containing custom property values in the same rule, the last value wins. The earlier duplicate should be removed.

a {
  color: var(--x);
  color: var(--x);
}

Expected

a{color:var(--x)}

Outputs

clean-css ✗

a{color:var(--x);color:var(--x)}

csskit ✗

a{color:var(--x);color:var(--x)}

csslop ✓

a{color:var(--x)}

cssnano ✓

a{color:var(--x)}

csso ✓

a{color:var(--x)}

esbuild ✓

a{color:var(--x)}

lightningcss ✓

a{color:var(--x)}

sass ✗

a{color:var(--x);color:var(--x)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✓ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Remove overridden properties in rules with multiple selectors

If the properties of all selectors in a rule get overridden later in the stylesheet, then that property can be removed from the earlier rule. If no properties are left in the rule (it is empty), the entire rule may be discarded.

h1, h2 {
  color: #001;
}
h1 {
  color: #002;
}
h2 {
  color: #003;
}

h3, h4 {
  margin: 1rem;
  padding: 1px;
}
h3 {
  padding: 2px;
}
h4 {
  padding: 3px;
}

Expected

h1{color:#002}h2{color:#003}h3,h4{margin:1rem}h3{padding:2px}h4{padding:3px}

Outputs

clean-css ✓

h1{color:#002}h2{color:#003}h3,h4{margin:1rem}h3{padding:2px}h4{padding:3px}

csskit ✗

h1,h2{color:#001}h1{color:#002}h2{color:#003}h3,h4{margin:1rem;padding:1px}h3{padding:2px}h4{padding:3px}

csslop ✓

h1{color:#002}h2{color:#003}h3,h4{margin:1rem}h3{padding:2px}h4{padding:3px}

cssnano ✗

h1,h2{color:#001}h1{color:#002}h2{color:#003}h3,h4{margin:1rem;padding:1px}h3{padding:2px}h4{padding:3px}

csso ✓

h1{color:#002}h2{color:#003}h3,h4{margin:1rem}h3{padding:2px}h4{padding:3px}

esbuild ✗

h1,h2{color:#001}h1{color:#002}h2{color:#003}h3,h4{margin:1rem;padding:1px}h3{padding:2px}h4{padding:3px}

lightningcss ✗

h1,h2{color:#001}h1{color:#002}h2{color:#003}h3,h4{margin:1rem;padding:1px}h3{padding:2px}h4{padding:3px}

sass ✗

h1,h2{color:#001}h1{color:#002}h2{color:#003}h3,h4{margin:1rem;padding:1px}h3{padding:2px}h4{padding:3px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

Convert double-colon before/after to single and de-dupe in same selector

The ::before and ::after pseudo-elements can safely be converted to :before or :after. After this conversion, the minifier should check for duplicate selectors and properties that can be removed.

h1::before,
h1:before {
  content: "j";
}

Expected

h1:before{content:"j"}

Outputs

clean-css ✗

h1::before,h1:before{content:"j"}

csskit ✗

h1::before,h1:before{content:"j"}

csslop ✓

h1:before{content:"j"}

cssnano ✓

h1:before{content:"j"}

csso ✗

h1::before,h1:before{content:"j"}

esbuild ✓

h1:before{content:"j"}

lightningcss ✗

h1:before,h1:before{content:"j"}

sass ✗

h1::before,h1:before{content:"j"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert double-colon before/after to single and de-dupe separate selector

The ::before and ::after pseudo-elements can safely be converted to :before or :after. After this conversion, the minifier should check for duplicate selectors and properties that can be removed.

h2:before {
  content: "i";
}
h2::before {
  content: "j";
}

Expected

h2:before{content:"j"}

Outputs

clean-css ✗

h2:before{content:"i"}h2::before{content:"j"}

csskit ✗

h2:before{content:"i"}h2::before{content:"j"}

csslop ✓

h2:before{content:"j"}

cssnano ✗

h2:before{content:"i";content:"j"}

csso ✗

h2:before{content:"i"}h2::before{content:"j"}

esbuild ✗

h2:before{content:"i"}h2:before{content:"j"}

lightningcss ✗

h2:before{content:"i";content:"j"}

sass ✗

h2:before{content:"i"}h2::before{content:"j"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Do not abstract Data-URIs to custom-properties

Data-URIs can be quite long. When the same one is used multiple times, it could be stored in a custom-property and referenced. However, when GZipped, the custom-property and references actually result in a larger size compared to just letting the duplicate Data-URi's exist and be GZipped away.

body {
  image-rendering: pixelated;
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);
  mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);
}

Expected

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

Also accepted

body{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);image-rendering:pixelated;mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

Outputs

clean-css ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

csskit ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

csslop ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

cssnano ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

csso ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

esbuild ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

lightningcss ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}

sass ✓

body{image-rendering:pixelated;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==);mask-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAAIklEQVR42mP4DwZpxjMxGQwQCojQGAg5AvowteK1D4hxIQApumnK5mwMyQAAAABJRU5ErkJggg==)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove duplicate property in rule when one uses custom-properties

Browsers ignore earlier declared property/value pair declarations when the same property is used in the same rule, unless the later version contains a value it does not recognize. Then it falls back to the most recent previous version of that property with a value it can use. All browsers support custom-properties so in this test, none of them will fall back to the primitive value version. Even if the custom property was never defined the browser will still prefer it over the fallback. This means these fallbacks will never be used and are redundant.

a {
  border: 1px solid tan;
  border: 1px solid var(--x);
  color: var(--a);
  color: red;
  color: var(--y);
  margin: var(--z);
  margin: 10px;
  margin: var(--z);
}

Expected

a{border:1px solid var(--x);color:var(--y);margin:var(--z)}

Also accepted

a{border:1px solid var(--x);margin:var(--z);color:var(--y)}
a{color:var(--y);border:1px solid var(--x);margin:var(--z)}
a{color:var(--y);margin:var(--z);border:1px solid var(--x)}
a{margin:var(--z);color:var(--y);border:1px solid var(--x)}
a{margin:var(--z);border:1px solid var(--x);color:var(--y)}

Outputs

clean-css ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--a);color:red;color:var(--y);margin:var(--z);margin:10px;margin:var(--z)}

csskit ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--a);color:red;color:var(--y);margin:var(--z);margin:10px;margin:var(--z)}

csslop ✓

a{border:1px solid var(--x);color:var(--y);margin:var(--z)}

cssnano ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--a);color:red;color:var(--y);margin:10px;margin:var(--z)}

csso ✗

a{border:1px solid tan;border:1px solid var(--x);color:red;color:var(--y);margin:10px;margin:var(--z)}

esbuild ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--a);color:red;color:var(--y);margin:10px;margin:var(--z)}

lightningcss ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--y);margin:var(--z);margin:10px;margin:var(--z)}

sass ✗

a{border:1px solid tan;border:1px solid var(--x);color:var(--a);color:red;color:var(--y);margin:var(--z);margin:10px;margin:var(--z)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 18 / 24 6 / 24 24 / 24 14 / 24 20 / 24 12 / 24 13 / 24 6 / 24
Percent 75% 25% 100% 58.33% 83.33% 50% 54.16% 25%
Duration 3ms 3ms 5ms 17ms 3ms 15ms 1ms 21ms

empty-rules #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Empty rule removal

a {} contains no declarations and should be removed entirely. Empty rules waste bytes and have no effect on rendering.

a {}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css ✓

b{color:red}

csskit ✓

b{color:red}

csslop ✓

b{color:red}

cssnano ✓

b{color:red}

csso ✓

b{color:red}

esbuild ✓

b{color:red}

lightningcss ✓

b{color:red}

sass ✓

b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Empty rule with comment

A rule containing only a comment has no declarations and should be removed. Comments are stripped during minification so the rule body becomes empty.

a {
  /* this rule is empty */
}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css ✓

b{color:red}

csskit ✓

b{color:red}

csslop ✓

b{color:red}

cssnano ✓

b{color:red}

csso ✓

b{color:red}

esbuild ✓

b{color:red}

lightningcss ✓

b{color:red}

sass ✓

b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Empty @media rule

An @media block with no rules inside should be removed entirely. It has no effect on rendering.

@media screen {
}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css ✓

b{color:red}

csskit ✓

b{color:red}

csslop ✓

b{color:red}

cssnano ✓

b{color:red}

csso ✓

b{color:red}

esbuild ✓

b{color:red}

lightningcss ✓

b{color:red}

sass ✓

b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Empty nested rule

An empty nested rule (& b {}) inside a parent should be removed while preserving the parent's own declarations.

a {
  color: red;
  & b {
  }
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
2ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Remove parent selector when all nested selectors are empty

This verifies that stray empty selectors are not left behind after empty child selectors are removed. If minified correctly, the result will be 0 bytes.

nav {
  a {
  }
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✓

lightningcss ✓

sass ✓

1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Remove empty rules with advanced selectors

This verifies that less common selectors do not impact the removal of empty rules during minification. If minified correctly, the result will be 0 bytes.

kbd {}
ul li:nth-of-type(2n) {}
fieldset > legend {}
input[type="checkbox"] {}
input[type=radio] {}
legend, option {}
*, *:before, *:after {}
button.secondary {}
#unique {}
details[open] {}
:focus {}
a[href*=".pdf"]:after {}
:is(html, body):has(:modal) {}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✓

lightningcss ✓

sass ✓

1ms
1ms
1ms
4ms
1ms
1ms
1ms
2ms
0007 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗
Details

Remove keyframes with only empty children

If all of the children of a keyframe definition are empty, the entire @keyframes can be removed.

@keyframes fade {
  0% {}
  50% {}
  100% {}
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✗

@keyframes fade{}

lightningcss ✗

@keyframes fade{0%{}50%{}to{}}

sass ✗

@keyframes fade{}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 7 / 7 7 / 7 7 / 7 7 / 7 7 / 7 6 / 7 6 / 7 6 / 7
Percent 100% 100% 100% 100% 100% 85.71% 85.71% 85.71%
Duration 1ms 3ms 1ms 8ms 1ms 4ms 1ms 5ms

escaping #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Escape sequence resolution in identifiers

\66 oo is a CSS escape sequence for foo. Minifiers should resolve escape sequences in identifiers to their plain UTF-8 equivalents when safe.

\66 oo {
  color: red;
}

Expected

foo{color:red}

Validate

Outputs

clean-css ✗

\66 oo{color:red}

csskit ✓

foo{color:red}

csslop ✓

foo{color:red}

cssnano ✗

\66 oo{color:red}

csso ✗

\66 oo{color:red}

esbuild ✓

foo{color:red}

lightningcss ✓

foo{color:red}

sass ✓

foo{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✗ ✓ ✓ ✓ ✓
Details

Escape sequence resolution in strings

'\66 oo' should resolve the escape to "foo". Tests both escape resolution within string values and normalization from single to double quotes.

a {
  content: '\66 oo';
}

Expected

a{content:"foo"}

Validate

Outputs

clean-css ✗

a{content:'\66 oo'}

csskit ✓

a{content:"foo"}

csslop ✓

a{content:"foo"}

cssnano ✗

a{content:"\66 oo"}

csso ✓

a{content:"foo"}

esbuild ✓

a{content:"foo"}

lightningcss ✓

a{content:"foo"}

sass ✓

a{content:"foo"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Multiple consecutive hex escapes in class selector

.\66\6f\6f encodes each letter of foo as a separate hex escape. All three can be resolved to plain ASCII, producing .foo.

.\66\6f\6f {
  color: red;
}

Expected

.foo{color:red}

Validate

Outputs

clean-css ✗

.\66\6f\6f{color:red}

csskit ✓

.foo{color:red}

csslop ✓

.foo{color:red}

cssnano ✗

.\66\6f\6f{color:red}

csso ✗

.\66\6f\6f {color:red}

esbuild ✓

.foo{color:red}

lightningcss ✓

.foo{color:red}

sass ✓

.foo{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Hex escape for hyphen-minus in element name

my\2d component encodes a hyphen-minus, producing my-component. The hyphen is a valid mid-identifier so the escape can be resolved to a literal -.

See also: escaping/0015

custom\2d component {
  color: red;
}

Expected

custom-component{color:red}

Validate

Outputs

clean-css ✗

custom\2d component{color:red}

csskit ✓

custom-component{color:red}

csslop ✓

custom-component{color:red}

cssnano ✗

custom\2d component{color:red}

csso ✗

custom\2d component{color:red}

esbuild ✓

custom-component{color:red}

lightningcss ✓

custom-component{color:red}

sass ✓

custom-component{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Hex escape in attribute selector name

hr\65 f uses a hex escape for e inside the attribute name, producing href. The escape can be resolved to a plain ASCII character.

a[hr\65 f=x] {
  color: red;
}

Expected

a[href=x]{color:red}

Validate

Outputs

clean-css ✗

a[hr\65f=x]{color:red}

csskit ✓

a[href=x]{color:red}

csslop ✓

a[href=x]{color:red}

cssnano ✗

a[hr\65 f=x]{color:red}

csso ✗

a[hr\65 f=x]{color:red}

esbuild ✓

a[href=x]{color:red}

lightningcss ✓

a[href=x]{color:red}

sass ✓

a[href=x]{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Digit-start identifier must keep escape

.\31 23 is the class selector .123. The leading digit must remain escaped because bare 123 is not a valid CSS identifier start. A minifier that resolves this to .123 would produce an invalid selector.

.\31 23 {
  color: red;
}

Expected

.\31 23{color:red}

Validate

Outputs

clean-css ✓

.\31 23{color:red}

csskit ✓

.\31 23{color:red}

csslop ✓

.\31 23{color:red}

cssnano ✓

.\31 23{color:red}

csso ✓

.\31 23{color:red}

esbuild ✓

.\31 23{color:red}

lightningcss ✓

.\31 23{color:red}

sass ✓

.\31 23{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

Newline escape in string must be preserved

"\a" is a CSS escape for a newline character (U+000A). It cannot be replaced with a literal newline inside the string, as that would terminate the string.

a {
  content: "\a";
}

Expected

a{content:"\a"}

Outputs

clean-css ✓

a{content:"\a"}

csskit ✓

a{content:"\a"}

csslop ✓

a{content:"\a"}

cssnano ✓

a{content:"\a"}

csso ✓

a{content:"\a"}

esbuild ✓

a{content:"\a"}

lightningcss ✗

a{content:"\a "}

sass ✓

a{content:"\a"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Escape of special character in class name

.\@ escapes the @ sign to use it in a class name. The escape must be preserved because @ unescaped is not valid in an identifier.

.\@ {
  color: red;
}

Expected

.\@{color:red}

Outputs

clean-css ✓

.\@{color:red}

csskit ✓

.\@{color:red}

csslop ✓

.\@{color:red}

cssnano ✓

.\@{color:red}

csso ✓

.\@{color:red}

esbuild ✓

.\@{color:red}

lightningcss ✓

.\@{color:red}

sass ✓

.\@{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Hex escape in ID selector

#\66 oo is the ID selector #foo with a hex escape for f. The escape can be resolved since foo is a valid identifier.

#\66 oo {
  color: red;
}

Expected

#foo{color:red}

Validate

Outputs

clean-css ✗

#\66 oo{color:red}

csskit ✓

#foo{color:red}

csslop ✓

#foo{color:red}

cssnano ✗

#\66 oo{color:red}

csso ✗

#\66 oo{color:red}

esbuild ✓

#foo{color:red}

lightningcss ✓

#foo{color:red}

sass ✓

#foo{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Hex escape in compound selector class

a.\62 ar is a.bar with a hex escape for b. Escape resolution applies in compound selectors (element + class).

a.\62 ar {
  color: red;
}

Expected

a.bar{color:red}

Validate

Outputs

clean-css ✗

a.\62 ar{color:red}

csskit ✓

a.bar{color:red}

csslop ✓

a.bar{color:red}

cssnano ✗

a.\62 ar{color:red}

csso ✗

a.\62 ar{color:red}

esbuild ✓

a.bar{color:red}

lightningcss ✓

a.bar{color:red}

sass ✓

a.bar{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✓
Details

Hex escape in standard property name

\63 olor encodes the c in color as a hex escape. Minifiers should resolve escape sequences in property names to their plain equivalents.

a {
  \63 olor: red;
}

Expected

a{color:red}

Validate

Outputs

clean-css ✗

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✗

a{\63 olor:red}

csso ✗

a{\63 olor:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

Hex escape in custom property name

\66 encodes f as a hex escape in a custom property name, resolving --\66 oo to --foo. The same escape appears in var(--\66 oo) and must resolve consistently.

a {
  --\66 oo: red;
  color: var(--\66 oo);
}

Expected

a{--foo:red;color:var(--foo)}

Validate

Outputs

clean-css ✗

a{color:var(--\66 oo)}

csskit ✓

a{--foo:red;color:var(--foo)}

csslop ✓

a{--foo:red;color:var(--foo)}

cssnano ✗

a{--\66 oo:red;color:var(--\66 oo)}

csso ✗

a{--\66 oo:red;color:var(--\66 oo)}

esbuild ✗

a{--foo: red;color:var(--foo)}

lightningcss ✓

a{--foo:red;color:var(--foo)}

sass ✗

a{--foo: red;color:var(--foo)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Escaping HREF attribute values

Attribute selectors can include values in optional quotes. Since HREF specifically deals with URL paths, they will naturally contain characters that need escaped if not wrapped in quotes (#, ., :, etc). A minifier should remove the quotes and favor escaping if the total characters in the escaped version is fewer than if it was kept in quotes. It should remove escapes and prefer quotes when that has fewer characters.

[href="#"],
[href="/"],
[href="file.html"],
[href=https\:\/\/example\.com] {
  color: red;
}

Expected

[href=\#],[href=\/],[href=file\.html],[href="https://example.com"]{color:red}

Validate

Outputs

clean-css ✗

[href="#"],[href="/"],[href="file.html"],[href=https\:\/\/example\.com]{color:red}

csskit ✗

[href="#"],[href="/"],[href="file.html"],[href=https\:\/\/example\.com]{color:red}

csslop ✓

[href=\#],[href=\/],[href=file\.html],[href="https://example.com"]{color:red}

cssnano ✗

[href="#"],[href="/"],[href="file.html"],[href=https\:\/\/example\.com]{color:red}

csso ✗

[href="#"],[href="/"],[href="file.html"],[href=https\:\/\/example\.com]{color:red}

esbuild ✗

[href="#"],[href="/"],[href="file.html"],[href="https://example.com"]{color:red}

lightningcss ✓

[href=\#],[href=\/],[href=file\.html],[href="https://example.com"]{color:red}

sass ✗

[href="#"],[href="/"],[href="file.html"],[href=https\:\/\/example\.com]{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0014 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Escape semi-colons in attribute selectors

When an attribute selector includes a semi-colon it must either be surrounded in quotes or escaped. If the total number of escapes required is 3 or more, then prefer quotes to avoid additional characters.

p[style$="maroon;"] {
  background: red;
}
p[style="color:maroon;padding:1px;"] {
  background: tan;
}

Expected

p[style$=maroon\;]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

Outputs

clean-css ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

csskit ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

csslop ✓

p[style$=maroon\;]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

cssnano ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

csso ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

esbuild ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

lightningcss ✓

p[style$=maroon\;]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}

sass ✗

p[style$="maroon;"]{background:red}p[style="color:maroon;padding:1px;"]{background:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✗
Details

Hex escape for hyphen-minus in class name

.\2d foo encodes a hyphen-minus followed by foo, producing .-foo. The hyphen is valid mid-identifier so the escape can be resolved to a literal -.

See also: escaping/0004

.\2d foo {
  color: red;
}

Expected

.-foo{color:red}

Validate

Outputs

clean-css ✗

.\2d foo{color:red}

csskit ✓

.-foo{color:red}

csslop ✓

.-foo{color:red}

cssnano ✗

.\2d foo{color:red}

csso ✗

.\2d foo{color:red}

esbuild ✓

.-foo{color:red}

lightningcss ✓

.-foo{color:red}

sass ✗

.\-foo{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 3 / 15 13 / 15 15 / 15 3 / 15 4 / 15 12 / 15 14 / 15 11 / 15
Percent 20% 86.66% 100% 20% 26.66% 80% 93.33% 73.33%
Duration 1ms 1ms 2ms 11ms 2ms 10ms 1ms 17ms

font-face #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@font-face whitespace removal

@font-face blocks must be preserved with whitespace stripped. This tests basic whitespace removal without any string or URL optimizations.

@font-face {
  font-display: swap;
  font-weight: 400;
}

Expected

@font-face{font-display:swap;font-weight:400}

Outputs

clean-css ✓

@font-face{font-display:swap;font-weight:400}

csskit ✓

@font-face{font-display:swap;font-weight:400}

csslop ✓

@font-face{font-display:swap;font-weight:400}

cssnano ✓

@font-face{font-display:swap;font-weight:400}

csso ✓

@font-face{font-display:swap;font-weight:400}

esbuild ✓

@font-face{font-display:swap;font-weight:400}

lightningcss ✓

@font-face{font-display:swap;font-weight:400}

sass ✓

@font-face{font-display:swap;font-weight:400}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

font-family quote removal

font-family: "Custom" can be unquoted to font-family: Custom when the value is a valid CSS identifier. This does not apply to names that are CSS-wide values or contain special characters.

@font-face {
  font-family: "Custom";
  font-display: swap;
}

Expected

@font-face{font-family:Custom;font-display:swap}

Outputs

clean-css ✓

@font-face{font-family:Custom;font-display:swap}

csskit ✗

@font-face{font-family:"Custom";font-display:swap}

csslop ✓

@font-face{font-family:Custom;font-display:swap}

cssnano ✓

@font-face{font-family:Custom;font-display:swap}

csso ✗

@font-face{font-family:"Custom";font-display:swap}

esbuild ✓

@font-face{font-family:Custom;font-display:swap}

lightningcss ✓

@font-face{font-family:Custom;font-display:swap}

sass ✗

@font-face{font-family:"Custom";font-display:swap}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

url() quote removal in @font-face src

Quotes inside url() can be removed when the URL contains no special characters (whitespace, parentheses, or single/double quotes). url("font.woff2") becomes url(font.woff2).

@font-face {
  font-family: Custom;
  src: url("font.woff2");
}

Expected

@font-face{font-family:Custom;src:url(font.woff2)}

Outputs

clean-css ✗

@font-face{font-family:Custom;src:url("font.woff2")}

csskit ✗

@font-face{font-family:Custom;src:url("font.woff2")}

csslop ✓

@font-face{font-family:Custom;src:url(font.woff2)}

cssnano ✓

@font-face{font-family:Custom;src:url(font.woff2)}

csso ✓

@font-face{font-family:Custom;src:url(font.woff2)}

esbuild ✓

@font-face{font-family:Custom;src:url(font.woff2)}

lightningcss ✓

@font-face{font-family:Custom;src:url(font.woff2)}

sass ✗

@font-face{font-family:Custom;src:url("font.woff2")}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

format() string to keyword conversion

The format() function in @font-face src accepts both string and keyword syntax per CSS Fonts 4. format("woff2") and format(woff2) are equivalent, so quotes can be safely stripped. Whitespace between url() and format() can also be removed.

@font-face {
  font-family: Custom;
  src: url(font.woff2) format("woff2");
}

Expected

@font-face{font-family:Custom;src:url(font.woff2)format(woff2)}

Outputs

clean-css ✗

@font-face{font-family:Custom;src:url(font.woff2) format("woff2")}

csskit ✗

@font-face{font-family:Custom;src:url(font.woff2)format("woff2")}

csslop ✓

@font-face{font-family:Custom;src:url(font.woff2)format(woff2)}

cssnano ✗

@font-face{font-family:Custom;src:url(font.woff2) format("woff2")}

csso ✗

@font-face{font-family:Custom;src:url(font.woff2)format("woff2")}

esbuild ✗

@font-face{font-family:Custom;src:url(font.woff2) format("woff2")}

lightningcss ✗

@font-face{font-family:Custom;src:url(font.woff2)format("woff2")}

sass ✗

@font-face{font-family:Custom;src:url(font.woff2) format("woff2")}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

font-family quotes required for generic family keywords

Unquoting "serif" produces the generic family keyword serif, which refers to the browser default serif font -- not a custom font named "serif". Quotes must be preserved when the family name matches a generic keyword.

@font-face {
  font-family: "serif";
  src: url(custom.woff2);
}

Expected

@font-face{font-family:"serif";src:url(custom.woff2)}

Outputs

clean-css ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

csskit ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

csslop ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

cssnano ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

csso ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

esbuild ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

lightningcss ✓

@font-face{font-family:"serif";src:url(custom.woff2)}

sass ✓

@font-face{font-family:"serif";src:url(custom.woff2)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

unicode-range wildcard compression

U+0000-00FF can be shortened to U+?? using wildcard notation. The ? replaces trailing hex digits that cover the full 0-F range, and leading zeros in the prefix can be dropped.

@font-face {
  font-family: test;
  src: local(test);
  unicode-range: U+0000-00FF;
}

Expected

@font-face{font-family:test;src:local(test);unicode-range:U+??}

Outputs

clean-css ✗

@font-face{font-family:test;src:local(test);unicode-range:U+0000-00FF}

csskit ✗

@font-face{font-family:test;src:local(test);unicode-range:U+0000-00FF}

csslop ✓

@font-face{font-family:test;src:local(test);unicode-range:U+??}

cssnano ✗

@font-face{font-family:test;src:local(test);unicode-range:u+00??}

csso ✗

@font-face{font-family:test;src:local(test);unicode-range:U+0000-00FF}

esbuild ✗

@font-face{font-family:test;src:local(test);unicode-range:U+0000-00FF}

lightningcss ✓

@font-face{font-family:test;src:local(test);unicode-range:U+??}

sass ✗

@font-face{font-family:test;src:local(test);unicode-range:U+0000-00FF}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Use ranges for adjacent unicode-range values

If there are multiple adjacent (off by 1) unicode values in a unicode range, the first and last item in a group can be combined in a range (U+2000,U+2001,U+2002 becomes U+2000-2002).

@font-face {
  unicode-range:
    U+1F170,
    U+1F172,
    U+1F173,
    U+1F175,
    U+1F177,
    U+1F178,
    U+1F179,
    U+1F17A,
    U+1F17C-1F180;
}

Expected

@font-face{unicode-range:U+1F170,U+1F172-1F173,U+1F175,U+1F177-1F17A,U+1F17C-1F180}

Outputs

clean-css ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}

csskit ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}

csslop ✓

@font-face{unicode-range:U+1F170,U+1F172-1F173,U+1F175,U+1F177-1F17A,U+1F17C-1F180}

cssnano ✗

@font-face{unicode-range:u+1f170,u+1f172,u+1f173,u+1f175,u+1f177,u+1f178,u+1f179,u+1f17a,u+1f17c-1f180}

csso ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}

esbuild ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}

lightningcss ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}

sass ✗

@font-face{unicode-range:U+1F170,U+1F172,U+1F173,U+1F175,U+1F177,U+1F178,U+1F179,U+1F17A,U+1F17C-1F180}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove duplicate unicode-range values

If a unicode-range contains a duplicate value it can be removed.

@font-face {
  unicode-range: U+2001, U+2001;
}

Expected

@font-face{unicode-range:U+2001}

Outputs

clean-css ✗

@font-face{unicode-range:U+2001,U+2001}

csskit ✗

@font-face{unicode-range:U+2001,U+2001}

csslop ✓

@font-face{unicode-range:U+2001}

cssnano ✗

@font-face{unicode-range:u+2001,u+2001}

csso ✗

@font-face{unicode-range:U+2001,U+2001}

esbuild ✗

@font-face{unicode-range:U+2001,U+2001}

lightningcss ✗

@font-face{unicode-range:U+2001,U+2001}

sass ✗

@font-face{unicode-range:U+2001,U+2001}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove unicode-range values that exist in other unicode ranges

If a unicode range contains a duplicate value that is stored in another range, the duplicate can be removed.

@font-face {
  unicode-range: U+2000-2002, U+2001;
}

Expected

@font-face{unicode-range:U+2000-2002}

Outputs

clean-css ✗

@font-face{unicode-range:U+2000-2002,U+2001}

csskit ✗

@font-face{unicode-range:U+2000-2002,U+2001}

csslop ✓

@font-face{unicode-range:U+2000-2002}

cssnano ✗

@font-face{unicode-range:u+2000-2002,u+2001}

csso ✗

@font-face{unicode-range:U+2000-2002,U+2001}

esbuild ✗

@font-face{unicode-range:U+2000-2002,U+2001}

lightningcss ✗

@font-face{unicode-range:U+2000-2002,U+2001}

sass ✗

@font-face{unicode-range:U+2000-2002,U+2001}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge overlapping unicode ranges

If unicode ranges overlap, merge them into one range.

@font-face {
  unicode-range: U+2000-2002, U+2001-2004;
}

Expected

@font-face{unicode-range:U+2000-2004}

Outputs

clean-css ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}

csskit ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}

csslop ✓

@font-face{unicode-range:U+2000-2004}

cssnano ✗

@font-face{unicode-range:u+2000-2002,u+2001-2004}

csso ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}

esbuild ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}

lightningcss ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}

sass ✗

@font-face{unicode-range:U+2000-2002,U+2001-2004}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge unicode ranges with wildcards that are encompassed by larger ranges

If a unicode range with wildcards is fully encompassed by other ranges, merge them into one range. For example, U+4?? represents the range of U+400-4FF. If a larger range encompasses those values, then the wildcard could be removed.

@font-face {
  unicode-range: U+4??, U+399-500;
}

Expected

@font-face{unicode-range:U+399-500}

Outputs

clean-css ✗

@font-face{unicode-range:U+4??,U+399-500}

csskit ✗

@font-face{unicode-range:U+4??,U+399-500}

csslop ✓

@font-face{unicode-range:U+399-500}

cssnano ✗

@font-face{unicode-range:u+4??,u+399-500}

csso ✗

@font-face{unicode-range:U+4??,U+399-500}

esbuild ✗

@font-face{unicode-range:U+4??,U+399-500}

lightningcss ✗

@font-face{unicode-range:U+4??,U+399-500}

sass ✗

@font-face{unicode-range:U+4??,U+399-500}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge overlapping unicode ranges with wildcards

If a unicode range with wildcards has a partial overlap with other ranges, merge them into one range. For example, U+4?? represents the range of U+400-4FF. If another range contains some of the same values, replace them both with a single range that handles all associated values.

@font-face {
  unicode-range: U+4??, U+455-555;
}

Expected

@font-face{unicode-range:U+400-555}

Outputs

clean-css ✗

@font-face{unicode-range:U+4??,U+455-555}

csskit ✗

@font-face{unicode-range:U+4??,U+455-555}

csslop ✓

@font-face{unicode-range:U+400-555}

cssnano ✗

@font-face{unicode-range:u+4??,u+455-555}

csso ✗

@font-face{unicode-range:U+4??,U+455-555}

esbuild ✗

@font-face{unicode-range:U+4??,U+455-555}

lightningcss ✗

@font-face{unicode-range:U+4??,U+455-555}

sass ✗

@font-face{unicode-range:U+4??,U+455-555}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge overlapping unicode ranges into wildcard

When unicode ranges map to a set of values that could be represented with wildcards, they should be replaced with the wildcard to reduce character count.

@font-face {
  unicode-range: U+500-503, U+504-5FF;
}

Expected

@font-face{unicode-range:U+5??}

Outputs

clean-css ✗

@font-face{unicode-range:U+500-503,U+504-5FF}

csskit ✗

@font-face{unicode-range:U+500-503,U+504-5FF}

csslop ✓

@font-face{unicode-range:U+5??}

cssnano ✗

@font-face{unicode-range:u+500-503,u+504-5ff}

csso ✗

@font-face{unicode-range:U+500-503,U+504-5FF}

esbuild ✗

@font-face{unicode-range:U+500-503,U+504-5FF}

lightningcss ✗

@font-face{unicode-range:U+500-503,U+504-5FF}

sass ✗

@font-face{unicode-range:U+500-503,U+504-5FF}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 3 / 13 2 / 13 13 / 13 4 / 13 3 / 13 4 / 13 5 / 13 2 / 13
Percent 23.07% 15.38% 100% 30.76% 23.07% 30.76% 38.46% 15.38%
Duration 1ms 3ms 2ms 7ms 1ms 9ms 1ms 5ms

gradients #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Default gradient direction omission

linear-gradient(to bottom, ...) can omit the direction since to bottom (180deg) is the default.

a {
  background: linear-gradient(to bottom, red, blue);
}

Expected

a{background:linear-gradient(red,#00f)}

Validate

Outputs

clean-css ✗

a{background:linear-gradient(to bottom,red,#00f)}

csskit ✗

a{background:linear-gradient(to bottom,red,blue)}

csslop ✓

a{background:linear-gradient(red,#00f)}

cssnano ✗

a{background:linear-gradient(180deg,red,blue)}

csso ✗

a{background:linear-gradient(to bottom,red,#00f)}

esbuild ✗

a{background:linear-gradient(to bottom,red,#00f)}

lightningcss ✓

a{background:linear-gradient(red,#00f)}

sass ✗

a{background:linear-gradient(to bottom, red, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

180deg gradient direction omission

linear-gradient(180deg, ...) is equivalent to the default direction and can be omitted.

a {
  background: linear-gradient(180deg, red, blue);
}

Expected

a{background:linear-gradient(red,#00f)}

Validate

Outputs

clean-css ✗

a{background:linear-gradient(180deg,red,#00f)}

csskit ✗

a{background:linear-gradient(180deg,red,blue)}

csslop ✓

a{background:linear-gradient(red,#00f)}

cssnano ✗

a{background:linear-gradient(180deg,red,blue)}

csso ✗

a{background:linear-gradient(180deg,red,#00f)}

esbuild ✗

a{background:linear-gradient(180deg,red,#00f)}

lightningcss ✓

a{background:linear-gradient(red,#00f)}

sass ✗

a{background:linear-gradient(180deg, red, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Gradient direction keyword to angle

to right can be replaced with 90deg which is shorter.

a {
  background: linear-gradient(to right, red, blue);
}

Expected

a{background:linear-gradient(90deg,red,#00f)}

Validate

Outputs

clean-css ✗

a{background:linear-gradient(to right,red,#00f)}

csskit ✗

a{background:linear-gradient(to right,red,blue)}

csslop ✓

a{background:linear-gradient(90deg,red,#00f)}

cssnano ✗

a{background:linear-gradient(90deg,red,blue)}

csso ✗

a{background:linear-gradient(to right,red,#00f)}

esbuild ✗

a{background:linear-gradient(to right,red,#00f)}

lightningcss ✓

a{background:linear-gradient(90deg,red,#00f)}

sass ✗

a{background:linear-gradient(to right, red, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

Redundant gradient stop positions

0% on the first stop and 100% on the last stop are defaults and can be removed.

a {
  background: linear-gradient(red 0%, tan 25%, #00f 100%);
}

Expected

a{background:linear-gradient(red,tan 25%,#00f)}

Validate

Outputs

clean-css ✗

a{background:linear-gradient(red 0,tan 25%,#00f 100%)}

csskit ✗

a{background:linear-gradient(red 0%,tan 25%,#00f 100%)}

csslop ✓

a{background:linear-gradient(red,tan 25%,#00f)}

cssnano ✓

a{background:linear-gradient(red,tan 25%,#00f)}

csso ✗

a{background:linear-gradient(red 0,tan 25%,#00f 100%)}

esbuild ✓

a{background:linear-gradient(red,tan 25%,#00f)}

lightningcss ✗

a{background:linear-gradient(red 0%,tan 25%,#00f 100%)}

sass ✗

a{background:linear-gradient(red 0%, tan 25%, #00f 100%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Default radial-gradient shape and position

ellipse at center is the default for radial-gradient and can be omitted entirely.

a {
  background: radial-gradient(ellipse at center, red, blue);
}

Expected

a{background:radial-gradient(red,#00f)}

Validate

Outputs

clean-css ✗

a{background:radial-gradient(ellipse at center,red,#00f)}

csskit ✗

a{background:radial-gradient(ellipse at center,red,blue)}

csslop ✓

a{background:radial-gradient(red,#00f)}

cssnano ✗

a{background:radial-gradient(ellipse at center,red,blue)}

csso ✗

a{background:radial-gradient(ellipse at center,red,#00f)}

esbuild ✗

a{background:radial-gradient(ellipse at center,red,#00f)}

lightningcss ✓

a{background:radial-gradient(red,#00f)}

sass ✗

a{background:radial-gradient(ellipse at center, red, blue)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine adjacent identical colors in linear-gradient and remove 0%/100%

If the same color has multiple adjacent color stops, the color stops can be combined, retaining only the first and last percent and separating them with a space. If the first percent is 0% or the last percent is 100% they can be removed as these are automatically implied. If the first percent is the same as the last percent of the previous color stop, it can become a unitless 0.

p {
  background: linear-gradient(
    red 0%,
    red 50%,
    tan 50%,
    tan 100%
  );
}

Expected

p{background:linear-gradient(red 50%,tan 0)}

Outputs

clean-css ✗

p{background:linear-gradient(red 0,red 50%,tan 50%,tan 100%)}

csskit ✗

p{background:linear-gradient(red 0%,red 50%,tan 50%,tan 100%)}

csslop ✓

p{background:linear-gradient(red 50%,tan 0)}

cssnano ✗

p{background:linear-gradient(red,red 50%,tan 0,tan)}

csso ✗

p{background:linear-gradient(red 0,red 50%,tan 50%,tan 100%)}

esbuild ✗

p{background:linear-gradient(red 0% 50%,tan 50% 100%)}

lightningcss ✗

p{background:linear-gradient(red 0% 50%,tan 50% 100%)}

sass ✗

p{background:linear-gradient(red 0%, red 50%, tan 50%, tan 100%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine adjacent identical colors in linear-gradient

If the same color has multiple adjacent color stops the color stops can be combined, retaining only the first and last percent and separating them with a space. When defining the start and stop of a color you can use 0 (without a percent sign) to indicate that the first value will be the same as the last value in the previous color stop.

p {
  background: linear-gradient(
    #fff,
    red 25%,
    red 35%,
    red 50%,
    tan 50%,
    tan 75%,
    tan 85%,
    #fff
  );
}

Expected

p{background:linear-gradient(#fff,red 25% 50%,tan 0 85%,#fff)}

Outputs

clean-css ✗

p{background:linear-gradient(#fff,red 25%,red 35%,red 50%,tan 50%,tan 75%,tan 85%,#fff)}

csskit ✗

p{background:linear-gradient(#fff,red 25%,red 35%,red 50%,tan 50%,tan 75%,tan 85%,#fff)}

csslop ✓

p{background:linear-gradient(#fff,red 25% 50%,tan 0 85%,#fff)}

cssnano ✗

p{background:linear-gradient(#fff,red 25%,red 35%,red 50%,tan 0,tan 75%,tan 85%,#fff)}

csso ✗

p{background:linear-gradient(#fff,red 25%,red 35%,red 50%,tan 50%,tan 75%,tan 85%,#fff)}

esbuild ✗

p{background:linear-gradient(#fff,red 25% 35%,red 50%,tan 50% 75%,tan 85%,#fff)}

lightningcss ✗

p{background:linear-gradient(#fff,red 25% 35%,red 50%,tan 50% 75%,tan 85%,#fff)}

sass ✗

p{background:linear-gradient(#fff, red 25%, red 35%, red 50%, tan 50%, tan 75%, tan 85%, #fff)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove 0% and 100% from grouped color stops in gradient

You can group the start and stop of a color in a gradient together with a space separation. However, 0% and 100% are not required because they are implied for the first and last color in the list. You can also use a unitless 0 to indicate the first position of a color is the same as the stop of the previous color.

p {
  background: linear-gradient(
    red 0% 40%,
    tan 0 70%,
    #00f 0 100%
  );
}

Expected

p{background:linear-gradient(red 40%,tan 0 70%,#00f 0)}

Outputs

clean-css ✗

p{background:linear-gradient(red 0 40%,tan 0 70%,#00f 0 100%)}

csskit ✗

p{background:linear-gradient(red 0% 40%,tan 0 70%,#00f 0 100%)}

csslop ✓

p{background:linear-gradient(red 40%,tan 0 70%,#00f 0)}

cssnano ✗

p{background:linear-gradient(red 0 40%,tan 0 70%,#00f 0 100%)}

csso ✗

p{background:linear-gradient(red 0 40%,tan 0 70%,#00f 0 100%)}

esbuild ✗

p{background:linear-gradient(red 0% 40%,tan 0 70%,#00f 0 100%)}

lightningcss ✗

p{background:linear-gradient(red 0% 40%,tan 0 70%,#00f 0 100%)}

sass ✗

p{background:linear-gradient(red 0% 40%, tan 0 70%, #00f 0 100%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Replace color-start with 0 when it matches the prior color-stop

You can group the start and stop of a color in a gradient together with a space separation. You can also use a unitless 0 to indicate the first position of a color is the same as the stop of the previous color.

p {
  background: linear-gradient(
    red 40%,
    tan 40% 70%,
    #00f 70%
  );
}

Expected

p{background:linear-gradient(red 40%,tan 0 70%,#00f 0)}

Outputs

clean-css ✗

p{background:linear-gradient(red 40%,tan 40% 70%,#00f 70%)}

csskit ✗

p{background:linear-gradient(red 40%,tan 40% 70%,#00f 70%)}

csslop ✓

p{background:linear-gradient(red 40%,tan 0 70%,#00f 0)}

cssnano ✓

p{background:linear-gradient(red 40%,tan 0 70%,#00f 0)}

csso ✗

p{background:linear-gradient(red 40%,tan 40% 70%,#00f 70%)}

esbuild ✗

p{background:linear-gradient(red 40%,tan 40% 70%,#00f 70%)}

lightningcss ✗

p{background:linear-gradient(red 40%,tan 40% 70%,#00f 70%)}

sass ✗

p{background:linear-gradient(red 40%, tan 40% 70%, #00f 70%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove triple 100% from end of gradient

If a gradient's color stop ends with 100%, and is followed by another color of 100% start and 100% end, both can be removed, as the 100% will be implied. Even though the prior color stop ends with the same percent as the start of the next color, since the start is 100% it can be removed, instead of replaced with unitless 0.

p {
  background: radial-gradient(
    circle farthest-side at 0% 0%,
    #000 0% 25%,
    #ff0 50% 100%,
    #000 100% 100%
  );
}

Expected

p{background:radial-gradient(circle farthest-side at 0% 0%,#000 25%,#ff0 50% 100%,#000)}

Outputs

clean-css ✗

p{background:radial-gradient(circle farthest-side at 0 0,#000 0 25%,#ff0 50% 100%,#000 100% 100%)}

csskit ✗

p{background:radial-gradient(circle farthest-side at 0% 0%,#000 0% 25%,#ff0 50% 100%,#000 100% 100%)}

csslop ✓

p{background:radial-gradient(circle farthest-side at 0% 0%,#000 25%,#ff0 50% 100%,#000)}

cssnano ✗

p{background:radial-gradient(circle farthest-side at 0 0,#000 0 25%,#ff0 50% 100%,#000 0 0)}

csso ✗

p{background:radial-gradient(circle farthest-side at 0 0,#000 0 25%,#ff0 50% 100%,#000 100% 100%)}

esbuild ✗

p{background:radial-gradient(circle farthest-side at 0% 0%,#000 0% 25%,#ff0 50% 100%,#000 100% 100%)}

lightningcss ✗

p{background:radial-gradient(circle farthest-side at 0 0,#000 0% 25%,#ff0 50% 100%,#000 100% 100%)}

sass ✗

p{background:radial-gradient(circle farthest-side at 0% 0%, #000 0% 25%, #ff0 50% 100%, #000 100% 100%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove triple 0% from start of gradient

If the first color in a gradient has a color start and stop of 0%, both can be removed. If the following color starts with the same percent as the stop of the previous color, it can be renamed to a unitless 0.

p {
  background: radial-gradient(
    circle farthest-side at 0% 0%,
    #0ff 0% 0%,
    #000 0% 25%,
    #ff0 50%
  );
}

Expected

p{background:radial-gradient(circle farthest-side at 0% 0%,#0ff,#000 0 25%,#ff0 50%)}

Outputs

clean-css ✗

p{background:radial-gradient(circle farthest-side at 0 0,#0ff 0 0,#000 0 25%,#ff0 50%)}

csskit ✗

p{background:radial-gradient(circle farthest-side at 0% 0%,#0ff 0% 0%,#000 0% 25%,#ff0 50%)}

csslop ✓

p{background:radial-gradient(circle farthest-side at 0% 0%,#0ff,#000 0 25%,#ff0 50%)}

cssnano ✗

p{background:radial-gradient(circle farthest-side at 0 0,#0ff 0 0,#000 0 25%,#ff0 50%)}

csso ✗

p{background:radial-gradient(circle farthest-side at 0 0,#0ff 0 0,#000 0 25%,#ff0 50%)}

esbuild ✗

p{background:radial-gradient(circle farthest-side at 0% 0%,#0ff 0% 0%,#000 0% 25%,#ff0 50%)}

lightningcss ✗

p{background:radial-gradient(circle farthest-side at 0 0,#0ff 0% 0%,#000 0% 25%,#ff0 50%)}

sass ✗

p{background:radial-gradient(circle farthest-side at 0% 0%, #0ff 0% 0%, #000 0% 25%, #ff0 50%)}
1ms
1ms
1ms
2ms
1ms
1ms
1ms
1ms
Subtotal 0 / 11 0 / 11 11 / 11 2 / 11 0 / 11 1 / 11 4 / 11 0 / 11
Percent 0% 0% 100% 18.18% 0% 9.09% 36.36% 0%
Duration 2ms 2ms 2ms 9ms 3ms 7ms 1ms 8ms

idempotency #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗
Details

Test idempotency

Given a test input like this: a{color:rebeccapurple;}, a minifier may produce an output like this a{color:#663399}. If we then run that output through the minfier and it produces a different value, like a{color:#639}, then it has failed at idempotency for that test.

A minifier is considered idempotent if the minified output could be minifed itself and no changes occur. This test takes the minified output of every test in the css-minify-tests Test Suite, and checks if they change when being minifed.

If a minifier fails idempotency for any test, the name of the test will be listed in the Outputs below.

/* See description for test details. */

Expected

Outputs

clean-css ✗

Failures: colors/0011, colors/0055, shorthands/0047

csskit ✓

csslop ✓

cssnano ✓

csso ✗

Failures: shorthands/0053

esbuild ✗

Failures: shorthand-values/0016

lightningcss ✗

Failures: combined/0002, selectors/0019

sass ✗

Failures: colors/0022
0ms
0ms
0ms
0ms
0ms
0ms
0ms
0ms
Subtotal 0 / 1 1 / 1 1 / 1 1 / 1 0 / 1 0 / 1 0 / 1 0 / 1
Percent 0% 100% 100% 100% 0% 0% 0% 0%
Duration 0ms 0ms 0ms 0ms 0ms 0ms 0ms 0ms

import #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

url() to string form in @import

@import url("foo.css") can be shortened to @import"foo.css". The CSS spec allows both forms and the string form is always shorter. No space needed between @import and the string token.

@import url("foo.css");

Expected

@import"foo.css";

Outputs

clean-css ✗

@import url(foo.css);

csskit ✓

@import"foo.css";

csslop ✓

@import"foo.css";

cssnano ✗

@import "foo.css";

csso ✗

@import url(foo.css);

esbuild ✓

@import"foo.css";

lightningcss ✗

@import "foo.css";

sass ✗

@import"foo.css"
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0002 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

@import with media query

@import url("foo.css") screen shortens to @import"foo.css"screen. The string form removes the url() wrapper and no space is needed between the closing quote and the media query keyword.

@import url("foo.css") screen;

Expected

@import"foo.css"screen;

Outputs

clean-css ✗

@import url(foo.css) screen;

csskit ✓

@import"foo.css"screen;

csslop ✓

@import"foo.css"screen;

cssnano ✗

@import "foo.css" screen;

csso ✗

@import url(foo.css)screen;

esbuild ✓

@import"foo.css"screen;

lightningcss ✗

@import "foo.css" screen;

sass ✗

@import"foo.css"screen
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Unquoted url() to string form in @import

@import url(foo.css) shortens to @import"foo.css". Even when the url() value is already unquoted, the string form is shorter because it removes the url( and ) wrapper (5 chars) and adds only 2 quote chars.

@import url(foo.css);

Expected

@import"foo.css";

Outputs

clean-css ✗

@import url(foo.css);

csskit ✓

@import"foo.css";

csslop ✓

@import"foo.css";

cssnano ✗

@import "foo.css";

csso ✗

@import url(foo.css);

esbuild ✓

@import"foo.css";

lightningcss ✗

@import "foo.css";

sass ✗

@import"foo.css"
1ms
1ms
1ms
1ms
1ms
1ms
1ms
7ms
0004 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

@import with layer keyword

@import url("foo.css") layer shortens to @import"foo.css"layer. The layer keyword follows the import URL and no space is needed after the closing quote.

@import url("foo.css") layer;

Expected

@import"foo.css"layer;

Outputs

clean-css ✗

@import url(foo.css) layer;

csskit ✓

@import"foo.css"layer;

csslop ✓

@import"foo.css"layer;

cssnano ✗

@import "foo.css" layer;

csso ✗

@import url(foo.css)layer;

esbuild ✓

@import"foo.css"layer;

lightningcss ✗

@import "foo.css" layer;

sass ✗

@import"foo.css"layer
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

@import with layer() and supports()

@import url("foo.css") layer(base) supports(foo: bar) removes url() wrapper, removes spaces between the closing ) and next keyword/function, and removes whitespace inside the supports() condition. Uses an unknown property so minifiers cannot elide the supports() condition.

@import url("foo.css") layer(base) supports(foo: bar);

Expected

@import"foo.css"layer(base)supports(foo:bar);

Outputs

clean-css ✗

@import url(foo.css) layer(base) supports(foo: bar);

csskit ✗

@import url("foo.css") layer(base) supports(foo:bar);

csslop ✓

@import"foo.css"layer(base)supports(foo:bar);

cssnano ✗

@import "foo.css" layer(base) supports(foo: bar);

csso ✗

@import url("foo.css") layer(base) supports(foo: bar);

esbuild ✗

@import"foo.css"layer(base) supports(foo: bar);

lightningcss ✗

@import "foo.css" layer(base) supports(foo:bar);

sass ✗

@import"foo.css"layer(base) supports(foo: bar)
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 5 4 / 5 5 / 5 0 / 5 0 / 5 4 / 5 0 / 5 0 / 5
Percent 0% 80% 100% 0% 0% 80% 0% 0%
Duration 1ms 1ms 1ms 2ms 1ms 3ms 1ms 10ms

initial #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert background-clip border-box value to initial

The initial value for background-clip is border-box, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  background-clip: border-box;
}

Expected

a{background-clip:initial}

Outputs

clean-css ✗

a{background-clip:border-box}

csskit ✗

a{background-clip:border-box}

csslop ✓

a{background-clip:initial}

cssnano ✓

a{background-clip:initial}

csso ✗

a{background-clip:border-box}

esbuild ✗

a{background-clip:border-box}

lightningcss ✗

a{background-clip:border-box}

sass ✗

a{background-clip:border-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert background-origin padding-box value to initial

The initial value for background-origin is padding-box, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  background-origin: padding-box;
}

Expected

a{background-origin:initial}

Outputs

clean-css ✗

a{background-origin:padding-box}

csskit ✗

a{background-origin:padding-box}

csslop ✓

a{background-origin:initial}

cssnano ✓

a{background-origin:initial}

csso ✗

a{background-origin:padding-box}

esbuild ✗

a{background-origin:padding-box}

lightningcss ✗

a{background-origin:padding-box}

sass ✗

a{background-origin:padding-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert border-collapse separate value to initial

The initial value for border-collapse is separate, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  border-collapse: separate;
}

Expected

a{border-collapse:initial}

Outputs

clean-css ✗

a{border-collapse:separate}

csskit ✗

a{border-collapse:separate}

csslop ✓

a{border-collapse:initial}

cssnano ✓

a{border-collapse:initial}

csso ✗

a{border-collapse:separate}

esbuild ✗

a{border-collapse:separate}

lightningcss ✗

a{border-collapse:separate}

sass ✗

a{border-collapse:separate}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert box-sizing content-box value to initial

The initial value for box-sizing is content-box, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

p {
  box-sizing: content-box;
}

Expected

p{box-sizing:initial}

Outputs

clean-css ✗

p{box-sizing:content-box}

csskit ✗

p{box-sizing:content-box}

csslop ✓

p{box-sizing:initial}

cssnano ✓

p{box-sizing:initial}

csso ✗

p{box-sizing:content-box}

esbuild ✗

p{box-sizing:content-box}

lightningcss ✗

p{box-sizing:content-box}

sass ✗

p{box-sizing:content-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert mask-clip border-box value to initial

The initial value for mask-clip is border-box, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  mask-clip: border-box;
}

Expected

a{mask-clip:initial}

Outputs

clean-css ✗

a{mask-clip:border-box}

csskit ✗

a{mask-clip:border-box}

csslop ✓

a{mask-clip:initial}

cssnano ✓

a{mask-clip:initial}

csso ✗

a{mask-clip:border-box}

esbuild ✗

a{mask-clip:border-box}

lightningcss ✗

a{mask-clip:border-box}

sass ✗

a{mask-clip:border-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert mask-origin border-box value to initial

The initial value for mask-origin is border-box, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  mask-origin: border-box;
}

Expected

a{mask-origin:initial}

Outputs

clean-css ✗

a{mask-origin:border-box}

csskit ✗

a{mask-origin:border-box}

csslop ✓

a{mask-origin:initial}

cssnano ✓

a{mask-origin:initial}

csso ✗

a{mask-origin:border-box}

esbuild ✗

a{mask-origin:border-box}

lightningcss ✗

a{mask-origin:border-box}

sass ✗

a{mask-origin:border-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Convert vertical-align baseline value to initial

The initial value for vertical-align is baseline, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  vertical-align: baseline;
}

Expected

a{vertical-align:initial}

Outputs

clean-css ✗

a{vertical-align:baseline}

csskit ✗

a{vertical-align:baseline}

csslop ✓

a{vertical-align:initial}

cssnano ✓

a{vertical-align:initial}

csso ✗

a{vertical-align:baseline}

esbuild ✗

a{vertical-align:baseline}

lightningcss ✗

a{vertical-align:baseline}

sass ✗

a{vertical-align:baseline}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert writing-mode horizontal-tb value to initial

The initial value for writing-mode is horizontal-tb, so they are equivalent. However, initial is both shorter and a global value (giving more opportunities for gzipping).

a {
  writing-mode: horizontal-tb;
}

Expected

a{writing-mode:initial}

Outputs

clean-css ✗

a{writing-mode:horizontal-tb}

csskit ✗

a{writing-mode:horizontal-tb}

csslop ✓

a{writing-mode:initial}

cssnano ✗

a{writing-mode:horizontal-tb}

csso ✗

a{writing-mode:horizontal-tb}

esbuild ✗

a{writing-mode:horizontal-tb}

lightningcss ✗

a{writing-mode:horizontal-tb}

sass ✗

a{writing-mode:horizontal-tb}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 8 0 / 8 8 / 8 7 / 8 0 / 8 0 / 8 0 / 8 0 / 8
Percent 0% 0% 100% 87.5% 0% 0% 0% 0%
Duration 1ms 2ms 1ms 3ms 1ms 4ms 1ms 2ms

keyframes #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

@keyframes 100% to to

The 100% keyframe stop can be replaced with the shorter to keyword.

@keyframes fade {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

Expected

@keyframes fade{0%{opacity:0}to{opacity:1}}

Outputs

clean-css ✗

@keyframes fade{0%{opacity:0}100%{opacity:1}}

csskit ✗

@keyframes fade{0%{opacity:0}100%{opacity:1}}

csslop ✓

@keyframes fade{0%{opacity:0}to{opacity:1}}

cssnano ✓

@keyframes fade{0%{opacity:0}to{opacity:1}}

csso ✓

@keyframes fade{0%{opacity:0}to{opacity:1}}

esbuild ✓

@keyframes fade{0%{opacity:0}to{opacity:1}}

lightningcss ✓

@keyframes fade{0%{opacity:0}to{opacity:1}}

sass ✗

@keyframes fade{0%{opacity:0}100%{opacity:1}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Keyframes from/to keyword optimization

from becomes 0% and 100% becomes to since these are shorter representations.

@keyframes fade {
  from {
    opacity: 0;
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 1;
  }
}

Expected

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

Outputs

clean-css ✗

@keyframes fade{from{opacity:0}50%{opacity:.5}100%{opacity:1}}

csskit ✗

@keyframes fade{from{opacity:0}50%{opacity:.5}100%{opacity:1}}

csslop ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

cssnano ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

csso ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

esbuild ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

lightningcss ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}to{opacity:1}}

sass ✗

@keyframes fade{from{opacity:0}50%{opacity:.5}100%{opacity:1}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 2 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2
Percent 0% 0% 100% 100% 100% 100% 100% 0%
Duration 1ms 1ms 1ms 1ms 1ms 1ms 1ms 1ms

lang #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Remove quotes from language-code in :lang()

Quotes surrounding a language code (en, fr-ca, etc) are optional in the CSS :lang() function.

:lang("fr") {
  color: red;
}

Expected

:lang(fr){color:red}

Outputs

clean-css ✗

:lang("fr"){color:red}

csskit ✗

:lang("fr"){color:red}

csslop ✓

:lang(fr){color:red}

cssnano ✓

:lang(fr){color:red}

csso ✗

esbuild ✗

:lang("fr"){color:red}

lightningcss ✓

:lang(fr){color:red}

sass ✗

:lang("fr"){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine language-codes in :lang() with comma-separation

The :lang() function accepts a comma-separated list of language codes.

:lang(de), :lang(ja) {
  color: red;
}

Expected

:lang(de,ja){color:red}

Outputs

clean-css ✗

:lang(de),:lang(ja){color:red}

csskit ✗

:lang(de),:lang(ja){color:red}

csslop ✓

:lang(de,ja){color:red}

cssnano ✗

:lang(de),:lang(ja){color:red}

csso ✗

:lang(de),:lang(ja){color:red}

esbuild ✗

:lang(de),:lang(ja){color:red}

lightningcss ✗

:lang(de),:lang(ja){color:red}

sass ✗

:lang(de),:lang(ja){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Lowercase language-codes in :lang()

Language-codes used by :lang() are case-insensitive. Lowercasing them consistently, much like with hex values in color, can potentially aid in gzip compression.

:lang(DE) {
  color: red;
}

Expected

:lang(de){color:red}

Outputs

clean-css ✗

:lang(DE){color:red}

csskit ✗

:lang(DE){color:red}

csslop ✓

:lang(de){color:red}

cssnano ✗

:lang(DE){color:red}

csso ✗

:lang(DE){color:red}

esbuild ✗

:lang(DE){color:red}

lightningcss ✗

:lang(DE){color:red}

sass ✗

:lang(DE){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove quotes from :lang() while escaping wildcard

Language-codes used by :lang() can contain an asterisk as wild card character. Because this character also has semantic significance in CSS, it must either be quoted, or escaped. In this case, escaping requires fewer characters.

:lang("*-latn") {
  color: red;
}

Expected

:lang(\*-latn){color:red}

Outputs

clean-css ✗

:lang("*-latn"){color:red}

csskit ✗

:lang("*-latn"){color:red}

csslop ✓

:lang(\*-latn){color:red}

cssnano ✗

:lang("*-latn"){color:red}

csso ✗

esbuild ✗

:lang("*-latn"){color:red}

lightningcss ✓

:lang(\*-latn){color:red}

sass ✗

:lang("*-latn"){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Add quotes to :lang() when shorter than escaping

Language-codes used by :lang() can contain an asterisk as wild card character. Because this character also has semantic significance in CSS, it must either be quoted, or escaped. In cases where you are escaping more than twice, it is shorter to wrap the language-code in quotes.

:lang(\*-\*-\*,fr) {
  color: red;
}

Expected

:lang("*-*-*",fr){color:red}

Outputs

clean-css ✗

:lang(\*-\*-\*,fr){color:red}

csskit ✗

:lang(\*-\*-\*,fr){color:red}

csslop ✓

:lang("*-*-*",fr){color:red}

cssnano ✗

:lang(\*-\*-\*,fr){color:red}

csso ✗

esbuild ✗

:lang(\*-\*-\*,fr){color:red}

lightningcss ✗

:lang(\*-\*-\*,fr){color:red}

sass ✗

:lang(\*-\*-\*,fr){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine selectors that only differ in language-code in :lang()

If a rule has selectors that are identical except for the language-code used in :lang(), they can be combined.

:lang(tr) .foo em,
:lang(zh-hant) .foo em {
  color: red;
}

Expected

:lang(tr,zh-hant) .foo em{color:red}

Outputs

clean-css ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

csskit ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

csslop ✓

:lang(tr,zh-hant) .foo em{color:red}

cssnano ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

csso ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

esbuild ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

lightningcss ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}

sass ✗

:lang(tr) .foo em,:lang(zh-hant) .foo em{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine rules that only differ in language-code in :lang()

If two rules are identical except for the language-code used in :lang(), they can be combined, with the language-codes comma-separated inside the :lang() function.

:lang(ar) .foo em {
  color: red;
}
:lang(ru) .foo em {
  color: red;
}

Expected

:lang(ar,ru) .foo em{color:red}

Outputs

clean-css ✗

:lang(ar) .foo em,:lang(ru) .foo em{color:red}

csskit ✗

:lang(ar) .foo em{color:red}:lang(ru) .foo em{color:red}

csslop ✓

:lang(ar,ru) .foo em{color:red}

cssnano ✗

:lang(ar) .foo em,:lang(ru) .foo em{color:red}

csso ✗

:lang(ar) .foo em,:lang(ru) .foo em{color:red}

esbuild ✗

:lang(ar) .foo em{color:red}:lang(ru) .foo em{color:red}

lightningcss ✗

:lang(ar) .foo em,:lang(ru) .foo em{color:red}

sass ✗

:lang(ar) .foo em{color:red}:lang(ru) .foo em{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 7 0 / 7 7 / 7 1 / 7 0 / 7 0 / 7 2 / 7 0 / 7
Percent 0% 0% 100% 14.28% 0% 0% 28.57% 0%
Duration 1ms 1ms 2ms 5ms 1ms 5ms 1ms 4ms

layer #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Duplicate @layer statement removal

Duplicate @layer declarations can be deduplicated since layer order is established by first occurrence.

@layer utilities;
@layer utilities;
a {
  color: red;
}

Expected

@layer utilities;a{color:red}

Outputs

clean-css ✗

@layer utilities;@layer utilities;a{color:red}

csskit ✗

@layer utilities;@layer utilities;a{color:red}

csslop ✓

@layer utilities;a{color:red}

cssnano ✗

@layer utilities;@layer utilities;a{color:red}

csso ✗

@layer utilities;@layer utilities;a{color:red}

esbuild ✗

@layer utilities;@layer utilities;a{color:red}

lightningcss ✓

@layer utilities;a{color:red}

sass ✗

@layer utilities;@layer utilities;a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Non-adjacent @layer blocks can merge

Layer ordering is determined by first appearance of the layer name, not by physical position of rule blocks. Two @layer base blocks separated by @layer utilities can be merged because layer precedence is unchanged.

@layer base {
  a {
    color: blue;
  }
}

@layer utilities {
  a {
    color: red;
  }
}

@layer base {
  a {
    font-weight: bold;
  }
}

Expected

@layer base{a{color:#00f;font-weight:700}}@layer utilities{a{color:red}}

Outputs

clean-css ✓

@layer base{a{color:#00f;font-weight:700}}@layer utilities{a{color:red}}

csskit ✗

@layer base{a{color:blue}}@layer utilities{a{color:red}}@layer base{a{font-weight:bold}}

csslop ✓

@layer base{a{color:#00f;font-weight:700}}@layer utilities{a{color:red}}

cssnano ✗

@layer base{a{color:blue}}@layer utilities{a{color:red}}@layer base{a{font-weight:700}}

csso ✗

@layer base{a{color:#00f}}@layer utilities{a{color:red}}@layer base{a{font-weight:700}}

esbuild ✗

@layer base{a{color:#00f}}@layer utilities{a{color:red}}@layer base{a{font-weight:700}}

lightningcss ✓

@layer base{a{color:#00f;font-weight:700}}@layer utilities{a{color:red}}

sass ✗

@layer base{a{color:blue}}@layer utilities{a{color:red}}@layer base{a{font-weight:bold}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✗ ✓ ✓ ✓ ✗
Details

Remove whitespace in comma-separated layer names

Spaces and returns between named cascade layers are optional.

@layer reset, base,
       components;
a{color:red}

Expected

@layer reset,base,components;a{color:red}

Outputs

clean-css ✗

csskit ✓

@layer reset,base,components;a{color:red}

csslop ✓

@layer reset,base,components;a{color:red}

cssnano ✗

@layer reset, base,
       components;a{color:red}

csso ✓

@layer reset,base,components;a{color:red}

esbuild ✓

@layer reset,base,components;a{color:red}

lightningcss ✓

@layer reset,base,components;a{color:red}

sass ✗

@layer reset, base,
       components;a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Remove @layer trailing semi-colon

If there is no other CSS after a @layer declaration, then the semi-colon is not separating it from anything and can be removed.

@layer reset,base;

Expected

@layer reset,base

Outputs

clean-css ✗

csskit ✗

@layer reset,base;

csslop ✓

@layer reset,base

cssnano ✗

@layer reset,base;

csso ✗

@layer reset,base;

esbuild ✗

@layer reset,base;

lightningcss ✗

@layer reset,base;

sass ✓

@layer reset,base
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 1 / 4 1 / 4 4 / 4 0 / 4 1 / 4 1 / 4 3 / 4 1 / 4
Percent 25% 25% 100% 0% 25% 25% 75% 25%
Duration 1ms 1ms 1ms 2ms 1ms 3ms 1ms 2ms

media #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@media whitespace removal

Strip whitespace inside a basic @media block with nested rules.

@media screen {
  a {
    color: red;
  }
}

Expected

@media screen{a{color:red}}

Outputs

clean-css ✓

@media screen{a{color:red}}

csskit ✓

@media screen{a{color:red}}

csslop ✓

@media screen{a{color:red}}

cssnano ✓

@media screen{a{color:red}}

csso ✓

@media screen{a{color:red}}

esbuild ✓

@media screen{a{color:red}}

lightningcss ✓

@media screen{a{color:red}}

sass ✓

@media screen{a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Nested @media must not be merged

Nested @media rules should be minified but NOT flattened into a single rule. Merging can change cascade behavior in edge cases.

@media screen {
  @media (width >= 768px) {
    a {
      color: red;
    }
  }
}

Expected

@media screen{@media(width>=768px){a{color:red}}}

Outputs

clean-css ✗

@media screen{@media (width >= 768px){a{color:red}}}

csskit ✓

@media screen{@media(width>=768px){a{color:red}}}

csslop ✓

@media screen{@media(width>=768px){a{color:red}}}

cssnano ✓

@media screen{@media(width>=768px){a{color:red}}}

csso ✗

esbuild ✓

@media screen{@media(width>=768px){a{color:red}}}

lightningcss ✗

@media screen{@media (width>=768px){a{color:red}}}

sass ✗

@media screen and (width >= 768px){a{color:red}}
1ms
1ms
1ms
2ms
1ms
4ms
1ms
1ms
0003 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Remove redundant all and in @media

@media all and (condition) is equivalent to @media (condition).

@media all and (width >= 500px) {
  a {
    color: red;
  }
}

Expected

@media(width>=500px){a{color:red}}

Outputs

clean-css ✗

@media all and (width >= 500px){a{color:red}}

csskit ✗

@media all and (width>=500px){a{color:red}}

csslop ✓

@media(width>=500px){a{color:red}}

cssnano ✓

@media(width>=500px){a{color:red}}

csso ✗

esbuild ✗

@media all and (width>=500px){a{color:red}}

lightningcss ✗

@media (width>=500px){a{color:red}}

sass ✗

@media all and (width >= 500px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
2ms
0004 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Adjacent identical @media merge

Adjacent @media blocks with the same query should be merged into one.

@media screen and (width >= 768px) {
  a {
    color: red;
  }
}
@media screen and (width >= 768px) {
  b {
    margin: 0;
  }
}

Expected

@media screen and (width>=768px){a{color:red}b{margin:0}}

Outputs

clean-css ✗

@media screen and (width >= 768px){a{color:red}b{margin:0}}

csskit ✗

@media screen and (width>=768px){a{color:red}}@media screen and (width>=768px){b{margin:0}}

csslop ✓

@media screen and (width>=768px){a{color:red}b{margin:0}}

cssnano ✓

@media screen and (width>=768px){a{color:red}b{margin:0}}

csso ✗

esbuild ✗

@media screen and (width>=768px){a{color:red}}@media screen and (width>=768px){b{margin:0}}

lightningcss ✓

@media screen and (width>=768px){a{color:red}b{margin:0}}

sass ✗

@media screen and (width >= 768px){a{color:red}}@media screen and (width >= 768px){b{margin:0}}
1ms
4ms
1ms
2ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert min-width to range syntax

(min-width: 768px) can be shortened to (width>=768px) using Media Queries Level 4 range syntax. Do not minify to width>767px as this is not technically accurate, because fractional units, like 767.5px are valid in CSS and would be included with >767px, but would not be included with >=768px.

@media (min-width: 768px) {
  a {
    color: red;
  }
}

Expected

@media(width>=768px){a{color:red}}

Outputs

clean-css ✗

@media (min-width:768px){a{color:red}}

csskit ✗

@media(min-width:768px){a{color:red}}

csslop ✓

@media(width>=768px){a{color:red}}

cssnano ✗

@media(min-width:768px){a{color:red}}

csso ✗

@media (min-width:768px){a{color:red}}

esbuild ✗

@media(min-width:768px){a{color:red}}

lightningcss ✗

@media (width>=768px){a{color:red}}

sass ✗

@media(min-width: 768px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert max-width to range syntax

(max-width: 1024px) can be shortened to (width<=1024px) using Media Queries Level 4 range syntax. Do not minify to width<1025px as this is not technically accurate, because fractional units, like 1024.5px are valid in CSS.

@media (max-width: 1024px) {
  a {
    color: red;
  }
}

Expected

@media(width<=1024px){a{color:red}}

Outputs

clean-css ✗

@media (max-width:1024px){a{color:red}}

csskit ✗

@media(max-width:1024px){a{color:red}}

csslop ✓

@media(width<=1024px){a{color:red}}

cssnano ✗

@media(max-width:1024px){a{color:red}}

csso ✗

@media (max-width:1024px){a{color:red}}

esbuild ✗

@media(max-width:1024px){a{color:red}}

lightningcss ✗

@media (width<=1024px){a{color:red}}

sass ✗

@media(max-width: 1024px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert min-height to range syntax

(min-height: 600px) can be shortened to (height>=600px) using Media Queries Level 4 range syntax. Do not minify to height>599px as this is not technically accurate, because fractional units, like 599.5px are valid in CSS and would be included with <599px, but would not be included with <=600px.

@media (min-height: 600px) {
  a {
    color: red;
  }
}

Expected

@media(height>=600px){a{color:red}}

Outputs

clean-css ✗

@media (min-height:600px){a{color:red}}

csskit ✗

@media(min-height:600px){a{color:red}}

csslop ✓

@media(height>=600px){a{color:red}}

cssnano ✗

@media(min-height:600px){a{color:red}}

csso ✗

@media (min-height:600px){a{color:red}}

esbuild ✗

@media(min-height:600px){a{color:red}}

lightningcss ✗

@media (height>=600px){a{color:red}}

sass ✗

@media(min-height: 600px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert max-height to range syntax

(max-height: 900px) can be shortened to (height<=900px) using Media Queries Level 4 range syntax. Do not minify to height<899px as this is not technically accurate, because fractional units, like 899.5px are valid in CSS and would be included with <899px, but would not be included with <=900px.

@media (max-height: 900px) {
  a {
    color: red;
  }
}

Expected

@media(height<=900px){a{color:red}}

Outputs

clean-css ✗

@media (max-height:900px){a{color:red}}

csskit ✗

@media(max-height:900px){a{color:red}}

csslop ✓

@media(height<=900px){a{color:red}}

cssnano ✗

@media(max-height:900px){a{color:red}}

csso ✗

@media (max-height:900px){a{color:red}}

esbuild ✗

@media(max-height:900px){a{color:red}}

lightningcss ✗

@media (height<=900px){a{color:red}}

sass ✗

@media(max-height: 900px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combined min-width/max-width to range syntax

(min-width: 768px) and (max-width: 1024px) can be collapsed into a single range condition (768px<=width<=1024px), eliminating the and keyword and one pair of parentheses. Do not minify to 767px<width<1025px as this is not technically accurate, because fractional units, like 767.5px are valid in CSS.

@media (min-width: 768px) and (max-width: 1024px) {
  a {
    color: red;
  }
}

Expected

@media(768px<=width<=1024px){a{color:red}}

Outputs

clean-css ✗

@media (min-width:768px) and (max-width:1024px){a{color:red}}

csskit ✗

@media(min-width:768px)and (max-width:1024px){a{color:red}}

csslop ✓

@media(768px<=width<=1024px){a{color:red}}

cssnano ✗

@media(min-width:768px) and (max-width:1024px){a{color:red}}

csso ✗

@media (min-width:768px) and (max-width:1024px){a{color:red}}

esbuild ✗

@media(min-width:768px)and (max-width:1024px){a{color:red}}

lightningcss ✗

@media (width>=768px) and (width<=1024px){a{color:red}}

sass ✗

@media(min-width: 768px)and (max-width: 1024px){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

Retain @custom-media

The @custom-media feature allows for abstracting and reusing media query conditions. It should be retained with optional whitespace removed.

@custom-media --modern (color), (hover);
@media (--modern) {
  a {
    color: red;
  }
}

Expected

@custom-media --modern (color),(hover);@media(--modern){a{color:red}}

Outputs

clean-css ✗

csskit ✗

@custom-media --modern (color),(hover);@media (--modern){a{color:red}}

csslop ✓

@custom-media --modern (color),(hover);@media(--modern){a{color:red}}

cssnano ✗

@custom-media --modern (color), (hover);@media(--modern){a{color:red}}

csso ✗

@custom-media --modern (color),(hover);@media (--modern){a{color:red}}

esbuild ✓

@custom-media --modern (color),(hover);@media(--modern){a{color:red}}

lightningcss ✗

@custom-media --modern (color), (hover);@media (--modern){a{color:red}}

sass ✗

@custom-media --modern (color), (hover);@media(--modern){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Remove space after @media before parens in media query

If the first condition in a media query is wrapped in parenthesis, then a space following @media is not required.

@media (orientation: landscape) {
  a {
    color: red;
  }
}

Expected

@media(orientation:landscape){a{color:red}}

Outputs

clean-css ✗

@media (orientation:landscape){a{color:red}}

csskit ✓

@media(orientation:landscape){a{color:red}}

csslop ✓

@media(orientation:landscape){a{color:red}}

cssnano ✓

@media(orientation:landscape){a{color:red}}

csso ✗

@media (orientation:landscape){a{color:red}}

esbuild ✓

@media(orientation:landscape){a{color:red}}

lightningcss ✗

@media (orientation:landscape){a{color:red}}

sass ✗

@media(orientation: landscape){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Remove space after parens before and in media query

If a media query contains an and following a condition wrapped in parenthesis, there is no need to include a space to separate them. It does however require a space after the and.

@media(width>30em) and (orientation: landscape) {
  a {
    color: red;
  }
}

Expected

@media(width>30em)and (orientation:landscape){a{color:red}}

Outputs

clean-css ✗

@media(width>30em) and (orientation:landscape){a{color:red}}

csskit ✓

@media(width>30em)and (orientation:landscape){a{color:red}}

csslop ✓

@media(width>30em)and (orientation:landscape){a{color:red}}

cssnano ✗

@media(width>30em) and (orientation:landscape){a{color:red}}

csso ✗

esbuild ✓

@media(width>30em)and (orientation:landscape){a{color:red}}

lightningcss ✗

@media (width>30em) and (orientation:landscape){a{color:red}}

sass ✗

@media(width > 30em)and (orientation: landscape){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

Merge identical media queries with differing whitespace

This tests validates that minifiers do not treat media queries differently when there is, or is not, a space following @media before the start of the conditions parenthesis (().

@media (prefers-color-scheme: dark) {
  a {
    color: red;
  }
}
@media(prefers-color-scheme: dark) {
  a {
    color: red;
  }
}

Expected

@media(prefers-color-scheme:dark){a{color:red}}

Outputs

clean-css ✗

@media (prefers-color-scheme:dark){a{color:red}}@media(prefers-color-scheme:dark){a{color:red}}

csskit ✗

@media(prefers-color-scheme:dark){a{color:red}}@media(prefers-color-scheme:dark){a{color:red}}

csslop ✓

@media(prefers-color-scheme:dark){a{color:red}}

cssnano ✓

@media(prefers-color-scheme:dark){a{color:red}}

csso ✗

@media (prefers-color-scheme:dark){a{color:red}}

esbuild ✓

@media(prefers-color-scheme:dark){a{color:red}}

lightningcss ✗

@media (prefers-color-scheme:dark){a{color:red}}

sass ✗

@media(prefers-color-scheme: dark){a{color:red}}@media(prefers-color-scheme: dark){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 1 / 13 4 / 13 13 / 13 6 / 13 1 / 13 6 / 13 2 / 13 1 / 13
Percent 7.69% 30.76% 100% 46.15% 7.69% 46.15% 15.38% 7.69%
Duration 2ms 5ms 1ms 10ms 2ms 12ms 1ms 12ms

merging #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Merge adjacent rules with same selector

Adjacent rules sharing the same selector can be merged into one rule with combined declarations.

a {
  color: red;
}
a {
  font-size: 16px;
}

Expected

a{color:red;font-size:16px}

Outputs

clean-css ✓

a{color:red;font-size:16px}

csskit ✗

a{color:red}a{font-size:16px}

csslop ✓

a{color:red;font-size:16px}

cssnano ✓

a{color:red;font-size:16px}

csso ✓

a{color:red;font-size:16px}

esbuild ✗

a{color:red}a{font-size:16px}

lightningcss ✓

a{color:red;font-size:16px}

sass ✗

a{color:red}a{font-size:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Merge adjacent rules with same declarations

Adjacent rules with identical declarations can merge their selectors into a comma-separated list.

.a {
  color: red;
}
.b {
  color: red;
}

Expected

.a,.b{color:red}

Outputs

clean-css ✓

.a,.b{color:red}

csskit ✗

.a{color:red}.b{color:red}

csslop ✓

.a,.b{color:red}

cssnano ✓

.a,.b{color:red}

csso ✓

.a,.b{color:red}

esbuild ✓

.a,.b{color:red}

lightningcss ✓

.a,.b{color:red}

sass ✗

.a{color:red}.b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Merge adjacent @media with same query

Adjacent @media rules with the same query can be merged into a single @media block.

@media screen {
  a {
    color: red;
  }
}
@media screen {
  b {
    margin: 0;
  }
}

Expected

@media screen{a{color:red}b{margin:0}}

Outputs

clean-css ✓

@media screen{a{color:red}b{margin:0}}

csskit ✗

@media screen{a{color:red}}@media screen{b{margin:0}}

csslop ✓

@media screen{a{color:red}b{margin:0}}

cssnano ✓

@media screen{a{color:red}b{margin:0}}

csso ✓

@media screen{a{color:red}b{margin:0}}

esbuild ✗

@media screen{a{color:red}}@media screen{b{margin:0}}

lightningcss ✓

@media screen{a{color:red}b{margin:0}}

sass ✗

@media screen{a{color:red}}@media screen{b{margin:0}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✗ ✓ ✓ ✓ ✗ ✗ ✗
Details

Merge rules with identical selectors separated by non-overlapping rules

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

In this example <p class="x"> should have background:#0ff;color:#0f0.

.x {
  color: #0f0;
}
p {
  color: #00f;
}
#foo {
  padding: 2px;
}
.x {
  background: #0ff;
}

Expected

p{color:#00f}#foo{padding:2px}.x{color:#0f0;background:#0ff}

Also accepted

.x{color:#0f0;background:#0ff}p{color:#00f}#foo{padding:2px}
p{color:#00f}#foo{padding:2px}.x{background:#0ff;color:#0f0}
.x{background:#0ff;color:#0f0}p{color:#00f}#foo{padding:2px}

Outputs

clean-css ✓

.x{color:#0f0;background:#0ff}p{color:#00f}#foo{padding:2px}

csskit ✗

.x{color:#0f0}p{color:#00f}#foo{padding:2px}.x{background:#0ff}

csslop ✓

p{color:#00f}#foo{padding:2px}.x{color:#0f0;background:#0ff}

cssnano ✓

.x{color:#0f0;background:#0ff}p{color:#00f}#foo{padding:2px}

csso ✓

.x{color:#0f0;background:#0ff}p{color:#00f}#foo{padding:2px}

esbuild ✗

.x{color:#0f0}p{color:#00f}#foo{padding:2px}.x{background:#0ff}

lightningcss ✗

.x{color:#0f0}p{color:#00f}#foo{padding:2px}.x{background:#0ff}

sass ✗

.x{color:lime}p{color:blue}#foo{padding:2px}.x{background:aqua}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✓ ✓ ✗ ✗ ✗
Details

Partial merge of shared declarations

When adjacent rules share some declarations, the common part can be factored out.

a {
  color: red;
}
b {
  color: red;
  font-size: 16px;
}

Expected

a,b{color:red}b{font-size:16px}

Outputs

clean-css ✗

a{color:red}b{color:red;font-size:16px}

csskit ✗

a{color:red}b{color:red;font-size:16px}

csslop ✓

a,b{color:red}b{font-size:16px}

cssnano ✓

a,b{color:red}b{font-size:16px}

csso ✓

a,b{color:red}b{font-size:16px}

esbuild ✗

a{color:red}b{color:red;font-size:16px}

lightningcss ✗

a{color:red}b{color:red;font-size:16px}

sass ✗

a{color:red}b{color:red;font-size:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Overridden @keyframes removal

When two @keyframes share the same name, only the last definition is kept.

@keyframes fade {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
@keyframes fade {
  0% {
    opacity: 0.5;
  }
  100% {
    opacity: 1;
  }
}

Expected

@keyframes fade{0%{opacity:.5}to{opacity:1}}

Outputs

clean-css ✗

@keyframes fade{0%{opacity:0;opacity:.5}100%{opacity:1}}

csskit ✗

@keyframes fade{0%{opacity:0}100%{opacity:1}}@keyframes fade{0%{opacity:.5}100%{opacity:1}}

csslop ✓

@keyframes fade{0%{opacity:.5}to{opacity:1}}

cssnano ✓

@keyframes fade{0%{opacity:.5}to{opacity:1}}

csso ✓

@keyframes fade{0%{opacity:.5}to{opacity:1}}

esbuild ✗

@keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:.5}to{opacity:1}}

lightningcss ✓

@keyframes fade{0%{opacity:.5}to{opacity:1}}

sass ✗

@keyframes fade{0%{opacity:0}100%{opacity:1}}@keyframes fade{0%{opacity:.5}100%{opacity:1}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

Merging rules is unsafe with the all property

Merging .foo{color:red} and .bar{all:unset;color:red} into .bar,.foo{color:red}.bar{all:unset} is unsafe because all:unset resets color, so the merged color declaration would be overridden.

.foo {
  color: red;
}

.bar {
  all: unset;
  color: red;
}

Expected

.foo{color:red}.bar{all:unset;color:red}

Validate

Outputs

clean-css ✓

.foo{color:red}.bar{all:unset;color:red}

csskit ✓

.foo{color:red}.bar{all:unset;color:red}

csslop ✓

.foo{color:red}.bar{all:unset;color:red}

cssnano ✓

.foo{color:red}.bar{all:unset;color:red}

csso ✗

.bar,.foo{color:red}.bar{all:unset}

esbuild ✓

.foo{color:red}.bar{all:unset;color:red}

lightningcss ✓

.foo{color:red}.bar{all:unset;color:red}

sass ✓

.foo{color:red}.bar{all:unset;color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Non-adjacent @media merge is unsafe due to cascade

Merging two identical @media (width >= 1px) blocks that have intervening rules between them changes the cascade order. The intervening a{color:green} must remain between the two @media blocks so it correctly overrides the first block's color:red.

@media (width >= 1px) {
  a {
    color: red;
  }
}

a {
  color: green;
}

@media (width >= 1px) {
  a {
    background: #0b0;
  }
}

Expected

@media(width>=1px){a{color:red}}a{color:green}@media(width>=1px){a{background:#0b0}}

Validate

Outputs

clean-css ✗

@media (width >= 1px){a{color:red;background:#0b0}}a{color:green}

csskit ✓

@media(width>=1px){a{color:red}}a{color:green}@media(width>=1px){a{background:#0b0}}

csslop ✓

@media(width>=1px){a{color:red}}a{color:green}@media(width>=1px){a{background:#0b0}}

cssnano ✗

@media(width>=1px){a{color:red;background:#0b0}}a{color:green}

csso ✗

a{color:green}

esbuild ✓

@media(width>=1px){a{color:red}}a{color:green}@media(width>=1px){a{background:#0b0}}

lightningcss ✗

@media (width>=1px){a{color:red}}a{color:green}@media (width>=1px){a{background:#0b0}}

sass ✗

@media(width >= 1px){a{color:red}}a{color:green}@media(width >= 1px){a{background:#0b0}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

Merging rules is unsafe with all:initial

Merging .foo{color:red} and .bar{all:initial;color:red} into .bar,.foo{color:red}.bar{all:initial} is unsafe because all:initial resets color to its initial value, so the merged color declaration would be overridden.

.foo {
  color: red;
}

.bar {
  all: initial;
  color: red;
}

Expected

.foo{color:red}.bar{all:initial;color:red}

Validate

Outputs

clean-css ✓

.foo{color:red}.bar{all:initial;color:red}

csskit ✓

.foo{color:red}.bar{all:initial;color:red}

csslop ✓

.foo{color:red}.bar{all:initial;color:red}

cssnano ✓

.foo{color:red}.bar{all:initial;color:red}

csso ✗

.bar,.foo{color:red}.bar{all:initial}

esbuild ✓

.foo{color:red}.bar{all:initial;color:red}

lightningcss ✓

.foo{color:red}.bar{all:initial;color:red}

sass ✓

.foo{color:red}.bar{all:initial;color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

Merging rules is unsafe with all:revert-layer

Merging .foo{color:red} and .bar{all:revert-layer;color:red} into .bar,.foo{color:red}.bar{all:revert-layer} is unsafe because all:revert-layer rolls back color to the previous cascade layer, so the merged color declaration would be overridden.

.foo {
  color: red;
}

.bar {
  all: revert-layer;
  color: red;
}

Expected

.foo{color:red}.bar{all:revert-layer;color:red}

Validate

Outputs

clean-css ✓

.foo{color:red}.bar{all:revert-layer;color:red}

csskit ✓

.foo{color:red}.bar{all:revert-layer;color:red}

csslop ✓

.foo{color:red}.bar{all:revert-layer;color:red}

cssnano ✓

.foo{color:red}.bar{all:revert-layer;color:red}

csso ✗

.bar,.foo{color:red}.bar{all:revert-layer}

esbuild ✓

.foo{color:red}.bar{all:revert-layer;color:red}

lightningcss ✓

.foo{color:red}.bar{all:revert-layer;color:red}

sass ✓

.foo{color:red}.bar{all:revert-layer;color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Merging rules is unsafe when custom idents differ

animation-name: pulse and animation-name: shake name different keyframes, so the rules cannot share a selector list. Minifiers need to check the actual value to ensure they don't merge different values.

a {
  animation-name: pulse;
}

b {
  animation-name: shake;
}

Expected

a{animation-name:pulse}b{animation-name:shake}

Outputs

clean-css ✓

a{animation-name:pulse}b{animation-name:shake}

csskit ✓

a{animation-name:pulse}b{animation-name:shake}

csslop ✓

a{animation-name:pulse}b{animation-name:shake}

cssnano ✓

a{animation-name:pulse}b{animation-name:shake}

csso ✓

a{animation-name:pulse}b{animation-name:shake}

esbuild ✓

a{animation-name:pulse}b{animation-name:shake}

lightningcss ✓

a{animation-name:pulse}b{animation-name:shake}

sass ✓

a{animation-name:pulse}b{animation-name:shake}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Merging rules is unsafe when strings differ

content: "i" and content: "j" insert different text, so the rules cannot share a selector list. A minifier needs to check the actual value and cannot merge these rules.

a {
  content: "i";
}

b {
  content: "j";
}

Expected

a{content:"i"}b{content:"j"}

Outputs

clean-css ✓

a{content:"i"}b{content:"j"}

csskit ✓

a{content:"i"}b{content:"j"}

csslop ✓

a{content:"i"}b{content:"j"}

cssnano ✓

a{content:"i"}b{content:"j"}

csso ✓

a{content:"i"}b{content:"j"}

esbuild ✓

a{content:"i"}b{content:"j"}

lightningcss ✓

a{content:"i"}b{content:"j"}

sass ✓

a{content:"i"}b{content:"j"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Merging rules is unsafe when function arguments differ

scale(2,3) and scale(3,2) scale the two axes by swapped factors, so the rules cannot be merged. A minifier might compare the arguments regardless of order, and treat these two transforms as the same transform.

a {
  transform: scale(2, 3);
}

b {
  transform: scale(3, 2);
}

Expected

a{transform:scale(2,3)}b{transform:scale(3,2)}

Outputs

clean-css ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

csskit ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

csslop ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

cssnano ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

csso ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

esbuild ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

lightningcss ✓

a{transform:scale(2,3)}b{transform:scale(3,2)}

sass ✗

a{transform:scale(2, 3)}b{transform:scale(3, 2)}
1ms
5ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✓
Details

Merging rules is unsafe when the value after a slash differs

The mask shorthand takes the mask size after a slash, so center/cover and center/contain scale the mask differently and the rules cannot be merged.

a {
  mask: center / cover;
}

b {
  mask: center / contain;
}

Expected

a{mask:center/cover}b{mask:center/contain}

Outputs

clean-css ✓

a{mask:center/cover}b{mask:center/contain}

csskit ✓

a{mask:center/cover}b{mask:center/contain}

csslop ✓

a{mask:center/cover}b{mask:center/contain}

cssnano ✓

a{mask:center/cover}b{mask:center/contain}

csso ✓

a{mask:center/cover}b{mask:center/contain}

esbuild ✗

a{mask:center / cover}b{mask:center / contain}

lightningcss ✗

a{mask:none 50%/cover}b{mask:none 50%/contain}

sass ✓

a{mask:center/cover}b{mask:center/contain}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Do not merge rules with identical selectors separated by overlapping rules

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

In this example <div class="x y"> should get background:#0ff;color:#00f.

.x {
  color: #0f0;
}
.y {
  background: #0f0;
  color: #00f;
}
#foo {
  padding: 2px;
}
.x {
  background: #0ff;
}

Expected

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

Also accepted

.x{color:#0f0}.y{color:#00f;background:#0f0}#foo{padding:2px}.x{background:#0ff}

Outputs

clean-css ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

csskit ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

csslop ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

cssnano ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

csso ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

esbuild ✓

.x{color:#0f0}.y{background:#0f0;color:#00f}#foo{padding:2px}.x{background:#0ff}

lightningcss ✓

.x{color:#0f0}.y{color:#00f;background:#0f0}#foo{padding:2px}.x{background:#0ff}

sass ✗

.x{color:lime}.y{background:lime;color:blue}#foo{padding:2px}.x{background:aqua}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Do not merge rules with identical selectors separated by overlapping rules

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

In this example <div class="x"> should get background:#0ff;color:#00f, because :where(.x) is a selector with 0 0 0 specificty, where as div has a higher specificity of 0 0 1.

:where(.x) {
  color: #0f0;
}
div {
  background: #0f0;
  color: #00f;
}
#foo {
  padding: 2px;
}
:where(.x) {
  background: #0ff;
}

Expected

:where(.x){color:#0f0}div{background:#0f0;color:#00f}#foo{padding:2px}:where(.x){background:#0ff}

Also accepted

:where(.x){color:#0f0}div{color:#00f;background:#0f0}#foo{padding:2px}:where(.x){background:#0ff}

Outputs

clean-css ✗

:where(.x){color:#0f0;background:#0ff}div{background:#0f0;color:#00f}#foo{padding:2px}

csskit ✓

:where(.x){color:#0f0}div{background:#0f0;color:#00f}#foo{padding:2px}:where(.x){background:#0ff}

csslop ✓

:where(.x){color:#0f0}div{background:#0f0;color:#00f}#foo{padding:2px}:where(.x){background:#0ff}

cssnano ✓

:where(.x){color:#0f0}div{background:#0f0;color:#00f}#foo{padding:2px}:where(.x){background:#0ff}

csso ✗

:where(.x){color:#0f0;background:#0ff}div{background:#0f0;color:#00f}#foo{padding:2px}

esbuild ✓

:where(.x){color:#0f0}div{background:#0f0;color:#00f}#foo{padding:2px}:where(.x){background:#0ff}

lightningcss ✓

:where(.x){color:#0f0}div{color:#00f;background:#0f0}#foo{padding:2px}:where(.x){background:#0ff}

sass ✗

:where(.x){color:lime}div{background:lime;color:blue}#foo{padding:2px}:where(.x){background:aqua}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✓ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Merge rules with identical selectors separated by rules that get overridden

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

If the separating rules could potentially override the first rule, but the last rule to merge has no conflicts, it can be merged into the first rule. This preserves any potential property overrides, while still reducing total character count.

In this example <div class="x y"> should get background:#0ff;color:#00f, because .y overrides the color value in .x when applied to the same element.

.x {
  color: #0f0;
}
.y {
  color: #00f;
}
#foo {
  padding: 2px;
}
.x {
  background: #0ff;
}

Expected

.x{color:#0f0;background:#0ff}.y{color:#00f}#foo{padding:2px}

Also accepted

.x{background:#0ff;color:#0f0}.y{color:#00f}#foo{padding:2px}

Outputs

clean-css ✓

.x{color:#0f0;background:#0ff}.y{color:#00f}#foo{padding:2px}

csskit ✗

.x{color:#0f0}.y{color:#00f}#foo{padding:2px}.x{background:#0ff}

csslop ✓

.x{color:#0f0;background:#0ff}.y{color:#00f}#foo{padding:2px}

cssnano ✓

.x{color:#0f0;background:#0ff}.y{color:#00f}#foo{padding:2px}

csso ✗

.x{color:#0f0}.y{color:#00f}#foo{padding:2px}.x{background:#0ff}

esbuild ✗

.x{color:#0f0}.y{color:#00f}#foo{padding:2px}.x{background:#0ff}

lightningcss ✗

.x{color:#0f0}.y{color:#00f}#foo{padding:2px}.x{background:#0ff}

sass ✗

.x{color:lime}.y{color:blue}#foo{padding:2px}.x{background:aqua}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✓ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Merge rules with identical selectors separated by rules potentially overridden

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

If the separating rules could potentially be overridden by the last rule, but do not potentially override anything in the first rule, then the first rule can be merged into the last rule. This preserves any potential property overrides, while still reducing total character count.

In this example <div class="x y"> should get background:#0ff;color:#0f0, because .x overrides the color value in .y when applied to the same element.

.x {
  background: #0ff;
}
.y {
  color: #00f;
}
#foo {
  padding: 2px;
}
.x {
  color: #0f0;
}

Expected

.y{color:#00f}#foo{padding:2px}.x{background:#0ff;color:#0f0}

Also accepted

.y{color:#00f}#foo{padding:2px}.x{color:#0f0;background:#0ff}

Outputs

clean-css ✓

.y{color:#00f}#foo{padding:2px}.x{background:#0ff;color:#0f0}

csskit ✗

.x{background:#0ff}.y{color:#00f}#foo{padding:2px}.x{color:#0f0}

csslop ✓

.y{color:#00f}#foo{padding:2px}.x{background:#0ff;color:#0f0}

cssnano ✓

.y{color:#00f}#foo{padding:2px}.x{background:#0ff;color:#0f0}

csso ✗

.x{background:#0ff}.y{color:#00f}#foo{padding:2px}.x{color:#0f0}

esbuild ✗

.x{background:#0ff}.y{color:#00f}#foo{padding:2px}.x{color:#0f0}

lightningcss ✗

.x{background:#0ff}.y{color:#00f}#foo{padding:2px}.x{color:#0f0}

sass ✗

.x{background:aqua}.y{color:blue}#foo{padding:2px}.x{color:lime}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✓ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Merge rules with identical selectors separated by non-overlapping rules

Adjacent rules with identical selectors can always be merged. However, if there are other rules between them, they can only be merged if the separating rules have no property declarations that could potentially override the values in the first rule in the merge, or if the selector is of a lower specificity.

.x {
  margin: 1px;
}
.y {
  color: red;
}
.x {
  padding: 1px;
}

Expected

.y{color:red}.x{margin:1px;padding:1px}

Also accepted

.y{color:red}.x{padding:1px;margin:1px}
.x{padding:1px;margin:1px}.y{color:red}
.x{margin:1px;padding:1px}.y{color:red}

Outputs

clean-css ✓

.x{margin:1px;padding:1px}.y{color:red}

csskit ✗

.x{margin:1px}.y{color:red}.x{padding:1px}

csslop ✓

.y{color:red}.x{margin:1px;padding:1px}

cssnano ✓

.x{margin:1px;padding:1px}.y{color:red}

csso ✗

.x{margin:1px}.y{color:red}.x{padding:1px}

esbuild ✗

.x{margin:1px}.y{color:red}.x{padding:1px}

lightningcss ✗

.x{margin:1px}.y{color:red}.x{padding:1px}

sass ✗

.x{margin:1px}.y{color:red}.x{padding:1px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Preserve distinct children when parent declarations overlap

When the similar rules overlap one another, merging them with a combinatory selector works only if they are an exact match. This test ensures minifiers do not exclude properties that are only in one rule.

.a {
  color: red;
}
.a .x {
  color: tan;
}
.b {
  color: red;
  padding: 1px;
}
.b .y {
  color: gold;
}

Expected

.a{color:red;.x{color:tan}}.b{color:red;padding:1px;.y{color:gold}}

Outputs

clean-css ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}

csskit ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}

csslop ✓

.a{color:red;.x{color:tan}}.b{color:red;padding:1px;.y{color:gold}}

cssnano ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}

csso ✗

.a,.b{color:red}.a .x{color:tan}.b{padding:1px}.b .y{color:gold}

esbuild ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}

lightningcss ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}

sass ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .y{color:gold}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Merge matching rules with !important properties separated by another rule

If two rules use the same selectors, they can be merged. Even with another rule between them. In this case of a property has a higher specificity via !important. Since the separating rule also applies to the same property, but has a lower specificity, it does not matter if the .a class is merged above or below the separator.

.a {
  color: red !important;
}
.b {
  color: tan;
}
.a {
  color: tan;
}

Expected

.a{color:red!important}.b{color:tan}

Also accepted

.b{color:tan}.a{color:red!important}

Outputs

clean-css ✗

.a{color:red!important}.a,.b{color:tan}

csskit ✗

.a{color:red!important}.b{color:tan}.a{color:tan}

csslop ✓

.a{color:red!important}.b{color:tan}

cssnano ✗

.a{color:red!important}.a,.b{color:tan}

csso ✓

.a{color:red!important}.b{color:tan}

esbuild ✗

.a{color:red!important}.b,.a{color:tan}

lightningcss ✗

.a{color:red!important}.b,.a{color:tan}

sass ✗

.a{color:red !important}.b{color:tan}.a{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Merge matching rules with !important properties separated by another rule

If two rules use the same selectors, they can be merged. Even with another rule between them. In this case both matching rules and the separating rule use the same property, but the later matching rule's property has a higher specificity via !important. If the an element has both a class of a and b applied, it will recieve tan whether .a is above or below it, so the placement of the merge can appear in either place.

.a {
  color: red;
}
.b {
  color: gold;
}
.a {
  color: tan !important;
}

Expected

.b{color:gold}.a{color:tan!important}

Outputs

clean-css ✓

.b{color:gold}.a{color:tan!important}

csskit ✗

.a{color:red}.b{color:gold}.a{color:tan!important}

csslop ✓

.b{color:gold}.a{color:tan!important}

cssnano ✗

.a{color:red}.b{color:gold}.a{color:tan!important}

csso ✓

.b{color:gold}.a{color:tan!important}

esbuild ✗

.a{color:red}.b{color:gold}.a{color:tan!important}

lightningcss ✓

.b{color:gold}.a{color:tan!important}

sass ✗

.a{color:red}.b{color:gold}.a{color:tan !important}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Merge matching rules with !important properties separated by another rule

If two rules use the same selectors, they can be merged. Even with another rule between them. In this case both matching rules and the separating rule use the same property, but he separating rule's property has a higher specificity via !important. If the an element has both a class of a and b applied, it will recieve gold whether .a is above or below it, so the placement of the merge can appear in either place.

.a {
  color: red;
}
.b {
  color: gold !important;
}
.a {
  color: tan;
}

Expected

.b{color:gold!important}.a{color:tan}

Also accepted

.a{color:tan}.b{color:gold!important}

Outputs

clean-css ✓

.b{color:gold!important}.a{color:tan}

csskit ✗

.a{color:red}.b{color:gold!important}.a{color:tan}

csslop ✓

.b{color:gold!important}.a{color:tan}

cssnano ✗

.a{color:red}.b{color:gold!important}.a{color:tan}

csso ✓

.b{color:gold!important}.a{color:tan}

esbuild ✗

.a{color:red}.b{color:gold!important}.a{color:tan}

lightningcss ✓

.b{color:gold!important}.a{color:tan}

sass ✗

.a{color:red}.b{color:gold !important}.a{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Merge matching rules with !important properties separated by another rule

If two rules use the same selectors, they can be merged. Even with another rule between them. In this case both matching rules and the separating rule use the same property, and all use !important. If the an element has both a class of a and b applied, it will recieve tan. This requires the merged .a rule to occur after the .b rule.

.a {
  color: red !important;
}
.b {
  color: gold !important;
}
.a {
  color: tan !important;
}

Expected

.b{color:gold!important}.a{color:tan!important}

Outputs

clean-css ✓

.b{color:gold!important}.a{color:tan!important}

csskit ✗

.a{color:red!important}.b{color:gold!important}.a{color:tan!important}

csslop ✓

.b{color:gold!important}.a{color:tan!important}

cssnano ✗

.a{color:red!important}.b{color:gold!important}.a{color:tan!important}

csso ✓

.b{color:gold!important}.a{color:tan!important}

esbuild ✗

.a{color:red!important}.b{color:gold!important}.a{color:tan!important}

lightningcss ✓

.b{color:gold!important}.a{color:tan!important}

sass ✗

.a{color:red !important}.b{color:gold !important}.a{color:tan !important}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 18 / 24 10 / 24 24 / 24 18 / 24 15 / 24 10 / 24 15 / 24 6 / 24
Percent 75% 41.66% 100% 75% 62.5% 41.66% 62.5% 25%
Duration 4ms 8ms 6ms 17ms 3ms 15ms 1ms 13ms

nesting #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Basic nesting whitespace removal

A nested style rule inside a parent rule should be minified in place, preserving the nesting structure while removing whitespace.

.foo {
  color: red;
  .bar {
    margin: 0;
  }
}

Expected

.foo{color:red;.bar{margin:0}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✓

.foo{color:red;.bar{margin:0}}

csslop ✓

.foo{color:red;.bar{margin:0}}

cssnano ✓

.foo{color:red;.bar{margin:0}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;.bar{margin:0}}

lightningcss ✗

.foo{color:red;& .bar{margin:0}}

sass ✗

.foo{color:red}.foo .bar{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Nested & selector with pseudo-class

&:hover inside a parent rule must be preserved as-is. The & refers to the parent selector and appending :hover creates a compound match.

a {
  color: red;
  &:hover {
    margin: 0;
  }
}

Expected

a{color:red;&:hover{margin:0}}

Outputs

clean-css ✗

a{color:red}

csskit ✓

a{color:red;&:hover{margin:0}}

csslop ✓

a{color:red;&:hover{margin:0}}

cssnano ✓

a{color:red;&:hover{margin:0}}

csso ✗

a{color:red}

esbuild ✓

a{color:red;&:hover{margin:0}}

lightningcss ✓

a{color:red;&:hover{margin:0}}

sass ✗

a{color:red}a:hover{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Nested & with compound class selector

&.bar refines the parent selector, matching elements that match both .foo and .bar. The compound form must be preserved during minification.

.foo {
  color: red;
  &.bar {
    margin: 0;
  }
}

Expected

.foo{color:red;&.bar{margin:0}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✓

.foo{color:red;&.bar{margin:0}}

csslop ✓

.foo{color:red;&.bar{margin:0}}

cssnano ✓

.foo{color:red;&.bar{margin:0}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;&.bar{margin:0}}

lightningcss ✓

.foo{color:red;&.bar{margin:0}}

sass ✗

.foo{color:red}.foo.bar{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Nested child combinator with implicit &

> .bar inside a parent rule uses a relative selector (implicit &). The child combinator form without an explicit & must be preserved.

.foo {
  color: red;
  > .bar {
    margin: 0;
  }
}

Expected

.foo{color:red;>.bar{margin:0}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✓

.foo{color:red;>.bar{margin:0}}

csslop ✓

.foo{color:red;>.bar{margin:0}}

cssnano ✗

.foo{color:red;> .bar{margin:0}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;>.bar{margin:0}}

lightningcss ✗

.foo{color:red;&>.bar{margin:0}}

sass ✗

.foo{color:red}.foo>.bar{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Multi-level nesting

Three levels of nesting (figure > figcaption > p) must all be preserved and minified correctly without flattening.

figure {
  margin: 0;
  > figcaption {
    color: red;
    > p {
      font-size: .9rem;
    }
  }
}

Expected

figure{margin:0;>figcaption{color:red;>p{font-size:.9rem}}}

Outputs

clean-css ✗

figure{margin:0}

csskit ✓

figure{margin:0;>figcaption{color:red;>p{font-size:.9rem}}}

csslop ✓

figure{margin:0;>figcaption{color:red;>p{font-size:.9rem}}}

cssnano ✗

figure{margin:0;> figcaption{color:red;> p{font-size:.9rem}}}

csso ✗

figure{margin:0}

esbuild ✓

figure{margin:0;>figcaption{color:red;>p{font-size:.9rem}}}

lightningcss ✗

figure{margin:0;&>figcaption{color:red;&>p{font-size:.9rem}}}

sass ✗

figure{margin:0}figure>figcaption{color:red}figure>figcaption>p{font-size:.9rem}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Nested @media inside a style rule

A conditional @media rule nested inside a style rule should be minified in place. The space after @media can be removed when the condition starts with (.

.foo {
  display: grid;
  @media (width >= 768px) {
    grid-auto-flow: column;
  }
}

Expected

.foo{display:grid;@media(width>=768px){grid-auto-flow:column}}

Outputs

clean-css ✗

.foo{display:grid}

csskit ✓

.foo{display:grid;@media(width>=768px){grid-auto-flow:column}}

csslop ✓

.foo{display:grid;@media(width>=768px){grid-auto-flow:column}}

cssnano ✓

.foo{display:grid;@media(width>=768px){grid-auto-flow:column}}

csso ✗

.foo{display:grid}

esbuild ✓

.foo{display:grid;@media(width>=768px){grid-auto-flow:column}}

lightningcss ✗

.foo{display:grid;@media (width>=768px){grid-auto-flow:column}}

sass ✗

.foo{display:grid}@media(width >= 768px){.foo{grid-auto-flow:column}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

Redundant & removal

& .bar is equivalent to .bar in a nesting context because a nested selector without & implies a descendant relationship. The explicit & is redundant and can be removed.

.foo {
  color: red;
  & .bar {
    margin: 0;
  }
}

Expected

.foo{color:red;.bar{margin:0}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✗

.foo{color:red;&.bar{margin:0}}

csslop ✓

.foo{color:red;.bar{margin:0}}

cssnano ✗

.foo{color:red;& .bar{margin:0}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;.bar{margin:0}}

lightningcss ✗

.foo{color:red;& .bar{margin:0}}

sass ✗

.foo{color:red}.foo .bar{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ERROR
Details

Doubled & selector

&& equals .foo.foo, doubling specificity. Must not collapse to a single &.

.foo {
  color: red;
  && {
    font-size: 16px;
  }
}

Expected

.foo{color:red;&&{font-size:16px}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✓

.foo{color:red;&&{font-size:16px}}

csslop ✓

.foo{color:red;&&{font-size:16px}}

cssnano ✓

.foo{color:red;&&{font-size:16px}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;&&{font-size:16px}}

lightningcss ✓

.foo{color:red;&&{font-size:16px}}

sass ✗

"&" may only used at the beginning of a compound selector.
  ╷
3 │   && {
  │    ^
  ╵
  - 3:4  root stylesheet
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

& in non-initial position

.parent & matches .parent .foo -- the & is not at the start of the selector. This form must be preserved; it cannot be simplified.

.foo {
  color: red;
  .parent & {
    margin: 0;
  }
}

Expected

.foo{color:red;.parent &{margin:0}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✗

.foo{color:red;.parent&{margin:0}}

csslop ✓

.foo{color:red;.parent &{margin:0}}

cssnano ✓

.foo{color:red;.parent &{margin:0}}

csso ✗

.foo{color:red}

esbuild ✓

.foo{color:red;.parent &{margin:0}}

lightningcss ✓

.foo{color:red;.parent &{margin:0}}

sass ✗

.foo{color:red}.parent .foo{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Introduce nesting to shorten repeated parent selectors

Three flat rules sharing .nav as parent combine into a single nested rule, eliminating repeated selector text.

.nav {
  display: flex;
}
.nav a {
  color: red;
}
.nav a:hover {
  margin: 0;
}

Expected

.nav{display:flex;a{color:red;&:hover{margin:0}}}

Outputs

clean-css ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

csskit ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

csslop ✓

.nav{display:flex;a{color:red;&:hover{margin:0}}}

cssnano ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

csso ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

esbuild ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

lightningcss ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}

sass ✗

.nav{display:flex}.nav a{color:red}.nav a:hover{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Introduce nesting for pseudo-class

a { ... } a:hover { ... } is shorter when nested as a{...;&:hover{...}}, eliminating the repeated a selector.

a {
  color: red;
}
a:hover {
  margin: 0;
}

Expected

a{color:red;&:hover{margin:0}}

Outputs

clean-css ✗

a{color:red}a:hover{margin:0}

csskit ✗

a{color:red}a:hover{margin:0}

csslop ✓

a{color:red;&:hover{margin:0}}

cssnano ✗

a{color:red}a:hover{margin:0}

csso ✗

a{color:red}a:hover{margin:0}

esbuild ✗

a{color:red}a:hover{margin:0}

lightningcss ✗

a{color:red}a:hover{margin:0}

sass ✗

a{color:red}a:hover{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Introduce nesting for child combinator

.card { ... } .card > .title { ... } is shorter when nested using a relative child selector: .card{...;>.title{...}}.

.card {
  padding: 1rem;
}
.card > .title {
  color: red;
}

Expected

.card{padding:1rem;>.title{color:red}}

Outputs

clean-css ✗

.card{padding:1rem}.card>.title{color:red}

csskit ✗

.card{padding:1rem}.card>.title{color:red}

csslop ✓

.card{padding:1rem;>.title{color:red}}

cssnano ✗

.card{padding:1rem}.card>.title{color:red}

csso ✗

.card{padding:1rem}.card>.title{color:red}

esbuild ✗

.card{padding:1rem}.card>.title{color:red}

lightningcss ✗

.card{padding:1rem}.card>.title{color:red}

sass ✗

.card{padding:1rem}.card>.title{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten descendant nesting with empty parent

When a parent rule has no declarations of its own, the nested child can be flattened into a single descendant selector. a { b { color: red } } becomes a b{color:red}, eliminating the empty wrapper.

a {
  b {
    color: red;
  }
}

Expected

a b{color:red}

Outputs

clean-css ✗

csskit ✗

a{b{color:red}}

csslop ✓

a b{color:red}

cssnano ✗

a{b{color:red}}

csso ✗

esbuild ✗

a{b{color:red}}

lightningcss ✗

a{& b{color:red}}

sass ✓

a b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten child combinator nesting with empty parent

a { > b { color: red } } can be flattened to a>b{color:red} when the parent has no own declarations. The relative child selector resolves to a direct child combinator.

a {
  > b {
    color: red;
  }
}

Expected

a>b{color:red}

Outputs

clean-css ✗

csskit ✗

a{>b{color:red}}

csslop ✓

a>b{color:red}

cssnano ✗

a{> b{color:red}}

csso ✗

esbuild ✗

a{>b{color:red}}

lightningcss ✗

a{&>b{color:red}}

sass ✓

a>b{color:red}
1ms
3ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten next-sibling combinator nesting with empty parent

a { + b { color: red } } can be flattened to a+b{color:red}. The relative next-sibling selector resolves to the adjacent sibling combinator when the parent wrapper has no declarations.

a {
  + b {
    color: red;
  }
}

Expected

a+b{color:red}

Outputs

clean-css ✗

csskit ✗

a{+b{color:red}}

csslop ✓

a+b{color:red}

cssnano ✗

a{+ b{color:red}}

csso ✗

esbuild ✗

a{+b{color:red}}

lightningcss ✗

a{&+b{color:red}}

sass ✓

a+b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten subsequent-sibling combinator nesting with empty parent

a { ~ b { color: red } } can be flattened to a~b{color:red}. The relative subsequent-sibling selector resolves to the general sibling combinator when the parent has no own declarations.

a {
  ~ b {
    color: red;
  }
}

Expected

a~b{color:red}

Outputs

clean-css ✗

csskit ✗

a{~b{color:red}}

csslop ✓

a~b{color:red}

cssnano ✗

a{~ b{color:red}}

csso ✗

esbuild ✗

a{~b{color:red}}

lightningcss ✗

a{&~b{color:red}}

sass ✓

a~b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

Nested rules with mixed combinators stay nested

When a parent has no own declarations but multiple nested children, keeping the nesting is shorter than flattening because the parent selector only appears once instead of being repeated for each child rule.

nav {
  a {
    color: red;
  }
  > .icon {
    width: 1rem;
  }
  + footer {
    margin: 0;
  }
}

Expected

nav{a{color:red}>.icon{width:1rem}+footer{margin:0}}

Outputs

clean-css ✗

csskit ✓

nav{a{color:red}>.icon{width:1rem}+footer{margin:0}}

csslop ✓

nav{a{color:red}>.icon{width:1rem}+footer{margin:0}}

cssnano ✗

nav{a{color:red}> .icon{width:1rem}+ footer{margin:0}}

csso ✗

esbuild ✓

nav{a{color:red}>.icon{width:1rem}+footer{margin:0}}

lightningcss ✗

nav{& a{color:red}&>.icon{width:1rem}&+footer{margin:0}}

sass ✗

nav a{color:red}nav>.icon{width:1rem}nav+footer{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten &:pseudo from empty parent

a { &:hover { color: red } } can be flattened to a:hover{color:red} when the parent has no own declarations. The & resolves to the parent selector, producing the compound a:hover.

a {
  &:hover {
    color: red;
  }
}

Expected

a:hover{color:red}

Outputs

clean-css ✗

csskit ✗

a{&:hover{color:red}}

csslop ✓

a:hover{color:red}

cssnano ✗

a{&:hover{color:red}}

csso ✗

esbuild ✗

a{&:hover{color:red}}

lightningcss ✗

a{&:hover{color:red}}

sass ✓

a:hover{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Duplicate nested rules should dedup and flatten

Two identical .bar rules nested inside .foo should collapse to one, then flatten to .foo .bar{...} since there are no other rules in the parent.

.foo {
  .bar {
    color: red;
    font-size: 1rem;
  }
  .bar {
    color: red;
    font-size: 1rem;
  }
}

Expected

.foo .bar{color:red;font-size:1rem}

Outputs

clean-css ✗

.foo{font-size:1rem}.bar{color:red;font-size:1rem}

csskit ✗

.foo{.bar{color:red;font-size:1rem}.bar{color:red;font-size:1rem}}

csslop ✓

.foo .bar{color:red;font-size:1rem}

cssnano ✗

.foo{.bar{color:red;font-size:1rem}}

csso ✗

esbuild ✗

.foo{.bar{color:red;font-size:1rem}}

lightningcss ✗

.foo{& .bar{color:red;font-size:1rem}}

sass ✗

.foo .bar{color:red;font-size:1rem}.foo .bar{color:red;font-size:1rem}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Introduce nesting to shorten repeated class selectors

This test validates simple repeated selectors with matching parent classes and different child classes get nested during minificaiton to reduce the duplication of the parent class in the selector.

.foo .a {
  margin: 1px;
}
.foo .b {
  margin: 2px;
}
.foo .c {
  margin: 3px;
}
.foo .d {
  margin: 4px;
}

Expected

.foo{.a{margin:1px}.b{margin:2px}.c{margin:3px}.d{margin:4px}}

Outputs

clean-css ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

csskit ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

csslop ✓

.foo{.a{margin:1px}.b{margin:2px}.c{margin:3px}.d{margin:4px}}

cssnano ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

csso ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

esbuild ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

lightningcss ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}

sass ✗

.foo .a{margin:1px}.foo .b{margin:2px}.foo .c{margin:3px}.foo .d{margin:4px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

Merge nested children with the same rules and specificity

Comma-separated nested selectors are treated the same as :is(), meaning they can only be combined when their specificity level is the same.

.a {
  .b.c { color: red; }
  .d.e, .f.g { color: red; }
}

Expected

.a{.b.c,.d.e,.f.g{color:red}}

Outputs

clean-css ✗

.d.e,.f.g{color:red}

csskit ✗

.a{.b.c{color:red}.d.e,.f.g{color:red}}

csslop ✓

.a{.b.c,.d.e,.f.g{color:red}}

cssnano ✓

.a{.b.c,.d.e,.f.g{color:red}}

csso ✗

esbuild ✓

.a{.b.c,.d.e,.f.g{color:red}}

lightningcss ✗

.a{& .b.c,& .d.e,& .f.g{color:red}}

sass ✗

.a .b.c{color:red}.a .d.e,.a .f.g{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Do not merge nested child selectors with different specificity

Comma-separated nested selectors are treated the same as :is(), meaning selectors can only be combined when their specificity level is the same to avoid changing the authored specificity.

.d {
  .e { color: tan; }
  #f { color: tan; }
}

Expected

.d{.e{color:tan}#f{color:tan}}

Outputs

clean-css ✗

#f{color:tan}

csskit ✓

.d{.e{color:tan}#f{color:tan}}

csslop ✓

.d{.e{color:tan}#f{color:tan}}

cssnano ✗

.d{#f,.e{color:tan}}

csso ✗

esbuild ✗

.d{.e,#f{color:tan}}

lightningcss ✗

.d{& .e,& #f{color:tan}}

sass ✗

.d .e{color:tan}.d #f{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✗
Details

Merge nested children with the same rules and specificity (IDs)

Comma-separated nested selectors are treated the same as :is(), meaning they can only be combined when their specificity level is the same.

In this test, an unnested equivalent would look like a :is(#b.c),a :is(.d.e,#f.g){color:red}. Notice how the :is() produces a specifity of 1-1-0 in both cases, allowing them to be merged to .a :is(#b.c,.d.e,#f.g){color:red}, which when re-nested is shorter .a{#b.c,.d.e,#f.g{color:red}}.

.a {
  #b.c { color: red; }
  .d.e, #f.g { color: red; }
}

Expected

.a{#b.c,.d.e,#f.g{color:red}}

Outputs

clean-css ✗

#f.g,.d.e{color:red}

csskit ✗

.a{#b.c{color:red}.d.e,#f.g{color:red}}

csslop ✓

.a{#b.c,.d.e,#f.g{color:red}}

cssnano ✗

.a{#b.c,#f.g,.d.e{color:red}}

csso ✗

esbuild ✓

.a{#b.c,.d.e,#f.g{color:red}}

lightningcss ✗

.a{& #b.c,& .d.e,& #f.g{color:red}}

sass ✗

.a #b.c{color:red}.a .d.e,.a #f.g{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Merge nested child :before/:after pseudo-elements with same rules

The :before and :after pseudo-elements have the same level of specificity. If no other selectors are included for their rules that have a different specificity level, and the rule declarations are identical, they can be merged.

.foo {
  color: red;
  &::after { content: ""; }
  &::before { content: ""; }
}

Expected

.foo{color:red;&:after,&:before{content:""}}

Outputs

clean-css ✗

.foo{color:red}

csskit ✗

.foo{color:red;&::after{content:""}&::before{content:""}}

csslop ✓

.foo{color:red;&:after,&:before{content:""}}

cssnano ✓

.foo{color:red;&:after,&:before{content:""}}

csso ✗

.foo{color:red}

esbuild ✗

.foo{color:red;&:after{content:""}&:before{content:""}}

lightningcss ✓

.foo{color:red;&:after,&:before{content:""}}

sass ✗

.foo{color:red}.foo::after{content:""}.foo::before{content:""}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Merge matching nested attribute selectors and :not()

The nested selectors [disabled] and :not(:active) both have the same 0-1-0 specificity. Since they also have an identical rule declaration, they can be merged.

input {
  &:not(:active) { opacity: .5; }
  &[disabled] { opacity: .5; }
}

Expected

input{&:not(:active),&[disabled]{opacity:.5}}

Outputs

clean-css ✗

csskit ✗

input{&:not(:active){opacity:.5}&[disabled]{opacity:.5}}

csslop ✓

input{&:not(:active),&[disabled]{opacity:.5}}

cssnano ✓

input{&:not(:active),&[disabled]{opacity:.5}}

csso ✗

esbuild ✗

input{&:not(:active){opacity:.5}&[disabled]{opacity:.5}}

lightningcss ✓

input{&:not(:active),&[disabled]{opacity:.5}}

sass ✗

input:not(:active){opacity:.5}input[disabled]{opacity:.5}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Un-nest unnecessarily nested pseudo-classes with empty parent

If there is only one nested selector and it is a pseudo-class, with no other styles applied to the parent, un-nest it.

a {
  &:hover {
    color: red;
  }
}

Expected

a:hover{color:red}

Outputs

clean-css ✗

csskit ✗

a{&:hover{color:red}}

csslop ✓

a:hover{color:red}

cssnano ✗

a{&:hover{color:red}}

csso ✗

esbuild ✗

a{&:hover{color:red}}

lightningcss ✗

a{&:hover{color:red}}

sass ✓

a:hover{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0027 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Un-nest unnecessarily nested classes with empty parent

If there is only one nested selector and it is a class, with no other styles applied to the parent, un-nest it.

.foo {
  &.bar {
    color: red;
  }
}

Expected

.foo.bar{color:red}

Outputs

clean-css ✗

csskit ✗

.foo{&.bar{color:red}}

csslop ✓

.foo.bar{color:red}

cssnano ✗

.foo{&.bar{color:red}}

csso ✗

esbuild ✗

.foo{&.bar{color:red}}

lightningcss ✗

.foo{&.bar{color:red}}

sass ✓

.foo.bar{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0028 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Nest general siblings

If an adjacent rule uses the same selector followed by an adjacent sibling, nest them together.

.a {
  color: red;
}
.a ~ .b {
  color: tan;
}

Expected

.a{color:red;~.b{color:tan}}

Outputs

clean-css ✗

.a{color:red}.a~.b{color:tan}

csskit ✗

.a{color:red}.a~.b{color:tan}

csslop ✓

.a{color:red;~.b{color:tan}}

cssnano ✗

.a{color:red}.a~.b{color:tan}

csso ✗

.a{color:red}.a~.b{color:tan}

esbuild ✗

.a{color:red}.a~.b{color:tan}

lightningcss ✗

.a{color:red}.a~.b{color:tan}

sass ✗

.a{color:red}.a~.b{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0029 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove redundant & from nested selectors

If a nested selector begins with an ampersand followed by a space, both characters can be removed.

.foo {
  color: red;
  & p {
    margin: 0;
  }
  & #bar {
    padding: 0;
  }
  & * {
    border: 0;
  }
}

Expected

.foo{color:red;p{margin:0}#bar{padding:0}*{border:0}}

Outputs

clean-css ✗

.foo{color:red}& #bar{padding:0}& *{border:0}

csskit ✗

.foo{color:red;&p{margin:0}&#bar{padding:0}&*{border:0}}

csslop ✓

.foo{color:red;p{margin:0}#bar{padding:0}*{border:0}}

cssnano ✗

.foo{color:red;& p{margin:0}& #bar{padding:0}& *{border:0}}

csso ✗

.foo{color:red}

esbuild ✗

.foo{color:red;& p{margin:0}#bar{padding:0}& *{border:0}}

lightningcss ✗

.foo{color:red;& p{margin:0}& #bar{padding:0}& *{border:0}}

sass ✗

.foo{color:red}.foo p{margin:0}.foo #bar{padding:0}.foo *{border:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0030 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge nested and un-nested rules that are otherwise identical

If a selector has a descendant with a rule declaration that is identical to a rule of the same descendant but nested, merge the two if adjacent.

.foo {
  color: #000;
  .bar {
    color: red;
  }
}
.foo .bar {
  color: red;
}

Expected

.foo{color:#000;.bar{color:red}}

Outputs

clean-css ✗

.foo{color:#000}

csskit ✗

.foo{color:#000;.bar{color:red}}.foo .bar{color:red}

csslop ✓

.foo{color:#000;.bar{color:red}}

cssnano ✗

.foo{color:#000;.bar{color:red}}.foo .bar{color:red}

csso ✗

.foo{color:#000}.foo .bar{color:red}

esbuild ✗

.foo{color:#000;.bar{color:red}}.foo .bar{color:red}

lightningcss ✗

.foo{color:#000;& .bar{color:red}}.foo .bar{color:red}

sass ✗

.foo{color:#000}.foo .bar{color:red}.foo .bar{color:red}
3ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0031 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Do not merge matching nested/un-nested rules with different specificty

Multiple selectors on the same level of a nested structure will be treated like :is, deferring to the highest specificity.

Example: #a,.b{.c{color:red}} is equivalent to :is(#a,.b) .c{color:red}. It is NOT equivalent to #a .c,.b .c{color:red}, which would result in a different level of specificity, as :is will always use the highest specificity found across all selectors inside it.

#a, .b {
  .c {
    color: tan;
  }
}
.b .c {
  color: tan;
}

Expected

#a,.b{.c{color:tan}}.b .c{color:tan}

Outputs

clean-css ✗

csskit ✓

#a,.b{.c{color:tan}}.b .c{color:tan}

csslop ✓

#a,.b{.c{color:tan}}.b .c{color:tan}

cssnano ✓

#a,.b{.c{color:tan}}.b .c{color:tan}

csso ✗

.b .c{color:tan}

esbuild ✓

#a,.b{.c{color:tan}}.b .c{color:tan}

lightningcss ✗

#a,.b{& .c{color:tan}}.b .c{color:tan}

sass ✗

#a .c,.b .c{color:tan}.b .c{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0032 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove useless :is from selector nesting

Comma-separated selectors in rules with nesting are treated as though the selectors are wrapped in an :is, resulting in shared specificity, using the more specific of all the selectors. In this test, the :is() contains an ID and a class, with another class outside of it, which will recieve whatever the result of the :is() evaluation anyways, making it pointless to include.

The #a,.b,.c in the test is equivalent to :is(#a,.b.c), which is equivalent to :is(:is(#a,.b),.c).

:is(#a, .b), .c {
  .d {
    color: red;
  }
}

Expected

#a,.b,.c{.d{color:red}}

Outputs

clean-css ✗

csskit ✗

:is(#a,.b),.c{.d{color:red}}

csslop ✓

#a,.b,.c{.d{color:red}}

cssnano ✗

.c,:is(#a,.b){.d{color:red}}

csso ✗

esbuild ✗

:is(#a,.b),.c{.d{color:red}}

lightningcss ✗

:is(#a,.b),.c{& .d{color:red}}

sass ✗

:is(#a,.b) .d,.c .d{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0033 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Do not remove :is when it contains potentially unsupported selectors

If a new CSS feature is used in a selector and the browser does not recognize it the entire rule will be ignored, even if there are other valid selectors attached to the rule (#a:foo,.b{color:red}). However, if the selectors are wrapped in an :is function then only the unrecognized selector is ignored, the rest of the selectors still apply the rule (:is(#a:foo,.b){color:red}).

.a, :is(#b:unsupported, .c) {
  .d, :is(.e:foo, .f) {
    color: red;
  }
}

Expected

.a,:is(#b:unsupported,.c){.d,:is(.e:foo,.f){color:red}}

Outputs

clean-css ✗

csskit ✓

.a,:is(#b:unsupported,.c){.d,:is(.e:foo,.f){color:red}}

csslop ✓

.a,:is(#b:unsupported,.c){.d,:is(.e:foo,.f){color:red}}

cssnano ✓

.a,:is(#b:unsupported,.c){.d,:is(.e:foo,.f){color:red}}

csso ✗

esbuild ✓

.a,:is(#b:unsupported,.c){.d,:is(.e:foo,.f){color:red}}

lightningcss ✗

.a,:is(#b:unsupported,.c){& .d,& :is(.e:foo,.f){color:red}}

sass ✗

.a .d,.a :is(.e:foo,.f),:is(#b:unsupported,.c) .d,:is(#b:unsupported,.c) :is(.e:foo,.f){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0034 ✗ ✓ ✓ ✓ ✗ ✓ ✗ ✗
Details

Retain nesting when child has multiple selectors

When a child selector in a nested group is a comma separated list, it will always be shorter to keep the selectors nested.

.a {
  .b,
  .c {
    color: red;
  }
}

Expected

.a{.b,.c{color:red}}

Outputs

clean-css ✗

csskit ✓

.a{.b,.c{color:red}}

csslop ✓

.a{.b,.c{color:red}}

cssnano ✓

.a{.b,.c{color:red}}

csso ✗

esbuild ✓

.a{.b,.c{color:red}}

lightningcss ✗

.a{& .b,& .c{color:red}}

sass ✗

.a .b,.a .c{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0035 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✓
Details

Flatten multi-level nesting of single selectors

If a child in a nested selector is not comma separated, it can be merged with the parent, if it is also not comma separated, removing the extra curly braces.

.foo {
  .bar {
    input {
      &:active {
        &:hover {
          color: red;
        }
      }
    }
  }
}

Expected

.foo .bar input:active:hover{color:red}

Also accepted

.foo .bar input:hover:active{color:red}

Outputs

clean-css ✗

csskit ✗

.foo{.bar{input{&:active{&:hover{color:red}}}}}

csslop ✓

.foo .bar input:active:hover{color:red}

cssnano ✗

.foo{.bar{input{&:active{&:hover{color:red}}}}}

csso ✗

esbuild ✗

.foo{.bar{input{&:active{&:hover{color:red}}}}}

lightningcss ✗

.foo{& .bar{& input{&:active{&:hover{color:red}}}}}

sass ✓

.foo .bar input:active:hover{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0036 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Normalize descendant whitespace before introducing nesting

There are 2 spaces in the second selector. These should be normalized to 1 before nesting. Or both removed during nesting.

.a {
  color: red;
}
.a  .b {
  color: tan;
}

Expected

.a{color:red;.b{color:tan}}

Outputs

clean-css ✗

.a{color:red}.a .b{color:tan}

csskit ✗

.a{color:red}.a .b{color:tan}

csslop ✓

.a{color:red;.b{color:tan}}

cssnano ✗

.a{color:red}.a .b{color:tan}

csso ✗

.a{color:red}.a .b{color:tan}

esbuild ✗

.a{color:red}.a .b{color:tan}

lightningcss ✗

.a{color:red}.a .b{color:tan}

sass ✗

.a{color:red}.a .b{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0037 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge identical rules with children, keep distinct properties in separate rule

In this example, the .a .x rule can be nested under .a, and the .b .x rule can be nested under .b. This results in 2 identical rules, with the exception of one distinct property in .b that can be moved to its own rule.

.a {
  color: red;
}
.a .x {
  color: tan;
}
.b {
  color: red;
  padding: 1px;
}
.b .x {
  color: tan;
}

Expected

.a,.b{color:red;.x{color:tan}}.b{padding:1px}

Outputs

clean-css ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}

csskit ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}

csslop ✓

.a,.b{color:red;.x{color:tan}}.b{padding:1px}

cssnano ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}

csso ✗

.a,.b{color:red}.a .x,.b .x{color:tan}.b{padding:1px}

esbuild ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}

lightningcss ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}

sass ✗

.a{color:red}.a .x{color:tan}.b{color:red;padding:1px}.b .x{color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 37 12 / 37 37 / 37 12 / 37 0 / 37 15 / 37 6 / 37 8 / 37
Percent 0% 32.43% 100% 32.43% 0% 40.54% 16.21% 21.62%
Duration 5ms 6ms 9ms 25ms 5ms 23ms 1ms 17ms

page #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@page whitespace removal

Basic whitespace removal in a @page rule. The space between @page and { can be removed, and internal whitespace is minified as usual.

@page {
  margin: 1in;
}

Expected

@page{margin:1in}

Outputs

clean-css ✓

@page{margin:1in}

csskit ✓

@page{margin:1in}

csslop ✓

@page{margin:1in}

cssnano ✓

@page{margin:1in}

csso ✓

@page{margin:1in}

esbuild ✓

@page{margin:1in}

lightningcss ✓

@page{margin:1in}

sass ✓

@page{margin:1in}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

Named @page pseudo-class

@page :first targets the first page. Whitespace between the pseudo-class and the opening brace can be removed.

@page :first {
  margin: 2in;
}

Expected

@page:first{margin:2in}

Outputs

clean-css ✗

@page :first{margin:2in}

csskit ✓

@page:first{margin:2in}

csslop ✓

@page:first{margin:2in}

cssnano ✗

@page :first{margin:2in}

csso ✗

@page :first{margin:2in}

esbuild ✗

@page :first{margin:2in}

lightningcss ✓

@page:first{margin:2in}

sass ✗

@page :first{margin:2in}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✗
Details

Empty @page removal

An empty @page {} rule with no declarations can be removed entirely.

@page {
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✓

lightningcss ✗

@page{}

sass ✗

@page{}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@page margin at-rules

@page can contain margin at-rules like @top-center. Whitespace is removed inside both the outer and nested at-rule blocks.

@page {
  @top-center {
    content: "Title";
  }
}

Expected

@page{@top-center{content:"Title"}}

Outputs

clean-css ✓

@page{@top-center{content:"Title"}}

csskit ✓

@page{@top-center{content:"Title"}}

csslop ✓

@page{@top-center{content:"Title"}}

cssnano ✓

@page{@top-center{content:"Title"}}

csso ✓

@page{@top-center{content:"Title"}}

esbuild ✓

@page{@top-center{content:"Title"}}

lightningcss ✓

@page{@top-center{content:"Title"}}

sass ✓

@page{@top-center{content:"Title"}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 3 / 4 4 / 4 4 / 4 3 / 4 3 / 4 3 / 4 3 / 4 2 / 4
Percent 75% 100% 100% 75% 75% 75% 75% 50%
Duration 1ms 1ms 1ms 1ms 1ms 3ms 1ms 1ms

property #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✓
Details

@property whitespace removal

Remove whitespace inside @property while preserving the descriptor values. The syntax string and initial-value must not be altered.

@property --brand-color {
  syntax: "<color>";
  inherits: false;
  initial-value: #000;
}

Expected

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

Outputs

clean-css ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

csskit ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

csslop ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

cssnano ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

csso ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

esbuild ✗

@property --brand-color{syntax: "<color>"; inherits: false; initial-value: #000;}

lightningcss ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}

sass ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Omit default inherits: true

The inherits descriptor defaults to true. When present with meaningful syntax and initial-value, it can be safely omitted.

See:

@property --size {
  syntax: "<length>";
  inherits: true;
  initial-value: 16px;
}

Expected

@property --size{syntax:"<length>";initial-value:16px}

Outputs

clean-css ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}

csskit ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}

csslop ✓

@property --size{syntax:"<length>";initial-value:16px}

cssnano ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}

csso ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}

esbuild ✗

@property --size{syntax: "<length>"; inherits: true; initial-value: 16px;}

lightningcss ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}

sass ✗

@property --size{syntax:"<length>";inherits:true;initial-value:16px}
1ms
3ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✓
Details

Remove comments inside @property

Comments inside of @property can be removed.

@property --foo {
  /* syntax: "*"; */
  initial-value: red;
}

Expected

@property --foo{initial-value:red}

Outputs

clean-css ✓

@property --foo{initial-value:red}

csskit ✓

@property --foo{initial-value:red}

csslop ✓

@property --foo{initial-value:red}

cssnano ✓

@property --foo{initial-value:red}

csso ✓

@property --foo{initial-value:red}

esbuild ✗

@property --foo{initial-value: red;}

lightningcss ✗

sass ✓

@property --foo{initial-value:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
7ms
0004 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

Remove comment-only @property rule

A rule with only a comment and no descriptors is equivalent to an unregistered property and can be safely removed.

See:

@property --foo {
  /*text*/
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✗

@property --foo{}

lightningcss ✓

sass ✗

@property --foo{}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove no-op @property rules

syntax defaults to "*", inherits defaults to true, and initial-value defaults to guaranteed-invalid. These three registrations use only default values and are equivalent to unregistered custom properties, so they can be safely removed.

@property --foo {
  inherits: true;
}

@property --bar {
  syntax: "*";
}

@property --baz {
  syntax: "*";
  inherits: true;
}

Expected

Outputs

clean-css ✗

@property --foo{inherits:true}@property --bar{syntax:"*"}@property --baz{syntax:"*";inherits:true}

csskit ✗

@property --foo{inherits:true}@property --bar{syntax:"*"}@property --baz{syntax:"*";inherits:true}

csslop ✓

cssnano ✗

@property --foo{inherits:true}@property --bar{syntax:"*"}@property --baz{syntax:"*";inherits:true}

csso ✗

@property --foo{inherits:true}@property --bar{syntax:"*"}@property --baz{syntax:"*";inherits:true}

esbuild ✗

@property --foo{inherits: true;}@property --bar{syntax: "*";}@property --baz{syntax: "*"; inherits: true;}

lightningcss ✗

@property --baz{syntax:"*";inherits:true}

sass ✗

@property --foo{inherits:true}@property --bar{syntax:"*"}@property --baz{syntax:"*";inherits:true}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Omit default syntax: "*"

The syntax descriptor defaults to "*" (universal syntax), so explicit syntax: "*" can be safely omitted. Non-default descriptors like inherits: false and initial-value are preserved when meaningful.

@property --foo {
  syntax: "*";
  inherits: false;
}

@property --bar {
  syntax: "*";
  initial-value: red;
}

@property --baz {
  syntax: "*";
  inherits: false;
  initial-value: red;
}

Expected

@property --foo{inherits:false}@property --bar{initial-value:red}@property --baz{inherits:false;initial-value:red}

Outputs

clean-css ✗

@property --foo{syntax:"*";inherits:false}@property --bar{syntax:"*";initial-value:red}@property --baz{syntax:"*";inherits:false;initial-value:red}

csskit ✗

@property --foo{syntax:"*";inherits:false}@property --bar{syntax:"*";initial-value:red}@property --baz{syntax:"*";inherits:false;initial-value:red}

csslop ✓

@property --foo{inherits:false}@property --bar{initial-value:red}@property --baz{inherits:false;initial-value:red}

cssnano ✗

@property --foo{syntax:"*";inherits:false}@property --bar{syntax:"*";initial-value:red}@property --baz{syntax:"*";inherits:false;initial-value:red}

csso ✗

@property --foo{syntax:"*";inherits:false}@property --bar{syntax:"*";initial-value:red}@property --baz{syntax:"*";inherits:false;initial-value:red}

esbuild ✗

@property --foo{syntax: "*"; inherits: false;}@property --bar{syntax: "*"; initial-value: red;}@property --baz{syntax: "*"; inherits: false; initial-value: red;}

lightningcss ✗

@property --foo{syntax:"*";inherits:false}@property --baz{syntax:"*";inherits:false;initial-value:red}

sass ✗

@property --foo{syntax:"*";inherits:false}@property --bar{syntax:"*";initial-value:red}@property --baz{syntax:"*";inherits:false;initial-value:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove whitespace around syntax values in @property

An @property rule can have multiple values set for the syntax descriptor, separated by a pipe symbol (|). Whitespace adjacent to the pipe can be removed.

@property --foo {
  syntax: "<length> | <percentage>";
  inherits: false;
  initial-value: 16px;
}

Expected

@property --foo{syntax:"<length>|<percentage>";inherits:false;initial-value:16px}

Outputs

clean-css ✗

@property --foo{syntax:"<length> | <percentage>";inherits:false;initial-value:16px}

csskit ✗

@property --foo{syntax:"<length> | <percentage>";inherits:false;initial-value:16px}

csslop ✓

@property --foo{syntax:"<length>|<percentage>";inherits:false;initial-value:16px}

cssnano ✗

@property --foo{syntax:"<length> | <percentage>";inherits:false;initial-value:16px}

csso ✗

@property --foo{syntax:"<length> | <percentage>";inherits:false;initial-value:16px}

esbuild ✗

@property --foo{syntax: "<length> | <percentage>"; inherits: false; initial-value: 16px;}

lightningcss ✓

@property --foo{syntax:"<length>|<percentage>";inherits:false;initial-value:16px}

sass ✗

@property --foo{syntax:"<length> | <percentage>";inherits:false;initial-value:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Custom property value can be minified with @property constraint

When @property declares syntax: "<color>", the value is parsed as a color at parse time. The serialization is well-defined, so rgb(0 0 0) can be safely minified to #000.

@property --brand-color {
  syntax: "<color>";
  inherits: false;
  initial-value: #000;
}
a {
  --brand-color: rgb(0 0 0);
}

Expected

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:#000}

Outputs

clean-css ✗

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:rgb(0 0 0)}

csskit ✗

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:rgb(0 0 0)}

csslop ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:#000}

cssnano ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:#000}

csso ✗

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:rgb(0 0 0)}

esbuild ✗

@property --brand-color{syntax: "<color>"; inherits: false; initial-value: #000;}a{--brand-color: rgb(0 0 0)}

lightningcss ✓

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color:#000}

sass ✗

@property --brand-color{syntax:"<color>";inherits:false;initial-value:#000}a{--brand-color: rgb(0 0 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 3 / 8 3 / 8 8 / 8 4 / 8 3 / 8 0 / 8 4 / 8 2 / 8
Percent 37.5% 37.5% 100% 50% 37.5% 0% 50% 25%
Duration 1ms 3ms 1ms 4ms 1ms 5ms 1ms 10ms

scope #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✗ ✗ ✓ ✓ ✗
Details

@scope whitespace removal

Basic whitespace removal in @scope. The space between @scope and the opening parenthesis can be removed.

@scope (.card) {
  .title {
    color: red;
  }
}

Expected

@scope(.card){.title{color:red}}

Outputs

clean-css ✗

csskit ✓

@scope(.card){.title{color:red}}

csslop ✓

@scope(.card){.title{color:red}}

cssnano ✗

@scope (.card){.title{color:red}}

csso ✗

@scope (.card){.title{color:red}}

esbuild ✓

@scope(.card){.title{color:red}}

lightningcss ✓

@scope(.card){.title{color:red}}

sass ✗

@scope (.card){.title{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✗ ✗ ✓ ✗ ✗
Details

@scope with proximity limit

@scope (.card) to (.card .content) uses a scope limit. Whitespace around the parentheses can be removed but the space between to and ( must be preserved to avoid to( being tokenized as a function token.

@scope (.card) to (.card .content) {
  a {
    color: red;
  }
}

Expected

@scope(.card)to (.card .content){a{color:red}}

Outputs

clean-css ✗

csskit ✓

@scope(.card)to (.card .content){a{color:red}}

csslop ✓

@scope(.card)to (.card .content){a{color:red}}

cssnano ✗

@scope (.card) to (.card .content){a{color:red}}

csso ✗

@scope (.card) to (.card .content){a{color:red}}

esbuild ✓

@scope(.card)to (.card .content){a{color:red}}

lightningcss ✗

@scope(.card) to (.card .content){a{color:red}}

sass ✗

@scope (.card) to (.card .content){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✗ ✗ ✗
Details

Empty @scope removal

An empty @scope block with no rules can be removed entirely.

@scope (.card) {
}

Expected

Outputs

clean-css ✓

csskit ✓

csslop ✓

cssnano ✓

csso ✓

esbuild ✗

@scope(.card){}

lightningcss ✗

@scope(.card){}

sass ✗

@scope (.card){}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 1 / 3 3 / 3 3 / 3 1 / 3 1 / 3 2 / 3 1 / 3 0 / 3
Percent 33.33% 100% 100% 33.33% 33.33% 66.66% 33.33% 0%
Duration 1ms 1ms 1ms 1ms 2ms 2ms 1ms 1ms

selectors #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

nth-child 2n+1 to odd

2n+1 is the definition of odd. The keyword form is shorter.

a:nth-child(2n + 1) {
  color: red;
}

Expected

a:nth-child(odd){color:red}

Outputs

clean-css ✓

a:nth-child(odd){color:red}

csskit ✗

a:nth-child(2n+ 1){color:red}

csslop ✓

a:nth-child(odd){color:red}

cssnano ✓

a:nth-child(odd){color:red}

csso ✗

a:nth-child(2n+1){color:red}

esbuild ✓

a:nth-child(odd){color:red}

lightningcss ✓

a:nth-child(odd){color:red}

sass ✗

a:nth-child(2n+1){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

nth-child redundant +0 removal

2n+0 simplifies to 2n. The +0 offset is redundant.

a:nth-child(2n + 0) {
  color: red;
}

Expected

a:nth-child(2n){color:red}

Outputs

clean-css ✗

a:nth-child(2n+0){color:red}

csskit ✗

a:nth-child(2n+ 0){color:red}

csslop ✓

a:nth-child(2n){color:red}

cssnano ✗

a:nth-child(2n+0){color:red}

csso ✗

a:nth-child(2n+0){color:red}

esbuild ✓

a:nth-child(2n){color:red}

lightningcss ✓

a:nth-child(2n){color:red}

sass ✗

a:nth-child(2n+0){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

nth-child positive sign removal

+5 is the same as 5. The redundant positive sign should be stripped.

a:nth-child(+5) {
  color: red;
}

Expected

a:nth-child(5){color:red}

Outputs

clean-css ✗

a:nth-child(+5){color:red}

csskit ✓

a:nth-child(5){color:red}

csslop ✓

a:nth-child(5){color:red}

cssnano ✓

a:nth-child(5){color:red}

csso ✓

a:nth-child(5){color:red}

esbuild ✓

a:nth-child(5){color:red}

lightningcss ✓

a:nth-child(5){color:red}

sass ✗

a:nth-child(+5){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

nth-child zero-step simplification

0n+3 simplifies to just 3. The zero-step term is redundant.

a:nth-child(0n + 3) {
  color: red;
}

Expected

a:nth-child(3){color:red}

Outputs

clean-css ✗

a:nth-child(0n+3){color:red}

csskit ✗

a:nth-child(0n+ 3){color:red}

csslop ✓

a:nth-child(3){color:red}

cssnano ✗

a:nth-child(0n+3){color:red}

csso ✗

a:nth-child(0n+3){color:red}

esbuild ✓

a:nth-child(3){color:red}

lightningcss ✓

a:nth-child(3){color:red}

sass ✗

a:nth-child(0n+3){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

nth-child(2n) preserved

2n is already the shortest form. Unlike 2n+1 -> odd, there is no shorter keyword for 2n (even is the same length). Minifiers should preserve 2n as-is.

a:nth-child(2n) {
  color: red;
}

Expected

a:nth-child(2n){color:red}

Outputs

clean-css ✓

a:nth-child(2n){color:red}

csskit ✓

a:nth-child(2n){color:red}

csslop ✓

a:nth-child(2n){color:red}

cssnano ✓

a:nth-child(2n){color:red}

csso ✓

a:nth-child(2n){color:red}

esbuild ✓

a:nth-child(2n){color:red}

lightningcss ✓

a:nth-child(2n){color:red}

sass ✓

a:nth-child(2n){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

nth-child(1) to first-child

:nth-child(1) is equivalent to :first-child, which is shorter.

a:nth-child(1) {
  color: red;
}

Expected

a:first-child{color:red}

Outputs

clean-css ✓

a:first-child{color:red}

csskit ✗

a:nth-child(1){color:red}

csslop ✓

a:first-child{color:red}

cssnano ✓

a:first-child{color:red}

csso ✗

a:nth-child(1){color:red}

esbuild ✗

a:nth-child(1){color:red}

lightningcss ✓

a:first-child{color:red}

sass ✗

a:nth-child(1){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

nth-last-child(1) to last-child

:nth-last-child(1) is equivalent to :last-child, which is shorter.

a:nth-last-child(1) {
  color: red;
}

Expected

a:last-child{color:red}

Outputs

clean-css ✓

a:last-child{color:red}

csskit ✗

a:nth-last-child(1){color:red}

csslop ✓

a:last-child{color:red}

cssnano ✓

a:last-child{color:red}

csso ✗

a:nth-last-child(1){color:red}

esbuild ✗

a:nth-last-child(1){color:red}

lightningcss ✓

a:last-child{color:red}

sass ✗

a:nth-last-child(1){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✓ ✓ ✓ ✗
Details

:nth-child(1n) shorthand

:nth-child(1n) (equivalently :nth-child(n)) matches every child element. The pseudo-class could be removed entirely, going from a:nth-child(1n) to a, however, that would change the level of specificity for the selector, which could result in different styles being applied. For correctness, :nth-child(1n) should become :nth-child(n), because it is shorter, but retains the same specificity.

a:nth-child(1n) {
  color: red;
}

Expected

a:nth-child(n){color:red}

Validate

Outputs

clean-css ✗

a:nth-child(1n){color:red}

csskit ✗

a:nth-child(1n){color:red}

csslop ✓

a:nth-child(n){color:red}

cssnano ✗

a:nth-child(1n){color:red}

csso ✓

a:nth-child(n){color:red}

esbuild ✓

a:nth-child(n){color:red}

lightningcss ✓

a:nth-child(n){color:red}

sass ✗

a:nth-child(1n){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

:nth-of-type(1) to :first-of-type

:nth-of-type(1) is equivalent to :first-of-type and the latter is shorter.

a:nth-of-type(1) {
  color: red;
}

Expected

a:first-of-type{color:red}

Outputs

clean-css ✓

a:first-of-type{color:red}

csskit ✗

a:nth-of-type(1){color:red}

csslop ✓

a:first-of-type{color:red}

cssnano ✓

a:first-of-type{color:red}

csso ✗

a:nth-of-type(1){color:red}

esbuild ✗

a:nth-of-type(1){color:red}

lightningcss ✓

a:first-of-type{color:red}

sass ✗

a:nth-of-type(1){color:red}
1ms
1ms
1ms
1ms
1ms
2ms
1ms
1ms
0010 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

:nth-last-of-type(1) to :last-of-type

:nth-last-of-type(1) is equivalent to :last-of-type and the latter is shorter.

a:nth-last-of-type(1) {
  color: red;
}

Expected

a:last-of-type{color:red}

Outputs

clean-css ✓

a:last-of-type{color:red}

csskit ✗

a:nth-last-of-type(1){color:red}

csslop ✓

a:last-of-type{color:red}

cssnano ✓

a:last-of-type{color:red}

csso ✗

a:nth-last-of-type(1){color:red}

esbuild ✗

a:nth-last-of-type(1){color:red}

lightningcss ✓

a:last-of-type{color:red}

sass ✗

a:nth-last-of-type(1){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Double-colon to single-colon pseudo-element

::before can be shortened to :before for CSS2.1 pseudo-elements.

a::before {
  content: "x";
}

Expected

a:before{content:"x"}

Outputs

clean-css ✗

a::before{content:"x"}

csskit ✗

a::before{content:"x"}

csslop ✓

a:before{content:"x"}

cssnano ✓

a:before{content:"x"}

csso ✗

a::before{content:"x"}

esbuild ✓

a:before{content:"x"}

lightningcss ✓

a:before{content:"x"}

sass ✗

a::before{content:"x"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Universal selector preservation

The * selector must be preserved as-is, not removed or altered.

* {
  margin: 0;
}

Expected

*{margin:0}

Outputs

clean-css ✓

*{margin:0}

csskit ✓

*{margin:0}

csslop ✓

*{margin:0}

cssnano ✓

*{margin:0}

csso ✓

*{margin:0}

esbuild ✓

*{margin:0}

lightningcss ✓

*{margin:0}

sass ✓

*{margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✓ ✗ ✓ ✓ ✓ ✗ ✓ ✗
Details

Redundant universal selector before ID

*#id is equivalent to #id. The universal selector is implied.

*#id {
  color: red;
}

Expected

#id{color:red}

Outputs

clean-css ✓

#id{color:red}

csskit ✗

*#id{color:red}

csslop ✓

#id{color:red}

cssnano ✓

#id{color:red}

csso ✓

#id{color:red}

esbuild ✗

*#id{color:red}

lightningcss ✓

#id{color:red}

sass ✗

*#id{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✓
Details

Attribute selector quote removal

Quotes around attribute values can be removed when the value is a valid identifier.

a[href="foo"] {
  color: red;
}

Expected

a[href=foo]{color:red}

Outputs

clean-css ✓

a[href=foo]{color:red}

csskit ✗

a[href="foo"]{color:red}

csslop ✓

a[href=foo]{color:red}

cssnano ✓

a[href=foo]{color:red}

csso ✓

a[href=foo]{color:red}

esbuild ✓

a[href=foo]{color:red}

lightningcss ✓

a[href=foo]{color:red}

sass ✓

a[href=foo]{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

::view-transition-group must keep double colon

::view-transition-group() is a modern pseudo-element and must NOT be shortened to a single colon. The :: -> : shortening is only valid for CSS2.1 pseudo-elements (::before, ::after, etc.).

::view-transition-group(header) {
  animation-duration: 0.3s;
}

Expected

::view-transition-group(header){animation-duration:.3s}

Outputs

clean-css ✓

::view-transition-group(header){animation-duration:.3s}

csskit ✓

::view-transition-group(header){animation-duration:.3s}

csslop ✓

::view-transition-group(header){animation-duration:.3s}

cssnano ✓

::view-transition-group(header){animation-duration:.3s}

csso ✓

::view-transition-group(header){animation-duration:.3s}

esbuild ✓

::view-transition-group(header){animation-duration:.3s}

lightningcss ✓

::view-transition-group(header){animation-duration:.3s}

sass ✓

::view-transition-group(header){animation-duration:.3s}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Escape singular space in attribute selector value

When an attribute selector's value contains a single space, the quotes can be removed and the space can be escaped to reduce character count.

[class="foo bar"] {
  color: red;
}

Expected

[class=foo\ bar]{color:red}

Outputs

clean-css ✗

[class="foo bar"]{color:red}

csskit ✗

[class="foo bar"]{color:red}

csslop ✓

[class=foo\ bar]{color:red}

cssnano ✗

[class="foo bar"]{color:red}

csso ✗

[class="foo bar"]{color:red}

esbuild ✗

[class="foo bar"]{color:red}

lightningcss ✓

[class=foo\ bar]{color:red}

sass ✗

[class="foo bar"]{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Multiple spaces in attribute value remain quoted

When an attribute selector's value contains multiple spaces, the quotes surrounding them must remain in tact to be valid syntax. The spaces should not be escaped, because it is equal or fewer characters to keep them quoted.

[title="a b c d"] {
  color: red;
}

Expected

[title="a b c d"]{color:red}

Outputs

clean-css ✓

[title="a b c d"]{color:red}

csskit ✓

[title="a b c d"]{color:red}

csslop ✓

[title="a b c d"]{color:red}

cssnano ✓

[title="a b c d"]{color:red}

csso ✓

[title="a b c d"]{color:red}

esbuild ✓

[title="a b c d"]{color:red}

lightningcss ✓

[title="a b c d"]{color:red}

sass ✓

[title="a b c d"]{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove usesless :is()

If a :is() function only contains one element, and there is no comma-separated list of other selectors, then the :is offers no value and can be removed from the rule.

:is(.x) :is(:unknown) {
  color: red;
}

Expected

.x :unknown{color:red}

Outputs

clean-css ✗

:is(.x) :is(:unknown){color:red}

csskit ✗

:is(.x) :is(:unknown){color:red}

csslop ✓

.x :unknown{color:red}

cssnano ✗

:is(.x) :is(:unknown){color:red}

csso ✗

:is(.x) :is(:unknown){color:red}

esbuild ✗

:is(.x) :is(:unknown){color:red}

lightningcss ✓

.x :unknown{color:red}

sass ✗

:is(.x) :is(:unknown){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove usesless :is(), but retain useful :is()

When any part of a selector is not recognized by the browser, the entire rule gets ignored, unless the unrecognized portion is wrapped by a :is() function. Selectors that are clearly recognizable by the browser (class, id, :hover, etc) that are wrapped by an :is() without any other selectors can be unwrapped. However, if a browser could potentially not recognize a selector wrapped in an :is(), and there is a comma separated list of other selectors that would be effected by the rule being ignored if the :is was removed, then the wrapper must be retained to protect the rule.

:is(.x) :is(:unknown),
.y {
  color: red;
}

Expected

.x :is(:unknown),.y{color:red}

Outputs

clean-css ✗

.y,:is(.x) :is(:unknown){color:red}

csskit ✗

:is(.x) :is(:unknown),.y{color:red}

csslop ✓

.x :is(:unknown),.y{color:red}

cssnano ✗

.y,:is(.x) :is(:unknown){color:red}

csso ✗

.y,:is(.x) :is(:unknown){color:red}

esbuild ✗

:is(.x) :is(:unknown),.y{color:red}

lightningcss ✗

.x :unknown,.y{color:red}

sass ✗

:is(.x) :is(:unknown),.y{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove usesless :is() around pseudo-classes

If an :is() wraps well-known pseudo-classes (:hover, :required, etc), it can safely be removed. The selectors have the same level of specificity. and there is no risk of the browser ignoring the rule because the selectors are common.

:is(:hover, :visited) {
  color: red;
}

Expected

:hover,:visited{color:red}

Outputs

clean-css ✗

:is(:hover,:visited){color:red}

csskit ✗

:is(:hover,:visited){color:red}

csslop ✓

:hover,:visited{color:red}

cssnano ✗

:is(:hover,:visited){color:red}

csso ✗

:is(:hover,:visited){color:red}

esbuild ✗

:is(:hover,:visited){color:red}

lightningcss ✗

:is(:hover,:visited){color:red}

sass ✗

:is(:hover,:visited){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 11 / 20 5 / 20 20 / 20 13 / 20 8 / 20 11 / 20 18 / 20 5 / 20
Percent 55% 25% 100% 65% 40% 55% 90% 25%
Duration 1ms 1ms 2ms 9ms 2ms 14ms 1ms 6ms

selectors-advanced #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:is() decomposition with equal-specificity selectors

:is(a, b) can be decomposed to a,b when all selectors are widely supported and share the same specificity. This is safe because :is() takes the highest specificity of its arguments, which equals each individual selector here.

:is( a , b ) {
  color: red;
}

Expected

a,b{color:red}

Outputs

clean-css ✗

:is( a ,b ){color:red}

csskit ✗

:is(a,b){color:red}

csslop ✓

a,b{color:red}

cssnano ✗

:is(a,b){color:red}

csso ✗

:is(a,b){color:red}

esbuild ✗

:is(a,b){color:red}

lightningcss ✗

:is(a,b){color:red}

sass ✗

:is(a,b){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

:is() must not decompose with mixed specificity

:is(#a, .b) cannot be decomposed to #a,.b because :is() takes the highest specificity of its arguments. Both .b and #a match with (1,0,0) inside :is(), but decomposed .b would only have (0,1,0).

:is(#a, .b) {
  color: red;
}

Expected

:is(#a,.b){color:red}

Outputs

clean-css ✓

:is(#a,.b){color:red}

csskit ✓

:is(#a,.b){color:red}

csslop ✓

:is(#a,.b){color:red}

cssnano ✓

:is(#a,.b){color:red}

csso ✓

:is(#a,.b){color:red}

esbuild ✓

:is(#a,.b){color:red}

lightningcss ✓

:is(#a,.b){color:red}

sass ✓

:is(#a,.b){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

:has() whitespace removal

Whitespace inside :has() around combinators can be removed, same as regular selector lists.

a:has(> b) {
  color: red;
}

Expected

a:has(>b){color:red}

Outputs

clean-css ✗

a:has(> b){color:red}

csskit ✓

a:has(>b){color:red}

csslop ✓

a:has(>b){color:red}

cssnano ✓

a:has(>b){color:red}

csso ✓

a:has(>b){color:red}

esbuild ✓

a:has(>b){color:red}

lightningcss ✓

a:has(>b){color:red}

sass ✓

a:has(>b){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

:where() whitespace removal

Whitespace inside :where() around the selector list commas can be removed. :where() itself must not be replaced with :is() as they differ in specificity (:where() is zero-specificity).

:where(.foo, .bar) {
  color: red;
}

Expected

:where(.foo,.bar){color:red}

Outputs

clean-css ✓

:where(.foo,.bar){color:red}

csskit ✓

:where(.foo,.bar){color:red}

csslop ✓

:where(.foo,.bar){color:red}

cssnano ✓

:where(.foo,.bar){color:red}

csso ✓

:where(.foo,.bar){color:red}

esbuild ✓

:where(.foo,.bar){color:red}

lightningcss ✓

:where(.foo,.bar){color:red}

sass ✓

:where(.foo,.bar){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

input:not(:invalid) to input:valid

On form elements :valid and :invalid are a complete partition -- every input is one or the other. So input:not(:invalid) can be replaced with the shorter input:valid. This only holds when the type selector restricts to elements that participate in constraint validation (input, textarea, button, etc). It does not apply to classes that could be applied to multiple element types. For example, div:not(:invalid) selects all divs, however div:valid selects none.

button:not(:invalid),
fieldset:not(:invalid),
form:not(:invalid),
input:not(:invalid),
select:not(:invalid),
textarea:not(:invalid),
div:not(:invalid),
custom-element:not(:invalid),
.foo:not(:invalid) {
  color: red;
}

Expected

button:valid,fieldset:valid,form:valid,input:valid,select:valid,textarea:valid,div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}

Outputs

clean-css ✗

.foo:not(:invalid),button:not(:invalid),custom-element:not(:invalid),div:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid){color:red}

csskit ✗

button:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid),div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}

csslop ✓

button:valid,fieldset:valid,form:valid,input:valid,select:valid,textarea:valid,div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}

cssnano ✗

.foo:not(:invalid),button:not(:invalid),custom-element:not(:invalid),div:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid){color:red}

csso ✗

.foo:not(:invalid),button:not(:invalid),custom-element:not(:invalid),div:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid){color:red}

esbuild ✗

button:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid),div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}

lightningcss ✗

button:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid),div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}

sass ✗

button:not(:invalid),fieldset:not(:invalid),form:not(:invalid),input:not(:invalid),select:not(:invalid),textarea:not(:invalid),div:not(:invalid),custom-element:not(:invalid),.foo:not(:invalid){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:not(:dir(ltr)) to :dir(rtl)

Every element has exactly one directionality -- ltr or rtl. The two values are a complete partition, so :not(:dir(ltr)) is equivalent to :dir(rtl).

a:not(:dir(ltr)) {
  color: red;
}

Expected

a:dir(rtl){color:red}

Outputs

clean-css ✗

a:not(:dir(ltr)){color:red}

csskit ✗

a:not(:dir(ltr)){color:red}

csslop ✓

a:dir(rtl){color:red}

cssnano ✗

a:not(:dir(ltr)){color:red}

csso ✗

a:not(:dir(ltr)){color:red}

esbuild ✗

a:not(:dir(ltr)){color:red}

lightningcss ✗

a:not(:dir(ltr)){color:red}

sass ✗

a:not(:dir(ltr)){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Double :not() cancellation

:not(:not(X)) is logically equivalent to X. The double negation can be removed, producing a shorter selector.

a:not(:not(.active)) {
  color: red;
}

Expected

a.active{color:red}

Outputs

clean-css ✗

a:not(:not(.active)){color:red}

csskit ✗

a:not(:not(.active)){color:red}

csslop ✓

a.active{color:red}

cssnano ✗

a:not(:not(.active)){color:red}

csso ✗

a:not(:not(.active)){color:red}

esbuild ✗

a:not(:not(.active)){color:red}

lightningcss ✗

a:not(:not(.active)){color:red}

sass ✗

a:not(:not(.active)){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

input:not(:enabled) to input:disabled

On form-associated elements :enabled and :disabled are a complete partition. input:not(:enabled) can be replaced with the shorter input:disabled.

input:not(:enabled) {
  color: red;
}

Expected

input:disabled{color:red}

Outputs

clean-css ✗

input:not(:enabled){color:red}

csskit ✗

input:not(:enabled){color:red}

csslop ✓

input:disabled{color:red}

cssnano ✗

input:not(:enabled){color:red}

csso ✗

input:not(:enabled){color:red}

esbuild ✗

input:not(:enabled){color:red}

lightningcss ✗

input:not(:enabled){color:red}

sass ✗

input:not(:enabled){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

input:not(:required) to input:optional

On form-associated elements :required and :optional are a complete partition. input:not(:required) can be replaced with input:optional.

input:not(:required) {
  color: red;
}

Expected

input:optional{color:red}

Outputs

clean-css ✗

input:not(:required){color:red}

csskit ✗

input:not(:required){color:red}

csslop ✓

input:optional{color:red}

cssnano ✗

input:not(:required){color:red}

csso ✗

input:not(:required){color:red}

esbuild ✗

input:not(:required){color:red}

lightningcss ✗

input:not(:required){color:red}

sass ✗

input:not(:required){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

a:not(:link) to a:visited

On hyperlink elements (<a>, <area>, <link> with href) :link and :visited are a complete partition. a:not(:link) can be replaced with the shorter a:visited. However, this should not apply to non-hyperlink elements (<div>, <h1>, etc), and should not apply to non-tag selectors, for example, a class could be applied to both <a class="x"> and <div class="x">. HREF attribute selectors should only receive this minification if they have a qualifying element of a, area, or link.

a:not(:link),
area:not(:link),
link:not(:link),
.foo:not(:link),
h1:not(:link),
[href]:not(:link),
[href="/foo.html#bar"]:not(:link),
a[href]:not(:link),
area[href="/foo.html#bar"]:not(:link),
:where(:not(:link)),
a:where(:not(:link)) {
  color: red;
}

Expected

a:visited,area:visited,link:visited,.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:visited,area[href="/foo.html#bar"]:visited,:where(:not(:link)),a:where(:visited){color:red}

Outputs

clean-css ✗

.foo:not(:link),:where(:not(:link)),[href="/foo.html#bar"]:not(:link),[href]:not(:link),a:not(:link),a:where(:not(:link)),a[href]:not(:link),area:not(:link),area[href="/foo.html#bar"]:not(:link),h1:not(:link),link:not(:link){color:red}

csskit ✗

a:not(:link),area:not(:link),link:not(:link),.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:not(:link),area[href="/foo.html#bar"]:not(:link),:where(:not(:link)),a:where(:not(:link)){color:red}

csslop ✓

a:visited,area:visited,link:visited,.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:visited,area[href="/foo.html#bar"]:visited,:where(:not(:link)),a:where(:visited){color:red}

cssnano ✗

.foo:not(:link),:where(:not(:link)),[href="/foo.html#bar"]:not(:link),[href]:not(:link),a:not(:link),a:where(:not(:link)),a[href]:not(:link),area:not(:link),area[href="/foo.html#bar"]:not(:link),h1:not(:link),link:not(:link){color:red}

csso ✗

.foo:not(:link),:where(:not(:link)),[href="/foo.html#bar"]:not(:link),[href]:not(:link),a:not(:link),a:where(:not(:link)),a[href]:not(:link),area:not(:link),area[href="/foo.html#bar"]:not(:link),h1:not(:link),link:not(:link){color:red}

esbuild ✗

a:not(:link),area:not(:link),link:not(:link),.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:not(:link),area[href="/foo.html#bar"]:not(:link),:where(:not(:link)),a:where(:not(:link)){color:red}

lightningcss ✗

a:not(:link),area:not(:link),link:not(:link),.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:not(:link),area[href="/foo.html#bar"]:not(:link),:where(:not(:link)),a:where(:not(:link)){color:red}

sass ✗

a:not(:link),area:not(:link),link:not(:link),.foo:not(:link),h1:not(:link),[href]:not(:link),[href="/foo.html#bar"]:not(:link),a[href]:not(:link),area[href="/foo.html#bar"]:not(:link),:where(:not(:link)),a:where(:not(:link)){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:is(:link, :visited) to :any-link

:any-link is equivalent to :is(:link, :visited) and is shorter. It matches any element that is a hyperlink source anchor (unvisited or visited).

:is(:link, :visited) {
  color: red;
}

Expected

:any-link{color:red}

Outputs

clean-css ✗

:is(:link,:visited){color:red}

csskit ✗

:is(:link,:visited){color:red}

csslop ✓

:any-link{color:red}

cssnano ✗

:is(:link,:visited){color:red}

csso ✗

:is(:link,:visited){color:red}

esbuild ✗

:is(:link,:visited){color:red}

lightningcss ✗

:is(:link,:visited){color:red}

sass ✗

:is(:link,:visited){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:is() scoped :not(:required) to :optional

When :is() restricts to form-associated elements, :required and :optional are a complete partition. :is(textarea, input):not(:required) can be replaced with :is(textarea, input):optional.

:is(textarea, input):not(:required) {
  color: red;
}

Expected

:is(textarea,input):optional{color:red}

Outputs

clean-css ✗

:is(textarea,input):not(:required){color:red}

csskit ✗

:is(textarea,input):not(:required){color:red}

csslop ✓

:is(textarea,input):optional{color:red}

cssnano ✗

:is(textarea,input):not(:required){color:red}

csso ✗

:is(textarea,input):not(:required){color:red}

esbuild ✗

:is(textarea,input):not(:required){color:red}

lightningcss ✗

:is(textarea,input):not(:required){color:red}

sass ✗

:is(textarea,input):not(:required){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

h1-h6 is not changed to :heading

h1,h2,h3,h4,h5,h6 can be replaced with :heading per CSS Selectors Level 5. The :heading pseudo-class matches any element with a heading level. However, its specificity is that of a class (0 1 0). Replacing the H1-H6 tags that have a lower specificity (0 0 1), with :heading may override styles that come later in the CSSOM, including other files or inserted styles from browser extensions. For correctness, a minifier should not perform this optimization.

To achieve the same level of specificity with :heading you would need to lower its specificity with :where, then wrap it in an :is with one of the heading tags, so it receives the same level as it: :is(h1,:where(:heading)), which works, but is longer than just using h1,h2,h3,h4,h5,h6.

h1, h2, h3, h4, h5, h6 {
  color: red;
}

Expected

h1,h2,h3,h4,h5,h6{color:red}

Validate

Outputs

clean-css ✓

h1,h2,h3,h4,h5,h6{color:red}

csskit ✓

h1,h2,h3,h4,h5,h6{color:red}

csslop ✓

h1,h2,h3,h4,h5,h6{color:red}

cssnano ✓

h1,h2,h3,h4,h5,h6{color:red}

csso ✓

h1,h2,h3,h4,h5,h6{color:red}

esbuild ✓

h1,h2,h3,h4,h5,h6{color:red}

lightningcss ✓

h1,h2,h3,h4,h5,h6{color:red}

sass ✓

h1,h2,h3,h4,h5,h6{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Sorting :is() selectors

Selector minification in :is can be complex. To keep expectations consistent the order should be alphabetized. This also increases the liklihood of text repetition, giving better compression in gzip/brotli.

:is(h2, a, h1, .foo, :visited, p, .example, .bar) {
  color: red;
}

Expected

:is(.bar,.example,.foo,:visited,a,h1,h2,p){color:red}

Outputs

clean-css ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

csskit ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

csslop ✓

:is(.bar,.example,.foo,:visited,a,h1,h2,p){color:red}

cssnano ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

csso ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

esbuild ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

lightningcss ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}

sass ✗

:is(h2,a,h1,.foo,:visited,p,.example,.bar){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

De-dupe selectors in :is()

Duplicate selectors in :is() serve no purpose and can be safely removed.

:is(.foo, .foo, h1, h1) {
  color: red;
}

Expected

:is(.foo,h1){color:red}

Outputs

clean-css ✗

:is(.foo,.foo,h1,h1){color:red}

csskit ✗

:is(.foo,.foo,h1,h1){color:red}

csslop ✓

:is(.foo,h1){color:red}

cssnano ✓

:is(.foo,h1){color:red}

csso ✗

:is(.foo,.foo,h1,h1){color:red}

esbuild ✓

:is(.foo,h1){color:red}

lightningcss ✗

:is(.foo,.foo,h1,h1){color:red}

sass ✗

:is(.foo,.foo,h1,h1){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Retaining correct specificity in :is

In 0011 (:is(:link, :visited) to :any-link), we can safely remove the :is because there are no other selectors inside it. But in this test, we must keep the :any-link inside the :is, because :is adopts the specificity of the most specific selector inside it.

/* :is has specificity of 010 */
:is(:link,:visited,:where(h1)){color:red}

/* :is has specificity of 000 */
:any-link,:is(:where(h1)){color:red}

/* :is has specificity of 010 */
:is(:any-link,:where(h1)){color:red}

The changing in specificity may effect the pixels painted to the screen. This test ensures correctness in the minification.

:is(:visited, :link, :where(.link)) {
  color: red;
}

Expected

:is(:any-link,:where(.link)){color:red}

Outputs

clean-css ✗

:is(:visited,:link,:where(.link)){color:red}

csskit ✗

:is(:visited,:link,:where(.link)){color:red}

csslop ✓

:is(:any-link,:where(.link)){color:red}

cssnano ✗

:is(:visited,:link,:where(.link)){color:red}

csso ✗

:is(:visited,:link,:where(.link)){color:red}

esbuild ✗

:is(:visited,:link,:where(.link)){color:red}

lightningcss ✗

:is(:visited,:link,:where(.link)){color:red}

sass ✗

:is(:visited,:link,:where(.link)){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Advanced :is() selector combination

This test combines the following :is-related tests:

  • selectors-advanced/0011 - :is(:link, :visited) to :any-link
  • selectors-advanced/0014 - Sorting :is() selectors
  • selectors-advanced/0015 - De-dupe selectors in :is()
  • selectors-advanced/0016 - Retaining correct specificity in :is

:is(h2, :link, h1, :visited, h1, :any-link, div) {
  color: red;
}

Expected

:is(:any-link,div,h1,h2){color:red}

Outputs

clean-css ✗

:is(h2,:link,h1,:visited,h1,:any-link,div){color:red}

csskit ✗

:is(h2,:link,h1,:visited,h1,:any-link,div){color:red}

csslop ✓

:is(:any-link,div,h1,h2){color:red}

cssnano ✗

:is(h2,:link,h1,:visited,:any-link,div){color:red}

csso ✗

:is(h2,:link,h1,:visited,h1,:any-link,div){color:red}

esbuild ✗

:is(h2,:link,h1,:visited,:any-link,div){color:red}

lightningcss ✗

:is(h2,:link,h1,:visited,h1,:any-link,div){color:red}

sass ✗

:is(h2,:link,h1,:visited,h1,:any-link,div){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:where() compound merge

Adjacent :where(A):where(B) with single selectors merges to :where(AB). Saves exactly 8 chars (one :where() wrapper). :where() contributes zero specificity either way, and A and B must both match simultaneously.

:where(.foo):where([bar]) {
  color: red;
}

Expected

:where(.foo[bar]){color:red}

Outputs

clean-css ✗

:where(.foo):where([bar]){color:red}

csskit ✗

:where(.foo):where([bar]){color:red}

csslop ✓

:where(.foo[bar]){color:red}

cssnano ✗

:where(.foo):where([bar]){color:red}

csso ✗

:where(.foo):where([bar]){color:red}

esbuild ✗

:where(.foo):where([bar]){color:red}

lightningcss ✗

:where(.foo):where([bar]){color:red}

sass ✗

:where(.foo):where([bar]){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:where() merge when shorter

:where(A):where(B,C) merges to :where(AB,AC) when the cartesian product is shorter. Here the saved :where() wrapper (8 chars) outweighs the cost of duplicating A across 2 selectors. The break even point is the product of the combined selectors needs to be less than 8.

:where(.foo):where(.bar, .baz) {
  color: red;
}

Expected

:where(.foo.bar,.foo.baz){color:red}

Outputs

clean-css ✗

:where(.foo):where(.bar,.baz){color:red}

csskit ✗

:where(.foo):where(.bar,.baz){color:red}

csslop ✓

:where(.foo.bar,.foo.baz){color:red}

cssnano ✗

:where(.foo):where(.bar,.baz){color:red}

csso ✗

:where(.foo):where(.bar,.baz){color:red}

esbuild ✗

:where(.foo):where(.bar,.baz){color:red}

lightningcss ✗

:where(.foo):where(.bar,.baz){color:red}

sass ✗

:where(.foo):where(.bar,.baz){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

:where() don't merge when longer

:where(.foo):where(.a,.b,.c,.d) should NOT be merged: the cartesian product :where(.foo.a,.foo.b,.foo.c,.foo.d) is 35 chars vs 31 chars unmerged. Merging always saves 8 chars of wrapper overhead but costs extra chars from duplicating the first selector; here 4*(4-1)=12 > 8 so keep it is more efficient to keep them separate.

:where(.foo):where(.a, .b, .c, .d) {
  color: red;
}

Expected

:where(.foo):where(.a,.b,.c,.d){color:red}

Outputs

clean-css ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

csskit ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

csslop ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

cssnano ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

csso ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

esbuild ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

lightningcss ✓

:where(.foo):where(.a,.b,.c,.d){color:red}

sass ✓

:where(.foo):where(.a,.b,.c,.d){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

:where() merge with type selector reordering

:where(:hover):where(a) merges to :where(a:hover), not :where(:hover a). A type selector must come first in a compound selector. Naive string concatenation might produce :where(:hover a) which is a complex selector (descendant combinator) with different semantics.

:where(:hover):where(a) {
  color: red;
}

Expected

:where(a:hover){color:red}

Outputs

clean-css ✗

:where(:hover):where(a){color:red}

csskit ✗

:where(:hover):where(a){color:red}

csslop ✓

:where(a:hover){color:red}

cssnano ✗

:where(:hover):where(a){color:red}

csso ✗

:where(:hover):where(a){color:red}

esbuild ✗

:where(:hover):where(a){color:red}

lightningcss ✗

:where(:hover):where(a){color:red}

sass ✗

:where(:hover):where(a){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

:where() with conflicting type selectors - no merge

:where(div):where(span) cannot be merged: a compound selector cannot contain two type selectors. The selector already matches nothing (no element is both div and span), but the correct output is to leave it unmerged rather than produce invalid CSS.

:where(div):where(span) {
  color: red;
}

Expected

:where(div):where(span){color:red}

Outputs

clean-css ✓

:where(div):where(span){color:red}

csskit ✓

:where(div):where(span){color:red}

csslop ✓

:where(div):where(span){color:red}

cssnano ✓

:where(div):where(span){color:red}

csso ✓

:where(div):where(span){color:red}

esbuild ✓

:where(div):where(span){color:red}

lightningcss ✓

:where(div):where(span){color:red}

sass ✓

:where(div):where(span){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove :is after de-duping selectors

Repeated selectors in a :is() can be removed. If all selectors have the same level of specificity, and are known to be browser-safe, the :is() can be removed too.

:is(div, span, div, span) {
  color: red;
}
:is(.bar, .foo, .bar, .foo) {
  color: tan;
}

Expected

div,span{color:red}.bar,.foo{color:tan}

Outputs

clean-css ✗

:is(div,span,div,span){color:red}:is(.bar,.foo,.bar,.foo){color:tan}

csskit ✗

:is(div,span,div,span){color:red}:is(.bar,.foo,.bar,.foo){color:tan}

csslop ✓

div,span{color:red}.bar,.foo{color:tan}

cssnano ✗

:is(div,span){color:red}:is(.bar,.foo){color:tan}

csso ✗

:is(div,span,div,span){color:red}:is(.bar,.foo,.bar,.foo){color:tan}

esbuild ✗

:is(div,span){color:red}:is(.bar,.foo){color:tan}

lightningcss ✗

:is(div,span,div,span){color:red}:is(.bar,.foo,.bar,.foo){color:tan}

sass ✗

:is(div,span,div,span){color:red}:is(.bar,.foo,.bar,.foo){color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove :is after de-duping selectors with custom tags

Repeated selectors in a :is() can be removed. If all selectors have the same level of specificity, and are known to be browser-safe, the :is() can be removed too.

:is(
  custom-component,
  custom-component,
  div,
  div
) {
  color: red;
}

Expected

custom-component,div{color:red}

Outputs

clean-css ✗

:is(
custom-component,custom-component,div,div
){color:red}

csskit ✗

:is(custom-component,custom-component,div,div){color:red}

csslop ✓

custom-component,div{color:red}

cssnano ✗

:is(custom-component,div){color:red}

csso ✗

:is(custom-component,custom-component,div,div){color:red}

esbuild ✗

:is(custom-component,div){color:red}

lightningcss ✗

:is(custom-component,custom-component,div,div){color:red}

sass ✗

:is(custom-component,custom-component,div,div){color:red}
1ms
3ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✗ ✗ ✓ ✓ ✗ ✓ ✗ ✗
Details

Retain :is after de-duping selectors if not all selectors are browser-safe

Repeated selectors in a :is() can be removed. If a selector is not recognized as being browser-safe the :is() must not be removed, as all selectors for the rule would then be ignored by the browser.

:is(
  :foo,
  :foo,
  :hover,
  :hover
) {
  color: red;
}

Expected

:is(:foo,:hover){color:red}

Outputs

clean-css ✗

:is(
:foo,:foo,:hover,:hover
){color:red}

csskit ✗

:is(:foo,:foo,:hover,:hover){color:red}

csslop ✓

:is(:foo,:hover){color:red}

cssnano ✓

:is(:foo,:hover){color:red}

csso ✗

:is(:foo,:foo,:hover,:hover){color:red}

esbuild ✓

:is(:foo,:hover){color:red}

lightningcss ✗

:is(:foo,:foo,:hover,:hover){color:red}

sass ✗

:is(:foo,:foo,:hover,:hover){color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✗ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Remove selector from list in rule when it is fully overridden later

If a selector is in a group of selectors on a rule, and all properties in that rule are overridden for that selector later, it is safe to remove it from the earlier list.

h1,
h2,
h3,
.foo {
  color: red;
}
h2 {
  color: tan;
}
.foo {
  color: #123;
  padding: 1px;
}

Expected

h1,h3{color:red}h2{color:tan}.foo{color:#123;padding:1px}

Outputs

clean-css ✗

.foo,h1,h2,h3{color:red}h2{color:tan}.foo{color:#123;padding:1px}

csskit ✗

h1,h2,h3,.foo{color:red}h2{color:tan}.foo{color:#123;padding:1px}

csslop ✓

h1,h3{color:red}h2{color:tan}.foo{color:#123;padding:1px}

cssnano ✗

.foo,h1,h2,h3{color:red}h2{color:tan}.foo{color:#123;padding:1px}

csso ✓

h1,h3{color:red}h2{color:tan}.foo{color:#123;padding:1px}

esbuild ✗

h1,h2,h3,.foo{color:red}h2{color:tan}.foo{color:#123;padding:1px}

lightningcss ✗

h1,h2,h3,.foo{color:red}h2{color:tan}.foo{color:#123;padding:1px}

sass ✗

h1,h2,h3,.foo{color:red}h2{color:tan}.foo{color:#123;padding:1px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 5 / 26 6 / 26 26 / 26 8 / 26 7 / 26 8 / 26 6 / 26 6 / 26
Percent 19.23% 23.07% 100% 30.76% 26.92% 30.76% 23.07% 23.07%
Duration 1ms 4ms 4ms 18ms 4ms 16ms 1ms 9ms

shorthand-values #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

margin 4-to-1 collapse

margin: 10px 10px 10px 10px collapses to margin:10px when all four values are identical.

a {
  margin: 10px 10px 10px 10px;
}

Expected

a{margin:10px}

Outputs

clean-css ✓

a{margin:10px}

csskit ✓

a{margin:10px}

csslop ✓

a{margin:10px}

cssnano ✓

a{margin:10px}

csso ✓

a{margin:10px}

esbuild ✓

a{margin:10px}

lightningcss ✓

a{margin:10px}

sass ✗

a{margin:10px 10px 10px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

margin 4-to-2 collapse

margin: 10px 20px 10px 20px collapses to margin:10px 20px when top/bottom and left/right pairs match.

a {
  margin: 10px 20px 10px 20px;
}

Expected

a{margin:10px 20px}

Outputs

clean-css ✓

a{margin:10px 20px}

csskit ✓

a{margin:10px 20px}

csslop ✓

a{margin:10px 20px}

cssnano ✓

a{margin:10px 20px}

csso ✓

a{margin:10px 20px}

esbuild ✓

a{margin:10px 20px}

lightningcss ✓

a{margin:10px 20px}

sass ✗

a{margin:10px 20px 10px 20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

margin 4-to-3 collapse

margin: 10px 20px 30px 20px collapses to margin:10px 20px 30px when left equals right.

a {
  margin: 10px 20px 30px 20px;
}

Expected

a{margin:10px 20px 30px}

Outputs

clean-css ✓

a{margin:10px 20px 30px}

csskit ✓

a{margin:10px 20px 30px}

csslop ✓

a{margin:10px 20px 30px}

cssnano ✓

a{margin:10px 20px 30px}

csso ✓

a{margin:10px 20px 30px}

esbuild ✓

a{margin:10px 20px 30px}

lightningcss ✓

a{margin:10px 20px 30px}

sass ✗

a{margin:10px 20px 30px 20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

background:none to background:0 0

background: none and background: 0 0 are equivalent. Both reset all background longhands to initial values -- 0 0 is parsed as background-position and all omitted longhands (including background-image) reset to their initial values. 0 0 is one byte shorter.

a {
  background: none;
}

Expected

a{background:0 0}

Outputs

clean-css ✓

a{background:0 0}

csskit ✗

a{background:none}

csslop ✓

a{background:0 0}

cssnano ✗

a{background:none}

csso ✓

a{background:0 0}

esbuild ✗

a{background:none}

lightningcss ✓

a{background:0 0}

sass ✗

a{background:none}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Color shortening and space removal before hash

border: 1px solid #000000 shortens the hex color to #000 and removes the space before # since # unambiguously starts a hash token -- no whitespace is needed to separate it from the preceding keyword.

a {
  border: 1px solid #000000;
}

Expected

a{border:1px solid#000}

Outputs

clean-css ✗

a{border:1px solid #000}

csskit ✓

a{border:1px solid#000}

csslop ✓

a{border:1px solid#000}

cssnano ✗

a{border:1px solid #000}

csso ✗

a{border:1px solid #000}

esbuild ✗

a{border:1px solid #000000}

lightningcss ✗

a{border:1px solid #000}

sass ✗

a{border:1px solid #000}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

inset shorthand collapse + zero-unit strip

inset: 0px 0px 0px 0px collapses to inset:0. Combines 4-to-1 shorthand collapse with zero-unit stripping. The inset property follows the same pattern as margin/padding.

a {
  inset: 0px 0px 0px 0px;
}

Expected

a{inset:0}

Outputs

clean-css ✗

a{inset:0px 0px 0px 0px}

csskit ✓

a{inset:0}

csslop ✓

a{inset:0}

cssnano ✓

a{inset:0}

csso ✗

a{inset:0 0 0 0}

esbuild ✓

a{inset:0}

lightningcss ✓

a{inset:0}

sass ✗

a{inset:0px 0px 0px 0px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

padding 4-value to 1-value collapse

padding: 10px 10px 10px 10px collapses to padding: 10px when all four values are identical.

a {
  padding: 10px 10px 10px 10px;
}

Expected

a{padding:10px}

Outputs

clean-css ✓

a{padding:10px}

csskit ✓

a{padding:10px}

csslop ✓

a{padding:10px}

cssnano ✓

a{padding:10px}

csso ✓

a{padding:10px}

esbuild ✓

a{padding:10px}

lightningcss ✓

a{padding:10px}

sass ✗

a{padding:10px 10px 10px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

padding 4-to-2 value collapse

When top equals bottom and right equals left, padding collapses from 4 values to 2.

a {
  padding: 5px 10px 5px 10px;
}

Expected

a{padding:5px 10px}

Outputs

clean-css ✓

a{padding:5px 10px}

csskit ✓

a{padding:5px 10px}

csslop ✓

a{padding:5px 10px}

cssnano ✓

a{padding:5px 10px}

csso ✓

a{padding:5px 10px}

esbuild ✓

a{padding:5px 10px}

lightningcss ✓

a{padding:5px 10px}

sass ✗

a{padding:5px 10px 5px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

padding 4-to-3 value collapse

When left equals right, the 4th value can be dropped. Top/bottom differ so 3 values remain.

a {
  padding: 5px 10px 15px 10px;
}

Expected

a{padding:5px 10px 15px}

Outputs

clean-css ✓

a{padding:5px 10px 15px}

csskit ✓

a{padding:5px 10px 15px}

csslop ✓

a{padding:5px 10px 15px}

cssnano ✓

a{padding:5px 10px 15px}

csso ✓

a{padding:5px 10px 15px}

esbuild ✓

a{padding:5px 10px 15px}

lightningcss ✓

a{padding:5px 10px 15px}

sass ✗

a{padding:5px 10px 15px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

border-radius 4-to-2 value collapse

border-radius follows the same top-right-bottom-left collapsing rules as margin/padding.

a {
  border-radius: 5px 10px 5px 10px;
}

Expected

a{border-radius:5px 10px}

Outputs

clean-css ✓

a{border-radius:5px 10px}

csskit ✓

a{border-radius:5px 10px}

csslop ✓

a{border-radius:5px 10px}

cssnano ✓

a{border-radius:5px 10px}

csso ✗

a{border-radius:5px 10px 5px 10px}

esbuild ✓

a{border-radius:5px 10px}

lightningcss ✓

a{border-radius:5px 10px}

sass ✗

a{border-radius:5px 10px 5px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

border-radius slash syntax collapse

When both horizontal and vertical radii have identical values across all 4 corners, each side collapses to 1 value. The / separator must be preserved.

a {
  border-radius: 10px 10px 10px 10px / 5px 5px 5px 5px;
}

Expected

a{border-radius:10px/5px}

Outputs

clean-css ✓

a{border-radius:10px/5px}

csskit ✓

a{border-radius:10px/5px}

csslop ✓

a{border-radius:10px/5px}

cssnano ✓

a{border-radius:10px/5px}

csso ✗

a{border-radius:10px 10px 10px 10px/5px 5px 5px 5px}

esbuild ✓

a{border-radius:10px/5px}

lightningcss ✓

a{border-radius:10px/5px}

sass ✗

a{border-radius:10px 10px 10px 10px/5px 5px 5px 5px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Inset 4-to-2 value collapse

The inset shorthand follows the same collapsing rules as margin/padding.

a {
  inset: 10px 20px 10px 20px;
}

Expected

a{inset:10px 20px}

Outputs

clean-css ✗

a{inset:10px 20px 10px 20px}

csskit ✓

a{inset:10px 20px}

csslop ✓

a{inset:10px 20px}

cssnano ✓

a{inset:10px 20px}

csso ✗

a{inset:10px 20px 10px 20px}

esbuild ✓

a{inset:10px 20px}

lightningcss ✓

a{inset:10px 20px}

sass ✗

a{inset:10px 20px 10px 20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Margin 2-to-1 value collapse

When both values in a 2-value margin are identical, collapse to a single value.

a {
  margin: 10px 10px;
}

Expected

a{margin:10px}

Outputs

clean-css ✓

a{margin:10px}

csskit ✓

a{margin:10px}

csslop ✓

a{margin:10px}

cssnano ✓

a{margin:10px}

csso ✓

a{margin:10px}

esbuild ✓

a{margin:10px}

lightningcss ✓

a{margin:10px}

sass ✗

a{margin:10px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Border default width omission

medium is the default border-width value and can be omitted from the border shorthand.

a {
  border: medium solid red;
}

Expected

a{border:solid red}

Outputs

clean-css ✓

a{border:solid red}

csskit ✗

a{border:medium solid red}

csslop ✓

a{border:solid red}

cssnano ✓

a{border:solid red}

csso ✗

a{border:medium solid red}

esbuild ✗

a{border:medium solid red}

lightningcss ✓

a{border:solid red}

sass ✗

a{border:medium solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Background shorthand default omission

Default background-position (0% 0%), background-repeat (repeat), and background-attachment (scroll) can be omitted from the background shorthand.

a {
  background: red url(bg.png) 0% 0% repeat scroll;
}

Expected

a{background:red url(bg.png)}

Outputs

clean-css ✗

a{background:url(bg.png) red}

csskit ✗

a{background:url(bg.png)0% 0% repeat scroll red}

csslop ✓

a{background:red url(bg.png)}

cssnano ✗

a{background:red url(bg.png) 0 0 repeat scroll}

csso ✗

a{background:red url(bg.png)0 0}

esbuild ✗

a{background:red url(bg.png) 0% 0% repeat scroll}

lightningcss ✓

a{background:red url(bg.png)}

sass ✗

a{background:red url(bg.png) 0% 0% repeat scroll}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

Font shorthand default value omission

normal is the default for both font-style and font-weight; both can be omitted from the font shorthand.

a {
  font: normal normal 16px/1.5 Arial, sans-serif;
}

Expected

a{font:16px/1.5 Arial,sans-serif}

Outputs

clean-css ✓

a{font:16px/1.5 Arial,sans-serif}

csskit ✗

a{font:normal normal 16px/1.5 Arial,sans-serif}

csslop ✓

a{font:16px/1.5 Arial,sans-serif}

cssnano ✗

a{font:normal normal 16px/1.5 Arial,sans-serif}

csso ✓

a{font:16px/1.5 Arial,sans-serif}

esbuild ✗

a{font: 16px/1.5 Arial,sans-serif}

lightningcss ✓

a{font:16px/1.5 Arial,sans-serif}

sass ✗

a{font:normal normal 16px/1.5 Arial,sans-serif}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✓ ✓ ✓ ✓ ✗ ✓ ✓
Details

Outline shorthand color optimization

Color values within shorthands like outline should still be optimized (#ff0000 -> red).

a {
  outline: 1px solid #ff0000;
}

Expected

a{outline:1px solid red}

Outputs

clean-css ✗

a{outline:#ff0000 solid 1px}

csskit ✓

a{outline:1px solid red}

csslop ✓

a{outline:1px solid red}

cssnano ✓

a{outline:1px solid red}

csso ✓

a{outline:1px solid red}

esbuild ✗

a{outline:1px solid #ff0000}

lightningcss ✓

a{outline:1px solid red}

sass ✓

a{outline:1px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Margin with auto values collapse

margin: 0 auto 0 auto collapses to margin: 0 auto. The auto keyword participates in the same collapsing rules as length values.

a {
  margin: 0 auto 0 auto;
}

Expected

a{margin:0 auto}

Outputs

clean-css ✓

a{margin:0 auto}

csskit ✓

a{margin:0 auto}

csslop ✓

a{margin:0 auto}

cssnano ✓

a{margin:0 auto}

csso ✓

a{margin:0 auto}

esbuild ✓

a{margin:0 auto}

lightningcss ✓

a{margin:0 auto}

sass ✗

a{margin:0 auto 0 auto}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

border-color 4-to-1 value collapse

border-color follows the same top-right-bottom-left collapsing rules as margin or padding.

a {
  border-color: red red red red;
}

Expected

a{border-color:red}

Outputs

clean-css ✓

a{border-color:red}

csskit ✓

a{border-color:red}

csslop ✓

a{border-color:red}

cssnano ✓

a{border-color:red}

csso ✓

a{border-color:red}

esbuild ✗

a{border-color:red red red red}

lightningcss ✓

a{border-color:red}

sass ✗

a{border-color:red red red red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Gap 2-to-1 value collapse

When row-gap and column-gap are the same in a 2-value gap, collapse to a single value.

a {
  gap: 10px 10px;
}

Expected

a{gap:10px}

Outputs

clean-css ✗

a{gap:10px 10px}

csskit ✓

a{gap:10px}

csslop ✓

a{gap:10px}

cssnano ✓

a{gap:10px}

csso ✗

a{gap:10px 10px}

esbuild ✗

a{gap:10px 10px}

lightningcss ✓

a{gap:10px}

sass ✗

a{gap:10px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Overflow 2-to-1 value collapse

When overflow-x and overflow-y are the same in a 2-value overflow, collapse to a single value.

a {
  overflow: hidden hidden;
}

Expected

a{overflow:hidden}

Outputs

clean-css ✗

a{overflow:hidden hidden}

csskit ✓

a{overflow:hidden}

csslop ✓

a{overflow:hidden}

cssnano ✓

a{overflow:hidden}

csso ✗

a{overflow:hidden hidden}

esbuild ✗

a{overflow:hidden hidden}

lightningcss ✓

a{overflow:hidden}

sass ✗

a{overflow:hidden hidden}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Transition shorthand default duration removal

transition: all 0s can be shortened to transition: all because 0s is the default value for transition-duration. The all keyword alone is a valid single-transition value with all other components taking their initial values.

a {
  transition: all 0s;
}

Expected

a{transition:all}

Outputs

clean-css ✗

a{transition:none}

csskit ✗

a{transition:all 0s}

csslop ✓

a{transition:all}

cssnano ✓

a{transition:all}

csso ✗

a{transition:all 0s}

esbuild ✗

a{transition:all 0s}

lightningcss ✓

a{transition:all}

sass ✗

a{transition:all 0s}
1ms
1ms
1ms
2ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Zero flex-basis can be omitted

flex: 0 0 0px shortens to flex: 0 0. When flex-grow and flex-shrink are specified as numbers, the omitted flex-basis defaults to 0%, which is equivalent to 0px (both resolve to zero length).

a {
  flex: 0 0 0px;
}

Expected

a{flex:0 0}

Outputs

clean-css ✗

a{flex:0 0 0px}

csskit ✗

a{flex:0 0 0}

csslop ✓

a{flex:0 0}

cssnano ✗

a{flex:0 0 0px}

csso ✗

a{flex:0 0 0px}

esbuild ✗

a{flex:0 0 0px}

lightningcss ✗

a{flex:0 0 0}

sass ✗

a{flex:0 0 0px}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Combine background position X and Y

If background-position-x and background-position-y are both used in the same rule, and there are no other background properties to be combined with, they can be combined into a single background-position.

a {
  background-position-x: 100%;
  background-position-y: 100%;
}

Expected

a{background-position:100% 100%}

Outputs

clean-css ✗

a{background-position-x:100%;background-position-y:100%}

csskit ✗

a{background-position-x:100%;background-position-y:100%}

csslop ✓

a{background-position:100% 100%}

cssnano ✗

a{background-position-x:100%;background-position-y:100%}

csso ✗

a{background-position-x:100%;background-position-y:100%}

esbuild ✗

a{background-position-x:100%;background-position-y:100%}

lightningcss ✓

a{background-position:100% 100%}

sass ✗

a{background-position-x:100%;background-position-y:100%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Retain 0 0 in flex declaration

The property/value pair of flex: 0 0 is shorthand for:

flex-grow: 0;
flex-shrink: 0;
flex-basis: 0%;

However, flex: 0 means:

flex-grow: 0;
flex-shrink: 1;
flex-basis: 0%;

So flex: 0 0 cannot be shortened to flex: 0.

a {
  flex: 0 0;
}

Expected

a{flex:0 0}

Outputs

clean-css ✓

a{flex:0 0}

csskit ✓

a{flex:0 0}

csslop ✓

a{flex:0 0}

cssnano ✓

a{flex:0 0}

csso ✓

a{flex:0 0}

esbuild ✓

a{flex:0 0}

lightningcss ✓

a{flex:0 0}

sass ✓

a{flex:0 0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 15 / 25 18 / 25 25 / 25 19 / 25 13 / 25 13 / 25 23 / 25 2 / 25
Percent 60% 72% 100% 76% 52% 52% 92% 8%
Duration 2ms 11ms 4ms 14ms 2ms 16ms 1ms 9ms

shorthands #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Overflow longhand to shorthand

overflow-x and overflow-y with the same value can be combined into overflow shorthand.

a {
  overflow-x: hidden;
  overflow-y: hidden;
}

Expected

a{overflow:hidden}

Outputs

clean-css ✗

a{overflow-x:hidden;overflow-y:hidden}

csskit ✗

a{overflow-x:hidden;overflow-y:hidden}

csslop ✓

a{overflow:hidden}

cssnano ✗

a{overflow-x:hidden;overflow-y:hidden}

csso ✗

a{overflow-x:hidden;overflow-y:hidden}

esbuild ✗

a{overflow-x:hidden;overflow-y:hidden}

lightningcss ✓

a{overflow:hidden}

sass ✗

a{overflow-x:hidden;overflow-y:hidden}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Gap longhand to shorthand

row-gap and column-gap with the same value can be combined into gap shorthand.

a {
  row-gap: 10px;
  column-gap: 10px;
}

Expected

a{gap:10px}

Outputs

clean-css ✗

a{row-gap:10px;column-gap:10px}

csskit ✗

a{row-gap:10px;column-gap:10px}

csslop ✓

a{gap:10px}

cssnano ✗

a{row-gap:10px;column-gap:10px}

csso ✗

a{row-gap:10px;column-gap:10px}

esbuild ✗

a{row-gap:10px;column-gap:10px}

lightningcss ✓

a{gap:10px}

sass ✗

a{row-gap:10px;column-gap:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Flex longhand to shorthand

flex-grow, flex-shrink, and flex-basis can be combined into flex shorthand.

a {
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: auto;
}

Expected

a{flex:1 0 auto}

Outputs

clean-css ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}

csskit ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}

csslop ✓

a{flex:1 0 auto}

cssnano ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}

csso ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}

esbuild ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}

lightningcss ✓

a{flex:1 0 auto}

sass ✗

a{flex-grow:1;flex-shrink:0;flex-basis:auto}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Outline longhand to shorthand

outline-width, outline-style, and outline-color can be combined into outline shorthand.

a {
  outline-width: 1px;
  outline-style: solid;
  outline-color: red;
}

Expected

a{outline:1px solid red}

Outputs

clean-css ✗

a{outline:red solid 1px}

csskit ✗

a{outline-width:1px;outline-style:solid;outline-color:red}

csslop ✓

a{outline:1px solid red}

cssnano ✗

a{outline-width:1px;outline-style:solid;outline-color:red}

csso ✗

a{outline-width:1px;outline-style:solid;outline-color:red}

esbuild ✗

a{outline-width:1px;outline-style:solid;outline-color:red}

lightningcss ✓

a{outline:1px solid red}

sass ✗

a{outline-width:1px;outline-style:solid;outline-color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Border-radius longhand to shorthand

All four border-*-radius longhands with the same value collapse into border-radius shorthand.

a {
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  border-bottom-right-radius: 5px;
  border-bottom-left-radius: 5px;
}

Expected

a{border-radius:5px}

Outputs

clean-css ✓

a{border-radius:5px}

csskit ✗

a{border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-right-radius:5px;border-bottom-left-radius:5px}

csslop ✓

a{border-radius:5px}

cssnano ✓

a{border-radius:5px}

csso ✗

a{border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-right-radius:5px;border-bottom-left-radius:5px}

esbuild ✓

a{border-radius:5px}

lightningcss ✓

a{border-radius:5px}

sass ✗

a{border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-right-radius:5px;border-bottom-left-radius:5px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Margin longhand to shorthand

Individual margin-top/right/bottom/left declarations collapse into margin shorthand with symmetric 2-value form.

a {
  margin-top: 10px;
  margin-right: 20px;
  margin-bottom: 10px;
  margin-left: 20px;
}

Expected

a{margin:10px 20px}

Outputs

clean-css ✓

a{margin:10px 20px}

csskit ✗

a{margin-top:10px;margin-right:20px;margin-bottom:10px;margin-left:20px}

csslop ✓

a{margin:10px 20px}

cssnano ✓

a{margin:10px 20px}

csso ✓

a{margin:10px 20px}

esbuild ✓

a{margin:10px 20px}

lightningcss ✓

a{margin:10px 20px}

sass ✗

a{margin-top:10px;margin-right:20px;margin-bottom:10px;margin-left:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Text-decoration longhand to shorthand

text-decoration-line/style/color collapse into text-decoration shorthand, omitting solid (the default style).

a {
  text-decoration-line: underline;
  text-decoration-style: solid;
  text-decoration-color: red;
}

Expected

a{text-decoration:underline red}

Outputs

clean-css ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

csskit ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

csslop ✓

a{text-decoration:underline red}

cssnano ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

csso ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

esbuild ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

lightningcss ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}

sass ✗

a{text-decoration-line:underline;text-decoration-style:solid;text-decoration-color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Padding longhand to shorthand

Four padding longhand properties collapse to padding shorthand with value deduped.

a {
  padding-top: 5px;
  padding-right: 10px;
  padding-bottom: 5px;
  padding-left: 10px;
}

Expected

a{padding:5px 10px}

Outputs

clean-css ✓

a{padding:5px 10px}

csskit ✗

a{padding-top:5px;padding-right:10px;padding-bottom:5px;padding-left:10px}

csslop ✓

a{padding:5px 10px}

cssnano ✓

a{padding:5px 10px}

csso ✓

a{padding:5px 10px}

esbuild ✓

a{padding:5px 10px}

lightningcss ✓

a{padding:5px 10px}

sass ✗

a{padding-top:5px;padding-right:10px;padding-bottom:5px;padding-left:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Border longhand to shorthand

All 12 border longhand properties collapse to a single border shorthand.

a {
  border-top-width: 1px;
  border-right-width: 1px;
  border-bottom-width: 1px;
  border-left-width: 1px;
  border-top-style: solid;
  border-right-style: solid;
  border-bottom-style: solid;
  border-left-style: solid;
  border-top-color: red;
  border-right-color: red;
  border-bottom-color: red;
  border-left-color: red;
}

Expected

a{border:1px solid red}

Outputs

clean-css ✗

a{border-width:1px;border-style:solid;border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-color:red}

csskit ✗

a{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}

csslop ✓

a{border:1px solid red}

cssnano ✗

a{border-color:red;border-style:solid;border-width:1px}

csso ✗

a{border-width:1px;border-style:solid;border-color:red}

esbuild ✗

a{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}

lightningcss ✓

a{border:1px solid red}

sass ✗

a{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}
1ms
1ms
1ms
2ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

place-items longhand merge (same value)

When align-items and justify-items are the same, they merge into a single-value place-items.

a {
  align-items: center;
  justify-items: center;
}

Expected

a{place-items:center}

Outputs

clean-css ✗

a{align-items:center;justify-items:center}

csskit ✗

a{align-items:center;justify-items:center}

csslop ✓

a{place-items:center}

cssnano ✓

a{place-items:center}

csso ✗

a{align-items:center;justify-items:center}

esbuild ✗

a{align-items:center;justify-items:center}

lightningcss ✓

a{place-items:center}

sass ✗

a{align-items:center;justify-items:center}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

place-content longhand merge (same value)

When align-content and justify-content are the same, they merge into a single-value place-content.

a {
  align-content: center;
  justify-content: center;
}

Expected

a{place-content:center}

Outputs

clean-css ✗

a{align-content:center;justify-content:center}

csskit ✗

a{align-content:center;justify-content:center}

csslop ✓

a{place-content:center}

cssnano ✓

a{place-content:center}

csso ✗

a{align-content:center;justify-content:center}

esbuild ✗

a{align-content:center;justify-content:center}

lightningcss ✓

a{place-content:center}

sass ✗

a{align-content:center;justify-content:center}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

place-self longhand merge (same value)

When align-self and justify-self are the same, they merge into a single-value place-self.

a {
  align-self: center;
  justify-self: center;
}

Expected

a{place-self:center}

Outputs

clean-css ✗

a{align-self:center;justify-self:center}

csskit ✗

a{align-self:center;justify-self:center}

csslop ✓

a{place-self:center}

cssnano ✓

a{place-self:center}

csso ✗

a{align-self:center;justify-self:center}

esbuild ✗

a{align-self:center;justify-self:center}

lightningcss ✓

a{place-self:center}

sass ✗

a{align-self:center;justify-self:center}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Gap longhand merge (different values)

When row-gap and column-gap differ, they merge into a 2-value gap shorthand (row then column).

a {
  column-gap: 10px;
  row-gap: 20px;
}

Expected

a{gap:20px 10px}

Outputs

clean-css ✗

a{column-gap:10px;row-gap:20px}

csskit ✗

a{column-gap:10px;row-gap:20px}

csslop ✓

a{gap:20px 10px}

cssnano ✗

a{column-gap:10px;row-gap:20px}

csso ✗

a{column-gap:10px;row-gap:20px}

esbuild ✗

a{column-gap:10px;row-gap:20px}

lightningcss ✓

a{gap:20px 10px}

sass ✗

a{column-gap:10px;row-gap:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Columns longhand merge

column-width and column-count merge into the columns shorthand.

a {
  column-width: 10px;
  column-count: 3;
}

Expected

a{columns:10px 3}

Outputs

clean-css ✗

a{column-width:10px;column-count:3}

csskit ✗

a{column-width:10px;column-count:3}

csslop ✓

a{columns:10px 3}

cssnano ✓

a{columns:10px 3}

csso ✗

a{column-width:10px;column-count:3}

esbuild ✗

a{column-width:10px;column-count:3}

lightningcss ✗

a{column-width:10px;column-count:3}

sass ✗

a{column-width:10px;column-count:3}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

list-style longhand merge

list-style-type, list-style-position, and list-style-image merge into list-style. Default values (disc type, none image) can be omitted.

a {
  list-style-type: disc;
  list-style-position: inside;
  list-style-image: none;
}

Expected

a{list-style:inside}

Outputs

clean-css ✗

a{list-style:disc inside}

csskit ✗

a{list-style-type:disc;list-style-position:inside;list-style-image:none}

csslop ✓

a{list-style:inside}

cssnano ✗

a{list-style-type:disc;list-style-position:inside;list-style-image:none}

csso ✗

a{list-style-type:disc;list-style-position:inside;list-style-image:none}

esbuild ✗

a{list-style-type:disc;list-style-position:inside;list-style-image:none}

lightningcss ✓

a{list-style:inside}

sass ✗

a{list-style-type:disc;list-style-position:inside;list-style-image:none}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Background longhand merge with defaults

Background longhands merge into background shorthand. All properties at their default values except background-color are omitted.

a {
  background-color: red;
  background-image: none;
  background-repeat: repeat;
  background-position: 0% 0%;
  background-attachment: scroll;
}

Expected

a{background:red}

Outputs

clean-css ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0 0;background-attachment:scroll}

csskit ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0% 0%;background-attachment:scroll}

csslop ✓

a{background:red}

cssnano ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0 0;background-attachment:scroll}

csso ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0 0;background-attachment:scroll}

esbuild ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0% 0%;background-attachment:scroll}

lightningcss ✗

a{background-color:red;background-image:none;background-position:0 0;background-repeat:repeat;background-attachment:scroll}

sass ✗

a{background-color:red;background-image:none;background-repeat:repeat;background-position:0% 0%;background-attachment:scroll}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Font longhand merge

font-style, font-weight, font-size, line-height, and font-family merge into font shorthand. bold optimizes to 700.

a {
  font-style: italic;
  font-weight: bold;
  font-size: 16px;
  line-height: 1.5;
  font-family: Arial, sans-serif;
}

Expected

a{font:italic 700 16px/1.5 Arial,sans-serif}

Outputs

clean-css ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csskit ✗

a{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csslop ✓

a{font:italic 700 16px/1.5 Arial,sans-serif}

cssnano ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csso ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

esbuild ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

lightningcss ✗

a{font-family:Arial,sans-serif;font-size:16px;font-style:italic;font-weight:700;line-height:1.5}

sass ✗

a{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✓ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Transition longhand merge

transition-property, transition-duration, transition-timing-function, and transition-delay merge into transition. Default values (ease timing, 0s delay) are omitted.

a {
  transition-property: opacity;
  transition-duration: 0.3s;
  transition-timing-function: ease;
  transition-delay: 0s;
}

Expected

a{transition:opacity .3s}

Outputs

clean-css ✓

a{transition:opacity .3s}

csskit ✗

a{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease;transition-delay:0s}

csslop ✓

a{transition:opacity .3s}

cssnano ✗

a{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease;transition-delay:0s}

csso ✗

a{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease;transition-delay:0s}

esbuild ✗

a{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease;transition-delay:0s}

lightningcss ✓

a{transition:opacity .3s}

sass ✗

a{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease;transition-delay:0s}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Animation longhand merge

All 8 animation longhands merge into animation shorthand. Default values (ease, 0s delay, 1 iteration, normal direction, none fill, running) are omitted.

a {
  animation-name: slide;
  animation-duration: 1s;
  animation-timing-function: ease;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: none;
  animation-play-state: running;
}

Expected

a{animation:slide 1s}

Outputs

clean-css ✗

a{animation:1s slide}

csskit ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

csslop ✓

a{animation:slide 1s}

cssnano ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

csso ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

esbuild ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

lightningcss ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-iteration-count:1;animation-direction:normal;animation-play-state:running;animation-delay:0s;animation-fill-mode:none}

sass ✗

a{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}
1ms
5ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

border shorthand must not clobber border-image (reorder required)

border-image is declared before border longhands. A minifier collapsing the longhands to border must reorder it before border-image, since border resets border-image to its initial value.

a {
  border-image: url(border.png) 30 round;
  border-width: 1px;
  border-style: solid;
  border-color: red;
}

Expected

a{border:1px solid red;border-image:url(border.png)30 round}

Outputs

clean-css ✗

a{border-image:url(border.png) 30 round;border:1px solid red}

csskit ✗

a{border-image:url(border.png)30 round;border-width:1px;border-style:solid;border-color:red}

csslop ✓

a{border:1px solid red;border-image:url(border.png)30 round}

cssnano ✗

a{border-image:url(border.png) 30 round;border-width:1px;border-style:solid;border-color:red}

csso ✗

a{border-image:url(border.png)30 round;border-width:1px;border-style:solid;border-color:red}

esbuild ✗

a{border-image:url(border.png) 30 round;border-width:1px;border-style:solid;border-color:red}

lightningcss ✗

a{border:1px solid red;border-image:url(border.png) 30 round}

sass ✗

a{border-image:url(border.png) 30 round;border-width:1px;border-style:solid;border-color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

border longhands then border-image (safe ordering)

Border longhands come before border-image. Collapsing longhands to border shorthand is safe since border-image already follows and overrides the reset.

a {
  border-width: 1px;
  border-style: solid;
  border-color: red;
  border-image: url(border.png) 30 round;
}

Expected

a{border:1px solid red;border-image:url(border.png)30 round}

Outputs

clean-css ✗

a{border:1px solid red;border-image:url(border.png) 30 round}

csskit ✗

a{border-width:1px;border-style:solid;border-color:red;border-image:url(border.png)30 round}

csslop ✓

a{border:1px solid red;border-image:url(border.png)30 round}

cssnano ✗

a{border-width:1px;border-style:solid;border-color:red;border-image:url(border.png) 30 round}

csso ✗

a{border-width:1px;border-style:solid;border-color:red;border-image:url(border.png)30 round}

esbuild ✗

a{border-width:1px;border-style:solid;border-color:red;border-image:url(border.png) 30 round}

lightningcss ✗

a{border:1px solid red;border-image:url(border.png) 30 round}

sass ✗

a{border-width:1px;border-style:solid;border-color:red;border-image:url(border.png) 30 round}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

border shorthand intentionally resets border-image

Declaring border-image then border resets border-image to none. The border-image declaration is dead code and can be removed.

a {
  border-image: url(border.png)30 round;
  border: 1px solid red;
}

Expected

a{border:1px solid red}

Outputs

clean-css ✗

a{border-image:url(border.png) 30 round;border:1px solid red}

csskit ✓

a{border:1px solid red}

csslop ✓

a{border:1px solid red}

cssnano ✗

a{border-image:url(border.png)30 round;border:1px solid red}

csso ✗

a{border-image:url(border.png)30 round;border:1px solid red}

esbuild ✗

a{border-image:url(border.png)30 round;border:1px solid red}

lightningcss ✓

a{border:1px solid red}

sass ✗

a{border-image:url(border.png) 30 round;border:1px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

border longhands with border-image longhands (collapse both)

Both border and border-image expressed as longhands. Minifier should collapse each group into its respective shorthand, with border ordered before border-image to avoid the reset clobbering border-image.

a {
  border-image-source: url(border.png);
  border-image-slice: 30;
  border-image-repeat: round;
  border-width: 4px;
  border-style: solid;
  border-color: #0000;
}

Expected

a{border:4px solid#0000;border-image:url(border.png)30 round}

Outputs

clean-css ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:#0000}

csskit ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:#0000}

csslop ✓

a{border:4px solid#0000;border-image:url(border.png)30 round}

cssnano ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:#0000}

csso ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:#0000}

esbuild ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:#0000}

lightningcss ✗

a{border:4px solid #0000;border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round}

sass ✗

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:rgba(0,0,0,0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

font shorthand must not clobber font-variant-ligatures (reorder required)

font-variant-ligatures is declared before font longhands. A minifier collapsing the longhands to font must reorder it before font-variant-ligatures, since font resets font-variant-ligatures to its initial value (normal).

a {
  font-variant-ligatures: no-common-ligatures;
  font-style: italic;
  font-weight: bold;
  font-size: 16px;
  line-height: 1.5;
  font-family: Arial, sans-serif;
}

Expected

a{font:italic 700 16px/1.5 Arial,sans-serif;font-variant-ligatures:no-common-ligatures}

Validate

Outputs

clean-css ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csskit ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csslop ✓

a{font:italic 700 16px/1.5 Arial,sans-serif;font-variant-ligatures:no-common-ligatures}

cssnano ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csso ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

esbuild ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

lightningcss ✗

a{font-variant-ligatures:no-common-ligatures;font-family:Arial,sans-serif;font-size:16px;font-style:italic;font-weight:700;line-height:1.5}

sass ✗

a{font-variant-ligatures:no-common-ligatures;font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

font longhands before font-feature-settings (safe ordering)

Font longhands come before font-feature-settings. Collapsing longhands to font shorthand is safe since font-feature-settings already follows and overrides the implicit reset.

a {
  font-style: italic;
  font-weight: bold;
  font-size: 16px;
  line-height: 1.5;
  font-family: Arial, sans-serif;
  font-feature-settings: "smcp" 1;
}

Expected

a{font:italic 700 16px/1.5 Arial,sans-serif;font-feature-settings:"smcp"1}

Validate

Outputs

clean-css ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp" 1}

csskit ✗

a{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp"1}

csslop ✓

a{font:italic 700 16px/1.5 Arial,sans-serif;font-feature-settings:"smcp"1}

cssnano ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp" 1}

csso ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp"1}

esbuild ✗

a{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp" 1}

lightningcss ✗

a{font-feature-settings:"smcp" 1;font-family:Arial,sans-serif;font-size:16px;font-style:italic;font-weight:700;line-height:1.5}

sass ✗

a{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif;font-feature-settings:"smcp" 1}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

font shorthand intentionally resets font-variant-numeric

Declaring font-variant-numeric then font resets font-variant-numeric to normal. The font-variant-numeric declaration is dead code and can be removed.

a {
  font-variant-numeric: tabular-nums;
  font: italic 16px/1.5 Arial, sans-serif;
}

Expected

a{font:italic 16px/1.5 Arial,sans-serif}

Validate

Outputs

clean-css ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}

csskit ✓

a{font:italic 16px/1.5 Arial,sans-serif}

csslop ✓

a{font:italic 16px/1.5 Arial,sans-serif}

cssnano ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}

csso ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}

esbuild ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}

lightningcss ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}

sass ✗

a{font-variant-numeric:tabular-nums;font:italic 16px/1.5 Arial,sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0027 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

font longhands with font-kerning longhand (collapse, preserve order)

Font longhands and font-kerning are both expressed as longhands. A minifier should collapse the font longhands into font shorthand, keeping font before font-kerning since font resets font-kerning to auto.

a {
  font-style: normal;
  font-weight: 400;
  font-size: 14px;
  line-height: 1.4;
  font-family: Georgia, serif;
  font-kerning: none;
}

Expected

a{font:14px/1.4 Georgia,serif;font-kerning:none}

Validate

Outputs

clean-css ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}

csskit ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}

csslop ✓

a{font:14px/1.4 Georgia,serif;font-kerning:none}

cssnano ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}

csso ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}

esbuild ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}

lightningcss ✗

a{font-kerning:none;font-family:Georgia,serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.4}

sass ✗

a{font-style:normal;font-weight:400;font-size:14px;line-height:1.4;font-family:Georgia,serif;font-kerning:none}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0028 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

mask shorthand must not clobber mask-border (reorder required)

mask-border is declared before mask longhands. A minifier collapsing the longhands to mask must reorder it before mask-border, since mask resets mask-border to its initial value.

a {
  mask-border: url(mask.png) 25 / 10px round;
  mask-image: linear-gradient(#000, #0000);
  mask-size: cover;
  mask-repeat: no-repeat;
}

Expected

a{mask:linear-gradient(#000,#0000)0 0/cover no-repeat;mask-border:url(mask.png)25/10px round}

Outputs

clean-css ✗

a{mask-border:url(mask.png) 25/10px round;mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat}

csskit ✗

a{mask-border:url(mask.png)25/10px round;mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat}

csslop ✓

a{mask:linear-gradient(#000,#0000)0 0/cover no-repeat;mask-border:url(mask.png)25/10px round}

cssnano ✗

a{mask-border:url(mask.png) 25/10px round;mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat}

csso ✗

a{mask-border:url(mask.png)25/10px round;mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat}

esbuild ✗

a{mask-border:url(mask.png) 25 / 10px round;mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat}

lightningcss ✗

a{mask-image:linear-gradient(#000,#0000);mask-size:cover;mask-repeat:no-repeat;mask-border:url(mask.png) 25/10px round}

sass ✗

a{mask-border:url(mask.png) 25/10px round;mask-image:linear-gradient(#000, rgba(0, 0, 0, 0));mask-size:cover;mask-repeat:no-repeat}
1ms
5ms
1ms
1ms
1ms
1ms
1ms
1ms
0029 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

mask longhands before mask-border (safe ordering)

Mask longhands come before mask-border. Collapsing longhands to mask shorthand is safe since mask-border already follows and overrides the implicit reset.

a {
  mask-image: linear-gradient(#000, #0000);
  mask-repeat: no-repeat;
  mask-border: url(mask.png) 25 round;
}

Expected

a{mask:linear-gradient(#000,#0000)no-repeat;mask-border:url(mask.png)25 round}

Outputs

clean-css ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}

csskit ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png)25 round}

csslop ✓

a{mask:linear-gradient(#000,#0000)no-repeat;mask-border:url(mask.png)25 round}

cssnano ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}

csso ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png)25 round}

esbuild ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}

lightningcss ✗

a{mask-image:linear-gradient(#000,#0000);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}

sass ✗

a{mask-image:linear-gradient(#000, rgba(0, 0, 0, 0));mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0030 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

mask shorthand intentionally resets mask-border

Declaring mask-border then mask resets mask-border to its initial value. The mask-border declaration is therefore dead code that can be removed.

a {
  mask-border: url(mask.png) 25 round;
  mask: linear-gradient(#000, #0000);
}

Expected

a{mask:linear-gradient(#000,#0000)}

Outputs

clean-css ✗

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(#000,#0000)}

csskit ✓

a{mask:linear-gradient(#000,#0000)}

csslop ✓

a{mask:linear-gradient(#000,#0000)}

cssnano ✗

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(#000,#0000)}

csso ✗

a{mask-border:url(mask.png)25 round;mask:linear-gradient(#000,#0000)}

esbuild ✗

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(#000,#0000)}

lightningcss ✗

a{mask:linear-gradient(#000,#0000);mask-border:url(mask.png) 25 round}

sass ✗

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(#000, rgba(0, 0, 0, 0))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0031 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

font shorthand must not clobber font-variation-settings (reorder required)

font-variation-settings is declared before font longhands. A minifier collapsing the longhands to font must reorder it before font-variation-settings, since font resets font-variation-settings to its initial value (normal).

a {
  font-variation-settings: "wght" 600, "wdth" 75;
  font-style: normal;
  font-weight: 400;
  font-size: 18px;
  line-height: 1.6;
  font-family: Inter, sans-serif;
}

Expected

a{font:18px/1.6 Inter,sans-serif;font-variation-settings:"wght"600,"wdth"75}

Validate

Outputs

clean-css ✗

a{font-variation-settings:"wght" 600,"wdth" 75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}

csskit ✗

a{font-variation-settings:"wght"600,"wdth"75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}

csslop ✓

a{font:18px/1.6 Inter,sans-serif;font-variation-settings:"wght"600,"wdth"75}

cssnano ✗

a{font-variation-settings:"wght" 600,"wdth" 75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}

csso ✗

a{font-variation-settings:"wght"600,"wdth"75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}

esbuild ✗

a{font-variation-settings:"wght" 600,"wdth" 75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}

lightningcss ✗

a{font-variation-settings:"wght" 600, "wdth" 75;font-family:Inter,sans-serif;font-size:18px;font-style:normal;font-weight:400;line-height:1.6}

sass ✗

a{font-variation-settings:"wght" 600,"wdth" 75;font-style:normal;font-weight:400;font-size:18px;line-height:1.6;font-family:Inter,sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
9ms
0032 ✓ ✓ ✓ ✗ ✓ ✗ ✓ ✗
Details

Longhand before shorthand is dead code

font-weight: bold followed by font: 14px serif is dead code because the font shorthand resets font-weight to normal. The longhand can be removed.

a {
  font-weight: bold;
  font: 14px serif;
}

Expected

a{font:14px serif}

Outputs

clean-css ✓

a{font:14px serif}

csskit ✓

a{font:14px serif}

csslop ✓

a{font:14px serif}

cssnano ✗

a{font-weight:700;font:14px serif}

csso ✓

a{font:14px serif}

esbuild ✗

a{font-weight:700;font:14px serif}

lightningcss ✓

a{font:14px serif}

sass ✗

a{font-weight:bold;font:14px serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0033 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Shorthand plus !important override for mixed importance

When one longhand has !important, the longhands can still be partially merged into a shorthand followed by the important longhand override. The !important declaration wins over the shorthand's reset of the same property.

a {
  margin-top: 10px !important;
  margin-right: 20px;
  margin-bottom: 10px;
  margin-left: 20px;
}

Expected

a{margin:10px 20px;margin-top:10px!important}

Outputs

clean-css ✗

a{margin-top:10px!important;margin-right:20px;margin-bottom:10px;margin-left:20px}

csskit ✗

a{margin-top:10px!important;margin-right:20px;margin-bottom:10px;margin-left:20px}

csslop ✓

a{margin:10px 20px;margin-top:10px!important}

cssnano ✗

a{margin-top:10px!important;margin-right:20px;margin-bottom:10px;margin-left:20px}

csso ✗

a{margin-top:10px!important;margin-right:20px;margin-bottom:10px;margin-left:20px}

esbuild ✗

a{margin-top:10px!important;margin-right:20px;margin-bottom:10px;margin-left:20px}

lightningcss ✗

a{margin-bottom:10px;margin-left:20px;margin-right:20px;margin-top:10px!important}

sass ✗

a{margin-top:10px !important;margin-right:20px;margin-bottom:10px;margin-left:20px}
1ms
1ms
2ms
1ms
1ms
1ms
1ms
1ms
0034 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Padding longhand merge safe with @property constraint

When each custom property has an @property rule constraining its syntax to <length>, the value is guaranteed valid at parse time. Invalid values are rejected and fall back to initial-value, so shorthand merge is safe.

@property --pt {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --pr {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --pb {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --pl {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
a {
  padding-top: var(--pt);
  padding-right: var(--pr);
  padding-bottom: var(--pb);
  padding-left: var(--pl);
}

Expected

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding:var(--pt)var(--pr)var(--pb)var(--pl)}

Outputs

clean-css ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csskit ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csslop ✓

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding:var(--pt)var(--pr)var(--pb)var(--pl)}

cssnano ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csso ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0}@property --pr{syntax:"<length>";inherits:false;initial-value:0}@property --pb{syntax:"<length>";inherits:false;initial-value:0}@property --pl{syntax:"<length>";inherits:false;initial-value:0}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

esbuild ✗

@property --pt{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --pr{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --pb{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --pl{syntax: "<length>"; inherits: false; initial-value: 0px;}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

lightningcss ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0}@property --pr{syntax:"<length>";inherits:false;initial-value:0}@property --pb{syntax:"<length>";inherits:false;initial-value:0}@property --pl{syntax:"<length>";inherits:false;initial-value:0}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

sass ✗

@property --pt{syntax:"<length>";inherits:false;initial-value:0px}@property --pr{syntax:"<length>";inherits:false;initial-value:0px}@property --pb{syntax:"<length>";inherits:false;initial-value:0px}@property --pl{syntax:"<length>";inherits:false;initial-value:0px}a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0035 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Border longhand merge safe with @property constraint

When each custom property has an @property rule constraining its syntax, invalid values are rejected at parse time and fall back to initial-value. The variable is guaranteed to produce a single valid component, so shorthand merge is safe.

@property --bw {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --bs {
  syntax: "<custom-ident>";
  inherits: false;
  initial-value: none;
}
@property --bc {
  syntax: "<color>";
  inherits: false;
  initial-value: red;
}
a {
  border-width: var(--bw);
  border-style: var(--bs);
  border-color: var(--bc);
}

Expected

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border:var(--bw)var(--bs)var(--bc)}

Outputs

clean-css ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

csskit ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

csslop ✓

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border:var(--bw)var(--bs)var(--bc)}

cssnano ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

csso ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

esbuild ✗

@property --bw{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --bs{syntax: "<custom-ident>"; inherits: false; initial-value: none;}@property --bc{syntax: "<color>"; inherits: false; initial-value: red;}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

lightningcss ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

sass ✗

@property --bw{syntax:"<length>";inherits:false;initial-value:0px}@property --bs{syntax:"<custom-ident>";inherits:false;initial-value:none}@property --bc{syntax:"<color>";inherits:false;initial-value:red}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0036 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

top/right/bottom/left to inset shorthand

top: 0; right: 0; bottom: 0; left: 0 merges into inset: 0. The inset shorthand sets all four physical inset properties and is shorter when all values are equal.

a {
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

Expected

a{inset:0}

Outputs

clean-css ✗

a{top:0;right:0;bottom:0;left:0}

csskit ✗

a{top:0;right:0;bottom:0;left:0}

csslop ✓

a{inset:0}

cssnano ✗

a{top:0;right:0;bottom:0;left:0}

csso ✗

a{top:0;right:0;bottom:0;left:0}

esbuild ✓

a{inset:0}

lightningcss ✓

a{inset:0}

sass ✗

a{top:0;right:0;bottom:0;left:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0037 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Logical longhands to margin-inline shorthand

margin-inline-start: 10px; margin-inline-end: 10px merges into margin-inline: 10px. When both logical sides are equal, the two-value form collapses to a single value.

a {
  margin-inline-start: 10px;
  margin-inline-end: 10px;
}

Expected

a{margin-inline:10px}

Outputs

clean-css ✗

a{margin-inline-start:10px;margin-inline-end:10px}

csskit ✗

a{margin-inline-start:10px;margin-inline-end:10px}

csslop ✓

a{margin-inline:10px}

cssnano ✗

a{margin-inline-start:10px;margin-inline-end:10px}

csso ✗

a{margin-inline-start:10px;margin-inline-end:10px}

esbuild ✗

a{margin-inline-start:10px;margin-inline-end:10px}

lightningcss ✓

a{margin-inline:10px}

sass ✗

a{margin-inline-start:10px;margin-inline-end:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0038 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

position-try-order + fallbacks to position-try shorthand

position-try-order: normal; position-try-fallbacks: flip-block, --custom merges into position-try: flip-block, --custom. The normal order is the initial value and can be omitted in the shorthand.

a {
  position-try-order: normal;
  position-try-fallbacks: flip-block, --custom;
}

Expected

a{position-try:flip-block,--custom}

Outputs

clean-css ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}

csskit ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}

csslop ✓

a{position-try:flip-block,--custom}

cssnano ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}

csso ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}

esbuild ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}

lightningcss ✗

a{position-try-order:normal;position-try-fallbacks:flip-block, --custom}

sass ✗

a{position-try-order:normal;position-try-fallbacks:flip-block,--custom}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0039 ✗ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

border-top before border is dead code

border-top: 1px solid blue followed by border: 2px solid red is dead code because the border shorthand resets all sides including border-top.

a {
  border-top: 1px solid blue;
  border: 2px solid red;
}

Expected

a{border:2px solid red}

Outputs

clean-css ✗

a{border-top:1px solid #00f;border:2px solid red}

csskit ✓

a{border:2px solid red}

csslop ✓

a{border:2px solid red}

cssnano ✓

a{border:2px solid red}

csso ✓

a{border:2px solid red}

esbuild ✗

a{border-top:1px solid blue;border:2px solid red}

lightningcss ✓

a{border:2px solid red}

sass ✗

a{border-top:1px solid blue;border:2px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0040 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

border-block-start before border-block is dead code

border-block-start: 1px solid blue followed by border-block: 2px solid red is dead code because border-block resets both border-block-start and border-block-end.

a {
  border-block-start: 1px solid blue;
  border-block: 2px solid red;
}

Expected

a{border-block:2px solid red}

Outputs

clean-css ✗

a{border-block-start:1px solid blue;border-block:2px solid red}

csskit ✓

a{border-block:2px solid red}

csslop ✓

a{border-block:2px solid red}

cssnano ✗

a{border-block-start:1px solid blue;border-block:2px solid red}

csso ✗

a{border-block-start:1px solid #00f;border-block:2px solid red}

esbuild ✗

a{border-block-start:1px solid blue;border-block:2px solid red}

lightningcss ✓

a{border-block:2px solid red}

sass ✗

a{border-block-start:1px solid blue;border-block:2px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0041 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

padding-inline-start before padding-inline is dead code

padding-inline-start: 10px followed by padding-inline: 20px is dead code because padding-inline resets both padding-inline-start and padding-inline-end.

a {
  padding-inline-start: 10px;
  padding-inline: 20px;
}

Expected

a{padding-inline:20px}

Outputs

clean-css ✗

a{padding-inline-start:10px;padding-inline:20px}

csskit ✓

a{padding-inline:20px}

csslop ✓

a{padding-inline:20px}

cssnano ✗

a{padding-inline-start:10px;padding-inline:20px}

csso ✗

a{padding-inline-start:10px;padding-inline:20px}

esbuild ✗

a{padding-inline-start:10px;padding-inline:20px}

lightningcss ✓

a{padding-inline:20px}

sass ✗

a{padding-inline-start:10px;padding-inline:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0042 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

margin-top before margin is dead code

margin-top: 10px followed by margin: 20px is dead code because the margin shorthand resets all four physical sides.

a {
  margin-top: 10px;
  margin: 20px;
}

Expected

a{margin:20px}

Outputs

clean-css ✓

a{margin:20px}

csskit ✓

a{margin:20px}

csslop ✓

a{margin:20px}

cssnano ✓

a{margin:20px}

csso ✓

a{margin:20px}

esbuild ✓

a{margin:20px}

lightningcss ✓

a{margin:20px}

sass ✗

a{margin-top:10px;margin:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0043 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

border-top-width before border-width is dead code

border-top-width: 1px followed by border-width: 2px is dead code because border-width resets all four physical border widths.

a {
  border-top-width: 1px;
  border-width: 2px;
}

Expected

a{border-width:2px}

Outputs

clean-css ✓

a{border-width:2px}

csskit ✓

a{border-width:2px}

csslop ✓

a{border-width:2px}

cssnano ✓

a{border-width:2px}

csso ✓

a{border-width:2px}

esbuild ✗

a{border-top-width:1px;border-width:2px}

lightningcss ✓

a{border-width:2px}

sass ✗

a{border-top-width:1px;border-width:2px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0044 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

border-top-color before border-top is dead code

border-top-color: blue followed by border-top: 2px solid red is dead code because border-top resets border-top-color, border-top-style, and border-top-width.

a {
  border-top-color: blue;
  border-top: 2px solid red;
}

Expected

a{border-top:2px solid red}

Outputs

clean-css ✓

a{border-top:2px solid red}

csskit ✓

a{border-top:2px solid red}

csslop ✓

a{border-top:2px solid red}

cssnano ✓

a{border-top:2px solid red}

csso ✓

a{border-top:2px solid red}

esbuild ✗

a{border-top-color:#00f;border-top:2px solid red}

lightningcss ✓

a{border-top:2px solid red}

sass ✗

a{border-top-color:blue;border-top:2px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0045 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

inset-block-start before inset-block is dead code

inset-block-start: 10px followed by inset-block: 20px is dead code because inset-block resets both inset-block-start and inset-block-end.

a {
  inset-block-start: 10px;
  inset-block: 20px;
}

Expected

a{inset-block:20px}

Outputs

clean-css ✗

a{inset-block-start:10px;inset-block:20px}

csskit ✓

a{inset-block:20px}

csslop ✓

a{inset-block:20px}

cssnano ✗

a{inset-block-start:10px;inset-block:20px}

csso ✗

a{inset-block-start:10px;inset-block:20px}

esbuild ✗

a{inset-block-start:10px;inset-block:20px}

lightningcss ✓

a{inset-block:20px}

sass ✗

a{inset-block-start:10px;inset-block:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0046 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Combine background properties into shorthand

The background properties shown in this test can all be combined into a single shorthange background property.

a {
  background-color: red;
  background-image: linear-gradient(red, tan);
  background-position-x: 100%;
  background-position-y: 100%;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-size: contain;
  background-origin: padding-box;
  background-clip: content-box;
}

Expected

a{background:red linear-gradient(red,tan)100% 100%/contain no-repeat fixed padding-box content-box}

Outputs

clean-css ✗

a{background-color:red;background-image:linear-gradient(red,tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:padding-box;background-clip:content-box}

csskit ✗

a{background-color:red;background-image:linear-gradient(red,tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:padding-box;background-clip:content-box}

csslop ✓

a{background:red linear-gradient(red,tan)100% 100%/contain no-repeat fixed padding-box content-box}

cssnano ✗

a{background-color:red;background-image:linear-gradient(red,tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:initial;background-clip:content-box}

csso ✗

a{background-color:red;background-image:linear-gradient(red,tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:padding-box;background-clip:content-box}

esbuild ✗

a{background-color:red;background-image:linear-gradient(red,tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:padding-box;background-clip:content-box}

lightningcss ✗

a{background:red linear-gradient(red,tan) 100% 100%/contain no-repeat fixed padding-box content-box}

sass ✗

a{background-color:red;background-image:linear-gradient(red, tan);background-position-x:100%;background-position-y:100%;background-repeat:no-repeat;background-attachment:fixed;background-size:contain;background-origin:padding-box;background-clip:content-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0047 ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Border shorthand with double inherit

Converting to shorthand when the border color and style are both inherit will result in border:2px inherit inherit, which is invalid CSS. Instead, just use border:inherit; as the shorthand. This causes the element to inherit all border properties. You can then override the border-width.

div {
  border-color: inherit;
  border-style: inherit;
  border-width: 2px;
}

Expected

div{border:inherit;border-width:2px}

Outputs

clean-css ✓

div{border:inherit;border-width:2px}

csskit ✗

div{border-color:inherit;border-style:inherit;border-width:2px}

csslop ✓

div{border:inherit;border-width:2px}

cssnano ✗

div{border-color:inherit;border-style:inherit;border-width:2px}

csso ✗

div{border-color:inherit;border-style:inherit;border-width:2px}

esbuild ✗

div{border-color:inherit;border-style:inherit;border-width:2px}

lightningcss ✗

div{border-color:inherit;border-style:inherit;border-width:2px}

sass ✗

div{border-color:inherit;border-style:inherit;border-width:2px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0048 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Combine border sides using shorthand when all match

If all border sides are defined with the same values, combine them using the border shorthand.

div {
  border-top: 1px solid red;
  border-right: 1px solid red;
  border-bottom: 1px solid red;
  border-left: 1px solid red;
}

Expected

div{border:1px solid red}

Outputs

clean-css ✗

div{border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-left:1px solid red}

csskit ✗

div{border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-left:1px solid red}

csslop ✓

div{border:1px solid red}

cssnano ✗

div{border-color:red;border-style:solid;border-width:1px}

csso ✗

div{border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-left:1px solid red}

esbuild ✗

div{border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-left:1px solid red}

lightningcss ✓

div{border:1px solid red}

sass ✗

div{border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-left:1px solid red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0049 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Border shorthand with two border-color values

If you have a singular border-width and border-style, with two border-colors, then you can create a border short hand using the first available color, then override the border-color and it will still be shorter than keeping each properties separate.

a {
  border-width: 50px;
  border-style: solid;
  border-color: #0000 red;
}

Expected

a{border:50px solid#0000;border-color:#0000 red}

Outputs

clean-css ✗

a{border-width:50px;border-style:solid;border-color:#0000 red}

csskit ✗

a{border-width:50px;border-style:solid;border-color:#0000 red}

csslop ✓

a{border:50px solid#0000;border-color:#0000 red}

cssnano ✗

a{border-color:#0000 red;border-style:solid;border-width:50px}

csso ✗

a{border-width:50px;border-style:solid;border-color:#0000 red}

esbuild ✗

a{border-width:50px;border-style:solid;border-color:#0000 red}

lightningcss ✗

a{border:50px solid #0000;border-color:#0000 red}

sass ✗

a{border-width:50px;border-style:solid;border-color:rgba(0,0,0,0) red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0050 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge equal SVG marker longhands

SVG 2 defines the marker shorthand which sets marker-start, marker-mid, and marker-end to the same value. When all three longhands are equal, merge into a single marker shorthand.

a {
  marker-start: url(x);
  marker-mid: url(x);
  marker-end: url(x);
}

Expected

a{marker:url(x)}

Outputs

clean-css ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

csskit ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

csslop ✓

a{marker:url(x)}

cssnano ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

csso ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

esbuild ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

lightningcss ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}

sass ✗

a{marker-start:url(x);marker-mid:url(x);marker-end:url(x)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0051 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge equal bidirectional gap decoration rules

CSS Gaps 1 defines the gap decoration shorthands: column-rule, and row-rule which set (row|column)-rule-width, (row|column)-rule-style, (row|column)-rule-color. The spec also defines the bidirectional shorthand variants of rule-width, rule-style and rule-color. When column and row properties are equal, they can be merged into the bidirectional shorthands.

a {
  column-rule-color: red;
  row-rule-color: red;
}
b {
  column-rule-style: solid;
  row-rule-style: solid;
}
c {
  column-rule-width: 1px;
  row-rule-width: 1px;
}
d {
  column-rule-break: intersection;
  row-rule-break: intersection;
}
e {
  column-rule-visibility-items: all;
  row-rule-visibility-items: all;
}

Expected

a{rule-color:red}b{rule-style:solid}c{rule-width:1px}d{rule-break:intersection}e{rule-visibility-items:all}

Outputs

clean-css ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

csskit ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

csslop ✓

a{rule-color:red}b{rule-style:solid}c{rule-width:1px}d{rule-break:intersection}e{rule-visibility-items:all}

cssnano ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

csso ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

esbuild ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

lightningcss ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}

sass ✗

a{column-rule-color:red;row-rule-color:red}b{column-rule-style:solid;row-rule-style:solid}c{column-rule-width:1px;row-rule-width:1px}d{column-rule-break:intersection;row-rule-break:intersection}e{column-rule-visibility-items:all;row-rule-visibility-items:all}
1ms
6ms
1ms
1ms
1ms
1ms
1ms
1ms
0052 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Preserve animation timeline and range resets

The animation shorthand resets animation-timeline and animation-range to their initial values despite not explicitly expressing them. When merging animation longhands into shorthand form, the shorthand must be ordered before any animation-timeline or animation-range declarations to preserve the resets.

a {
  animation-timeline: scroll();
  animation-range: entry;
  animation-name: spin;
  animation-duration: 1s;
}

Expected

a{animation:spin 1s;animation-timeline:scroll();animation-range:entry}

Outputs

clean-css ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}

csskit ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}

csslop ✓

a{animation:spin 1s;animation-timeline:scroll();animation-range:entry}

cssnano ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}

csso ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}

esbuild ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}

lightningcss ✗

a{animation-name:spin;animation-duration:1s;animation-timeline:scroll();animation-range:entry}

sass ✗

a{animation-timeline:scroll();animation-range:entry;animation-name:spin;animation-duration:1s}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0053 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Merge shorthand override when shorter

A longhand following its shorthand can fold back into the shorthand, but only when that is shorter than keeping both declarations.

a {
  padding: 10px;
  padding-right: 20px;
}

b {
  padding: 10000px;
  padding-right: 200px;
}

c {
  padding: 10px;
  padding-top: 20px;
}

d {
  padding: 1px 2px 3px 4px;
  padding-right: 4px;
}

e {
  padding: inherit;
  padding-right: 20px;
}

f {
  padding: var(--padding);
  padding-right: 20px;
}

g {
  padding: 10px;
  padding-right: 20px !important;
}

Expected

a{padding:10px 20px 10px 10px}b{padding:10000px;padding-right:200px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

Outputs

clean-css ✗

a{padding:10px 20px 10px 10px}b{padding:10000px 200px 10000px 10000px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

csskit ✗

a{padding:10px;padding-right:20px}b{padding:10000px;padding-right:200px}c{padding:10px;padding-top:20px}d{padding:1px 2px 3px 4px;padding-right:4px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

csslop ✓

a{padding:10px 20px 10px 10px}b{padding:10000px;padding-right:200px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

cssnano ✗

a{padding:10px 20px 10px 10px}b{padding:1e4px 200px 1e4px 1e4px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

csso ✗

a{padding:10px 20px 10px 10px}b{padding:10000px 200px 10000px 10000px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e,f{padding-right:20px}e{padding:inherit}f{padding:var(--padding)}g{padding:10px;padding-right:20px!important}

esbuild ✗

a{padding:10px 20px 10px 10px}b{padding:10000px 200px 10000px 10000px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

lightningcss ✗

a{padding:10px 20px 10px 10px}b{padding:10000px 200px 10000px 10000px}c{padding:20px 10px 10px}d{padding:1px 4px 3px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px!important}

sass ✗

a{padding:10px;padding-right:20px}b{padding:10000px;padding-right:200px}c{padding:10px;padding-top:20px}d{padding:1px 2px 3px 4px;padding-right:4px}e{padding:inherit;padding-right:20px}f{padding:var(--padding);padding-right:20px}g{padding:10px;padding-right:20px !important}
1ms
1ms
2ms
1ms
1ms
1ms
1ms
1ms
0054 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Border side shorthands collapse to multi-value longhands

margin-top/right/bottom/left collapse into one to four margin values, but border is not a four-sided shorthand: it takes a width, a style and a colour. Four differing border-top/right/bottom/left declarations can't be represented by border effectively, so treating it like a shorthand with sides would result in an invalid declaration such as border: 1px solid red 2px dotted tan.

They can, however, be reduced to a shorter value by using the border-width, border-style, border-color syntax, which will result in overall smaller CSS. While any order creates valid CSS, the parts should be written in grammar order (the order border itself takes them) or alphabetical, to improve the chance of good gzip compressibility.

a {
  border-top: 1px solid red;
  border-right: 2px dotted tan;
  border-bottom: 1px solid red;
  border-left: 2px dotted tan;
}

Expected

a{border-width:1px 2px;border-style:solid dotted;border-color:red tan}

Also accepted

a{border-color:red tan;border-style:solid dotted;border-width:1px 2px}

Outputs

clean-css ✗

a{border-top:1px solid red;border-right:2px dotted tan;border-bottom:1px solid red;border-left:2px dotted tan}

csskit ✗

a{border-top:1px solid red;border-right:2px dotted tan;border-bottom:1px solid red;border-left:2px dotted tan}

csslop ✓

a{border-width:1px 2px;border-style:solid dotted;border-color:red tan}

cssnano ✓

a{border-color:red tan;border-style:solid dotted;border-width:1px 2px}

csso ✗

a{border-top:1px solid red;border-right:2px dotted tan;border-bottom:1px solid red;border-left:2px dotted tan}

esbuild ✗

a{border-top:1px solid red;border-right:2px dotted tan;border-bottom:1px solid red;border-left:2px dotted tan}

lightningcss ✗

a{border:1px solid red;border-left:2px dotted tan;border-right:2px dotted tan}

sass ✗

a{border-top:1px solid red;border-right:2px dotted tan;border-bottom:1px solid red;border-left:2px dotted tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0055 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Columns longhand merge, with initial values

column-width, column-height, and column-count can be merged into the columns shorthand. All properties at their default values can be omitted.

a {
  column-width: auto;
  column-count: 3;
  column-height: auto;
}

Expected

a{columns:3}

Outputs

clean-css ✗

a{column-width:auto;column-count:3;column-height:auto}

csskit ✗

a{column-width:auto;column-count:3;column-height:auto}

csslop ✓

a{columns:3}

cssnano ✗

a{column-width:auto;column-count:3;column-height:auto}

csso ✗

a{column-width:auto;column-count:3;column-height:auto}

esbuild ✗

a{column-width:auto;column-count:3;column-height:auto}

lightningcss ✗

a{column-width:auto;column-count:3;column-height:auto}

sass ✗

a{column-width:auto;column-count:3;column-height:auto}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0056 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Fold longhands into mask including earlier declarations

mask resets mask-position, so an earlier declaration must be folded into the shorthand, rather than resetting the value.

a {
  mask-position: 10px;
  mask-image: url(mask.svg);
  mask-repeat: repeat-x;
}

Expected

a{mask:url(mask.svg)10px repeat-x}

Outputs

clean-css ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}

csskit ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}

csslop ✓

a{mask:url(mask.svg)10px repeat-x}

cssnano ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}

csso ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}

esbuild ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}

lightningcss ✗

a{mask-image:url(mask.svg);mask-position:10px;mask-repeat:repeat-x}

sass ✗

a{mask-position:10px;mask-image:url(mask.svg);mask-repeat:repeat-x}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0057 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Border-color shorthand with color keyword & hex touching

The top/bottom and left/right match in this example, so they can be combined when converting to the shorthand for border color. The blue value can be converted to hex, and there is no need to put a space after red, since the color keyword and hex values can be parsed as separate tokens, even when touching.

a {
  border-top-color: red;
  border-right-color: blue;
  border-bottom-color: red;
  border-left-color: blue;
}

Expected

a{border-color:red#00f}

Outputs

clean-css ✗

a{border-color:red #00f}

csskit ✗

a{border-top-color:red;border-right-color:blue;border-bottom-color:red;border-left-color:blue}

csslop ✓

a{border-color:red#00f}

cssnano ✗

a{border-color:red blue}

csso ✗

a{border-color:red #00f}

esbuild ✗

a{border-top-color:red;border-right-color:#00f;border-bottom-color:red;border-left-color:#00f}

lightningcss ✗

a{border-color:red #00f}

sass ✗

a{border-top-color:red;border-right-color:blue;border-bottom-color:red;border-left-color:blue}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 9 / 57 11 / 57 57 / 57 12 / 57 7 / 57 5 / 57 26 / 57 0 / 57
Percent 15.78% 19.29% 100% 21.05% 12.28% 8.77% 45.61% 0%
Duration 5ms 47ms 20ms 33ms 6ms 35ms 1ms 27ms

shorthands-unsafe #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Padding longhand merge unsafe with var()

Merging padding-top/right/bottom/left into padding shorthand is unsafe when values use var(). If any variable is undefined or empty, shorthand fallback behavior differs from individual longhands.

a {
  padding-top: var(--pt);
  padding-right: var(--pr);
  padding-bottom: var(--pb);
  padding-left: var(--pl);
}

Expected

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

Outputs

clean-css ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csskit ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csslop ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

cssnano ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

csso ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

esbuild ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

lightningcss ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}

sass ✓

a{padding-top:var(--pt);padding-right:var(--pr);padding-bottom:var(--pb);padding-left:var(--pl)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Border longhand merge unsafe with var()

Merging border-width, border-style, border-color into border shorthand is unsafe when values use var(). The variables may expand to multi-value strings (e.g. --border-width: 0 0 0 1px) which are valid in longhands but not in the shorthand form.

a {
  border-width: var(--border-width);
  border-style: var(--border-style);
  border-color: var(--border-color);
}

Expected

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

Outputs

clean-css ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

csskit ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

csslop ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

cssnano ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

csso ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

esbuild ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

lightningcss ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}

sass ✓

a{border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Padding longhand merge unsafe even with var() fallback

A valid fallback does not make merging safe. The fallback only triggers when the property is undefined. If the property is defined with an invalid value (e.g. --pt: banana), the fallback is ignored and the entire shorthand declaration fails at computed value time.

a {
  padding-top: var(--pt, 10px);
  padding-right: var(--pr, 20px);
  padding-bottom: var(--pb, 10px);
  padding-left: var(--pl, 20px);
}

Expected

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

Outputs

clean-css ✓

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

csskit ✓

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

csslop ✓

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

cssnano ✓

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

csso ✗

a{padding-top:var(--pt, 10px);padding-right:var(--pr, 20px);padding-bottom:var(--pb, 10px);padding-left:var(--pl, 20px)}

esbuild ✗

a{padding-top:var(--pt, 10px);padding-right:var(--pr, 20px);padding-bottom:var(--pb, 10px);padding-left:var(--pl, 20px)}

lightningcss ✓

a{padding-top:var(--pt,10px);padding-right:var(--pr,20px);padding-bottom:var(--pb,10px);padding-left:var(--pl,20px)}

sass ✗

a{padding-top:var(--pt, 10px);padding-right:var(--pr, 20px);padding-bottom:var(--pb, 10px);padding-left:var(--pl, 20px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Padding longhand merge unsafe when var() fallback is unresolvable

When var() fallbacks are themselves var() references, the final value cannot be statically determined. Merging into shorthand is unsafe because the nested variable may resolve to an invalid or multi-value result.

a {
  padding-top: var(--pt, var(--fallback));
  padding-right: var(--pr, var(--fallback));
  padding-bottom: var(--pb, var(--fallback));
  padding-left: var(--pl, var(--fallback));
}

Expected

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

Outputs

clean-css ✓

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

csskit ✓

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

csslop ✓

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

cssnano ✓

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

csso ✗

a{padding-top:var(--pt, var(--fallback));padding-right:var(--pr, var(--fallback));padding-bottom:var(--pb, var(--fallback));padding-left:var(--pl, var(--fallback))}

esbuild ✗

a{padding-top:var(--pt, var(--fallback));padding-right:var(--pr, var(--fallback));padding-bottom:var(--pb, var(--fallback));padding-left:var(--pl, var(--fallback))}

lightningcss ✓

a{padding-top:var(--pt,var(--fallback));padding-right:var(--pr,var(--fallback));padding-bottom:var(--pb,var(--fallback));padding-left:var(--pl,var(--fallback))}

sass ✗

a{padding-top:var(--pt, var(--fallback));padding-right:var(--pr, var(--fallback));padding-bottom:var(--pb, var(--fallback));padding-left:var(--pl, var(--fallback))}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Border longhand merge unsafe even with var() fallback

A valid fallback does not make merging safe. The fallback only triggers when the property is undefined. If the property is defined with an invalid value, the fallback is ignored and the entire shorthand declaration fails at computed value time.

a {
  border-width: var(--bw, 1px);
  border-style: var(--bs, solid);
  border-color: var(--bc, red);
}

Expected

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

Outputs

clean-css ✓

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

csskit ✓

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

csslop ✓

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

cssnano ✓

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

csso ✗

a{border-width:var(--bw, 1px);border-style:var(--bs, solid);border-color:var(--bc, red)}

esbuild ✗

a{border-width:var(--bw, 1px);border-style:var(--bs, solid);border-color:var(--bc, red)}

lightningcss ✓

a{border-width:var(--bw,1px);border-style:var(--bs,solid);border-color:var(--bc,red)}

sass ✗

a{border-width:var(--bw, 1px);border-style:var(--bs, solid);border-color:var(--bc, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Border longhand merge unsafe when any var() fallback is unresolvable

If even one var() has an unresolvable fallback (nested var(), no fallback, etc.), the longhands cannot be safely merged into border shorthand because the runtime value may expand to something invalid in shorthand context.

a {
  border-width: var(--bw, var(--fallback));
  border-style: var(--bs, solid);
  border-color: var(--bc, red);
}

Expected

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

Outputs

clean-css ✓

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

csskit ✓

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

csslop ✓

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

cssnano ✓

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

csso ✗

a{border-width:var(--bw, var(--fallback));border-style:var(--bs, solid);border-color:var(--bc, red)}

esbuild ✗

a{border-width:var(--bw, var(--fallback));border-style:var(--bs, solid);border-color:var(--bc, red)}

lightningcss ✓

a{border-width:var(--bw,var(--fallback));border-style:var(--bs,solid);border-color:var(--bc,red)}

sass ✗

a{border-width:var(--bw, var(--fallback));border-style:var(--bs, solid);border-color:var(--bc, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✗ ✓ ✗ ✗ ✗
Details

Border Shorthand Reset Hazard

The border shorthand resets border-image, which can't be expressed with border. If a rule only uses the longhand border properties, then compacting to the border shorthand is potentially unsafe it may inadvertently reset other rules that use border-image.

.a {
  border-image: url(border.png) 30 round;
}

.b {
  border-top-width: 1px;
  border-right-width: 1px;
  border-bottom-width: 1px;
  border-left-width: 1px;
  border-top-style: solid;
  border-right-style: solid;
  border-bottom-style: solid;
  border-left-style: solid;
  border-top-color: red;
  border-right-color: red;
  border-bottom-color: red;
  border-left-color: red;
}

Expected

.a{border-image:url(border.png)30 round}.b{border-width:1px;border-style:solid;border-color:red}

Also accepted

.a{border-image:url(border.png)30 round}.b{border-color:red;border-style:solid;border-width:1px}

Outputs

clean-css ✗

.a{border-image:url(border.png) 30 round}.b{border-width:1px;border-style:solid;border-top:1px solid red;border-right:1px solid red;border-bottom:1px solid red;border-color:red}

csskit ✗

.a{border-image:url(border.png)30 round}.b{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}

csslop ✓

.a{border-image:url(border.png)30 round}.b{border-width:1px;border-style:solid;border-color:red}

cssnano ✗

.a{border-image:url(border.png) 30 round}.b{border-color:red;border-style:solid;border-width:1px}

csso ✓

.a{border-image:url(border.png)30 round}.b{border-width:1px;border-style:solid;border-color:red}

esbuild ✗

.a{border-image:url(border.png) 30 round}.b{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}

lightningcss ✗

.a{border-image:url(border.png) 30 round}.b{border:1px solid red}

sass ✗

.a{border-image:url(border.png) 30 round}.b{border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;border-top-style:solid;border-right-style:solid;border-bottom-style:solid;border-left-style:solid;border-top-color:red;border-right-color:red;border-bottom-color:red;border-left-color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✗ ✓ ✓ ✓ ✓ ✗ ✗
Details

Font Shorthand Reset Hazard

The font shorthand resets font-kerning, which can't be expressed with font. If a rule only uses the longhand font properties, then compacting to the font shorthand is potentially unsafe it may inadvertently reset other rules that use font-kerning.

.a {
  font-kerning: none;
}

.b {
  font-style: italic;
  font-weight: bold;
  font-size: 16px;
  line-height: 1.5;
  font-family: Arial, sans-serif;
}

Expected

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

Outputs

clean-css ✓

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csskit ✗

.a{font-kerning:none}.b{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csslop ✓

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

cssnano ✓

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

csso ✓

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

esbuild ✓

.a{font-kerning:none}.b{font-style:italic;font-weight:700;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}

lightningcss ✗

.a{font-kerning:none}.b{font-family:Arial,sans-serif;font-size:16px;font-style:italic;font-weight:700;line-height:1.5}

sass ✗

.a{font-kerning:none}.b{font-style:italic;font-weight:bold;font-size:16px;line-height:1.5;font-family:Arial,sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

Animation Shorthand Reset Hazard

The animation shorthand encompasses only eight of the ten animation-related longhands, it cannot express animation-timeline and animation-range. If a rule only uses the longhand animation properties, then compacting to the animation shorthand is potentially unsafe it may inadvertently reset other rules that use animation-timeline or animation-range.

.a {
  animation-timeline: scroll();
  animation-range: entry;
}

.b {
  animation-name: slide;
  animation-duration: 1s;
  animation-timing-function: ease;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: none;
  animation-play-state: running;
}

Expected

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

Outputs

clean-css ✗

.a{animation-timeline:scroll();animation-range:entry}.b{animation:1s slide}

csskit ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

csslop ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

cssnano ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

csso ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

esbuild ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}

lightningcss ✗

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-iteration-count:1;animation-direction:normal;animation-play-state:running;animation-delay:0s;animation-fill-mode:none}

sass ✓

.a{animation-timeline:scroll();animation-range:entry}.b{animation-name:slide;animation-duration:1s;animation-timing-function:ease;animation-delay:0s;animation-iteration-count:1;animation-direction:normal;animation-fill-mode:none;animation-play-state:running}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Mask Shorthand Reset Hazard

The mask shorthand resets mask-border which can't be expressed with mask. If a rule only uses the longhand animation properties, then compacting to the mask shorthand is potentially unsafe it may inadvertently reset other rules that use mask-border.

.a {
  mask-border: url(mask.png) 25 / 10px round;
}

.b {
  mask-image: url(mask.svg);
  mask-position: center;
  mask-size: cover;
  mask-repeat: no-repeat;
  mask-origin: border-box;
  mask-clip: border-box;
  mask-composite: add;
  mask-mode: alpha;
}

Expected

.a{mask-border:url(mask.png)25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:initial;mask-clip:initial;mask-composite:add;mask-mode:alpha}

Outputs

clean-css ✗

.a{mask-border:url(mask.png) 25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:border-box;mask-clip:border-box;mask-composite:add;mask-mode:alpha}

csskit ✗

.a{mask-border:url(mask.png)25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:border-box;mask-clip:border-box;mask-composite:add;mask-mode:alpha}

csslop ✓

.a{mask-border:url(mask.png)25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:initial;mask-clip:initial;mask-composite:add;mask-mode:alpha}

cssnano ✗

.a{mask-border:url(mask.png) 25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:initial;mask-clip:initial;mask-composite:add;mask-mode:alpha}

csso ✗

.a{mask-border:url(mask.png)25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:border-box;mask-clip:border-box;mask-composite:add;mask-mode:alpha}

esbuild ✗

.a{mask-border:url(mask.png) 25 / 10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:border-box;mask-clip:border-box;mask-composite:add;mask-mode:alpha}

lightningcss ✗

.a{mask-border:url(mask.png) 25/10px round}.b{mask:url(mask.svg) 50%/cover no-repeat alpha}

sass ✗

.a{mask-border:url(mask.png) 25/10px round}.b{mask-image:url(mask.svg);mask-position:center;mask-size:cover;mask-repeat:no-repeat;mask-origin:border-box;mask-clip:border-box;mask-composite:add;mask-mode:alpha}
1ms
4ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Repeated var() in a shorthand value cannot be deduplicated

margin: 1px 1px reduces to margin: 1px, but the same reduction across two var() references is unsafe because the variable could contain an unknown number of components, for example if --a expands to three or four values the doubled form has six or eight components and is invalid at computed-value time, while the reduced form remains valid.

a {
  margin: var(--a) var(--a);
}

Expected

a{margin:var(--a)var(--a)}

Outputs

clean-css ✗

a{margin:var(--a) var(--a)}

csskit ✓

a{margin:var(--a)var(--a)}

csslop ✓

a{margin:var(--a)var(--a)}

cssnano ✗

a{margin:var(--a) var(--a)}

csso ✗

a{margin:var(--a) var(--a)}

esbuild ✗

a{margin:var(--a) var(--a)}

lightningcss ✗

a{margin:var(--a) var(--a)}

sass ✗

a{margin:var(--a) var(--a)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 7 / 11 8 / 11 11 / 11 8 / 11 5 / 11 4 / 11 6 / 11 3 / 11
Percent 63.63% 72.72% 100% 72.72% 45.45% 36.36% 54.54% 27.27%
Duration 1ms 5ms 3ms 6ms 1ms 6ms 1ms 4ms

starting-style #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@starting-style whitespace removal

Basic whitespace removal in @starting-style. Defines the initial style for CSS transitions when an element is first displayed.

@starting-style {
  .card {
    opacity: 0;
  }
}

Expected

@starting-style{.card{opacity:0}}

Outputs

clean-css ✗

csskit ✓

@starting-style{.card{opacity:0}}

csslop ✓

@starting-style{.card{opacity:0}}

cssnano ✓

@starting-style{.card{opacity:0}}

csso ✓

@starting-style{.card{opacity:0}}

esbuild ✓

@starting-style{.card{opacity:0}}

lightningcss ✓

@starting-style{.card{opacity:0}}

sass ✓

@starting-style{.card{opacity:0}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

@starting-style with discrete animation must not be removed

@starting-style with display: none and opacity: 0 defines the entry state for a discrete transition. A minifier must not remove this block even though the values appear to match the element's hidden state -- without it the entry animation does not play.

.card {
  transition: opacity 0.5s, display 0.5s allow-discrete;
}

@starting-style {
  .card {
    opacity: 0;
    display: none;
  }
}

Expected

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

Outputs

clean-css ✗

@starting-style{display:none}

csskit ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

csslop ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

cssnano ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

csso ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

esbuild ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

lightningcss ✗

.card{transition:opacity .5s, display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}

sass ✓

.card{transition:opacity .5s,display .5s allow-discrete}@starting-style{.card{opacity:0;display:none}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 2 / 2 1 / 2 2 / 2
Percent 0% 100% 100% 100% 100% 100% 50% 100%
Duration 1ms 1ms 1ms 1ms 1ms 1ms 1ms 1ms

supports #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✗ ✓ ✗ ✓ ✗ ✓ ✗
Details

@supports condition whitespace

Spaces inside the @supports condition parentheses are removable. Uses a property without universal support so the @supports wrapper cannot be statically elided.

@supports (hanging-punctuation: first) {
  a {
    hanging-punctuation: first;
  }
}

Expected

@supports (hanging-punctuation:first){a{hanging-punctuation:first}}

Outputs

clean-css ✓

@supports (hanging-punctuation:first){a{hanging-punctuation:first}}

csskit ✗

@supports(hanging-punctuation:first){a{hanging-punctuation:first}}

csslop ✓

@supports (hanging-punctuation:first){a{hanging-punctuation:first}}

cssnano ✗

@supports(hanging-punctuation:first){a{hanging-punctuation:first}}

csso ✓

@supports (hanging-punctuation:first){a{hanging-punctuation:first}}

esbuild ✗

@supports (hanging-punctuation: first){a{hanging-punctuation:first}}

lightningcss ✓

@supports (hanging-punctuation:first){a{hanging-punctuation:first}}

sass ✗

@supports(hanging-punctuation: first){a{hanging-punctuation:first}}
1ms
1ms
1ms
10ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Remove empty @supports rule

An @supports rule with no content should be removed entirely, like empty style rules.

@supports (display: grid) {
}
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Elide @supports for universally supported property

display: grid is supported in all current browsers. The @supports wrapper always evaluates to true and can be removed, unwrapping the inner rules.

@supports (display: grid) {
  a {
    display: grid;
  }
}

Expected

a{display:grid}

Outputs

clean-css ✗

@supports (display:grid){a{display:grid}}

csskit ✗

@supports(display:grid){a{display:grid}}

csslop ✓

a{display:grid}

cssnano ✗

@supports(display:grid){a{display:grid}}

csso ✗

@supports (display:grid){a{display:grid}}

esbuild ✗

@supports (display: grid){a{display:grid}}

lightningcss ✗

@supports (display:grid){a{display:grid}}

sass ✗

@supports(display: grid){a{display:grid}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Elide @supports with multiple inner rules

display: flex is universally supported. The @supports wrapper can be removed and all inner rules unwrapped to the parent context.

@supports (display: flex) {
  a {
    display: flex;
  }
  b {
    color: red;
  }
}

Expected

a{display:flex}b{color:red}

Outputs

clean-css ✗

@supports (display:flex){a{display:flex}b{color:red}}

csskit ✗

@supports(display:flex){a{display:flex}b{color:red}}

csslop ✓

a{display:flex}b{color:red}

cssnano ✗

@supports(display:flex){a{display:flex}b{color:red}}

csso ✗

@supports (display:flex){a{display:flex}b{color:red}}

esbuild ✗

@supports (display: flex){a{display:flex}b{color:red}}

lightningcss ✗

@supports (display:flex){a{display:flex}b{color:red}}

sass ✗

@supports(display: flex){a{display:flex}b{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

@supports not with unsupported property must be preserved

hanging-punctuation lacks universal support, so @supports not cannot be statically resolved. The wrapper must remain even though the negation might suggest removal.

@supports not (hanging-punctuation: first) {
  a {
    text-indent: 1em;
  }
}

Expected

@supports not (hanging-punctuation:first){a{text-indent:1em}}

Outputs

clean-css ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

csskit ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

csslop ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

cssnano ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

csso ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

esbuild ✗

@supports not (hanging-punctuation: first){a{text-indent:1em}}

lightningcss ✓

@supports not (hanging-punctuation:first){a{text-indent:1em}}

sass ✗

@supports not (hanging-punctuation: first){a{text-indent:1em}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 3 / 5 2 / 5 5 / 5 2 / 5 3 / 5 1 / 5 3 / 5 1 / 5
Percent 60% 40% 100% 40% 60% 20% 60% 20%
Duration 1ms 1ms 1ms 12ms 1ms 3ms 1ms 3ms

transforms #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

translate3d to translate

translate3d(x, 0, 0) can be simplified to translate(x) since the Y and Z components are zero.

a {
  transform: translate3d(10px, 0, 0);
}

Expected

a{transform:translate(10px)}

Outputs

clean-css ✗

a{transform:translate3d(10px,0,0)}

csskit ✗

a{transform:translate3d(10px,0,0)}

csslop ✓

a{transform:translate(10px)}

cssnano ✗

a{transform:translate3d(10px,0,0)}

csso ✗

a{transform:translate3d(10px,0,0)}

esbuild ✗

a{transform:translate3d(10px,0,0)}

lightningcss ✓

a{transform:translate(10px)}

sass ✗

a{transform:translate3d(10px, 0, 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

translate to translateY

translate(0, y) can be simplified to translateY(y) when the X component is zero.

a {
  transform: translate(0, 20px);
}

Expected

a{transform:translateY(20px)}

Outputs

clean-css ✗

a{transform:translate(0,20px)}

csskit ✗

a{transform:translate(0,20px)}

csslop ✓

a{transform:translateY(20px)}

cssnano ✓

a{transform:translateY(20px)}

csso ✗

a{transform:translate(0,20px)}

esbuild ✓

a{transform:translateY(20px)}

lightningcss ✓

a{transform:translateY(20px)}

sass ✗

a{transform:translate(0, 20px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

scale with equal axes

scale(x, x) can omit the second argument when both axes are identical: scale(x).

a {
  transform: scale(1.5, 1.5);
}

Expected

a{transform:scale(1.5)}

Outputs

clean-css ✗

a{transform:scale(1.5,1.5)}

csskit ✗

a{transform:scale(1.5,1.5)}

csslop ✓

a{transform:scale(1.5)}

cssnano ✓

a{transform:scale(1.5)}

csso ✗

a{transform:scale(1.5,1.5)}

esbuild ✓

a{transform:scale(1.5)}

lightningcss ✓

a{transform:scale(1.5)}

sass ✗

a{transform:scale(1.5, 1.5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
6ms
0004 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

rotateZ to rotate

rotateZ(a) is equivalent to rotate(a) and shorter. The Z axis is the default rotation axis.

a {
  transform: rotateZ(45deg);
}

Expected

a{transform:rotate(45deg)}

Outputs

clean-css ✗

a{transform:rotateZ(45deg)}

csskit ✗

a{transform:rotateZ(45deg)}

csslop ✓

a{transform:rotate(45deg)}

cssnano ✗

a{transform:rotateZ(45deg)}

csso ✗

a{transform:rotateZ(45deg)}

esbuild ✓

a{transform:rotate(45deg)}

lightningcss ✓

a{transform:rotate(45deg)}

sass ✗

a{transform:rotateZ(45deg)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

scale3d to scale

scale3d(x, y, 1) can be simplified to scale(x, y) when the Z component is 1.

a {
  transform: scale3d(1.5, 2, 1);
}

Expected

a{transform:scale(1.5,2)}

Outputs

clean-css ✗

a{transform:scale3d(1.5,2,1)}

csskit ✗

a{transform:scale3d(1.5,2,1)}

csslop ✓

a{transform:scale(1.5,2)}

cssnano ✗

a{transform:scale3d(1.5,2,1)}

csso ✗

a{transform:scale3d(1.5,2,1)}

esbuild ✗

a{transform:scale3d(1.5,2,1)}

lightningcss ✓

a{transform:scale(1.5,2)}

sass ✗

a{transform:scale3d(1.5, 2, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

rotate3d to rotateY

rotate3d(0, 1, 0, a) is equivalent to rotateY(a) and shorter.

a {
  transform: rotate3d(0, 1, 0, 30deg);
}

Expected

a{transform:rotateY(30deg)}

Outputs

clean-css ✗

a{transform:rotate3d(0,1,0,30deg)}

csskit ✗

a{transform:rotate3d(0,1,0,30deg)}

csslop ✓

a{transform:rotateY(30deg)}

cssnano ✓

a{transform:rotateY(30deg)}

csso ✗

a{transform:rotate3d(0,1,0,30deg)}

esbuild ✓

a{transform:rotateY(30deg)}

lightningcss ✓

a{transform:rotateY(30deg)}

sass ✗

a{transform:rotate3d(0, 1, 0, 30deg)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

scale(x, 1) to scaleX

scale(1.5, 1) only scales the X axis, equivalent to scaleX(1.5).

a {
  transform: scale(1.5, 1);
}

Expected

a{transform:scaleX(1.5)}

Outputs

clean-css ✗

a{transform:scale(1.5,1)}

csskit ✗

a{transform:scale(1.5,1)}

csslop ✓

a{transform:scaleX(1.5)}

cssnano ✓

a{transform:scaleX(1.5)}

csso ✗

a{transform:scale(1.5,1)}

esbuild ✓

a{transform:scaleX(1.5)}

lightningcss ✓

a{transform:scaleX(1.5)}

sass ✗

a{transform:scale(1.5, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

translate3d(0,0,z) to translateZ

When X and Y are zero, translate3d simplifies to translateZ.

a {
  transform: translate3d(0, 0, 5px);
}

Expected

a{transform:translateZ(5px)}

Outputs

clean-css ✗

a{transform:translate3d(0,0,5px)}

csskit ✗

a{transform:translate3d(0,0,5px)}

csslop ✓

a{transform:translateZ(5px)}

cssnano ✓

a{transform:translateZ(5px)}

csso ✗

a{transform:translate3d(0,0,5px)}

esbuild ✓

a{transform:translateZ(5px)}

lightningcss ✓

a{transform:translateZ(5px)}

sass ✗

a{transform:translate3d(0, 0, 5px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

rotate3d(1,0,0,a) to rotateX

Rotation around the X axis only simplifies to rotateX.

a {
  transform: rotate3d(1, 0, 0, 20deg);
}

Expected

a{transform:rotateX(20deg)}

Outputs

clean-css ✗

a{transform:rotate3d(1,0,0,20deg)}

csskit ✗

a{transform:rotate3d(1,0,0,20deg)}

csslop ✓

a{transform:rotateX(20deg)}

cssnano ✓

a{transform:rotateX(20deg)}

csso ✗

a{transform:rotate3d(1,0,0,20deg)}

esbuild ✓

a{transform:rotateX(20deg)}

lightningcss ✓

a{transform:rotateX(20deg)}

sass ✗

a{transform:rotate3d(1, 0, 0, 20deg)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✗ ✓ ✗ ✗ ✓ ✓ ✗
Details

scale() percentage to number

scale(50%) is equivalent to scale(.5). Per the CSS Transforms spec, scale functions accept <number> or <percentage>, where 100% = 1. Converting percentages to numbers is shorter and avoids the % character.

a {
  transform: scale(50%);
}

Expected

a{transform:scale(.5)}

Outputs

clean-css ✗

a{transform:scale(50%)}

csskit ✗

a{transform:scale(50%)}

csslop ✓

a{transform:scale(.5)}

cssnano ✗

a{transform:scale(50%)}

csso ✗

a{transform:scale(50%)}

esbuild ✓

a{transform:scale(.5)}

lightningcss ✓

a{transform:scale(.5)}

sass ✗

a{transform:scale(50%)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

scale property percentage to number

The individual scale property accepts <number> or <percentage>, where 100% = 1. scale: 200% is equivalent to scale: 2. Converting to a number saves 2 bytes (drops % and avoids the longer percentage representation).

a {
  scale: 200%;
}

Expected

a{scale:2}

Outputs

clean-css ✗

a{scale:200%}

csskit ✗

a{scale:200%}

csslop ✓

a{scale:2}

cssnano ✗

a{scale:200%}

csso ✗

a{scale:200%}

esbuild ✗

a{scale:200%}

lightningcss ✓

a{scale:2}

sass ✗

a{scale:200%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 11 0 / 11 11 / 11 6 / 11 0 / 11 8 / 11 11 / 11 0 / 11
Percent 0% 0% 100% 54.54% 0% 72.72% 100% 0%
Duration 1ms 1ms 1ms 5ms 1ms 6ms 1ms 8ms

values #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

font-weight: bold to 700

bold is equivalent to 700 and shorter as a numeric value.

a {
  font-weight: bold;
}

Expected

a{font-weight:700}

Outputs

clean-css ✓

a{font-weight:700}

csskit ✗

a{font-weight:bold}

csslop ✓

a{font-weight:700}

cssnano ✓

a{font-weight:700}

csso ✓

a{font-weight:700}

esbuild ✓

a{font-weight:700}

lightningcss ✓

a{font-weight:700}

sass ✗

a{font-weight:bold}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

font-weight: normal to 400

normal is equivalent to 400 and shorter as a numeric value.

a {
  font-weight: normal;
}

Expected

a{font-weight:400}

Outputs

clean-css ✓

a{font-weight:400}

csskit ✗

a{font-weight:normal}

csslop ✓

a{font-weight:400}

cssnano ✓

a{font-weight:400}

csso ✓

a{font-weight:400}

esbuild ✓

a{font-weight:400}

lightningcss ✓

a{font-weight:400}

sass ✗

a{font-weight:normal}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

`url()`` quote removal

Quotes inside url() are optional when the URL contains no special characters. They can be removed to save bytes.

a {
  background: url("image.png");
}

Expected

a{background:url(image.png)}

Outputs

clean-css ✗

a{background:url("image.png")}

csskit ✗

a{background:url("image.png")}

csslop ✓

a{background:url(image.png)}

cssnano ✓

a{background:url(image.png)}

csso ✓

a{background:url(image.png)}

esbuild ✓

a{background:url(image.png)}

lightningcss ✓

a{background:url(image.png)}

sass ✗

a{background:url("image.png")}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Trailing zero removal in dimensions

1.0000px should be normalized to 1px. Trailing fractional zeros are redundant.

a {
  width: 1.0000px;
}

Expected

a{width:1px}

Outputs

clean-css ✓

a{width:1px}

csskit ✓

a{width:1px}

csslop ✓

a{width:1px}

cssnano ✓

a{width:1px}

csso ✓

a{width:1px}

esbuild ✓

a{width:1px}

lightningcss ✓

a{width:1px}

sass ✓

a{width:1px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✗ ✓ ✓ ✓ ✓ ✗ ✓ ✓
Details

Redundant positive sign removal

+1.5px should be normalized to 1.5px. The explicit + sign on positive numeric values is redundant outside of contexts that require it (e.g. calc()).

a {
  margin: +1.5px;
}

Expected

a{margin:1.5px}

Outputs

clean-css ✗

a{margin:+1.5px}

csskit ✓

a{margin:1.5px}

csslop ✓

a{margin:1.5px}

cssnano ✓

a{margin:1.5px}

csso ✓

a{margin:1.5px}

esbuild ✗

a{margin:+1.5px}

lightningcss ✓

a{margin:1.5px}

sass ✓

a{margin:1.5px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✗ ✓ ✓ ✓ ✓ ✗ ✓ ✓
Details

Leading zero padding removal

001 should be normalized to 1. Leading zeros on integer values are redundant and can be safely stripped.

a {
  z-index: 001;
}

Expected

a{z-index:1}

Outputs

clean-css ✗

a{z-index:001}

csskit ✓

a{z-index:1}

csslop ✓

a{z-index:1}

cssnano ✓

a{z-index:1}

csso ✓

a{z-index:1}

esbuild ✗

a{z-index:001}

lightningcss ✓

a{z-index:1}

sass ✓

a{z-index:1}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

URL whitespace trimming

url( image.png ) should be trimmed to url(image.png). Leading and trailing whitespace inside unquoted url() tokens is insignificant.

a {
  background: url(  image.png  );
}

Expected

a{background:url(image.png)}

Outputs

clean-css ✓

a{background:url(image.png)}

csskit ✓

a{background:url(image.png)}

csslop ✓

a{background:url(image.png)}

cssnano ✓

a{background:url(image.png)}

csso ✓

a{background:url(image.png)}

esbuild ✓

a{background:url(image.png)}

lightningcss ✓

a{background:url(image.png)}

sass ✓

a{background:url(image.png)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Quote normalization

'hello world' should be normalized to "hello world". CSS allows both single and double quotes for strings; normalizing to one form is valid. Uses content to ensure quotes cannot be removed entirely.

a {
  content: 'hello world';
}

Expected

a{content:"hello world"}

Outputs

clean-css ✗

a{content:'hello world'}

csskit ✓

a{content:"hello world"}

csslop ✓

a{content:"hello world"}

cssnano ✓

a{content:"hello world"}

csso ✓

a{content:"hello world"}

esbuild ✓

a{content:"hello world"}

lightningcss ✓

a{content:"hello world"}

sass ✓

a{content:"hello world"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✗
Details

Milliseconds to seconds conversion

500ms should be converted to .5s when the seconds representation is shorter. The leading zero is also stripped (0.5s -> .5s).

a {
  transition-duration: 500ms;
}

Expected

a{transition-duration:.5s}

Outputs

clean-css ✓

a{transition-duration:.5s}

csskit ✓

a{transition-duration:.5s}

csslop ✓

a{transition-duration:.5s}

cssnano ✓

a{transition-duration:.5s}

csso ✗

a{transition-duration:500ms}

esbuild ✓

a{transition-duration:.5s}

lightningcss ✓

a{transition-duration:.5s}

sass ✗

a{transition-duration:500ms}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Absolute unit conversion to px

12pt converts to 16px. Absolute units (pt, pc, cm, mm, in, Q) have fixed ratios to px. Normalizing to px also improves gzip compression by reducing the number of distinct unit strings in the output. A minifier should convert pt to px when the resulting value has the same or fewer characters.

a {
  border-width: 3pt;
  width: 12pt;
  height: 15pt;
  font-size: 75pt;
  padding: 13pt;
}

Expected

a{border-width:4px;width:16px;height:20px;font-size:75pt;padding:13pt}

Outputs

clean-css ✗

a{border-width:3pt;width:12pt;height:15pt;font-size:75pt;padding:13pt}

csskit ✓

a{border-width:4px;width:16px;height:20px;font-size:75pt;padding:13pt}

csslop ✓

a{border-width:4px;width:16px;height:20px;font-size:75pt;padding:13pt}

cssnano ✗

a{border-width:3pt;width:12pt;height:15pt;font-size:75pt;padding:13pt}

csso ✗

a{border-width:3pt;width:12pt;height:15pt;font-size:75pt;padding:13pt}

esbuild ✗

a{border-width:3pt;width:12pt;height:15pt;font-size:75pt;padding:13pt}

lightningcss ✗

a{border-width:3pt;width:12pt;height:15pt;padding:13pt;font-size:75pt}

sass ✗

a{border-width:3pt;width:12pt;height:15pt;font-size:75pt;padding:13pt}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
Details

Retain escape slash in quote-less URLs

If a URL contains only one space, its quotes can be removed and the space can be escaped, resulting in fewer characters. If this optimization has already been performed, the escaped space should be retained.

.a {
  mask-image: url(one\ space.svg);
}

Expected

.a{mask-image:url(one\ space.svg)}

Outputs

clean-css ✓

.a{mask-image:url(one\ space.svg)}

csskit ✓

.a{mask-image:url(one\ space.svg)}

csslop ✓

.a{mask-image:url(one\ space.svg)}

cssnano ✓

.a{mask-image:url(one\ space.svg)}

csso ✓

.a{mask-image:url(one\ space.svg)}

esbuild ✓

.a{mask-image:url(one\ space.svg)}

lightningcss ✗

.a{mask-image:url("one space.svg")}

sass ✓

.a{mask-image:url(one\ space.svg)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

`var()`` custom property preservation

var(--main-color) must be preserved as-is. Custom property references cannot be resolved or optimized at minification time since their values are defined at runtime.

a {
  color: var(--main-color);
}

Expected

a{color:var(--main-color)}

Outputs

clean-css ✓

a{color:var(--main-color)}

csskit ✓

a{color:var(--main-color)}

csslop ✓

a{color:var(--main-color)}

cssnano ✓

a{color:var(--main-color)}

csso ✓

a{color:var(--main-color)}

esbuild ✓

a{color:var(--main-color)}

lightningcss ✓

a{color:var(--main-color)}

sass ✓

a{color:var(--main-color)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Gradient function whitespace

Whitespace inside linear-gradient() around commas and arguments should be stripped. Uses a numeric angle and short hex colors to avoid confounding direction keyword or color conversions.

a {
  background: linear-gradient( 45deg, #111 , #999 );
}

Expected

a{background:linear-gradient(45deg,#111,#999)}

Outputs

clean-css ✗

a{background:linear-gradient(45deg,#111 ,#999)}

csskit ✓

a{background:linear-gradient(45deg,#111,#999)}

csslop ✓

a{background:linear-gradient(45deg,#111,#999)}

cssnano ✓

a{background:linear-gradient(45deg,#111,#999)}

csso ✓

a{background:linear-gradient(45deg,#111,#999)}

esbuild ✓

a{background:linear-gradient(45deg,#111,#999)}

lightningcss ✓

a{background:linear-gradient(45deg,#111,#999)}

sass ✗

a{background:linear-gradient(45deg, #111, #999)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✗ ✓ ✓ ✓ ✓ ✗ ✗
Details

Remove quotes and escape single space in URL

If a url contains 1 or fewer spaces, the space can be escaped and the quotes can be removed. If there are 2 or more spaces, then keeping the quotes results in the same or smaller output.

.a {
  background-image: url("no-spaces.png");
}
.b {
  mask-image: url('one space.svg');
}
.c {
  background-image: url('more than one space.png');
}

Expected

.a{background-image:url(no-spaces.png)}.b{mask-image:url(one\ space.svg)}.c{background-image:url("more than one space.png")}

Outputs

clean-css ✗

.a{background-image:url("no-spaces.png")}.b{mask-image:url('one space.svg')}.c{background-image:url('more than one space.png')}

csskit ✗

.a{background-image:url("no-spaces.png")}.b{mask-image:url("one space.svg")}.c{background-image:url("more than one space.png")}

csslop ✓

.a{background-image:url(no-spaces.png)}.b{mask-image:url(one\ space.svg)}.c{background-image:url("more than one space.png")}

cssnano ✓

.a{background-image:url(no-spaces.png)}.b{mask-image:url(one\ space.svg)}.c{background-image:url("more than one space.png")}

csso ✓

.a{background-image:url(no-spaces.png)}.b{mask-image:url(one\ space.svg)}.c{background-image:url("more than one space.png")}

esbuild ✓

.a{background-image:url(no-spaces.png)}.b{mask-image:url(one\ space.svg)}.c{background-image:url("more than one space.png")}

lightningcss ✗

.a{background-image:url(no-spaces.png)}.b{mask-image:url("one space.svg")}.c{background-image:url("more than one space.png")}

sass ✗

.a{background-image:url("no-spaces.png")}.b{mask-image:url("one space.svg")}.c{background-image:url("more than one space.png")}
1ms
2ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Remove current directory indicator from start of URL

If a file path in a URL (for a background, mask, etc) begins with a period and a forward-slash (./), they can be removed. They indicate that the file path starts from the same directory as the CSS file. However, when removed, the browser will assume this automatically.

a {
  background-image: url(./folder/file.png);
}

Expected

a{background-image:url(folder/file.png)}

Outputs

clean-css ✗

a{background-image:url(./folder/file.png)}

csskit ✗

a{background-image:url(./folder/file.png)}

csslop ✓

a{background-image:url(folder/file.png)}

cssnano ✓

a{background-image:url(folder/file.png)}

csso ✗

a{background-image:url(./folder/file.png)}

esbuild ✗

a{background-image:url(./folder/file.png)}

lightningcss ✗

a{background-image:url(./folder/file.png)}

sass ✗

a{background-image:url(./folder/file.png)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Convert quotes: none equivalents to none

All of these are equivalent:

  • quotes: "" "" "" "" "" "";
  • quotes: '' '' '' '';
  • quotes: "" "";
  • quotes:"""";
  • quotes:none;

Effectively: "don't use any quotes". A minifier could convert all of these examples to either none or """", as both have the shortest character count. However, none is a commonly used CSS value by many properties, and therefore has a greater chance at being gzipped. Quotes aren't uncommon, but 4 of them in a row are, making them a worse candidate for gzipping across a stylesheet.

.a {
  quotes: "" "";
}
.b {
  color: red;
}
.c {
  quotes: '' '' '' '';
}
.d {
  color: tan;
}
.e {
  quotes: none;
}

Expected

.a{quotes:none}.b{color:red}.c{quotes:none}.d{color:tan}.e{quotes:none}

Outputs

clean-css ✗

.a{quotes:"" ""}.b{color:red}.c{quotes:'' '' '' ''}.d{color:tan}.e{quotes:none}

csskit ✗

.a{quotes:""""}.b{color:red}.c{quotes:""""""""}.d{color:tan}.e{quotes:none}

csslop ✓

.a{quotes:none}.b{color:red}.c{quotes:none}.d{color:tan}.e{quotes:none}

cssnano ✗

.a{quotes:"" ""}.b{color:red}.c{quotes:"" "" "" ""}.d{color:tan}.e{quotes:none}

csso ✗

.a{quotes:""""}.b{color:red}.c{quotes:""""""""}.d{color:tan}.e{quotes:none}

esbuild ✗

.a{quotes:"" ""}.b{color:red}.c{quotes:"" "" "" ""}.d{color:tan}.e{quotes:none}

lightningcss ✗

.a{quotes:"" ""}.b{color:red}.c{quotes:"" "" "" ""}.d{color:tan}.e{quotes:none}

sass ✗

.a{quotes:"" ""}.b{color:red}.c{quotes:"" "" "" ""}.d{color:tan}.e{quotes:none}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Invalid quotes are removed

The quotes property requires values in pairs of two. If you provide an odd number of values, then the entire property is ignored by the browser, and can therefore be removed.

.a {
  quotes: "a";
  color: red;
}
.b {
  quotes: "a" "b" "c";
  color: tan;
}

Expected

.a{color:red}.b{color:tan}

Outputs

clean-css ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}

csskit ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}

csslop ✓

.a{color:red}.b{color:tan}

cssnano ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}

csso ✗

.a{quotes:"a";color:red}.b{quotes:"a""b""c";color:tan}

esbuild ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}

lightningcss ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}

sass ✗

.a{quotes:"a";color:red}.b{quotes:"a" "b" "c";color:tan}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Trailing zero removal

0.50000 should be reduced to .5 by removing trailing zeros and leading zero.

a {
  opacity: 0.50000;
}

Expected

a{opacity:.5}

Outputs

clean-css ✓

a{opacity:.5}

csskit ✓

a{opacity:.5}

csslop ✓

a{opacity:.5}

cssnano ✓

a{opacity:.5}

csso ✓

a{opacity:.5}

esbuild ✓

a{opacity:.5}

lightningcss ✓

a{opacity:.5}

sass ✓

a{opacity:.5}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Grid template areas whitespace minification

Extra whitespace inside grid-template-areas strings is collapsed, and consecutive dots (....) for unnamed cells are reduced to a single dot.

a {
  grid-template-areas: "head  head"
                       "nav   main"
                       "foot  ....";
}

Expected

a{grid-template-areas:"head head""nav main""foot ."}

Outputs

clean-css ✗

a{grid-template-areas:"head  head" "nav   main" "foot  ...."}

csskit ✗

a{grid-template-areas:"head  head""nav   main""foot  ...."}

csslop ✓

a{grid-template-areas:"head head""nav main""foot ."}

cssnano ✗

a{grid-template-areas:"head  head" "nav   main" "foot  ...."}

csso ✗

a{grid-template-areas:"head  head""nav   main""foot  ...."}

esbuild ✗

a{grid-template-areas:"head  head" "nav   main" "foot  ...."}

lightningcss ✗

a{grid-template-areas:"head head""nav main""foot."}

sass ✗

a{grid-template-areas:"head  head" "nav   main" "foot  ...."}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Background position center to percentage

center center resolves to 50% -- when both axes are the same, a single value suffices.

a {
  background-position: center center;
}

Expected

a{background-position:50%}

Outputs

clean-css ✗

a{background-position:center center}

csskit ✗

a{background-position:center center}

csslop ✓

a{background-position:50%}

cssnano ✓

a{background-position:50%}

csso ✗

a{background-position:center center}

esbuild ✗

a{background-position:center center}

lightningcss ✓

a{background-position:50%}

sass ✗

a{background-position:center center}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✓ ✓ ✗ ✓ ✗ ✗ ✗
Details

Remove spaces from quotes declarations

The quotes property allows multiple values to be defined, in pairs of 2. Each value must be surround in double (") or single (') quotes. However, you do not need to put spaces between these sets of values. The quotes handle the separation of the values, even without spaces between them.

.a {
  quotes: "«" "»";
}
.b {
  quotes: "a" "b" "c" "d";
}
.c {
  quotes: "a" "b" "c" "d" "e" "f";
}

Expected

.a{quotes:"«""»"}.b{quotes:"a""b""c""d"}.c{quotes:"a""b""c""d""e""f"}

Outputs

clean-css ✗

.a{quotes:"«" "»"}.b{quotes:"a" "b" "c" "d"}.c{quotes:"a" "b" "c" "d" "e" "f"}

csskit ✓

.a{quotes:"«""»"}.b{quotes:"a""b""c""d"}.c{quotes:"a""b""c""d""e""f"}

csslop ✓

.a{quotes:"«""»"}.b{quotes:"a""b""c""d"}.c{quotes:"a""b""c""d""e""f"}

cssnano ✗

.a{quotes:"«" "»"}.b{quotes:"a" "b" "c" "d"}.c{quotes:"a" "b" "c" "d" "e" "f"}

csso ✓

.a{quotes:"«""»"}.b{quotes:"a""b""c""d"}.c{quotes:"a""b""c""d""e""f"}

esbuild ✗

.a{quotes:"\ab" "\bb"}.b{quotes:"a" "b" "c" "d"}.c{quotes:"a" "b" "c" "d" "e" "f"}

lightningcss ✗

.a{quotes:"«" "»"}.b{quotes:"a" "b" "c" "d"}.c{quotes:"a" "b" "c" "d" "e" "f"}

sass ✗

.a{quotes:"«" "»"}.b{quotes:"a" "b" "c" "d"}.c{quotes:"a" "b" "c" "d" "e" "f"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0022 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Multi-keyword display to single keyword

display: block flow is the two-value syntax equivalent of display: block.

a {
  display: block flow;
}

Expected

a{display:block}

Outputs

clean-css ✗

a{display:block flow}

csskit ✗

a{display:block flow}

csslop ✓

a{display:block}

cssnano ✓

a{display:block}

csso ✗

a{display:block flow}

esbuild ✗

a{display:block flow}

lightningcss ✓

a{display:block}

sass ✗

a{display:block flow}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0023 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

cubic-bezier to linear keyword

cubic-bezier(0, 0, 1, 1) is the functional form of linear.

a {
  transition: color cubic-bezier(0, 0, 1, 1);
}

Expected

a{transition:color linear}

Outputs

clean-css ✗

a{transition:color cubic-bezier(0, 0, 1, 1)}

csskit ✗

a{transition:color cubic-bezier(0,0,1,1)}

csslop ✓

a{transition:color linear}

cssnano ✓

a{transition:color linear}

csso ✗

a{transition:color cubic-bezier(0,0,1,1)}

esbuild ✗

a{transition:color cubic-bezier(0,0,1,1)}

lightningcss ✗

a{transition:color cubic-bezier(0,0,1,1)}

sass ✗

a{transition:color cubic-bezier(0, 0, 1, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0024 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

cubic-bezier to ease keyword

cubic-bezier(0.25, 0.1, 0.25, 1) is the functional form of ease.

a {
  transition: color cubic-bezier(0.25, 0.1, 0.25, 1);
}

Expected

a{transition:color ease}

Outputs

clean-css ✗

a{transition:color cubic-bezier(.25, .1, .25, 1)}

csskit ✗

a{transition:color cubic-bezier(.25,.1,.25,1)}

csslop ✓

a{transition:color ease}

cssnano ✗

a{transition:color}

csso ✗

a{transition:color cubic-bezier(.25,.1,.25,1)}

esbuild ✗

a{transition:color cubic-bezier(.25,.1,.25,1)}

lightningcss ✗

a{transition:color}

sass ✗

a{transition:color cubic-bezier(0.25, 0.1, 0.25, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0025 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Two-value background-repeat collapse

no-repeat no-repeat collapses to no-repeat when both axes are identical.

a {
  background-repeat: no-repeat no-repeat;
}

Expected

a{background-repeat:no-repeat}

Outputs

clean-css ✗

a{background-repeat:no-repeat no-repeat}

csskit ✗

a{background-repeat:no-repeat no-repeat}

csslop ✓

a{background-repeat:no-repeat}

cssnano ✓

a{background-repeat:no-repeat}

csso ✗

a{background-repeat:no-repeat no-repeat}

esbuild ✗

a{background-repeat:no-repeat no-repeat}

lightningcss ✓

a{background-repeat:no-repeat}

sass ✗

a{background-repeat:no-repeat no-repeat}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0026 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Two-value background-repeat to repeat-x

repeat no-repeat is equivalent to the repeat-x keyword.

a {
  background-repeat: repeat no-repeat;
}

Expected

a{background-repeat:repeat-x}

Outputs

clean-css ✗

a{background-repeat:repeat no-repeat}

csskit ✗

a{background-repeat:repeat no-repeat}

csslop ✓

a{background-repeat:repeat-x}

cssnano ✓

a{background-repeat:repeat-x}

csso ✗

a{background-repeat:repeat no-repeat}

esbuild ✗

a{background-repeat:repeat no-repeat}

lightningcss ✓

a{background-repeat:repeat-x}

sass ✗

a{background-repeat:repeat no-repeat}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0027 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Turn to degrees conversion

0.25turn equals 90deg. Degrees are shorter for common turn fractions.

a {
  transform: rotate(0.25turn);
}

Expected

a{transform:rotate(90deg)}

Outputs

clean-css ✗

a{transform:rotate(.25turn)}

csskit ✗

a{transform:rotate(.25turn)}

csslop ✓

a{transform:rotate(90deg)}

cssnano ✓

a{transform:rotate(90deg)}

csso ✗

a{transform:rotate(.25turn)}

esbuild ✗

a{transform:rotate(.25turn)}

lightningcss ✗

a{transform:rotate(.25turn)}

sass ✗

a{transform:rotate(0.25turn)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0028 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

min-width: initial to auto

The initial value of min-width is auto (CSS Sizing Level 3), not 0. auto is shorter than initial and semantically equivalent.

a {
  min-width: initial;
}

Expected

a{min-width:auto}

Outputs

clean-css ✗

a{min-width:initial}

csskit ✗

a{min-width:initial}

csslop ✓

a{min-width:auto}

cssnano ✓

a{min-width:auto}

csso ✗

a{min-width:initial}

esbuild ✗

a{min-width:initial}

lightningcss ✗

a{min-width:initial}

sass ✗

a{min-width:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0029 ✓ ✓ ✓ ✓ ✗ ✗ ✓ ✗
Details

Border shorthand canonical order

border: solid 1px red should be reordered to border: 1px solid red (width style color).

a {
  border: solid 1px red;
}

Expected

a{border:1px solid red}

Outputs

clean-css ✓

a{border:1px solid red}

csskit ✓

a{border:1px solid red}

csslop ✓

a{border:1px solid red}

cssnano ✓

a{border:1px solid red}

csso ✗

a{border:solid 1px red}

esbuild ✗

a{border:solid 1px red}

lightningcss ✓

a{border:1px solid red}

sass ✗

a{border:solid 1px red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0030 ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗
Details

flex-flow canonical order

flex-flow: wrap row should be reordered to flex-flow: row wrap (direction then wrap).

a {
  flex-flow: wrap row;
}

Expected

a{flex-flow:row wrap}

Outputs

clean-css ✗

a{flex-flow:wrap row}

csskit ✓

a{flex-flow:row wrap}

csslop ✓

a{flex-flow:row wrap}

cssnano ✓

a{flex-flow:row wrap}

csso ✗

a{flex-flow:wrap row}

esbuild ✗

a{flex-flow:wrap row}

lightningcss ✗

a{flex-flow:wrap}

sass ✗

a{flex-flow:wrap row}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0031 ✗ ✗ ✓ ✓ ✗ ✓ ✓ ✗
Details

Unquote font-family name

Quotes around font-family names can be removed when the name is a valid CSS identifier sequence.

a {
  font-family: "Helvetica Neue";
}

Expected

a{font-family:Helvetica Neue}

Outputs

clean-css ✗

a{font-family:"Helvetica Neue"}

csskit ✗

a{font-family:"Helvetica Neue"}

csslop ✓

a{font-family:Helvetica Neue}

cssnano ✓

a{font-family:Helvetica Neue}

csso ✗

a{font-family:"Helvetica Neue"}

esbuild ✓

a{font-family:Helvetica Neue}

lightningcss ✓

a{font-family:Helvetica Neue}

sass ✗

a{font-family:"Helvetica Neue"}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0032 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Deduplicate font-family list

Duplicate font names in a font-family list can be removed.

a {
  font-family: Helvetica, Arial, Helvetica, sans-serif;
}

Expected

a{font-family:Helvetica,Arial,sans-serif}

Outputs

clean-css ✗

a{font-family:Helvetica,Arial,Helvetica,sans-serif}

csskit ✗

a{font-family:Helvetica,Arial,Helvetica,sans-serif}

csslop ✓

a{font-family:Helvetica,Arial,sans-serif}

cssnano ✓

a{font-family:Helvetica,Arial,sans-serif}

csso ✗

a{font-family:Helvetica,Arial,Helvetica,sans-serif}

esbuild ✗

a{font-family:Helvetica,Arial,Helvetica,sans-serif}

lightningcss ✓

a{font-family:Helvetica,Arial,sans-serif}

sass ✗

a{font-family:Helvetica,Arial,Helvetica,sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0033 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Multi-keyword display: inline flow-root to inline-block

display: inline flow-root is the two-value equivalent of inline-block.

a {
  display: inline flow-root;
}

Expected

a{display:inline-block}

Outputs

clean-css ✗

a{display:inline flow-root}

csskit ✗

a{display:inline flow-root}

csslop ✓

a{display:inline-block}

cssnano ✓

a{display:inline-block}

csso ✗

a{display:inline flow-root}

esbuild ✗

a{display:inline flow-root}

lightningcss ✓

a{display:inline-block}

sass ✗

a{display:inline flow-root}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0034 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

steps(1, start) to step-start keyword

steps(1, start) is the functional form of the step-start timing keyword. The order of values canonically must end with the keyframe names (fade).

a {
  animation: steps(1, start) fade;
}

Expected

a{animation:step-start fade}

Outputs

clean-css ✗

a{animation:steps(1,start) fade}

csskit ✗

a{animation:steps(1,start)fade}

csslop ✓

a{animation:step-start fade}

cssnano ✗

a{animation:fade step-start}

csso ✗

a{animation:steps(1,start) fade}

esbuild ✗

a{animation:steps(1,start) fade}

lightningcss ✓

a{animation:step-start fade}

sass ✗

a{animation:steps(1, start) fade}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0035 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

background-position keywords to numeric

left top is equivalent to 0 0 and shorter.

a {
  background-position: left top;
}

Expected

a{background-position:0 0}

Outputs

clean-css ✗

a{background-position:left top}

csskit ✗

a{background-position:left top}

csslop ✓

a{background-position:0 0}

cssnano ✓

a{background-position:0 0}

csso ✗

a{background-position:left top}

esbuild ✗

a{background-position:left top}

lightningcss ✓

a{background-position:0 0}

sass ✗

a{background-position:left top}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0036 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

cubic-bezier to ease-in keyword

cubic-bezier(0.42, 0, 1, 1) is the functional form of ease-in.

a {
  transition: color cubic-bezier(0.42, 0, 1, 1);
}

Expected

a{transition:color ease-in}

Outputs

clean-css ✗

a{transition:color cubic-bezier(.42, 0, 1, 1)}

csskit ✗

a{transition:color cubic-bezier(.42,0,1,1)}

csslop ✓

a{transition:color ease-in}

cssnano ✓

a{transition:color ease-in}

csso ✗

a{transition:color cubic-bezier(.42,0,1,1)}

esbuild ✗

a{transition:color cubic-bezier(.42,0,1,1)}

lightningcss ✓

a{transition:color ease-in}

sass ✗

a{transition:color cubic-bezier(0.42, 0, 1, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0037 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

cubic-bezier to ease-out keyword

cubic-bezier(0, 0, 0.58, 1) is the functional form of ease-out.

a {
  transition: color cubic-bezier(0, 0, 0.58, 1);
}

Expected

a{transition:color ease-out}

Outputs

clean-css ✗

a{transition:color cubic-bezier(0, 0, .58, 1)}

csskit ✗

a{transition:color cubic-bezier(0,0,.58,1)}

csslop ✓

a{transition:color ease-out}

cssnano ✓

a{transition:color ease-out}

csso ✗

a{transition:color cubic-bezier(0,0,.58,1)}

esbuild ✗

a{transition:color cubic-bezier(0,0,.58,1)}

lightningcss ✓

a{transition:color ease-out}

sass ✗

a{transition:color cubic-bezier(0, 0, 0.58, 1)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0038 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Two-value background-repeat to repeat-y

no-repeat repeat is equivalent to the repeat-y keyword.

a {
  background-repeat: no-repeat repeat;
}

Expected

a{background-repeat:repeat-y}

Outputs

clean-css ✗

a{background-repeat:no-repeat repeat}

csskit ✗

a{background-repeat:no-repeat repeat}

csslop ✓

a{background-repeat:repeat-y}

cssnano ✓

a{background-repeat:repeat-y}

csso ✗

a{background-repeat:no-repeat repeat}

esbuild ✗

a{background-repeat:no-repeat repeat}

lightningcss ✓

a{background-repeat:repeat-y}

sass ✗

a{background-repeat:no-repeat repeat}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0039 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Convert background-position to percent when possible

The top, bottom, left, right, and center keywords can be represented as percents and always yield a shorter outcome. However, when combined with non-percent values, or when used with 3 or 4 arguments, the extra numerical inputs count as offsets, and require the keywords to be retained. When just providing the Y-axis (bottom) without the X-axis (left), retain the keyword.

.a {
  background-position: left;
}
.b {
  background-position: center;
}
.c {
  background-position: bottom;
}
.d {
  background-position: left top;
}
.e {
  background-position: center center;
}
.f {
  background-position: right bottom;
}
.g {
  background-position: top 10px;
}
.h {
  background-position: right 10% bottom 20%;
}

Expected

.a{background-position:0}.b{background-position:50%}.c{background-position:bottom}.d{background-position:0 0}.e{background-position:50%}.f{background-position:100% 100%}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

Outputs

clean-css ✗

.a{background-position:left}.b{background-position:center}.c{background-position:bottom}.d{background-position:left top}.e{background-position:center center}.f{background-position:right bottom}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

csskit ✗

.a{background-position:left}.b{background-position:center}.c{background-position:bottom}.d{background-position:left top}.e{background-position:center center}.f{background-position:right bottom}.g{background-position:10px top}.h{background-position:right 10% bottom 20%}

csslop ✓

.a{background-position:0}.b{background-position:50%}.c{background-position:bottom}.d{background-position:0 0}.e{background-position:50%}.f{background-position:100% 100%}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

cssnano ✓

.a{background-position:0}.b{background-position:50%}.c{background-position:bottom}.d{background-position:0 0}.e{background-position:50%}.f{background-position:100% 100%}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

csso ✗

.a{background-position:left}.b{background-position:center}.c{background-position:bottom}.d{background-position:left top}.e{background-position:center center}.f{background-position:right bottom}.g{background-position:top 10px}.h{background-position:right 10%bottom 20%}

esbuild ✗

.a{background-position:left}.b{background-position:center}.c{background-position:bottom}.d{background-position:left top}.e{background-position:center center}.f{background-position:right bottom}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

lightningcss ✓

.a{background-position:0}.b{background-position:50%}.c{background-position:bottom}.d{background-position:0 0}.e{background-position:50%}.f{background-position:100% 100%}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}

sass ✗

.a{background-position:left}.b{background-position:center}.c{background-position:bottom}.d{background-position:left top}.e{background-position:center center}.f{background-position:right bottom}.g{background-position:top 10px}.h{background-position:right 10% bottom 20%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0040 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Convert time lengths

A length of time can be represented in ms or s. The s representation is shorter in all cases above 99ms and should be preferred by the minifier. A time length of 0 must not be unitless, and in this case s is shorter.

.a {
  transition-duration: 0ms;
}
.b {
  transition: color 99ms;
}
.c {
  transition: color 100ms;
}
.d {
  transition-duration: 2000ms;
}
.e {
  transition-delay: 1234ms;
}

Expected

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1.234s}

Outputs

clean-css ✗

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1234ms}

csskit ✗

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1234ms}

csslop ✓

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1.234s}

cssnano ✗

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1234ms}

csso ✗

.a{transition-duration:0ms}.b{transition:color 99ms}.c{transition:color 100ms}.d{transition-duration:2000ms}.e{transition-delay:1234ms}

esbuild ✗

.a{transition-duration:0ms}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1234ms}

lightningcss ✓

.a{transition-duration:0s}.b{transition:color 99ms}.c{transition:color .1s}.d{transition-duration:2s}.e{transition-delay:1.234s}

sass ✗

.a{transition-duration:0ms}.b{transition:color 99ms}.c{transition:color 100ms}.d{transition-duration:2000ms}.e{transition-delay:1234ms}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0041 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Shorten background-color: initial to use #0000

The initial value of background-color is transparent, which is longer than initial. So it should not be replaced with the transparent keyword. However, transparent is equivalent to #0000, which is shorter than initial (See: color/0063).

a {
  background-color: initial;
}

Expected

a{background-color:#0000}

Outputs

clean-css ✗

a{background-color:initial}

csskit ✗

a{background-color:initial}

csslop ✓

a{background-color:#0000}

cssnano ✗

a{background-color:initial}

csso ✗

a{background-color:initial}

esbuild ✗

a{background-color:initial}

lightningcss ✗

a{background-color:initial}

sass ✗

a{background-color:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0042 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

margin: initial to 0

The initial value of margin is 0, which is shorter than initial.

a {
  margin: initial;
}

Expected

a{margin:0}

Outputs

clean-css ✗

a{margin:initial}

csskit ✗

a{margin:initial}

csslop ✓

a{margin:0}

cssnano ✗

a{margin:initial}

csso ✗

a{margin:initial}

esbuild ✗

a{margin:initial}

lightningcss ✗

a{margin:initial}

sass ✗

a{margin:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0043 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

z-index: initial to auto

The initial value of z-index is auto, which is shorter than initial.

a {
  z-index: initial;
}

Expected

a{z-index:auto}

Outputs

clean-css ✗

a{z-index:initial}

csskit ✗

a{z-index:initial}

csslop ✓

a{z-index:auto}

cssnano ✓

a{z-index:auto}

csso ✗

a{z-index:initial}

esbuild ✗

a{z-index:initial}

lightningcss ✗

a{z-index:initial}

sass ✗

a{z-index:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0044 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

display: initial must not shorten

The initial value of display is inline, which is the same length as initial. No replacement saves bytes.

a {
  display: initial;
}

Expected

a{display:initial}

Outputs

clean-css ✓

a{display:initial}

csskit ✓

a{display:initial}

csslop ✓

a{display:initial}

cssnano ✓

a{display:initial}

csso ✓

a{display:initial}

esbuild ✓

a{display:initial}

lightningcss ✓

a{display:initial}

sass ✓

a{display:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0045 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

opacity: initial to 1

The initial value of opacity is 1, which is shorter than initial.

a {
  opacity: initial;
}

Expected

a{opacity:1}

Outputs

clean-css ✗

a{opacity:initial}

csskit ✗

a{opacity:initial}

csslop ✓

a{opacity:1}

cssnano ✓

a{opacity:1}

csso ✗

a{opacity:initial}

esbuild ✗

a{opacity:initial}

lightningcss ✗

a{opacity:initial}

sass ✗

a{opacity:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0046 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✓
Details

max() with known values resolves to largest

When all arguments to max() are static comparable values, the function can be replaced with the largest value.

a {
  width: max(10px, 20px);
}

Expected

a{width:20px}

Outputs

clean-css ✗

a{width:max(10px,20px)}

csskit ✗

a{width:max(10px,20px)}

csslop ✓

a{width:20px}

cssnano ✗

a{width:calc(20px)}

csso ✗

a{width:max(10px,20px)}

esbuild ✗

a{width:max(10px,20px)}

lightningcss ✓

a{width:20px}

sass ✓

a{width:20px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0047 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✓
Details

clamp() with identical arguments resolves to single value

When min, preferred, and max are all the same known value, clamp() can be replaced with that value.

a {
  width: clamp(10px, 10px, 10px);
}

Expected

a{width:10px}

Outputs

clean-css ✗

a{width:clamp(10px,10px,10px)}

csskit ✗

a{width:clamp(10px,10px,10px)}

csslop ✓

a{width:10px}

cssnano ✗

a{width:calc(10px)}

csso ✗

a{width:clamp(10px,10px,10px)}

esbuild ✗

a{width:clamp(10px,10px,10px)}

lightningcss ✓

a{width:10px}

sass ✓

a{width:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0048 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Empty content string must not be removed

content: "" generates a pseudo-element with no text. Removing the declaration or changing the value to none/normal would prevent the pseudo-element from being generated, which is a semantic change.

a:before {
  content: "";
}

Expected

a:before{content:""}

Outputs

clean-css ✓

a:before{content:""}

csskit ✓

a:before{content:""}

csslop ✓

a:before{content:""}

cssnano ✓

a:before{content:""}

csso ✓

a:before{content:""}

esbuild ✓

a:before{content:""}

lightningcss ✓

a:before{content:""}

sass ✓

a:before{content:""}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0049 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

url() quotes required when URL contains 2 or more parentheses

Removing quotes from url("image (1).png") would produce url(image (1).png) which is a parse error -- the unquoted form cannot contain ( or ). You could escape these characters to avoid a syntax error: url(image\ \(1\).png), however, that removes two characters (quotes) and adds in 3 (slashes), in this case. If there is only one opening or one closing parenthesis, then escaping would be shorter.

a {
  background: url("image (1).png");
}

Expected

a{background:url("image (1).png")}

Outputs

clean-css ✓

a{background:url("image (1).png")}

csskit ✓

a{background:url("image (1).png")}

csslop ✓

a{background:url("image (1).png")}

cssnano ✓

a{background:url("image (1).png")}

csso ✓

a{background:url("image (1).png")}

esbuild ✓

a{background:url("image (1).png")}

lightningcss ✓

a{background:url("image (1).png")}

sass ✓

a{background:url("image (1).png")}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0050 ✗ ✗ ✓ ✓ ✓ ✓ ✓ ✗
Details

Escaping parenthesis and removing quotes in url() when shorter

If there is only one opening or one closing parenthesis, you can remove the quotes in a url() and escape the value. Doing so removes two characters (the quotes), and adds one (the slash), making it shorter.

a {
  background: url('8-).gif');
}

Expected

a{background:url(8-\).gif)}

Outputs

clean-css ✗

a{background:url('8-).gif')}

csskit ✗

a{background:url("8-).gif")}

csslop ✓

a{background:url(8-\).gif)}

cssnano ✓

a{background:url(8-\).gif)}

csso ✓

a{background:url(8-\).gif)}

esbuild ✓

a{background:url(8-\).gif)}

lightningcss ✓

a{background:url(8-\).gif)}

sass ✗

a{background:url("8-).gif")}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
7ms
0051 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Unrecognized properties must not be removed

A rule with unrecognized property names is syntactically valid CSS. Minifiers must not discard it -- unknown properties may be meaningful to other tools or future CSS specifications.

a {
  foo: bar;
}
b {
  color: red;
}

Expected

a{foo:bar}b{color:red}

Outputs

clean-css ✓

a{foo:bar}b{color:red}

csskit ✓

a{foo:bar}b{color:red}

csslop ✓

a{foo:bar}b{color:red}

cssnano ✓

a{foo:bar}b{color:red}

csso ✓

a{foo:bar}b{color:red}

esbuild ✓

a{foo:bar}b{color:red}

lightningcss ✓

a{foo:bar}b{color:red}

sass ✓

a{foo:bar}b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0052 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✓
Details

Resolve `min()`` with all-known comparable values

When all arguments to min() are known comparable values, the function can be fully resolved. min(10px, 20px) resolves to 10px.

a {
  width: min(10px, 20px);
}

Expected

a{width:10px}

Outputs

clean-css ✗

a{width:min(10px,20px)}

csskit ✗

a{width:min(10px,20px)}

csslop ✓

a{width:10px}

cssnano ✗

a{width:calc(10px)}

csso ✗

a{width:min(10px,20px)}

esbuild ✗

a{width:min(10px,20px)}

lightningcss ✓

a{width:10px}

sass ✓

a{width:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0053 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Empty custom property declaration must not be discarded

--foo: ; is valid CSS that sets a custom property to an empty value (a single space token). This is used to "reset" inherited custom properties. Discarding this declaration as "empty" changes behavior because var(--foo, red) would then use the fallback instead.

a {
  --foo: ;
  color: var(--foo, red);
}

Expected

a{--foo: ;color:var(--foo,red)}

Outputs

clean-css ✓

a{--foo: ;color:var(--foo,red)}

csskit ✗

a{--foo:;color:var(--foo,red)}

csslop ✓

a{--foo: ;color:var(--foo,red)}

cssnano ✓

a{--foo: ;color:var(--foo,red)}

csso ✗

a{--foo: ;color:var(--foo, red)}

esbuild ✗

a{--foo: ;color:var(--foo, red)}

lightningcss ✓

a{--foo: ;color:var(--foo,red)}

sass ✗

a{--foo: ;color:var(--foo, red)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0054 ✗ ✗ ✓ ✗ ✗ ✓ ✗ ✓
Details

Custom property values must not be color-minified

Custom property values may be read by JavaScript via getComputedStyle, where the serialized form matters. Converting rgb(0 0 0) to #000 inside a custom property changes JS-visible serialization.

a {
  --brand-color: rgb(0 0 0);
}

Expected

a{--brand-color: rgb(0 0 0)}

Outputs

clean-css ✗

a{--brand-color:rgb(0 0 0)}

csskit ✗

a{--brand-color:rgb(0 0 0)}

csslop ✓

a{--brand-color: rgb(0 0 0)}

cssnano ✗

a{--brand-color:#000}

csso ✗

a{--brand-color:rgb(0 0 0)}

esbuild ✓

a{--brand-color: rgb(0 0 0)}

lightningcss ✗

a{--brand-color:#000}

sass ✓

a{--brand-color: rgb(0 0 0)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0055 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove redundant auto from hyphenate-limit-chars

The default for the second and third values in hyphenate-limit-chars is auto, so if set to that, they can be removed.

a {
  hyphenate-limit-chars: auto auto auto;
}

Expected

a{hyphenate-limit-chars:auto}

Outputs

clean-css ✗

a{hyphenate-limit-chars:auto auto auto}

csskit ✗

a{hyphenate-limit-chars:auto auto auto}

csslop ✓

a{hyphenate-limit-chars:auto}

cssnano ✗

a{hyphenate-limit-chars:auto auto auto}

csso ✗

a{hyphenate-limit-chars:auto auto auto}

esbuild ✗

a{hyphenate-limit-chars:auto auto auto}

lightningcss ✗

a{hyphenate-limit-chars:auto auto auto}

sass ✗

a{hyphenate-limit-chars:auto auto auto}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0056 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove redundant auto from hyphenate-limit-chars

The default for the second and third values in hyphenate-limit-chars is auto, so if set to that, they can be removed.

a {
  hyphenate-limit-chars: auto auto;
}

Expected

a{hyphenate-limit-chars:auto}

Outputs

clean-css ✗

a{hyphenate-limit-chars:auto auto}

csskit ✗

a{hyphenate-limit-chars:auto auto}

csslop ✓

a{hyphenate-limit-chars:auto}

cssnano ✗

a{hyphenate-limit-chars:auto auto}

csso ✗

a{hyphenate-limit-chars:auto auto}

esbuild ✗

a{hyphenate-limit-chars:auto auto}

lightningcss ✗

a{hyphenate-limit-chars:auto auto}

sass ✗

a{hyphenate-limit-chars:auto auto}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0057 ✗ ✗ ✓ ✓ ✗ ✗ ✗ ✗
Details

Change initial value to auto for hyphenate-limit-chars

The initial value for hyphenate-limit-chars is auto, which is shorter than initial.

a {
  hyphenate-limit-chars: initial;
}

Expected

a{hyphenate-limit-chars:auto}

Outputs

clean-css ✗

a{hyphenate-limit-chars:initial}

csskit ✗

a{hyphenate-limit-chars:initial}

csslop ✓

a{hyphenate-limit-chars:auto}

cssnano ✓

a{hyphenate-limit-chars:auto}

csso ✗

a{hyphenate-limit-chars:initial}

esbuild ✗

a{hyphenate-limit-chars:initial}

lightningcss ✗

a{hyphenate-limit-chars:initial}

sass ✗

a{hyphenate-limit-chars:initial}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 14 / 57 18 / 57 57 / 57 41 / 57 19 / 57 19 / 57 36 / 57 16 / 57
Percent 24.56% 31.57% 100% 71.92% 33.33% 33.33% 63.15% 28.07%
Duration 2ms 7ms 7ms 26ms 3ms 35ms 1ms 19ms

vendors #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Vendor-prefixed duplicate can be dropped when targeting modern browsers

With a modern browserslist target, the webkit, moz, ms, and o vendor prefixes for transform are unnecessary because all target browsers support unprefixed transform. The prefix is dead code.

a {
  -webkit-transform: rotate(45deg);
  -moz-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  -o-transform: rotate(45deg);
  transform: rotate(45deg);
}

Expected

a{transform:rotate(45deg)}

Outputs

clean-css ✗

a{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}

csskit ✗

a{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}

csslop ✓

a{transform:rotate(45deg)}

cssnano ✓

a{transform:rotate(45deg)}

csso ✗

a{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}

esbuild ✗

a{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}

lightningcss ✓

a{transform:rotate(45deg)}

sass ✗

a{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✗ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Remove -webkit-box-flex and -ms-flex when flex is present

The vendor prefix -webkit-box-flex and -ms-flex are no longer needed, all browsers support flex if it is provided.

a {
  -webkit-box-flex: 1;
  -ms-flex: 1;
  flex: 1;
}

Expected

a{flex:1}

Outputs

clean-css ✗

a{-webkit-box-flex:1;-ms-flex:1;flex:1}

csskit ✗

a{-webkit-box-flex:1;-ms-flex:1;flex:1}

csslop ✓

a{flex:1}

cssnano ✓

a{flex:1}

csso ✗

a{-webkit-box-flex:1;-ms-flex:1;flex:1}

esbuild ✗

a{-webkit-box-flex:1;-ms-flex:1;flex:1}

lightningcss ✓

a{flex:1}

sass ✗

a{-webkit-box-flex:1;-ms-flex:1;flex:1}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗
Details

Remove vendor specific box-sizing

box-sizing has been widely available since 2015, no vendor prefixes are needed.

hr {
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
      -ms-box-sizing: border-box;
       -o-box-sizing: border-box;
          box-sizing: border-box;
}

Expected

hr{box-sizing:border-box}

Outputs

clean-css ✗

hr{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}

csskit ✗

hr{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}

csslop ✓

hr{box-sizing:border-box}

cssnano ✗

hr{-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}

csso ✗

hr{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}

esbuild ✗

hr{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}

lightningcss ✗

hr{box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box}

sass ✗

hr{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 0 / 3 0 / 3 3 / 3 2 / 3 0 / 3 0 / 3 2 / 3 0 / 3
Percent 0% 0% 100% 66.66% 0% 0% 66.66% 0%
Duration 1ms 1ms 1ms 2ms 1ms 2ms 1ms 1ms

whitespace #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Basic whitespace removal

Remove newlines, indentation, and spaces around braces and colons. Also removes trailing semi-colon.

a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css ✓

a{color:red}

csskit ✓

a{color:red}

csslop ✓

a{color:red}

cssnano ✓

a{color:red}

csso ✓

a{color:red}

esbuild ✓

a{color:red}

lightningcss ✓

a{color:red}

sass ✓

a{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Inter-rule whitespace

Blank lines between rules must be collapsed. No separator is needed between } and the next selector.

a {
  margin: 0;
}

b {
  padding: 0;
}

Expected

a{margin:0}b{padding:0}

Outputs

clean-css ✓

a{margin:0}b{padding:0}

csskit ✓

a{margin:0}b{padding:0}

csslop ✓

a{margin:0}b{padding:0}

cssnano ✓

a{margin:0}b{padding:0}

csso ✓

a{margin:0}b{padding:0}

esbuild ✓

a{margin:0}b{padding:0}

lightningcss ✓

a{margin:0}b{padding:0}

sass ✓

a{margin:0}b{padding:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Tab characters and multi-line values

Values split across lines with tab indentation must collapse correctly.

a {
  color: red;
  font-size: 16px;
}

Expected

a{color:red;font-size:16px}

Outputs

clean-css ✓

a{color:red;font-size:16px}

csskit ✓

a{color:red;font-size:16px}

csslop ✓

a{color:red;font-size:16px}

cssnano ✓

a{color:red;font-size:16px}

csso ✓

a{color:red;font-size:16px}

esbuild ✓

a{color:red;font-size:16px}

lightningcss ✓

a{color:red;font-size:16px}

sass ✓

a{color:red;font-size:16px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Selector list whitespace

Spaces around commas in selector lists (a , b) must be removed.

a,
b {
  color: red;
}

Expected

a,b{color:red}

Outputs

clean-css ✓

a,b{color:red}

csskit ✓

a,b{color:red}

csslop ✓

a,b{color:red}

cssnano ✓

a,b{color:red}

csso ✓

a,b{color:red}

esbuild ✓

a,b{color:red}

lightningcss ✓

a,b{color:red}

sass ✓

a,b{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Trailing semicolon removal

The semicolon after the last declaration in a rule can be dropped.

a {
  color: red;
  margin: 0;
  padding: 10px;
}

Expected

a{color:red;margin:0;padding:10px}

Outputs

clean-css ✓

a{color:red;margin:0;padding:10px}

csskit ✓

a{color:red;margin:0;padding:10px}

csslop ✓

a{color:red;margin:0;padding:10px}

cssnano ✓

a{color:red;margin:0;padding:10px}

csso ✓

a{color:red;margin:0;padding:10px}

esbuild ✓

a{color:red;margin:0;padding:10px}

lightningcss ✓

a{color:red;margin:0;padding:10px}

sass ✓

a{color:red;margin:0;padding:10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Consecutive semicolon removal

color: red;; should collapse redundant semicolons. Multiple consecutive semicolons are syntactically valid but wasteful; only one is needed between declarations, and the final one before } can also be dropped.

a {
  color: red;;
  margin: 0;;;
}

Expected

a{color:red;margin:0}

Outputs

clean-css ✓

a{color:red;margin:0}

csskit ✓

a{color:red;margin:0}

csslop ✓

a{color:red;margin:0}

cssnano ✓

a{color:red;margin:0}

csso ✓

a{color:red;margin:0}

esbuild ✓

a{color:red;margin:0}

lightningcss ✓

a{color:red;margin:0}

sass ✓

a{color:red;margin:0}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@media not -- space before media type required

@media not print requires a space between not and print. Without it, notprint becomes a single ident token parsed as an unknown media type, changing the query from "not print media" to "unknown media type" which never matches.

@media not print {
  a {
    color: red;
  }
}

Expected

@media not print{a{color:red}}

Validate

Outputs

clean-css ✓

@media not print{a{color:red}}

csskit ✓

@media not print{a{color:red}}

csslop ✓

@media not print{a{color:red}}

cssnano ✓

@media not print{a{color:red}}

csso ✓

@media not print{a{color:red}}

esbuild ✓

@media not print{a{color:red}}

lightningcss ✓

@media not print{a{color:red}}

sass ✓

@media not print{a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

@media -- space around and keyword required

@media screen and (color) requires spaces around and. Without a space before and, screenand becomes a single ident token (unknown media type). Without a space after and, and( becomes a function token instead of the and keyword followed by (, making the query invalid.

@media screen and (color) {
  a {
    color: red;
  }
}

Expected

@media screen and (color){a{color:red}}

Validate

Outputs

clean-css ✓

@media screen and (color){a{color:red}}

csskit ✓

@media screen and (color){a{color:red}}

csslop ✓

@media screen and (color){a{color:red}}

cssnano ✓

@media screen and (color){a{color:red}}

csso ✓

@media screen and (color){a{color:red}}

esbuild ✓

@media screen and (color){a{color:red}}

lightningcss ✓

@media screen and (color){a{color:red}}

sass ✓

@media screen and (color){a{color:red}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

@supports not -- space before ( required

@supports not (display: flex) requires a space between not and (. The CSS tokenizer treats not( as a function token, not the not keyword followed by (. This changes the grammar from a negation condition to an invalid/unknown function call, causing the condition to fail.

@supports not (display: flex) {
  a {
    display: block;
  }
}

Expected

@supports not (display:flex){a{display:block}}

Validate

Outputs

clean-css ✓

@supports not (display:flex){a{display:block}}

csskit ✓

@supports not (display:flex){a{display:block}}

csslop ✓

@supports not (display:flex){a{display:block}}

cssnano ✓

@supports not (display:flex){a{display:block}}

csso ✓

@supports not (display:flex){a{display:block}}

esbuild ✗

@supports not (display: flex){a{display:block}}

lightningcss ✓

@supports not (display:flex){a{display:block}}

sass ✗

@supports not (display: flex){a{display:block}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

@supports -- space after and combinator required

@supports (X) and (Y) can be minified to @supports(X)and (Y). Spaces after and are required to disambiguate between a Function token and an Ident, but the space before and can be dropped, as )and & ) and tokenise identically. Additionally (s are not allowed in identifiers, so @supports( tokenises identically to @supports (.

@supports (display: grid) and (gap: 10px) {
  a {
    display: grid;
    gap: 10px;
  }
}

Expected

@supports(display:grid)and (gap:10px){a{display:grid;gap:10px}}

Validate

Outputs

clean-css ✗

@supports (display:grid) and (gap:10px){a{display:grid;gap:10px}}

csskit ✓

@supports(display:grid)and (gap:10px){a{display:grid;gap:10px}}

csslop ✓

@supports(display:grid)and (gap:10px){a{display:grid;gap:10px}}

cssnano ✗

@supports(display:grid) and (gap:10px){a{display:grid;gap:10px}}

csso ✗

@supports (display:grid) and (gap:10px){a{display:grid;gap:10px}}

esbuild ✗

@supports (display: grid) and (gap: 10px){a{display:grid;gap:10px}}

lightningcss ✗

@supports (display:grid) and (gap:10px){a{gap:10px;display:grid}}

sass ✗

@supports(display: grid) and (gap: 10px){a{display:grid;gap:10px}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

calc() space around + operator required

Spaces around + inside calc() are required by the CSS spec, just like -. Without the space, +20px is tokenized as a single positive dimension token, leaving two values with no operator, which is invalid and causes the declaration to be silently dropped.

a {
  width: calc(100% + 20px);
}

Expected

a{width:calc(100% + 20px)}

Validate

Outputs

clean-css ✓

a{width:calc(100% + 20px)}

csskit ✓

a{width:calc(100% + 20px)}

csslop ✓

a{width:calc(100% + 20px)}

cssnano ✓

a{width:calc(100% + 20px)}

csso ✓

a{width:calc(100% + 20px)}

esbuild ✓

a{width:calc(100% + 20px)}

lightningcss ✓

a{width:calc(100% + 20px)}

sass ✓

a{width:calc(100% + 20px)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✗ ✓ ✗ ✗ ✓
Details

calc() spaces around * and / are removable

Unlike + and -, the * and / operators in calc() do not require surrounding spaces. They are delim tokens that cannot be confused with number signs, so calc(100%*2) and calc(10px/2) are valid.

a {
  margin: calc(var(--foo) * 2);
  padding: calc(var(--foo) / 2);
}

Expected

a{margin:calc(var(--foo)*2);padding:calc(var(--foo)/2)}

Validate

Outputs

clean-css ✗

a{margin:calc(var(--foo) * 2);padding:calc(var(--foo)/ 2)}

csskit ✓

a{margin:calc(var(--foo)*2);padding:calc(var(--foo)/2)}

csslop ✓

a{margin:calc(var(--foo)*2);padding:calc(var(--foo)/2)}

cssnano ✗

a{margin:calc(var(--foo) * 2);padding:calc(var(--foo) / 2)}

csso ✓

a{margin:calc(var(--foo)*2);padding:calc(var(--foo)/2)}

esbuild ✗

a{margin:calc(var(--foo) * 2);padding:calc(var(--foo) / 2)}

lightningcss ✗

a{margin:calc(var(--foo) * 2);padding:calc(var(--foo) / 2)}

sass ✓

a{margin:calc(var(--foo)*2);padding:calc(var(--foo)/2)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

transform -- no space needed between adjacent functions

In transform: rotate(45deg) scale(2), the space between ) and scale( can be removed. The ) token is always its own token and cannot merge with the following function token, so rotate(45deg)scale(2) parses identically.

a {
  transform: rotate(45deg) scale(2);
}

Expected

a{transform:rotate(45deg)scale(2)}

Validate

Outputs

clean-css ✗

a{transform:rotate(45deg) scale(2)}

csskit ✓

a{transform:rotate(45deg)scale(2)}

csslop ✓

a{transform:rotate(45deg)scale(2)}

cssnano ✗

a{transform:rotate(45deg) scale(2)}

csso ✗

a{transform:rotate(45deg) scale(2)}

esbuild ✗

a{transform:rotate(45deg) scale(2)}

lightningcss ✓

a{transform:rotate(45deg)scale(2)}

sass ✗

a{transform:rotate(45deg) scale(2)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0014 ✗ ✓ ✓ ✗ ✓ ✗ ✗ ✗
Details

background -- no space needed between ) and keyword

In background: url(bg.png) no-repeat, the space between ) and no-repeat can be removed. The ) token always terminates cleanly and cannot merge with the following ident token, so url(bg.png)no-repeat parses identically.

a {
  background: url(bg.png) no-repeat;
}

Expected

a{background:url(bg.png)no-repeat}

Validate

Outputs

clean-css ✗

a{background:url(bg.png) no-repeat}

csskit ✓

a{background:url(bg.png)no-repeat}

csslop ✓

a{background:url(bg.png)no-repeat}

cssnano ✗

a{background:url(bg.png) no-repeat}

csso ✓

a{background:url(bg.png)no-repeat}

esbuild ✗

a{background:url(bg.png) no-repeat}

lightningcss ✗

a{background:url(bg.png) no-repeat}

sass ✗

a{background:url(bg.png) no-repeat}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0015 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

font shorthand -- no space needed around /

In font: 16px / 1.5 sans-serif, the spaces around / separating font-size from line-height can be removed. The / is a delim token that always stands alone, so 16px/1.5 parses identically. Note the space before sans-serif must remain because 1.5sans-serif would become a single dimension token.

a {
  font: 16px / 1.5 sans-serif;
}

Expected

a{font:16px/1.5 sans-serif}

Validate

Outputs

clean-css ✓

a{font:16px/1.5 sans-serif}

csskit ✓

a{font:16px/1.5 sans-serif}

csslop ✓

a{font:16px/1.5 sans-serif}

cssnano ✓

a{font:16px/1.5 sans-serif}

csso ✓

a{font:16px/1.5 sans-serif}

esbuild ✓

a{font:16px/1.5 sans-serif}

lightningcss ✓

a{font:16px/1.5 sans-serif}

sass ✓

a{font:16px/1.5 sans-serif}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0016 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Descendant combinator -- space is the selector itself

In div p, the space IS the descendant combinator. Removing it produces divp, a single type selector for an element named divp which matches nothing. The space between simple selectors forming a descendant relationship must always be preserved.

div p {
  color: red;
}

Expected

div p{color:red}

Validate

Outputs

clean-css ✓

div p{color:red}

csskit ✓

div p{color:red}

csslop ✓

div p{color:red}

cssnano ✓

div p{color:red}

csso ✓

div p{color:red}

esbuild ✓

div p{color:red}

lightningcss ✓

div p{color:red}

sass ✓

div p{color:red}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0017 ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗
Details

Space before hash token is removable

# unambiguously starts a hash token in CSS, so no whitespace is needed to separate it from a preceding ident or keyword. solid #fff becomes solid#fff.

a {
  border: solid #fff;
}

Expected

a{border:solid#fff}

Outputs

clean-css ✗

a{border:solid #fff}

csskit ✓

a{border:solid#fff}

csslop ✓

a{border:solid#fff}

cssnano ✗

a{border:solid #fff}

csso ✗

a{border:solid #fff}

esbuild ✗

a{border:solid #fff}

lightningcss ✗

a{border:solid #fff}

sass ✗

a{border:solid #fff}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0018 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Space before !important removable

color: red !important can drop the space before !. The ! delimiter token is not an ident character so no ambiguity arises when adjacent to the value.

a {
  color: red !important;
}

Expected

a{color:red!important}

Outputs

clean-css ✓

a{color:red!important}

csskit ✓

a{color:red!important}

csslop ✓

a{color:red!important}

cssnano ✓

a{color:red!important}

csso ✓

a{color:red!important}

esbuild ✓

a{color:red!important}

lightningcss ✓

a{color:red!important}

sass ✗

a{color:red !important}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0019 ✗ ✓ ✓ ✗ ✓ ✓ ✓ ✓
Details

Space preservation around + inside clamp()

Spaces around + and - operators inside clamp() are required just like in calc(). Stripping them silently breaks the declaration.

a {
  font-size: clamp(0.5rem, 2vw + 0.5rem, 2rem);
}

Expected

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

Outputs

clean-css ✗

a{font-size:clamp(.5rem, 2vw + .5rem, 2rem)}

csskit ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

csslop ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

cssnano ✗

a{font-size:calc(clamp(.5rem,2vw + .5rem,2rem))}

csso ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

esbuild ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

lightningcss ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}

sass ✓

a{font-size:clamp(.5rem,2vw + .5rem,2rem)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0020 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Minify multi-line tabular formatted custom-property

Sometimes values are indented, or add leading or trailing zeroes to make repetition visually line up like a table of data. Reduce numeric values (-0.020em => -.02em), and remove extra whitespace, while still retaining at least one space to represent multiple consecutive spaces/tabs/returns.

:root {
  --dropShadow: 0.02em -0.02em 0.015em #45a6,
               -0.02em  0.02em 0.015em #45a6,
               -0.02em -0.02em 0.020em #defa,
                0.02em  0.02em 0.030em #106a,
                0.10em  0.10em 0.090em #246a;
}

Expected

:root{--dropShadow:.02em -.02em .015em #45a6, -.02em .02em .015em #45a6, -.02em -.02em .02em #defa, .02em .02em .03em #106a, .1em .1em .09em #246a}

Outputs

clean-css ✗

:root{--dropShadow:0.02em -0.02em 0.015em #45a6,-0.02em 0.02em 0.015em #45a6,-0.02em -0.02em 0.020em #defa,0.02em 0.02em 0.030em #106a,0.10em 0.10em 0.090em #246a}

csskit ✗

:root{--dropShadow:.02em -.02em .015em #45a6,-.02em .02em .015em #45a6,-.02em -.02em .02em #defa,.02em .02em .03em #106a,.1em .1em .09em #246a}

csslop ✓

:root{--dropShadow:.02em -.02em .015em #45a6, -.02em .02em .015em #45a6, -.02em -.02em .02em #defa, .02em .02em .03em #106a, .1em .1em .09em #246a}

cssnano ✗

:root{--dropShadow:0.02em -0.02em 0.015em #45a6,
               -0.02em  0.02em 0.015em #45a6,
               -0.02em -0.02em 0.020em #defa,
                0.02em  0.02em 0.030em #106a,
                0.10em  0.10em 0.090em #246a}

csso ✗

:root{--dropShadow:0.02em -0.02em 0.015em #45a6,
               -0.02em  0.02em 0.015em #45a6,
               -0.02em -0.02em 0.020em #defa,
                0.02em  0.02em 0.030em #106a,
                0.10em  0.10em 0.090em #246a}

esbuild ✗

:root{--dropShadow: .02em -.02em .015em #45a6, -.02em .02em .015em #45a6, -.02em -.02em .02em #defa, .02em .02em .03em #106a, .1em .1em .09em #246a}

lightningcss ✓

:root{--dropShadow:.02em -.02em .015em #45a6, -.02em .02em .015em #45a6, -.02em -.02em .02em #defa, .02em .02em .03em #106a, .1em .1em .09em #246a}

sass ✗

:root{--dropShadow: 0.02em -0.02em 0.015em #45a6, -0.02em 0.02em 0.015em #45a6, -0.02em -0.02em 0.020em #defa, 0.02em 0.02em 0.030em #106a, 0.10em 0.10em 0.090em #246a}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0021 ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove whitespace inside custom-property parenthesis

Whitespace in a custom-property represets a token that must be retained, however, each token can be collapsed to a single space. Spaces inside var() following a comma can be removed.

a {
  --foo: (
    var(--bar, 1.5) * 1em
  );
}

Expected

a{--foo:( var(--bar,1.5) * 1em )}

Outputs

clean-css ✗

a{--foo:(
    var(--bar, 1.5) * 1em
  )}

csskit ✗

a{--foo:(var(--bar,1.5) * 1em)}

csslop ✓

a{--foo:( var(--bar,1.5) * 1em )}

cssnano ✗

a{--foo:(
    var(--bar, 1.5) * 1em
  )}

csso ✗

a{--foo:(
    var(--bar, 1.5) * 1em
  )}

esbuild ✗

a{--foo: ( var(--bar, 1.5) * 1em )}

lightningcss ✓

a{--foo:( var(--bar,1.5) * 1em )}

sass ✗

a{--foo: ( var(--bar, 1.5) * 1em )}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 13 / 21 19 / 21 21 / 21 13 / 21 16 / 21 13 / 21 17 / 21 13 / 21
Percent 61.9% 90.47% 100% 61.9% 76.19% 61.9% 80.95% 61.9%
Duration 1ms 2ms 2ms 10ms 1ms 14ms 1ms 7ms

zero-units #

testclean-csscsskitcsslopcssnanocssoesbuildlightningcsssass
0001 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗
Details

Strip unit on zero length

0px equals 0 for length properties. The unit is redundant.

a {
  margin: 0px;
}

Expected

a{margin:0}

Outputs

clean-css ✓

a{margin:0}

csskit ✓

a{margin:0}

csslop ✓

a{margin:0}

cssnano ✓

a{margin:0}

csso ✓

a{margin:0}

esbuild ✓

a{margin:0}

lightningcss ✓

a{margin:0}

sass ✗

a{margin:0px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0002 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Keep unit on zero time

0s must NOT be simplified to 0. Unitless zero is invalid for <time> values. Stripping the unit breaks transitions and animations.

a {
  transition-delay: 0s;
}

Expected

a{transition-delay:0s}

Outputs

clean-css ✓

a{transition-delay:0s}

csskit ✓

a{transition-delay:0s}

csslop ✓

a{transition-delay:0s}

cssnano ✓

a{transition-delay:0s}

csso ✓

a{transition-delay:0s}

esbuild ✓

a{transition-delay:0s}

lightningcss ✓

a{transition-delay:0s}

sass ✓

a{transition-delay:0s}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0003 ✓ ✓ ✓ ✗ ✓ ✓ ✓ ✓
Details

Keep unit on zero percent

0% is not always equivalent to 0 (e.g. in width). The unit must be preserved.

a {
  width: 0%;
}

Expected

a{width:0%}

Outputs

clean-css ✓

a{width:0%}

csskit ✓

a{width:0%}

csslop ✓

a{width:0%}

cssnano ✗

a{width:0}

csso ✓

a{width:0%}

esbuild ✓

a{width:0%}

lightningcss ✓

a{width:0%}

sass ✓

a{width:0%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0004 ✗ ✓ ✓ ✓ ✓ ✗ ✓ ✗
Details

Zero-unit strip in padding

padding-block: 0px 10px can strip 0px to 0

a {
  padding-block: 0px 10px;
}

Expected

a{padding-block:0 10px}

Outputs

clean-css ✗

a{padding-block:0px 10px}

csskit ✓

a{padding-block:0 10px}

csslop ✓

a{padding-block:0 10px}

cssnano ✓

a{padding-block:0 10px}

csso ✓

a{padding-block:0 10px}

esbuild ✗

a{padding-block:0px 10px}

lightningcss ✓

a{padding-block:0 10px}

sass ✗

a{padding-block:0px 10px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0005 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Leading zero removal

0.5em can be shortened to .5em.

a {
  margin: 0.5em;
}

Expected

a{margin:.5em}

Outputs

clean-css ✓

a{margin:.5em}

csskit ✓

a{margin:.5em}

csslop ✓

a{margin:.5em}

cssnano ✓

a{margin:.5em}

csso ✓

a{margin:.5em}

esbuild ✓

a{margin:.5em}

lightningcss ✓

a{margin:.5em}

sass ✓

a{margin:.5em}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0006 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Trailing zero removal

1.0 can be shortened to 1. Trailing fractional zeros are redundant.

a {
  opacity: 1.0;
}

Expected

a{opacity:1}

Outputs

clean-css ✓

a{opacity:1}

csskit ✓

a{opacity:1}

csslop ✓

a{opacity:1}

cssnano ✓

a{opacity:1}

csso ✓

a{opacity:1}

esbuild ✓

a{opacity:1}

lightningcss ✓

a{opacity:1}

sass ✓

a{opacity:1}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0007 ✗ ✓ ✓ ✗ ✓ ✗ ✓ ✗
Details

Drop unit from zero-length flex-basis

flex-basis: 0px can be shortened to flex-basis: 0. Unitless zero is valid for <length> values, and 0px and 0 resolve identically.

a {
  flex-basis: 0px;
}

Expected

a{flex-basis:0}

Outputs

clean-css ✗

a{flex-basis:0px}

csskit ✓

a{flex-basis:0}

csslop ✓

a{flex-basis:0}

cssnano ✗

a{flex-basis:0px}

csso ✓

a{flex-basis:0}

esbuild ✗

a{flex-basis:0px}

lightningcss ✓

a{flex-basis:0}

sass ✗

a{flex-basis:0px}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0008 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

Keep unit on zero angle

0deg must NOT be simplified to 0 in the rotate property. Unlike legacy transform functions (where browsers accept unitless zero for compatibility), the individual transform properties reject unitless zero per spec. Stripping the unit produces an invalid declaration.

a {
  rotate: 0deg;
}

Expected

a{rotate:0deg}

Outputs

clean-css ✓

a{rotate:0deg}

csskit ✓

a{rotate:0deg}

csslop ✓

a{rotate:0deg}

cssnano ✓

a{rotate:0deg}

csso ✓

a{rotate:0deg}

esbuild ✓

a{rotate:0deg}

lightningcss ✓

a{rotate:0deg}

sass ✓

a{rotate:0deg}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0009 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

0% in keyframes must keep the percent sign

In keyframe selectors, 0% must not be stripped to bare 0. A 0 without % is not valid as a keyframe selector. The % unit must be preserved.

@keyframes fade {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.5;
  }
}

Expected

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

Outputs

clean-css ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

csskit ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

csslop ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

cssnano ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

csso ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

esbuild ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

lightningcss ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}

sass ✓

@keyframes fade{0%{opacity:0}50%{opacity:.5}}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0010 ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
Details

0fr must keep unit

Unitless zero is invalid for <flex> values. 0fr in grid track sizing must not be stripped to 0, which would be parsed as a <length>.

a {
  grid-template-columns: 0fr 1fr 2fr;
}

Expected

a{grid-template-columns:0fr 1fr 2fr}

Outputs

clean-css ✓

a{grid-template-columns:0fr 1fr 2fr}

csskit ✓

a{grid-template-columns:0fr 1fr 2fr}

csslop ✓

a{grid-template-columns:0fr 1fr 2fr}

cssnano ✓

a{grid-template-columns:0fr 1fr 2fr}

csso ✓

a{grid-template-columns:0fr 1fr 2fr}

esbuild ✓

a{grid-template-columns:0fr 1fr 2fr}

lightningcss ✓

a{grid-template-columns:0fr 1fr 2fr}

sass ✓

a{grid-template-columns:0fr 1fr 2fr}
1ms
3ms
1ms
1ms
1ms
1ms
1ms
1ms
0011 ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✓
Details

Preserve unit on zero-percentage flex-basis

flex-basis: 0% must NOT be shortened to flex-basis: 0. When the flex container has no definite size, 0% resolves to auto, whereas 0 resolves to zero length. Since 0% is shorter than auto, it is already optimal.

a {
  flex-basis: 0%;
}

Expected

a{flex-basis:0%}

Outputs

clean-css ✓

a{flex-basis:0%}

csskit ✓

a{flex-basis:0%}

csslop ✓

a{flex-basis:0%}

cssnano ✓

a{flex-basis:0%}

csso ✗

a{flex-basis:0}

esbuild ✓

a{flex-basis:0%}

lightningcss ✓

a{flex-basis:0%}

sass ✓

a{flex-basis:0%}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0012 ✗ ✓ ✓ ✗ ✗ ✗ ✓ ✗
Details

Remove leading zero on alpha in HSLA/RGBA in custom property

The leading zero can safely be removed in rgba and hsla colors defined inside a custom property.

.foo {
  --foo: hsla(var(--my-h),var(--my-s),var(--my-l),0.5);
  --bar: rgba(var(--my-r),var(--my-g),var(--my-b),0.5);
}

Expected

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),.5)}

Outputs

clean-css ✗

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),0.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),0.5)}

csskit ✓

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),.5)}

csslop ✓

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),.5)}

cssnano ✗

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),0.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),0.5)}

csso ✗

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),0.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),0.5)}

esbuild ✗

.foo{--foo: hsla(var(--my-h),var(--my-s),var(--my-l),.5);--bar: rgba(var(--my-r),var(--my-g),var(--my-b),.5)}

lightningcss ✓

.foo{--foo:hsla(var(--my-h),var(--my-s),var(--my-l),.5);--bar:rgba(var(--my-r),var(--my-g),var(--my-b),.5)}

sass ✗

.foo{--foo: hsla(var(--my-h),var(--my-s),var(--my-l),0.5);--bar: rgba(var(--my-r),var(--my-g),var(--my-b),0.5)}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
0013 ✓ ✗ ✓ ✓ ✗ ✗ ✓ ✗
Details

Remove 0 time from transition shorthand

A transition time length of 0s or 0ms can safely be removed from a transition shorthand.

.a {
  transition: color 0ms;
}
.b {
  transition: background 0s;
}

Expected

.a{transition:color}.b{transition:background}

Outputs

clean-css ✓

.a{transition:color}.b{transition:background}

csskit ✗

.a{transition:color 0s}.b{transition:background 0s}

csslop ✓

.a{transition:color}.b{transition:background}

cssnano ✓

.a{transition:color}.b{transition:background}

csso ✗

.a{transition:color 0ms}.b{transition:background 0s}

esbuild ✗

.a{transition:color 0ms}.b{transition:background 0s}

lightningcss ✓

.a{transition:color}.b{transition:background}

sass ✗

.a{transition:color 0ms}.b{transition:background 0s}
1ms
1ms
1ms
1ms
1ms
1ms
1ms
1ms
Subtotal 10 / 13 12 / 13 13 / 13 10 / 13 10 / 13 9 / 13 13 / 13 8 / 13
Percent 76.92% 92.3% 100% 76.92% 76.92% 69.23% 100% 61.53%
Duration 1ms 3ms 1ms 5ms 1ms 8ms 1ms 3ms

Real-world tests


        

      

The following are the results of testing the minifiers on a corpus of 202 real world, open source, CSS files/libraries. The durations track the amount of time it took the minifier to minify that real-world file. Idempotency checks if the minified output, when minified again, produces the exact same result.

Library Original Size clean-css csskit csslop cssnano csso esbuild lightningcss sass
30 Days 30 Submits #18
v0.0.0
100% 71.79% 72.01% 62.59% 69.55% 64.17% 72.01% 71.63% 73.95%
Duration 3.819ms 3.797ms 5.328ms 11.525ms 3.327ms 2.188ms 0.47ms 5.07ms
Idempotency
3D transforms
v0.0.0
100% 73.37% 73.04% 68.73% 70.63% 73.74% 72.78% 72.38% 75.43%
Duration 8.849ms 7.872ms 13.706ms 26.18ms 7.207ms 1.773ms 0.591ms 16.392ms
Idempotency
960.gs
v0.0.0
100% 57.31% 57.31% 44.9% 57.31% 57.27% 57.31% 57.31% 57.31%
Duration 3.569ms 1.913ms 8.158ms 8.375ms 3.215ms 1.114ms 0.21ms 4.67ms
Idempotency
98
v0.1.18
100% 82.43% 84.47% 74.56% 82.02% 82.16% 82.92% 81.83% ERROR
Duration 21.251ms 8.619ms 25.048ms 34.55ms 14.688ms 1.95ms 0.918ms 3.379ms
Idempotency
Academicons
v1.9.6
100% 77.41% 71.33% 70.85% 77.3% 71.04% 77.3% 71% 71.39%
Duration 4.055ms 3.693ms 8.022ms 10.966ms 3.34ms 1.381ms 0.338ms 6.677ms
Idempotency
AdminLTE
v4.0.0
100% 77.04% 82.51% 76.7% 74.29% 76.99% 78.05% 76.56% 83.91%
Duration 227.072ms 77.653ms 231.481ms 453.063ms 143.157ms 22.065ms 11.443ms 260.405ms
Idempotency
AdminHub
v1.0.0
100% 80.52% 81.1% 65.52% 79.55% 79.11% 80.73% 79.95% 80.9%
Duration 3.134ms 3.063ms 8.158ms 10.502ms 3.66ms 1.142ms 0.514ms 8.767ms
Idempotency
AMOLED-cord
v5.0.11
100% 87.21% 90.97% 75.61% 82.7% 84.04% 87% 86.19% 91.43%
Duration 6.579ms 3.045ms 10.676ms 16.44ms 4.814ms 2.042ms 0.637ms 17.849ms
Idempotency
Animate
v4.1.1
100% 78.07% 77.82% 37.28% 25.16% 76.71% 76.96% 25.08% 81.4%
Duration 44.865ms 14.186ms 17.606ms 37.322ms 23.108ms 5.501ms 1.474ms 53.071ms
Idempotency
Animating Hamburger Icons
v0.1.0
100% 61.13% 61.15% 44.39% 44.39% 59.87% 60.92% 47.21% 61.56%
Duration 5.025ms 2.381ms 6.423ms 14.165ms 2.954ms 1.082ms 0.343ms 8.097ms
Idempotency
Animo
v1.0.3
100% 74.43% 75.23% 17.89% 20.08% 72.98% 74.32% 60.87% 75.35%
Duration 17.867ms 6.067ms 9.026ms 28.922ms 22.265ms 3.743ms 1.138ms 33.807ms
Idempotency
Animsition
v4.0.2
100% 73.6% 73.14% 37.02% 26.86% 72.35% 73.06% 26.28% 74.13%
Duration 9.949ms 3.723ms 6.961ms 13.108ms 7.036ms 2.127ms 0.577ms 16.862ms
Idempotency
Awesome Bootstrap Checkbox
v0.3.7
100% 84.83% 86.52% 75.7% 73.14% 82.2% 84.03% 80.91% 85.13%
Duration 2.352ms 1.825ms 4.777ms 11.084ms 2.543ms 0.832ms 0.255ms 4.938ms
Idempotency
Balloon
v1.0.0
100% 85.74% 87.52% 83.78% 84.57% 85.22% 86.33% 84.87% 86.97%
Duration 2.465ms 1.277ms 3.168ms 7.849ms 1.542ms 1.038ms 0.235ms 3.583ms
Idempotency
Basscss
v8.1.0
100% 33.34% 33.7% 27.13% 85.06% 32.82% 33.58% 33.47% 34.73%
Duration 6.418ms 2.086ms 10.936ms 26.612ms 4.235ms 1.741ms 0.536ms 11.778ms
Idempotency
Beer CSS
v4.0.21
100% 82.32% 83.86% 79.11% 81.72% 81.89% 82.69% 82.22% 83.48%
Duration 30.548ms 14.999ms 73.651ms 95.387ms 44.174ms 6.804ms 3.698ms 70.633ms
Idempotency
Blooger_Website
v0.0.0
100% 66.41% 67.48% 55.01% 66.74% 66.21% 67.56% 66.44% 68.24%
Duration 2.531ms 2.033ms 6.57ms 8.305ms 4.005ms 1.337ms 0.398ms 8.97ms
Idempotency
Blueprint
v0.9.1
100% 85.21% 86.71% 85.14% 84.51% 89.32% 86.61% 86.51% 86.81%
Duration 6.295ms 3.375ms 11.05ms 14.739ms 7.445ms 1.772ms 0.468ms 7.268ms
Idempotency
Boilerform
v1.1.2
100% 58.48% 58.94% 49.97% 57.53% 58.37% 58.64% 59.14% 58.94%
Duration 1.357ms 1.248ms 3.741ms 5.014ms 1.176ms 0.614ms 0.219ms 6.481ms
Idempotency
Bojler
v3.2.1
100% 35.51% 40.85% 35.4% 32.1% 34.43% 35.71% 35.6% 41.07%
Duration 5.398ms 1.802ms 8.665ms 10.427ms 3.588ms 1.44ms 0.309ms 7.094ms
Idempotency
Bootplus
v1.0.5
100% 69.82% 81.25% 55.29% 57.64% 74.78% 79.62% 54.07% 83.12%
Duration 66.517ms 32.823ms 72.741ms 129.691ms 63.718ms 11.738ms 3.778ms 83.025ms
Idempotency
Bootstrap 4
v4.6.2
100% 79.86% 80.17% 73.12% 70.51% 80.02% 80.32% 71.8% 81.57%
Duration 71.551ms 30.144ms 106.18ms 164.491ms 60.064ms 33.895ms 4.873ms 106.828ms
Idempotency
Bootstrap 5
v5.3.8
100% 82.12% 82.24% 78.34% 78.8% 81.43% 82.88% 81.01% 83.9%
Duration 73.433ms 30.398ms 126.707ms 165.868ms 65.917ms 12.643ms 6.729ms 148.116ms
Idempotency
Bttn
v0.2.4
100% 74.07% 81.4% 62.64% 58.75% 72.95% 82.07% 57.96% 87.07%
Duration 16.454ms 7.471ms 17.162ms 29.252ms 11.462ms 2.37ms 1.054ms 30.89ms
Idempotency
Cardinal
v3.7.0
100% 43.71% 59.31% 43.44% 39.75% 54.78% 57.48% 43.9% 60.75%
Duration 48.951ms 14.706ms 44.526ms 76.55ms 31.555ms 7.658ms 2.624ms 97.4ms
Idempotency
Checka11y
v2.5.0
100% 71.2% 91.44% 70.9% 54.25% 63.37% 91.55% 72.45% 91.77%
Duration 24.408ms 6.886ms 21.019ms 50.382ms 12.359ms 3.559ms 1.262ms 32.192ms
Idempotency
Chota
v0.9.2
100% 80.61% 80.08% 65% 57.92% 77.84% 81.07% 72% 79.11%
Duration 11.195ms 3.775ms 10.957ms 20.802ms 5.982ms 2ms 0.994ms 15.762ms
Idempotency
Classless
v1.0.0
100% 76.67% 76.86% 72.56% 78.46% 80.84% 77.06% 75.55% 77.81%
Duration 9.259ms 3.249ms 10.921ms 20.704ms 5.404ms 1.611ms 0.738ms 12.305ms
Idempotency
CleanSlate
v0.10.1
100% 66.55% 73.97% 50.18% 44.44% 125.11% 72% 60.27% 75.59%
Duration 7.682ms 6.938ms 12.228ms 13.914ms 17.166ms 1.583ms 0.532ms 8.35ms
Idempotency
ClickEffects
v0.0.0
100% 79.49% 78.18% 44.23% 39.95% 77.41% 77.45% 41.06% 81.78%
Duration 15.1ms 6.218ms 10.149ms 14.618ms 10.769ms 2.691ms 0.731ms 14.902ms
Idempotency
CodeFrame
v4.0.9
100% 78.44% 78.34% 77.76% 75.81% 78.3% 78.87% 78.44% 79.4%
Duration 64.497ms 23.29ms 99.015ms 120.046ms 59.707ms 14.061ms 6.114ms 131.159ms
Idempotency
Colofilter
v0.0.0
100% 81.58% 87.9% 60.64% 68.32% 79.58% 83.21% 68.32% 84.83%
Duration 1.597ms 1.957ms 4.003ms 7.957ms 1.725ms 0.831ms 0.327ms 8.598ms
Idempotency
Concise
v4.0.1
100% 73.64% 74.86% 66.64% 60.71% 73.17% 74.49% 62.94% 74.7%
Duration 5.632ms 3.269ms 10.044ms 15.779ms 5.365ms 1.685ms 0.648ms 13.301ms
Idempotency
Crayon
v1.0.0
100% 82.93% 82.93% 82.93% 82.58% 82.93% 86.91% 82.58% 86.91%
Duration 2.081ms 0.406ms 3.703ms 6.421ms 1.288ms 0.765ms 0.214ms 2.322ms
Idempotency
CrookedStyleSheets
v0.0.0
100% 72.13% 74.25% 70.97% 71.99% 71.32% 71.51% 72.67% 86.85%
Duration 1.036ms 1.098ms 2.124ms 3.692ms 2.013ms 0.662ms 0.177ms 5.953ms
Idempotency
cs16.css
v0.0.0
100% 38.07% 85.67% 83.05% 75.15% 28.24% 85.85% 86.3% 89.36%
Duration 12.662ms 2.147ms 8.399ms 21.583ms 2.812ms 1.635ms 0.502ms 8.433ms
Idempotency
CSS Diner
v0.0.0
100% 75.95% 79.06% 66.52% 71.74% 75.88% 75.59% 72.05% ERROR
Duration 13.576ms 3.497ms 14.216ms 27.61ms 9.28ms 2.186ms 0.828ms 10.021ms
Idempotency
css-extras
v0.4.0
100% 25.33% 24.86% 24.08% 25.07% 24.93% 26.26% 25.91% 25.75%
Duration 1.963ms 0.689ms 3.186ms 4.023ms 2.777ms 1.307ms 0.343ms 2.919ms
Idempotency
CSS Social Buttons
v1.4.0
100% 92.79% 91.61% 84.82% 84.44% 93.74% 93.44% 87.42% 94.09%
Duration 9.646ms 3.872ms 16.549ms 35.218ms 8.712ms 2.53ms 0.696ms 15.539ms
Idempotency
CSS Spinners
v2.2.0
100% 77.75% 85.55% 38.06% 19.87% 74.49% 84.33% 19.85% 85.71%
Duration 62.976ms 12.144ms 27.807ms 48.502ms 28.105ms 7.837ms 2.287ms 59.693ms
Idempotency
CSS3 Buttons
v0.0.0
100% 78.87% 79.18% 37.6% 41.6% 77.43% 78.6% 72.66% 81.72%
Duration 12.088ms 6.08ms 20.105ms 28.823ms 10.728ms 2.176ms 0.872ms 14.485ms
Idempotency
CSSCO
v1.1.0
100% 52.12% 51.73% 37.21% 42.78% 50.73% 51.79% 42.83% 52.59%
Duration 1.282ms 0.786ms 3.042ms 3.097ms 1.257ms 0.703ms 0.173ms 2.945ms
Idempotency
cssicon
v1.0.0
100% 81.26% 81.25% 70.42% 66.43% 68.63% 81.9% 68.06% 82.46%
Duration 100.618ms 57.167ms 109.987ms 300.953ms 91.84ms 17.456ms 6.07ms 140.103ms
Idempotency
CSS Plot
v0.0.0
100% 87.23% 87.31% 41.93% 85.89% 86.88% 87.11% 85.97% 87.32%
Duration 6.68ms 4.689ms 16.677ms 20.795ms 6.5ms 2.117ms 0.654ms 33.559ms
Idempotency
CSS Zen Garden #215
v0.0.0
100% 90.36% 91.05% 86.18% 69.32% 90.13% 90.86% 87.02% 91.06%
Duration 9.286ms 2.326ms 18.94ms 49.909ms 12.075ms 3.002ms 0.67ms 12.939ms
Idempotency
Cutestrap1
v1.3.1
100% 14.2% 14.32% 13.66% 198.36% 14.05% 14.38% 13.85% 14.47%
Duration 6.271ms 2.113ms 6.957ms 16.228ms 3.315ms 1.704ms 0.476ms 6.811ms
Idempotency
Cutestrap2
v2.0.0
100% 51.7% 52.11% 50.91% 49.73% 50.58% 52.63% 50.83% 52.56%
Duration 4.399ms 1.618ms 7.875ms 9.821ms 3.444ms 1.562ms 0.52ms 10.374ms
Idempotency
Destyle
v4.0.1
100% 33.91% 34.33% 34.32% 33.32% 33.72% 35.59% 34.86% 35.78%
Duration 1.491ms 0.572ms 2.111ms 3.207ms 1.329ms 1.107ms 0.18ms 2.436ms
Idempotency
Devices
v0.2.0
100% 84.34% 84.12% 70.93% 82.14% 83.93% 83.52% 81.31% 85.54%
Duration 28.366ms 11.684ms 32.003ms 44.866ms 17.67ms 4.32ms 1.428ms 31.732ms
Idempotency
Doxygen Awesome
v2.4.2
100% 78.22% 78.94% 74.55% 75.12% 77.81% 79.41% 83.92% ERROR
Duration 22.734ms 10.259ms 33.335ms 41.815ms 22.134ms 4.026ms 2.305ms 22.447ms
Idempotency
Effeckt
v0.5.0
100% 77.93% 79.78% 51.53% 48.8% 75.04% 78.96% 50.4% 80.29%
Duration 29.001ms 20.65ms 44.277ms 70.897ms 43.285ms 6.405ms 2.537ms 70.328ms
Idempotency
ElegantFin
v25.12.31
100% 73.84% 73.37% 69.25% 70.7% 73.72% 73.73% 71.98% 74.57%
Duration 22.21ms 13.241ms 49.473ms 52.746ms 41.544ms 5.221ms 2.499ms 47.85ms
Idempotency
Enferno
v13.1.1
100% 65.98% 63.22% 56.87% 62.26% 66.88% 63.35% 62.2% 67.97%
Duration 0.496ms 0.359ms 1.176ms 1.589ms 0.757ms 1.048ms 0.127ms 1.325ms
Idempotency
Evil
v0.0.0
100% 37.18% ERROR 23.86% ERROR 32.92% 46.22% 17.93% ERROR
Duration 2.394ms 0.425ms 1.089ms 0.929ms 1.425ms 0.56ms 0.115ms 0.918ms
Idempotency
Facebook Buttons
v1.0.0
100% 56.66% 60.97% 42.48% 43.97% 60.51% 61.15% 37.59% 61.93%
Duration 4.912ms 1.25ms 3.152ms 13.806ms 2.521ms 0.733ms 0.221ms 2.917ms
Idempotency
FF-Ultima
v4.3.0
100% 47.08% 68.72% 65.04% 65.2% 32.18% 67.24% 65.96% ERROR
Duration 23.875ms 3.904ms 21.76ms 26.434ms 13.323ms 2.712ms 1.261ms 2.767ms
Idempotency
Fileicon
v0.1.1
100% 80.43% 75.55% 73.53% 78.16% 80.36% 80.07% 74.83% 79.82%
Duration 1.487ms 0.531ms 2.297ms 2.575ms 0.942ms 0.807ms 0.137ms 1.635ms
Idempotency
Finimalism
v12.0.0
100% 75.99% ERROR 73% ERROR 75.19% 77.21% 75.83% ERROR
Duration 64.347ms 6.357ms 77.942ms 1.783ms 51.657ms 9.06ms 4.261ms 1.799ms
Idempotency
Firefox-ONE
v3.7.0
100% 69.55% 74.3% 71.08% 72.35% 70.37% 75.11% 71.65% 76.21%
Duration 1.93ms 1.341ms 4.58ms 5.124ms 2.373ms 1.221ms 0.358ms 4.264ms
Idempotency
Firefox-UWP-Style
v0.0.0
100% 58.63% 80.03% 74.94% 75.08% 40.09% 77.44% 75.32% ERROR
Duration 15.73ms 3.023ms 19.876ms 22.297ms 11.332ms 2.654ms 1.234ms 1.12ms
Idempotency
Flakes
v1.0.1
100% 74.53% 75.23% 53.49% 70.22% 73.39% 75.29% 70.91% 76.67%
Duration 94.743ms 33.006ms 96.072ms 146.962ms 73.324ms 24.009ms 6.447ms 134.738ms
Idempotency
Flex Layout Attribute
v1.0.3
100% 64.09% 66.91% 53.18% 49.55% 63.8% 63.84% 49.19% 64.04%
Duration 2.207ms 1.656ms 4.649ms 7.818ms 2.507ms 1.157ms 0.308ms 7.271ms
Idempotency
FluentBird
v1.1.2
100% 71.68% 75.89% 59.4% ERROR 65.17% 73.17% 70.83% ERROR
Duration 5.573ms 2.043ms 12.764ms 0.995ms 6.249ms 2.264ms 0.609ms 2.976ms
Idempotency
Font Awesome
v7.2.0
100% 68.1% 66.07% 67.9% 66.82% 67.36% 69.77% 61.06% 77.64%
Duration 31.178ms 12.139ms 66.245ms 62.482ms 32.734ms 8.14ms 3.611ms 78.843ms
Idempotency
Foundation
v6.9.0
100% 76.48% 81.48% 71.71% 62.87% 75.83% 77.85% 68% 82.03%
Duration 48.603ms 14.771ms 71.534ms 110.054ms 55.807ms 9.726ms 3.91ms 89.929ms
Idempotency
Freebies
v0.0.0
100% 47.44% 47.7% 47.04% 47.04% 47.38% 47.65% 47.28% 48.09%
Duration 3.5ms 1.856ms 7.642ms 8.989ms 3.245ms 1.675ms 0.523ms 7.386ms
Idempotency
Furatto
v3.1.1
100% 56.35% 59.05% 49.56% 48.5% 57.32% 58.1% 52.39% 59.45%
Duration 22.52ms 13.169ms 31.492ms 45.285ms 27.355ms 5.016ms 1.883ms 37.946ms
Idempotency
Gallery
v1.0.2
100% 65.34% 78.14% 40.24% 36.78% 64.24% 65.63% 41.43% 78.43%
Duration 3.752ms 1.284ms 3.084ms 5.801ms 3.146ms 1.045ms 0.313ms 5.938ms
Idempotency
GitHub-Markdown
v5.9.0
100% 86.11% 86.38% 74.11% 76.95% 84.37% 87.08% 84.5% 87.32%
Duration 6.251ms 4.091ms 14.261ms 16.741ms 7.635ms 1.958ms 0.798ms 16.865ms
Idempotency
gitweb-theme
v0.0.0
100% 71.4% 74.23% 67.56% 68.59% 71.61% 74.08% 72.31% 75.48%
Duration 5.963ms 2.53ms 9.032ms 11.705ms 6.309ms 1.588ms 0.533ms 11.228ms
Idempotency
google-type
v0.0.0
100% 79.13% 80.22% 62.47% ERROR 78.52% 79.83% 77.81% ERROR
Duration 16.754ms 10.187ms 27.173ms 2.16ms 19.877ms 3.963ms 1.438ms 14.206ms
Idempotency
Gridism
v0.2.2
100% 50.14% 50.14% 47.32% 48.08% 50.37% 50.56% 48.36% 50.88%
Duration 0.794ms 0.452ms 1.73ms 2.246ms 0.852ms 0.653ms 0.146ms 2.729ms
Idempotency
Gridlex
v2.7.1
100% 79.5% 83.92% 76.39% 57.15% 76.95% 76.92% 76.46% 76.95%
Duration 10.783ms 4.525ms 12.518ms 22.07ms 10.583ms 2.41ms 0.874ms 17.693ms
Idempotency
Gumby
v2.6.4
100% 89.18% 90.47% 76.01% 78.91% 89.16% 91.04% 82.08% 90.78%
Duration 45.727ms 27.469ms 70.687ms 118.487ms 64.589ms 11.254ms 3.986ms 82.898ms
Idempotency
Gutenberg
v0.7.0
100% 46.5% 48.22% 45.61% 46.58% 46.5% 49.55% 49.55% 50.4%
Duration 3.618ms 1.365ms 4.539ms 6.439ms 2.642ms 1.285ms 0.372ms 4.751ms
Idempotency
HalfStyle
v2.0.2
100% 45.51% 43.54% 41.03% ERROR 38.87% ERROR 44.89% ERROR
Duration 1.27ms 0.803ms 2.015ms 0.714ms 1.074ms 3.669ms 0.137ms 1.985ms
Idempotency
Hint
v3.0.0
100% 66.35% 66.85% 60.15% 65.81% 67.67% 68.34% 65.92% 69.6%
Duration 2.996ms 1.642ms 4.823ms 6.075ms 2.446ms 1.379ms 0.336ms 6.522ms
Idempotency
Holmes
v6.8.12
100% 58.15% 59.34% 48.61% 54.93% 59.15% 58.07% 57.25% 59.46%
Duration 3.022ms 1.18ms 2.397ms 6.179ms 2.308ms 1.223ms 0.221ms 5.185ms
Idempotency
Hover Buttons
v0.15.0
100% 75.58% 79.63% 66.89% 75.61% 55.69% 77.99% 74.39% 80.85%
Duration 19.357ms 7.843ms 17.401ms 36.989ms 9.646ms 3.395ms 1.169ms 29.368ms
Idempotency
HTML Sheets of Paper
v0.0.0
100% 23.76% 20.5% 22.47% 23.16% 23.82% 23.33% 22.84% 24.89%
Duration 1.229ms 0.483ms 1.28ms 2.385ms 0.639ms 0.556ms 0.127ms 5.184ms
Idempotency
Hugo Universal Theme
v1.4.1
100% 78.52% 82.79% 68.31% 69.62% 76.15% 81.22% 74.8% 83.86%
Duration 28.733ms 13.009ms 37.053ms 60.878ms 31.366ms 4.88ms 2.205ms 40.415ms
Idempotency
Icon Hover Effects
v0.0.0
100% 79.4% 79.7% 42.95% 43.77% 75.61% 77.98% 59.37% 79.27%
Duration 4.321ms 2.216ms 7.102ms 8.642ms 4.727ms 1.89ms 0.541ms 7.932ms
Idempotency
Instagram.css
v0.1.4
100% 88.55% 85.24% 46% 49.15% 88.45% 86.35% 56.2% 91.91%
Duration 2.822ms 2.223ms 7.633ms 7.449ms 3.755ms 1.277ms 0.385ms 12.949ms
Idempotency
Justified
v0.0.0
100% 53.57% 53.57% 53.21% 51.43% 53.21% 53.57% 53.57% 53.57%
Duration 0.077ms 0.04ms 0.205ms 0.803ms 0.111ms 0.929ms 0.022ms 0.431ms
Idempotency
Kickoff
v8.0.0
100% 18.48% 18.41% 16.89% 76.61% 17.82% 18.43% 14.66% 18.66%
Duration 23.903ms 6.614ms 22.175ms 34.087ms 12.012ms 5.614ms 1.408ms 32.452ms
Idempotency
KNACSS
v8.2.0
100% ERROR 72.99% 72.16% 73.45% ERROR 74.04% 72.84% 81.5%
Duration 1.83ms 4.02ms 12.979ms 16.739ms 5.187ms 2.124ms 1.037ms 20.423ms
Idempotency
Less Framework
v0.0.0
100% 38.27% 41.49% 36.49% 37.73% 38.51% 39.1% 37.67% 39.15%
Duration 2.652ms 0.439ms 1.528ms 2.397ms 1.891ms 0.896ms 0.144ms 1.733ms
Idempotency
Linktree
v0.0.0
100% 86.45% 86.55% 81.67% 47.07% 47.1% 86.66% 86.46% 86.8%
Duration 22.784ms 9.105ms 11.801ms 16.038ms 9.592ms 3.399ms 1.044ms 19.703ms
Idempotency
littlebox
v0.0.4
100% 74.4% 77.75% 63.82% 61.57% 63.61% 76.46% 67.67% 76.27%
Duration 49.765ms 10.44ms 46.361ms 92.993ms 29.024ms 6.693ms 2.43ms 54.581ms
Idempotency
Load Awesome
v1.1.0
100% 92.73% 98.73% 44.11% 39.78% 93.22% 98.05% 41.74% 100.05%
Duration 91.38ms 14.166ms 60.186ms 95.069ms 62.665ms 12.994ms 4.06ms 100.24ms
Idempotency
Loaders
v0.1.2
100% 69.31% 73.53% 38.31% 36.37% 67.24% 73.46% 35.81% 76.47%
Duration 14.142ms 7.016ms 15.03ms 22.956ms 10.68ms 3.234ms 1.038ms 23.691ms
Idempotency
Luxbar
v0.3.0
100% 83.4% 82.86% 79.81% 80.21% 80.09% 82.63% 82.18% 84%
Duration 1.96ms 1.215ms 4.093ms 6.605ms 2.017ms 1.226ms 0.269ms 6.28ms
Idempotency
Lynx
v1.4.0
100% 72.05% 72.36% 68.73% 67.87% 71.9% 72.6% 67.63% 72.79%
Duration 6.922ms 4.734ms 15.523ms 21.554ms 6.983ms 1.966ms 0.929ms 22.743ms
Idempotency
markdown-css
v0.0.0
100% 80.95% 83.37% 72.98% 72.9% 80.69% 82.86% 83.93% 83.4%
Duration 1.062ms 0.724ms 2.249ms 3.141ms 1.845ms 1.034ms 0.166ms 2.204ms
Idempotency
Material for Bootstrap
v4.1.1
100% 81.01% 83.56% 73.57% 66.78% 81.08% 81.35% 73.82% 83.34%
Duration 96.551ms 24.221ms 130.711ms 184.739ms 91.745ms 17.773ms 7.556ms 162.389ms
Idempotency
medium.css
v1.0.2
100% 71.63% 71.87% 65.82% 64.56% 70.08% 71.81% 65.73% 72.89%
Duration 1.44ms 1.81ms 4.258ms 4.493ms 1.823ms 0.742ms 0.299ms 7.648ms
Idempotency
Meyer
v2.0.0
100% 66.35% 65.06% 64.03% 66.27% 64.12% 66.35% 66.35% 66.35%
Duration 0.442ms 0.201ms 1.22ms 1.485ms 0.947ms 0.516ms 0.064ms 0.894ms
Idempotency
Microsoft Metro Buttons
v1.2.0
100% 72.74% 84.29% 45.57% 51.55% 83.78% 85.04% 79.54% 86.92%
Duration 5.745ms 1.658ms 12.653ms 11.345ms 7.081ms 2.058ms 0.607ms 8.441ms
Idempotency
Microtip
v0.2.2
100% 67.62% 72.98% 65.68% 62.58% 69.81% 70.38% 68.21% 71.82%
Duration 1.649ms 0.951ms 2.973ms 5.192ms 1.688ms 1.096ms 0.235ms 7.896ms
Idempotency
Mini
v3.0.1
100% 83.28% 84.14% 80.76% 85.79% 82.25% 84.21% 82.65% 84.07%
Duration 16.027ms 6.685ms 26.037ms 36.952ms 16.111ms 3.251ms 1.575ms 27.278ms
Idempotency
Mini Reset
v0.0.7
100% 70.29% 60% 70.29% 70% 70.44% 70.29% 70.15% 71.03%
Duration 0.337ms 0.149ms 0.597ms 1.116ms 0.514ms 0.448ms 0.089ms 0.792ms
Idempotency
Missing
v1.3.0
100% ERROR 79.59% 72.86% 79.87% 58.69% 80.66% 79.99% 90.98%
Duration 2.998ms 11.58ms 43.768ms 47.013ms 26.468ms 5.043ms 2.586ms 60.72ms
Idempotency
Mobi
v3.1.1
100% 77.02% 77.03% 67.96% 62.7% 81.78% 78.06% 64.87% 77.77%
Duration 8.659ms 2.045ms 9.512ms 10.68ms 4.215ms 1.875ms 0.487ms 10.558ms
Idempotency
Mocka
v1.0.1
100% 70.43% 77.3% 54.46% 56.38% 69.87% 77.34% 71.69% 77.55%
Duration 1.336ms 0.526ms 1.534ms 2.238ms 0.883ms 0.769ms 0.135ms 1.69ms
Idempotency
Modern CSS Resets
v1.4.0
100% 46.29% 47.28% 46.67% 46.37% 46.75% 46.67% 46.44% 47.36%
Duration 0.358ms 0.171ms 0.59ms 1.125ms 0.388ms 0.576ms 0.063ms 0.857ms
Idempotency
MVCSS
v4.1.0
100% 50.33% 57.7% 55.9% 57.59% 48.86% 57.81% 57.35% 58.02%
Duration 7.196ms 2.551ms 11.471ms 13.366ms 5.435ms 2.176ms 0.601ms 14.816ms
Idempotency
MVP
v1.17.3
100% 77.49% 78.78% 76.7% 77.54% 75.57% 79.12% 78.41% 79.48%
Duration 3.593ms 1.709ms 5.863ms 6.986ms 4.963ms 1.282ms 0.43ms 6.215ms
Idempotency
My Internet
v1.0.0
100% 44.14% 68.66% 61.23% 66.81% 56.18% 68.17% 67.62% 101.62%
Duration 3.38ms 2.275ms 7.388ms 8.5ms 4.061ms 1.672ms 0.497ms 8.975ms
Idempotency
Natural Selection
v0.0.0
100% 0% 0% 0% 0% 0% 0% 0% 0%
Duration 0.795ms 0.414ms 0.687ms 3.126ms 0.352ms 1.134ms 0.127ms 2.719ms
Idempotency
NES.css
v2.3.0
100% 91.98% 91.91% 90.6% 90.5% 90.66% 91.85% 90% 92.08%
Duration 129.242ms 35.755ms 115.142ms 157.034ms 83.981ms 17.966ms 7.295ms 142.694ms
Idempotency
Normalize
v8.0.1
100% 28.05% 28.39% 27.35% 28.11% 27.24% 29.22% 31.19% 29.29%
Duration 1.077ms 0.54ms 1.866ms 2.822ms 1.17ms 0.794ms 0.179ms 2.704ms
Idempotency
object-fit Polyfill
v0.3.4
100% 81.9% 73.13% 77.18% 76.9% 81.94% 80.06% 80.1% 84.58%
Duration 0.446ms 0.204ms 0.993ms 1.551ms 0.407ms 0.5ms 0.058ms 1.12ms
Idempotency
Obnoxious
v3.5.2
100% 70.43% 70.05% 67.83% 69.76% 70.44% 69.72% 68.72% 72.08%
Duration 4.568ms 2.331ms 6.569ms 10.02ms 19.442ms 1.811ms 0.456ms 11.966ms
Idempotency
Obsidian Colored Sidebar
v2.0.0
100% 77.34% 69.19% 64.63% 75.23% 77.34% 71.77% 70.34% 71.9%
Duration 1.611ms 1.206ms 6.104ms 4.652ms 1.851ms 1.259ms 0.373ms 8.883ms
Idempotency
Obsidian Modular CSS Layout
v0.10.0
100% 47.52% 49.36% 44.87% 48.2% 46.14% 48.66% 48.44% 50.25%
Duration 7.194ms 3.601ms 11.347ms 16.6ms 5.464ms 1.88ms 0.755ms 15.697ms
Idempotency
Obsidian Notebook Themes
v2.2.3
100% 72.14% 70.51% 69.88% 68.29% 68.98% 72.08% 70.94% 72.21%
Duration 1.088ms 0.527ms 3.37ms 2.611ms 1.166ms 1.308ms 0.254ms 2.426ms
Idempotency
OffCanvasMenuEffects
v0.0.0
100% 76.21% 75.94% 43.21% 27.83% 68.1% 63.69% 27.93% 90.33%
Duration 2.327ms 1.965ms 3.612ms 5.129ms 4.978ms 1.582ms 0.319ms 7.4ms
Idempotency
Orbit
v1.4.11
100% 84.72% 85.82% 84.74% 82.06% 83.5% 81.62% 84.43% 86.83%
Duration 29.772ms 29.33ms 93.542ms 79.339ms 49.449ms 9.103ms 4.868ms 77.949ms
Idempotency
OrgCSS
v9.2.3
100% 49.05% 50.75% 49.22% 47.94% 48.88% 49.05% 49.05% 50.89%
Duration 8.488ms 4.005ms 14.558ms 18.627ms 7.771ms 2.61ms 0.738ms 20.64ms
Idempotency
Paper
v0.4.1
100% 67.93% 67.61% 67.42% 67.49% 67.8% 67.61% 67.61% 67.93%
Duration 0.569ms 0.379ms 1.285ms 2.836ms 0.763ms 0.563ms 0.099ms 2.193ms
Idempotency
Pesticide
v1.3.0
100% 22.87% 65.03% 22.6% 23.1% 22.84% 42.65% 23.12% 80.74%
Duration 10.794ms 4.338ms 5.511ms 5.897ms 7.449ms 1.811ms 0.363ms 12.344ms
Idempotency
Phonon
v1.5.1
100% 78.9% 79.41% 74.64% 73.07% 76.42% 79.29% 77.59% 80.59%
Duration 46.714ms 20.521ms 63.751ms 99.298ms 48.52ms 9.982ms 3.746ms 80.738ms
Idempotency
Photon
v0.1.2
100% 72.68% 69.1% 59.39% 65.13% 68.89% 72.89% 62.33% 70.46%
Duration 8.997ms 4.731ms 17.688ms 28.344ms 11.173ms 2.59ms 0.998ms 19.544ms
Idempotency
Picnic
v7.1.0
100% 65.46% 66.49% 62.67% 63.62% 67.13% 66.64% 64.28% 67.15%
Duration 22.247ms 7.835ms 22.652ms 45.281ms 17.306ms 4.33ms 1.676ms 34.346ms
Idempotency
Pico
v2.1.1
100% 89.12% 89.2% 83.53% 85.7% 90.43% 89.82% 89.45% 90.1%
Duration 18.94ms 9.801ms 35.114ms 70.067ms 22.091ms 5.744ms 2.969ms 37.963ms
Idempotency
Pills
v1.0.1
100% 76.58% 78.67% 76.6% 72.34% 78.09% 78.09% 75.48% 79.07%
Duration 1.603ms 0.79ms 2.765ms 6.043ms 1.736ms 1.22ms 0.276ms 2.935ms
Idempotency
Portfolio Template
v0.0.0
100% 62.98% 65.16% 61.87% 63.84% 63.98% 65.27% 65.35% 66.13%
Duration 3.948ms 1.467ms 4.701ms 12.637ms 2.528ms 1.413ms 0.405ms 5.023ms
Idempotency
Preboot
v2.0.0
100% 45.65% 45.57% 41.72% 43.13% 45.71% 46.52% 43.51% 47.57%
Duration 4.056ms 2.045ms 6.94ms 10.825ms 4.43ms 1.939ms 0.443ms 9.614ms
Idempotency
Pretty Checkbox
v3.0.3
100% 80.44% 83.24% 66.29% 59.33% 76.49% 83.25% 64.72% 84.64%
Duration 4.833ms 3.76ms 8.869ms 16.465ms 5.215ms 2.017ms 0.642ms 10.234ms
Idempotency
Progress Tracker
v3.0.0
100% 78.7% 78.69% 70.67% 76.58% 78.38% 78.98% 76.4% 79.42%
Duration 1.607ms 1.151ms 4.547ms 5.648ms 1.78ms 1.288ms 0.326ms 7.817ms
Idempotency
ProtonMail Themes
v4.0.8
100% 92.64% 83.79% 84.89% 91.83% 92.88% 89.45% 85.91% 89.53%
Duration 1.146ms 0.375ms 3.184ms 2.765ms 0.904ms 0.696ms 0.145ms 3.383ms
Idempotency
ProxMorph
v2.7.3
100% 54.45% 55.62% 51.49% 50.95% 53.19% 54.96% 53.6% 56.89%
Duration 24.741ms 9.259ms 33.881ms 53.603ms 20.84ms 5.43ms 1.792ms 64.586ms
Idempotency
Pure
v3.0.0
100% 58.7% 59.08% 57.97% 53.91% 59.93% 59.69% 59.04% 59.89%
Duration 6.971ms 2.788ms 10.052ms 16.739ms 5.463ms 1.963ms 0.616ms 13.573ms
Idempotency
Pygments Fruity
v1.0.0
100% 40.37% 54.62% 24.22% 40.37% 39.81% 40.37% 40.37% 54.62%
Duration 1.292ms 0.653ms 1.836ms 2.716ms 1.014ms 1.207ms 0.135ms 2.612ms
Idempotency
Ratchet
v2.0.2
100% 80.36% 78.27% 70.61% 68.81% 80% 80.22% 73.45% 80.34%
Duration 7.43ms 4.137ms 13.838ms 19.749ms 6.957ms 2.436ms 0.756ms 11.769ms
Idempotency
Remedy Quotes
v0.1.0
100% 35.59% 47.34% 26.01% 35.59% 33.96% 60.47% 34.32% 49.6%
Duration 52.638ms 1.499ms 14.012ms 13.796ms 4.366ms 1.774ms 0.829ms 10.219ms
Idempotency
Remedy
v0.1.0
100% 17.95% 18.75% 18.54% 18.54% 18.27% 18.61% 18.61% 18.66%
Duration 0.601ms 0.252ms 0.927ms 1.671ms 0.678ms 1.018ms 0.094ms 5.227ms
Idempotency
Repaintless
v1.4.0
100% 73.63% 73.38% 36.24% 27.15% 68.76% 72.79% 26.99% 73.87%
Duration 2.422ms 1.21ms 2.659ms 5.431ms 2.954ms 1.559ms 0.282ms 5.694ms
Idempotency
Reset
v1.0.2
100% 50.93% 51.01% 50.69% 50.85% 50.97% 50.93% 52.26% 51.05%
Duration 0.419ms 0.293ms 1.079ms 2.28ms 0.558ms 1.169ms 0.107ms 1.725ms
Idempotency
Responsive 4
v4.1.4
100% 72.44% 74.84% 70.7% 70.57% 72.32% 73.14% 71.16% 74.03%
Duration 23.72ms 8.253ms 35.179ms 54.071ms 26.785ms 5.233ms 2.228ms 44.696ms
Idempotency
Responsive 5
v5.0.0
100% 71.11% 75.25% 70.56% 68.96% 70.87% 71.99% 70.62% 74.17%
Duration 18.632ms 7.481ms 30.407ms 46.306ms 21.936ms 4.996ms 1.886ms 43.954ms
Idempotency
Ress
v5.0.2
100% 32.7% 34.25% 32.32% 33.2% 33% 33.58% 35.37% 33.96%
Duration 1.312ms 0.593ms 2.142ms 2.746ms 1.222ms 0.675ms 0.225ms 2.347ms
Idempotency
Sakura
v1.5.1
100% 71.91% 74.81% 70.44% 71.93% 67.66% 73.78% 79.88% 75.56%
Duration 1.382ms 0.736ms 2.458ms 3.616ms 1.553ms 1.149ms 0.167ms 2.213ms
Idempotency
Sanitize
v13.0.0
100% 29.81% 29.74% 27.41% 28.31% 29.37% 29.71% 27.97% 29.79%
Duration 0.972ms 0.616ms 1.86ms 2.818ms 0.946ms 0.68ms 0.136ms 2.08ms
Idempotency
SAPC-APCA
v0.0.0
100% 61.85% 63.08% 60.17% 60.61% 61.41% 62.64% 60.69% 64.89%
Duration 39.669ms 22.914ms 79.112ms 96.424ms 70.766ms 9.789ms 4.247ms 72.605ms
Idempotency
Select
v3.2.0
100% 64.74% 64.74% 54.87% 62.34% 64.62% 63.82% 61.84% 65.33%
Duration 0.616ms 0.487ms 1.606ms 2.362ms 0.985ms 0.627ms 0.151ms 1.464ms
Idempotency
Select2 Bootstrap
v1.2.5
100% 74.22% 74.28% 63.69% 65.08% 75.55% 74.71% 63.85% 75.75%
Duration 0.737ms 0.616ms 1.602ms 2.547ms 0.788ms 0.596ms 0.096ms 1.447ms
Idempotency
Shards
v3.0.0
100% 83.71% 83.22% 77.81% 77.9% 83.22% 82.96% 80.51% 85.77%
Duration 29.842ms 18.209ms 54.327ms 80.225ms 33.867ms 7.279ms 2.809ms 53.554ms
Idempotency
Shina Fox
v0.1.0
100% 71.51% 75.21% 67.17% 67.65% 71.54% 72.85% 70.14% 75.79%
Duration 7.819ms 3.774ms 12.961ms 20.364ms 6.166ms 2.099ms 0.821ms 16.662ms
Idempotency
Sierra
v3.5.0
100% 63.47% 64.63% 51.83% 44.07% 62.3% 64.5% 44.37% 65.28%
Duration 12.202ms 6.341ms 18.304ms 37.31ms 11.73ms 3.585ms 1.066ms 22.578ms
Idempotency
Simple Grid
v0.0.0
100% 64.64% 64.64% 63.83% 64.49% 64.06% 64.45% 64.14% 64.68%
Duration 0.701ms 0.436ms 2.453ms 2.46ms 1.003ms 0.801ms 0.131ms 1.747ms
Idempotency
Simple Line Icons
v2.5.4
100% 83.81% 78.06% 77.82% 83.71% 77.92% 83.71% 77.89% 78.1%
Duration 2.578ms 1.18ms 7.229ms 9.301ms 5.969ms 1.555ms 0.318ms 5.534ms
Idempotency
Simple
v2.3.7
100% 68.17% 69.5% 67.67% 67.84% 67.64% 69.57% 68.45% 69.86%
Duration 3.782ms 2.245ms 8.116ms 10.873ms 4.174ms 1.638ms 0.511ms 7.817ms
Idempotency
SimpTip
v1.0.4
100% 83.72% 84.78% 70.85% 64.4% 84.39% 85.03% 69.74% 85.53%
Duration 2.872ms 2.06ms 5.512ms 7.411ms 2.46ms 1.326ms 0.272ms 7.411ms
Idempotency
Skeleton
v2.0.4
100% 50.65% 50.98% 49% 50.16% 50.41% 50.99% 49.67% 51.14%
Duration 3.039ms 1.206ms 4.577ms 7.227ms 2.693ms 0.833ms 0.289ms 3.946ms
Idempotency
SlitSlider
v1.1.0
100% 72.32% 72.5% 43.88% 50.22% 72.32% 72.63% 53.08% 73.66%
Duration 0.533ms 0.49ms 1.281ms 2.047ms 0.752ms 0.817ms 0.074ms 1.16ms
Idempotency
Social Sign-in Buttons
v0.0.0
100% 71.58% 72.16% 38.54% 40.74% 71.84% 71.83% 45.58% 73.85%
Duration 2.186ms 1.371ms 5.216ms 6.274ms 1.877ms 0.972ms 0.269ms 6.661ms
Idempotency
SPCSS
v0.9.0
100% 73.74% 73.96% 71.74% 66.11% 74.12% 74.07% 72.66% 74.23%
Duration 0.779ms 0.443ms 1.563ms 3.416ms 0.878ms 1.11ms 0.102ms 1.404ms
Idempotency
Spectre
v0.5.9
100% 74.22% 74.1% 68.94% 68.06% 73.33% 74.1% 70.89% 75.11%
Duration 19.637ms 9.77ms 32.478ms 51.518ms 20.812ms 4.249ms 1.79ms 31.498ms
Idempotency
Spicetify Bloom
v0.0.0
100% 79.9% 81.51% 72.23% 77.58% 78.9% 81.2% 75.24% 82.81%
Duration 15.409ms 9.305ms 32.946ms 49.967ms 17.732ms 3.75ms 1.687ms 33.999ms
Idempotency
Spicetify Dribbblish
v0.0.0
100% 80.07% 81.07% 75.72% 75.01% 78.84% 80.78% 78.84% 82.01%
Duration 4.469ms 3.033ms 10.989ms 16.59ms 4.519ms 1.85ms 0.643ms 9.657ms
Idempotency
Spicetify Text
v0.0.0
100% 78.99% 79.2% 75.84% 77.23% 79.04% 80.36% 78.5% 79.84%
Duration 5.703ms 3.068ms 14.264ms 16.226ms 7.545ms 1.806ms 0.775ms 16.668ms
Idempotency
Spinkit
v2.0.1
100% 63.94% 63.92% 53.13% 63.78% 62.1% 63.74% 63.53% 64.53%
Duration 2.852ms 1.678ms 5.524ms 7.254ms 3.484ms 1.404ms 0.357ms 9.261ms
Idempotency
StackOverflow-Dark
v5.1.2
100% 72.38% 77.92% 75.64% 69.9% ERROR 80.3% 0% ERROR
Duration 49.051ms 21.755ms 56.018ms 116.601ms 16.325ms 10.931ms 0.808ms 2.47ms
Idempotency
Swagger UI Themes 2
v2.1.0
100% 74.04% 90.5% 53.46% 75.98% 73.32% 89.31% 78.2% 90.71%
Duration 14.09ms 10.056ms 24.026ms 33.209ms 20.123ms 3.662ms 1.291ms 25.106ms
Idempotency
Swagger UI Themes 3
v3.0.1
100% 71.21% 82.32% 54.94% 66.24% 71.03% 80.72% 59.09% 83.49%
Duration 14.013ms 6.62ms 22.009ms 26.263ms 13.746ms 2.797ms 1.074ms 16.384ms
Idempotency
System
v0.1.11
100% 82.02% 82.81% 71.98% 79.25% 77.84% 81.95% 81.25% 82.82%
Duration 4.118ms 2.825ms 10.778ms 11.559ms 4.469ms 1.98ms 0.629ms 8.024ms
Idempotency
Tablecloth
v1.0.0
100% 84.52% 85.94% 68.91% ERROR 83.86% 87.04% 80.58% ERROR
Duration 3.198ms 2.172ms 6.298ms 0.624ms 3.186ms 0.918ms 0.351ms 1.236ms
Idempotency
Tachyons
v4.12.0
100% 64.32% 65.44% 61.9% 61.49% 66.29% 65.36% 62.03% 67.02%
Duration 36.041ms 14.267ms 57.548ms 71.343ms 39.941ms 8.725ms 2.934ms 60.108ms
Idempotency
Tent
v1.4.3
100% 76.48% 76.48% 67.46% 66.64% 76.64% 76.76% 67.05% 77.24%
Duration 11.534ms 3.27ms 11.4ms 15.991ms 5.886ms 1.8ms 0.653ms 10.71ms
Idempotency
The 50 Front-end Project #44
v0.0.0
100% 71.87% 72.98% 68.16% 71.64% 68.13% 73.21% 71.66% 73.39%
Duration 2.426ms 1.495ms 5.237ms 7.084ms 4.238ms 1.026ms 0.37ms 4.997ms
Idempotency
The New CSS Reset
v1.11.3
100% 31.71% 31.94% 28.96% 28.85% 31.71% 31.74% 28.85% 31.81%
Duration 0.52ms 0.301ms 0.854ms 1.795ms 0.558ms 0.736ms 0.083ms 4.836ms
Idempotency
Toast
v1.1.0
100% 71.11% 70.88% 69.79% 70.5% 70.77% 71.06% 70.29% 70.92%
Duration 1.668ms 0.577ms 3.96ms 4.207ms 1.66ms 1.204ms 0.141ms 2.727ms
Idempotency
Tocas
v5.7.0
100% 72.3% 71.14% 64.05% 70.69% 70.16% 72.23% 69.57% 71.48%
Duration 82.932ms 49.285ms 186.221ms 259.308ms 108.659ms 61.653ms 10.252ms 180.456ms
Idempotency
Tooltip Twispy
v1.0.6
100% 80.84% 85.01% 55.91% 80.18% 80.54% 85.07% 82.82% 85.31%
Duration 1.645ms 0.851ms 3.386ms 6.294ms 1.284ms 0.743ms 0.2ms 2.455ms
Idempotency
Tootik
v1.0.2
100% 20.4% 27.42% 17.18% 208.22% 22.04% 28.14% 15.37% 29.31%
Duration 6.066ms 2.101ms 6.778ms 12.446ms 2.721ms 1.382ms 0.349ms 8.229ms
Idempotency
Tuesday
v1.3.0
100% 75.02% 74.54% 36.88% 28.3% 73.96% 73.95% 27.63% 76.84%
Duration 4.688ms 2.082ms 3.881ms 8.005ms 4.889ms 2.008ms 0.382ms 10.095ms
Idempotency
Tufte
v1.8.0
100% 74.05% 74.42% 69.01% 70.1% 72.09% 73.85% 73.8% 74.68%
Duration 4.065ms 1.432ms 5.805ms 7.876ms 3.038ms 1.457ms 0.418ms 4.845ms
Idempotency
Tui
v2.1.2
100% 73.74% 73.2% 65.48% 68.44% 66.55% 74.09% 70.22% 75.07%
Duration 11.632ms 7.582ms 24.887ms 34.359ms 13.956ms 3.498ms 1.137ms 25.524ms
Idempotency
tw-animate-css
v1.4.0
100% 53.29% 83.66% 72.22% 84.34% 48.73% 88.94% 87.13% ERROR
Duration 1.316ms 1.133ms 4.219ms 6.872ms 2.491ms 1.644ms 0.447ms 2.16ms
Idempotency
UIkit
v3.25.17
100% 69.93% 71.97% 69.35% 59.59% 69.8% 70.14% 70.14% 71.05%
Duration 88.061ms 34.334ms 148.031ms 214.617ms 98.156ms 17.407ms 7.295ms 151.726ms
Idempotency
Unfold
v0.0.0
100% 76.24% 78.85% 28.24% 24.5% 73.71% 77.48% 30.73% 79.3%
Duration 19.94ms 10.083ms 31.812ms 39.254ms 29.747ms 5.225ms 1.865ms 44.209ms
Idempotency
Utility OpenType
v0.1.4
100% 83.18% 83.17% 79.5% 55.11% 82.61% 83.66% 84.11% 83.94%
Duration 1.912ms 3.538ms 4.293ms 5.182ms 2.643ms 0.961ms 0.296ms 5.982ms
Idempotency
Vim CSS3 Syntax
v2.10.0
100% 81.82% 83.57% 53.91% ERROR 64.61% 83.69% 83.46% ERROR
Duration 7.541ms 2.315ms 17.729ms 1.143ms 13.284ms 2.998ms 1.134ms 2.347ms
Idempotency
Virtual Bookshelf
v1.1.0
100% 86.76% 71.13% 69.08% 69.61% 85.73% 75.93% 62.69% 77.9%
Duration 1.636ms 0.811ms 3.438ms 2.52ms 1.086ms 1.255ms 0.21ms 1.447ms
Idempotency
VitePress
v1.6.4
100% 74.36% 75.76% 66.11% 70.48% 73.56% 75.46% 73.28% 75.29%
Duration 5.489ms 3.113ms 7.059ms 9.659ms 3.278ms 1.609ms 0.42ms 5.926ms
Idempotency
Vivify
v1.0.0
100% 80.62% 79.87% 67.41% 39.15% 76.11% 79.28% 38.06% 84.43%
Duration 27.151ms 6.868ms 16.093ms 18.561ms 13.31ms 4.574ms 0.971ms 28.654ms
Idempotency
Voxels
v1.0.0
100% 72.19% 74.31% 71.03% 72.05% 71.38% 71.57% 72.73% 86.92%
Duration 1.1ms 0.831ms 2.093ms 3.972ms 2.133ms 0.856ms 0.19ms 4.427ms
Idempotency
w3css
v4.10.0
100% 97.04% 97.19% 94.79% 94.58% 96.32% 97.01% 95.93% 98.44%
Duration 8.366ms 3.495ms 17.734ms 18.655ms 8.007ms 2.487ms 0.757ms 13.593ms
Idempotency
Waffle Grid
v1.3.6
100% 79.24% 75.56% 72.68% 72.01% 77.21% 75.92% 71.21% 75.97%
Duration 2.103ms 1.269ms 6.642ms 5.334ms 2.429ms 1.578ms 0.29ms 5.588ms
Idempotency
Water
v2.1.1
100% 52.8% 83.14% 53.67% 68.68% 65.37% 80.37% 75.32% 84.44%
Duration 13.179ms 4.334ms 14.708ms 20.22ms 7.038ms 2.417ms 0.929ms 19.789ms
Idempotency
Weather Icons
v2.0.10
100% 84.57% 84.96% 33.48% 33.75% 83.27% 86.11% 32.94% 85.47%
Duration 32.299ms 41.112ms 72.075ms 68.841ms 38.944ms 10.057ms 2.81ms 80.072ms
Idempotency
Webgradients
v0.0.0
100% 71.56% 72.71% 63.37% 63.87% 71.66% 64.56% 67.52% 75.59%
Duration 4.365ms 2.518ms 16.135ms 12.403ms 8.592ms 2.134ms 0.496ms 14.07ms
Idempotency
Wenk
v1.0.8
100% 22.83% 57.07% 55.61% 55.54% 6.85% 62.26% 55.35% 70.76%
Duration 0.459ms 0.547ms 1.936ms 2.19ms 0.38ms 0.755ms 0.131ms 6.812ms
Idempotency
Windows 95
v0.0.0
100% 74.27% 74.73% 63.45% 70.84% 71.09% 74.99% 73.05% 75.6%
Duration 2.147ms 1.577ms 7.071ms 6.505ms 4.283ms 1.272ms 0.243ms 4.374ms
Idempotency
Windows 98
v0.0.1
100% 73.48% 81.85% 55.74% 68.98% 70% 81.05% 69.52% 82.05%
Duration 1.403ms 1.066ms 3.582ms 5.301ms 1.386ms 0.642ms 0.174ms 2.625ms
Idempotency
Wing
v1.0.0
100% 69.09% 69.04% 66.35% 68.18% 68.55% 69.24% 67.96% 69.56%
Duration 2.354ms 1.257ms 5.533ms 5.994ms 2.511ms 0.933ms 0.256ms 5.167ms
Idempotency
Woah
v1.3.1
100% 59.57% 63.49% 54.86% 62.6% 56.64% 64.01% 59.53% 67.95%
Duration 3.822ms 2.472ms 6.624ms 7.464ms 4.924ms 1.53ms 0.401ms 13.729ms
Idempotency
WTF Forms
v2.2.0
100% 74.01% 74.04% 68.32% 51.49% 72.87% 74.24% 71.09% 74.39%
Duration 1.91ms 0.872ms 4.806ms 7.005ms 2.068ms 1.214ms 0.259ms 4.152ms
Idempotency
YaCy
v4.1.2
100% 66.14% 77.66% 53.25% 53.48% 76.69% 79.43% 61.59% 79.95%
Duration 5.954ms 3.335ms 14.602ms 12.464ms 6.741ms 1.854ms 0.554ms 11.217ms
Idempotency
Yue
v1.1.1
100% 79.3% 79.11% 72.26% 76.63% 77.07% 79.15% 79% 79.3%
Duration 0.819ms 0.896ms 3.087ms 3.073ms 1.395ms 1.01ms 0.146ms 2.355ms
Idempotency
Zotero Dark Theme
v0.0.0
100% 67.53% 71.98% 62.5% 63.92% 59.68% 70.32% 65.94% 74.73%
Duration 3.459ms 2.027ms 6.329ms 9.214ms 4.22ms 1.276ms 0.297ms 7.104ms
Idempotency
clean-css csskit csslop cssnano csso esbuild lightningcss sass
Total Errors 2 2 0 7 2 1 0 14
Totals (186) 9,468,132
(9.03MB)
6,863,730
(6.55MB)
7,100,888
(6.77MB)
5,900,471
(5.63MB)
6,159,158
(5.87MB)
6,752,364
(6.44MB)
7,038,885
(6.71MB)
6,073,686
(5.79MB)
7,205,337
(6.87MB)
Percent of Original Size 100% 72.49% 75% 62.32% 65.05% 71.32% 74.34% 64.15% 76.1%
Total Durations 2.866s 1.210s 3.888s 5.902s 2.613s 680ms 220ms 4.695s
Total Idempotency Failures 176 178 116 145 105 188 136 190