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

Summary

testclean-csscsskitcssnanocssoesbuildlightningcsssass
v5.3.3v0.0.22v8.0.1v5.0.5v0.28.0v1.32.0v1.99.0
anchor 2 / 5 0 / 5 2 / 5 2 / 5 1 / 5 0 / 5 1 / 5
charset 1 / 2 0 / 2 1 / 2 1 / 2 0 / 2 1 / 2 1 / 2
colors 21 / 59 51 / 59 29 / 59 23 / 59 28 / 59 35 / 59 19 / 59
comments 4 / 5 2 / 5 3 / 5 3 / 5 3 / 5 2 / 5 3 / 5
container 2 / 6 2 / 6 1 / 6 1 / 6 1 / 6 4 / 6 0 / 6
counter-style 1 / 3 2 / 3 1 / 3 3 / 3 0 / 3 1 / 3 0 / 3
duplicates 15 / 19 5 / 19 10 / 19 15 / 19 10 / 19 13 / 19 5 / 19
empty-rules 4 / 4 0 / 4 4 / 4 4 / 4 4 / 4 4 / 4 4 / 4
escaping 3 / 12 12 / 12 4 / 12 4 / 12 11 / 12 11 / 12 10 / 12
font-face 3 / 6 2 / 6 4 / 6 3 / 6 4 / 6 5 / 6 2 / 6
gradients 0 / 5 0 / 5 0 / 5 0 / 5 1 / 5 4 / 5 0 / 5
import 0 / 5 4 / 5 0 / 5 0 / 5 4 / 5 0 / 5 0 / 5
keyframes 0 / 2 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2
layer 1 / 2 0 / 2 0 / 2 0 / 2 0 / 2 2 / 2 0 / 2
media 1 / 9 1 / 9 1 / 9 1 / 9 1 / 9 8 / 9 1 / 9
merging 7 / 10 3 / 10 9 / 10 6 / 10 5 / 10 9 / 10 4 / 10
nesting 0 / 19 8 / 19 8 / 19 0 / 19 10 / 19 4 / 19 5 / 19
page 3 / 4 3 / 4 3 / 4 3 / 4 3 / 4 3 / 4 2 / 4
property 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2 2 / 2 2 / 2
scope 1 / 3 0 / 3 1 / 3 1 / 3 2 / 3 1 / 3 0 / 3
selectors 10 / 15 3 / 15 11 / 15 6 / 15 9 / 15 14 / 15 4 / 15
selectors-advanced 2 / 13 3 / 13 3 / 13 3 / 13 3 / 13 3 / 13 3 / 13
shorthands 29 / 74 8 / 74 22 / 74 21 / 74 19 / 74 54 / 74 3 / 74
starting-style 0 / 2 2 / 2 1 / 2 2 / 2 2 / 2 1 / 2 2 / 2
supports 3 / 5 1 / 5 3 / 5 3 / 5 1 / 5 3 / 5 1 / 5
transforms 0 / 11 0 / 11 7 / 11 0 / 11 8 / 11 11 / 11 0 / 11
values 16 / 70 18 / 70 57 / 70 19 / 70 34 / 70 55 / 70 31 / 70
whitespace 13 / 19 15 / 19 14 / 19 16 / 19 13 / 19 15 / 19 12 / 19
zero-units 9 / 11 11 / 11 9 / 11 10 / 11 9 / 11 11 / 11 8 / 11
Total 153 / 402 158 / 402 212 / 402 154 / 402 188 / 402 278 / 402 123 / 402
Total (%) 38.05% 39.3% 52.73% 38.3% 46.76% 69.15% 30.59%

Historical Trends

Pass rate over time across 24 recorded runs.

anchor

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

a {
  position-area: center center;
}

Expected

a{position-area:center}

Outputs

clean-css

a{position-area:center center}

csskit

a{position-area:center 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}
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.

Source

a {
  position-area: top center;
}

Expected

a{position-area:top}

Outputs

clean-css

a{position-area:top center}

csskit

a{position-area:center 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}
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.

Source

@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}

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}
0004
Details

Empty @position-try removal

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

Source

@position-try --empty {
}

Expected

Outputs

clean-css

csskit

@position-try --empty{}

cssnano

csso

esbuild

@position-try --empty{}

lightningcss

@position-try --empty{ }

sass

@position-try --empty{}
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.

Source

@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}

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}
Subtotal 2 / 5 0 / 5 2 / 5 2 / 5 1 / 5 0 / 5 1 / 5

charset

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@charset removal

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

Source

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

Expected

a{color:red}

Outputs

clean-css

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

csskit

@charset "UTF-8";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}
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.

Source

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

Expected

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

Outputs

clean-css

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

csskit

@charset "ISO-8859-1";@charset "ISO-8859-15";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}
Subtotal 1 / 2 0 / 2 1 / 2 1 / 2 0 / 2 1 / 2 1 / 2

colors

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Hex shortening

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

Source

a {
  color: #ffffff;
}

Expected

a{color:#fff}

Outputs

clean-css

a{color:#fff}

csskit

a{color:#fff}

cssnano

a{color:#fff}

csso

a{color:#fff}

esbuild

a{color:#fff}

lightningcss

a{color:#fff}

sass

a{color:#fff}
0002
Details

Hex to named color

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

Source

a {
  color: #ff0000;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0003
Details

rgb() to named color

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

Source

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

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
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.

Source

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

Expected

a{color:#000}

Outputs

clean-css

a{color:#000}

csskit

a{color:#000}

cssnano

a{color:#000}

csso

a{color:#000}

esbuild

a{color:#000}

lightningcss

a{color:#000}

sass

a{color:#000}
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.

Source

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

Expected

a{color:tan}

Outputs

clean-css

a{color:tan}

csskit

a{color:tan}

cssnano

a{color:tan}

csso

a{color:tan}

esbuild

a{color:tan}

lightningcss

a{color:tan}

sass

a{color:tan}
0006
Details

Hex shortening (non-white)

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

Source

a {
  color: #aabbcc;
}

Expected

a{color:#abc}

Outputs

clean-css

a{color:#abc}

csskit

a{color:#abc}

cssnano

a{color:#abc}

csso

a{color:#abc}

esbuild

a{color:#abc}

lightningcss

a{color:#abc}

sass

a{color:#abc}
0007
Details

RGBA to hex alpha notation

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

Source

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}

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)}
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.

Source

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

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
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.

Source

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

Expected

a{color:#0000}

Outputs

clean-css

a{color:transparent}

csskit

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)}
0010
Details

HWB to shorter representation

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

Source

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

Expected

a{color:red}

Outputs

clean-css

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

csskit

a{color:red}

cssnano

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

csso

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

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0011
Details

Named color to shorter hex

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

Source

a {
  color: rebeccapurple;
}

Expected

a{color:#639}

Outputs

clean-css

a{color:#663399}

csskit

a{color:#639}

cssnano

a{color:#639}

csso

a{color:#639}

esbuild

a{color:#639}

lightningcss

a{color:#639}

sass

a{color:#639}
0012
Details

RGB to named color (olive)

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

Source

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

Expected

a{color:olive}

Outputs

clean-css

a{color:olive}

csskit

a{color:olive}

cssnano

a{color:olive}

csso

a{color:olive}

esbuild

a{color:olive}

lightningcss

a{color:olive}

sass

a{color:olive}
0013
Details

HSL to named color (green)

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

Source

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

Expected

a{color:green}

Outputs

clean-css

a{color:#007f00}

csskit

a{color:green}

cssnano

a{color:green}

csso

a{color:green}

esbuild

a{color:green}

lightningcss

a{color:green}

sass

a{color:rgb(0,127.5,0)}
0014
Details

Uppercase hex to lowercase shorthand

#AABB11 should be lowercased and collapsed to #ab1.

Source

a {
  color: #AABB11;
}

Expected

a{color:#ab1}

Outputs

clean-css

a{color:#ab1}

csskit

a{color:#ab1}

cssnano

a{color:#ab1}

csso

a{color:#ab1}

esbuild

a{color:#ab1}

lightningcss

a{color:#ab1}

sass

a{color:#ab1}
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.

Source

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}

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)}
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.

Source

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}

cssnano

a{color:red}

csso

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

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
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.

Source

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}

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)}
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.

Source

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}

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%)}
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) = purple.

Source

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}

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)}
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.

Source

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:color(from red srgb r g b / .5)}

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)}
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.

Source

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

Expected

a{color:green}

Validate

Outputs

clean-css

a{color:rgb(none 128 0)}

csskit

a{color:rgb(none 128 0)}

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)}
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.

Source

a {
  color: #ff00007a;
}

Expected

a{color:#ff00007a}

Outputs

clean-css

a{color:#ff00007a}

csskit

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)}
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.

Source

a {
  color: purple;
}

Expected

a{color:purple}

Outputs

clean-css

a{color:purple}

csskit

a{color:purple}

cssnano

a{color:purple}

csso

a{color:purple}

esbuild

a{color:purple}

lightningcss

a{color:purple}

sass

a{color:purple}
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.

Source

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:color-mix(in srgb,rgb(none 0 0),#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%)}
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.

Source

a {
  color: currentColor;
}

Expected

a{color:currentColor}

Outputs

clean-css

a{color:currentColor}

csskit

a{color:currentColor}

cssnano

a{color:currentColor}

csso

a{color:currentColor}

esbuild

a{color:currentColor}

lightningcss

a{color:currentColor}

sass

a{color:currentColor}
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.

Source

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)}

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)}
0027
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.

Source

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)}

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)}
0028
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.

Source

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}

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)}
0029
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.

Source

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)}

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%)}
0030
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.

Source

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)}

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)}
0031
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.

Source

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))}

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))}
0032
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.

Source

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))}

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))}
0033
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.

Source

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))}

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))}
0034
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.

Source

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}

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}
0035 ERR
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.

Source

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)}

cssnano

a{color:color(display-p3 1.2-0.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
0036
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.

Source

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}

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)}
0037
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.

Source

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}

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)}
0038
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).

Source

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)}

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%)}
0039
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).

Source

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)}

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)}
0040
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.

Source

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)}

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)}
0041
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.

Source

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}

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%)}
0042
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.

Source

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}

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%)}
0043
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.

Source

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}

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%)}
0044
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.

Source

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)}

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)}
0045
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).

Source

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}

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(127.6,64.4,191.5)}
0046
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.

Source

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)}

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)}
0047
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.

Source

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)}

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)}
0048
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.

Source

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)}

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)}
0049
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.

Source

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)}

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)}
0050
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.

Source

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)}

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)}
0051
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.

Source

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)}

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)}
0052
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.

Source

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)}

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)}
0053
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.

Source

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)}

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)}
0054
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.

Source

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)}

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)}
0055
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.

Source

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}

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)}
0056
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.

Source

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}

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)}
0057
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.

Source

a {
  content: "green";
}

Expected

a{content:"green"}

Outputs

clean-css

a{content:"green"}

csskit

a{content:"green"}

cssnano

a{content:"green"}

csso

a{content:"green"}

esbuild

a{content:"green"}

lightningcss

a{content:"green"}

sass

a{content:"green"}
0058
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).

Source

a {
  content: "#FF0000";
}

Expected

a{content:"#FF0000"}

Outputs

clean-css

a{content:"#FF0000"}

csskit

a{content:"#FF0000"}

cssnano

a{content:"#FF0000"}

csso

a{content:"#FF0000"}

esbuild

a{content:"#FF0000"}

lightningcss

a{content:"#FF0000"}

sass

a{content:"#FF0000"}
0059
Details

URL strings containing color names

When a URL path string contains a color name, it should not be minified to a hex value.

Source

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)}

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)}
Subtotal 21 / 59 51 / 59 29 / 59 23 / 59 28 / 59 35 / 59 19 / 59

comments

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Basic comment removal

Standard /* ... */ comments should be stripped entirely.

Source

/* comment */
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0002
Details

Inline comment removal

A comment embedded within a declaration value (color: /* inline */ red) must be removed without breaking the surrounding value.

Source

a {
  color: /* inline */ red;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0003
Details

Important comment preservation

Comments starting with /*! are license/legal markers and must be preserved. Stripping them can cause legal compliance issues.

Source

/*! important */
a {
  color: red;
}

Expected

/*! important */a{color:red}

Outputs

clean-css

/*! important */a{color:red}

csskit

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}
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.

Source

a {
  --bar: a/**/b;
}

Expected

a{--bar:a/**/b}

Outputs

clean-css

a{--bar:a/**/b}

csskit

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}
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.

Source

@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}}

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}}
Subtotal 4 / 5 2 / 5 3 / 5 3 / 5 3 / 5 2 / 5 3 / 5

container

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@container whitespace removal

Remove whitespace inside @container query while preserving the container name and condition. The space between name and ( is required.

Source

@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}}

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}}
0002
Details

Empty @container removal

An @container rule with no child rules has no effect and can be removed.

Source

@container (min-width: 700px) {
}

Expected

Outputs

clean-css

csskit

@container (min-width:700px){}

cssnano

csso

esbuild

lightningcss

sass

@container (min-width: 700px){}
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.

Source

@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}}

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}}
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 (.

Source

@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}}

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}}
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 (.

Source

@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}}

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}}
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&gt;=700px).

Source

@container sidebar (min-width: 700px) {
  a {
    color: red;
  }
}

Expected

@container sidebar (width&gt;=700px){a{color:red}}

Outputs

clean-css

@container sidebar (min-width:700px){a{color:red}}

csskit

@container sidebar (min-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}}
Subtotal 2 / 6 2 / 6 1 / 6 1 / 6 1 / 6 4 / 6 0 / 6

counter-style

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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 👍.

Source

@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:" "}

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:" "}
0002
Details

Empty @counter-style removal

An empty @counter-style with no descriptors can be removed entirely.

Source

@counter-style empty {
}

Expected

Outputs

clean-css

csskit

@counter-style empty{}

cssnano

csso

esbuild

@counter-style empty{}

lightningcss

@counter-style empty{}

sass

@counter-style empty{}
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.

Source

@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:"("")"}

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:"(" ")"}
Subtotal 1 / 3 2 / 3 1 / 3 3 / 3 0 / 3 1 / 3 0 / 3

duplicates

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

a {
  color: red;
  color: blue;
}

Expected

a{color:#00f}

Outputs

clean-css

a{color:#00f}

csskit

a{color:red;color:blue}

cssnano

a{color:red;color:blue}

csso

a{color:#00f}

esbuild

a{color:red;color:#00f}

lightningcss

a{color:#00f}

sass

a{color:red;color:blue}
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.

Source

a {
  color: red;
}
a {
  color: blue;
}

Expected

a{color:#00f}

Outputs

clean-css

a{color:#00f}

csskit

a{color:red}a{color:blue}

cssnano

a{color:red;color:blue}

csso

a{color:#00f}

esbuild

a{color:red}a{color:#00f}

lightningcss

a{color:#00f}

sass

a{color:red}a{color:blue}
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.

Source

a {
  color: red;
  font-size: 16px;
}
b {
  margin: 0;
}
a {
  color: blue;
  font-size: 16px;
}

Expected

b{margin:0}a{color:#00f;font-size:16px}

Outputs

clean-css

b{margin:0}a{color:#00f;font-size:16px}

csskit

a{color:red;font-size:16px}b{margin:0}a{color:blue;font-size:16px}

cssnano

a{color:red}b{margin:0}a{color:blue;font-size:16px}

csso

a{color:#00f;font-size:16px}b{margin:0}

esbuild

a{color:red;font-size:16px}b{margin:0}a{color:#00f;font-size:16px}

lightningcss

b{margin:0}a{color:#00f;font-size:16px}

sass

a{color:red;font-size:16px}b{margin:0}a{color:blue;font-size:16px}
0004
Details

Duplicate selector removal within a rule

Duplicate selectors in a selector list should be deduplicated.

Source

h1, h2, h3, h4, h5, h5, h6 {
  color: red;
}

Expected

h1,h2,h3,h4,h5,h6{color:red}

Outputs

clean-css

h1,h2,h3,h4,h5,h6{color:red}

csskit

h1,h2,h3,h4,h5,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,h5,h6{color:red}

sass

h1,h2,h3,h4,h5,h5,h6{color:red}
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().

Source

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))}

cssnano

a{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))}
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.

Source

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}

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}
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.

Source

a {
  color: red;
  font-size: 16px;
}
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}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}
0008
Details

Vendor-prefixed duplicate can be dropped when targeting modern browsers

With a modern browserslist target, -webkit-transform is unnecessary because all target browsers support unprefixed transform. The prefix is dead code.

Source

a {
  -webkit-transform: rotate(45deg);
  transform: rotate(45deg);
}

Expected

a{transform:rotate(45deg)}

Outputs

clean-css

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}

csskit

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}

cssnano

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}

csso

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}

esbuild

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}

lightningcss

a{transform:rotate(45deg)}

sass

a{-webkit-transform:rotate(45deg);transform:rotate(45deg)}
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.

Source

.nav .item, .footer, .nav .item {
  color: red;
}

Expected

.nav .item,.footer{color:red}

Outputs

clean-css

.footer,.nav .item{color:red}

csskit

.nav.item,.footer,.nav.item{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}
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.

Source

h1, h2 {
  color: red;
}
h2, h1 {
  color: red;
}

Expected

h1,h2{color:red}

Outputs

clean-css

h1,h2{color:red}

csskit

h1,h2{color:red}h2,h1{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}
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.

Source

.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}}

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}
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.

Source

a {
  color: red;
}
a {
  color: red;
  font-size: 1rem;
}

Expected

a{color:red;font-size:1rem}

Outputs

clean-css

a{color:red;font-size:1rem}

csskit

a{color:red}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}
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.

Source

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}

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}
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.

Source

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}

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}
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.

Source

a {
  border-top: 1px solid red;
  border-block: 2px solid red;
}

Expected

a{border-top:1px solid red;border-block:2px 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}

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}
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.

Source

a {
  margin-left: 10px;
  margin-inline-start: 20px;
}

Expected

a{margin-left:10px;margin-inline-start:20px}

Outputs

clean-css

a{margin-left:10px;margin-inline-start:20px}

csskit

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}
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.

Source

a {
  padding-top: 10px;
  padding-block-start: 20px;
}

Expected

a{padding-top:10px;padding-block-start:20px}

Outputs

clean-css

a{padding-top:10px;padding-block-start:20px}

csskit

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}
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.

Source

a {
  inset-block-start: 10px;
  top: 20px;
}

Expected

a{inset-block-start:10px;top:20px}

Outputs

clean-css

a{inset-block-start:10px;top:20px}

csskit

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}
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.

Source

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)}

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)}
Subtotal 15 / 19 5 / 19 10 / 19 15 / 19 10 / 19 13 / 19 5 / 19

empty-rules

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Empty rule removal

a {} contains no declarations and should be removed entirely. Empty rules waste bytes and have no effect on rendering.

Source

a {}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css

b{color:red}

csskit

a{}b{color:red}

cssnano

b{color:red}

csso

b{color:red}

esbuild

b{color:red}

lightningcss

b{color:red}

sass

b{color:red}
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.

Source

a {
  /* this rule is empty */
}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css

b{color:red}

csskit

a{}b{color:red}

cssnano

b{color:red}

csso

b{color:red}

esbuild

b{color:red}

lightningcss

b{color:red}

sass

b{color:red}
0003
Details

Empty @media rule

An @media block with no rules inside should be removed entirely. It has no effect on rendering.

Source

@media screen {
}
b {
  color: red;
}

Expected

b{color:red}

Outputs

clean-css

b{color:red}

csskit

@media screen{}b{color:red}

cssnano

b{color:red}

csso

b{color:red}

esbuild

b{color:red}

lightningcss

b{color:red}

sass

b{color:red}
0004
Details

Empty nested rule

An empty nested rule (&amp; b {}) inside a parent should be removed while preserving the parent's own declarations.

Source

a {
  color: red;
  &amp; b {
  }
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red;&b{}}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
Subtotal 4 / 4 0 / 4 4 / 4 4 / 4 4 / 4 4 / 4 4 / 4

escaping

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

\66 oo {
  color: red;
}

Expected

foo{color:red}

Validate

Outputs

clean-css

\66 oo{color:red}

csskit

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}
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.

Source

a {
  content: '\66 oo';
}

Expected

a{content:"foo"}

Validate

Outputs

clean-css

a{content:'\66 oo'}

csskit

a{content:"foo"}

cssnano

a{content:"\66 oo"}

csso

a{content:"foo"}

esbuild

a{content:"foo"}

lightningcss

a{content:"foo"}

sass

a{content:"foo"}
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.

Source

.\66\6f\6f {
  color: red;
}

Expected

.foo{color:red}

Validate

Outputs

clean-css

.\66\6f\6f{color:red}

csskit

.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}
0004
Details

Hex escape for hyphen-minus in element 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 -.

Source

\2d foo {
  color: red;
}

Expected

-foo{color:red}

Validate

Outputs

clean-css

\2d foo{color:red}

csskit

-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}
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.

Source

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}

cssnano

a[href=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}
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.

Source

.\31 23 {
  color: red;
}

Expected

.\31 23{color:red}

Validate

Outputs

clean-css

.\31 23{color:red}

csskit

.\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}
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.

Source

a {
  content: "\a";
}

Expected

a{content:"\a"}

Outputs

clean-css

a{content:"\a"}

csskit

a{content:"\a"}

cssnano

a{content:"\a"}

csso

a{content:"\a"}

esbuild

a{content:"\a"}

lightningcss

a{content:"\a "}

sass

a{content:"\a"}
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.

Source

.\@ {
  color: red;
}

Expected

.\@{color:red}

Outputs

clean-css

.\@{color:red}

csskit

.\@{color:red}

cssnano

.\@{color:red}

csso

.\@{color:red}

esbuild

.\@{color:red}

lightningcss

.\@{color:red}

sass

.\@{color:red}
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.

Source

#\66 oo {
  color: red;
}

Expected

#foo{color:red}

Validate

Outputs

clean-css

#\66 oo{color:red}

csskit

#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}
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).

Source

a.\62 ar {
  color: red;
}

Expected

a.bar{color:red}

Validate

Outputs

clean-css

a.\62 ar{color:red}

csskit

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}
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.

Source

a {
  \63 olor: red;
}

Expected

a{color:red}

Validate

Outputs

clean-css

csskit

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}
0012
Details

Hex escape in custom property name

--\66 oo encodes f as a hex escape in a custom property name, resolving to --foo. The same escape appears in var(--\66 oo) and must resolve consistently.

Source

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)}

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)}
Subtotal 3 / 12 12 / 12 4 / 12 4 / 12 11 / 12 11 / 12 10 / 12

font-face

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

@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}

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}
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.

Source

@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}

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}
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).

Source

@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")}

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")}
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.

Source

@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")}

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")}
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.

Source

@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)}

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)}
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.

Source

@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:U0 -0FF}

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}
Subtotal 3 / 6 2 / 6 4 / 6 3 / 6 4 / 6 5 / 6 2 / 6

gradients

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Default gradient direction omission

linear-gradient(to bottom, ...) can omit the direction since to bottom (180deg) is the default.

Source

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)}

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)}
0002
Details

180deg gradient direction omission

linear-gradient(180deg, ...) is equivalent to the default direction and can be omitted.

Source

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)}

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)}
0003
Details

Gradient direction keyword to angle

to right can be replaced with 90deg which is shorter.

Source

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)}

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)}
0004
Details

Redundant gradient stop positions

0% on the first stop and 100% on the last stop are defaults and can be removed.

Source

a {
  background: linear-gradient(red 0%, blue 100%);
}

Expected

a{background:linear-gradient(red,#00f)}

Validate

Outputs

clean-css

a{background:linear-gradient(red 0,#00f 100%)}

csskit

a{background:linear-gradient(red 0%,blue 100%)}

cssnano

a{background:linear-gradient(red,blue)}

csso

a{background:linear-gradient(red 0,#00f 100%)}

esbuild

a{background:linear-gradient(red,#00f)}

lightningcss

a{background:linear-gradient(red 0%,#00f 100%)}

sass

a{background:linear-gradient(red 0%, blue 100%)}
0005
Details

Default radial-gradient shape and position

ellipse at center is the default for radial-gradient and can be omitted entirely.

Source

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)}

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)}
Subtotal 0 / 5 0 / 5 0 / 5 0 / 5 1 / 5 4 / 5 0 / 5

import

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

@import url("foo.css");

Expected

@import"foo.css";

Outputs

clean-css

@import url(foo.css);

csskit

@import"foo.css";

cssnano

@import url("foo.css");

csso

@import url(foo.css);

esbuild

@import"foo.css";

lightningcss

@import "foo.css";

sass

@import"foo.css"
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.

Source

@import url("foo.css") screen;

Expected

@import"foo.css"screen;

Outputs

clean-css

@import url(foo.css) screen;

csskit

@import"foo.css"screen;

cssnano

@import url("foo.css") screen;

csso

@import url(foo.css)screen;

esbuild

@import"foo.css"screen;

lightningcss

@import "foo.css" screen;

sass

@import"foo.css"screen
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.

Source

@import url(foo.css);

Expected

@import"foo.css";

Outputs

clean-css

@import url(foo.css);

csskit

@import"foo.css";

cssnano

@import url(foo.css);

csso

@import url(foo.css);

esbuild

@import"foo.css";

lightningcss

@import "foo.css";

sass

@import"foo.css"
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.

Source

@import url("foo.css") layer;

Expected

@import"foo.css"layer;

Outputs

clean-css

@import url(foo.css) layer;

csskit

@import"foo.css"layer;

cssnano

@import url("foo.css") layer;

csso

@import url(foo.css)layer;

esbuild

@import"foo.css"layer;

lightningcss

@import "foo.css" layer;

sass

@import"foo.css"layer
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.

Source

@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);

cssnano

@import url("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)
Subtotal 0 / 5 4 / 5 0 / 5 0 / 5 4 / 5 0 / 5 0 / 5

keyframes

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@keyframes 100% to to

The 100% keyframe stop can be replaced with the shorter to keyword.

Source

@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}}

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}}
0002
Details

Keyframes from/to keyword optimization

from becomes 0% and 100% becomes to since these are shorter representations.

Source

@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}}

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}}
Subtotal 0 / 2 0 / 2 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2

layer

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Duplicate @layer statement removal

Duplicate @layer declarations can be deduplicated since layer order is established by first occurrence.

Source

@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}

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}
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.

Source

@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}}

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}}
Subtotal 1 / 2 0 / 2 0 / 2 0 / 2 0 / 2 2 / 2 0 / 2

media

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@media whitespace removal

Strip whitespace inside a basic @media block with nested rules.

Source

@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}}

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}}
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.

Source

@media screen {
  @media (width &gt;= 768px) {
    a {
      color: red;
    }
  }
}

Expected

@media screen{@media (width&gt;=768px){a{color:red}}}

Outputs

clean-css

@media screen{@media (width >= 768px){a{color:red}}}

csskit

@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}}
0003
Details

Remove redundant 'all and' in @media

@media all and (condition) is equivalent to @media (condition).

Source

@media all and (width &gt;= 500px) {
  a {
    color: red;
  }
}

Expected

@media (width&gt;=500px){a{color:red}}

Outputs

clean-css

@media all and (width >= 500px){a{color:red}}

csskit

@media all and (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}}
0004
Details

Adjacent identical @media merge

Adjacent @media blocks with the same query should be merged into one.

Source

@media screen and (width &gt;= 768px) {
  a {
    color: red;
  }
}
@media screen and (width &gt;= 768px) {
  b {
    margin: 0;
  }
}

Expected

@media screen and (width&gt;=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}}

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}}
0005
Details

Convert min-width to range syntax

(min-width: 768px) can be shortened to (width&gt;=768px) using Media Queries Level 4 range syntax.

Source

@media (min-width: 768px) {
  a {
    color: red;
  }
}

Expected

@media (width&gt;=768px){a{color:red}}

Outputs

clean-css

@media (min-width:768px){a{color:red}}

csskit

@media(min-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}}
0006
Details

Convert max-width to range syntax

(max-width: 1024px) can be shortened to (width&lt;=1024px) using Media Queries Level 4 range syntax.

Source

@media (max-width: 1024px) {
  a {
    color: red;
  }
}

Expected

@media (width&lt;=1024px){a{color:red}}

Outputs

clean-css

@media (max-width:1024px){a{color:red}}

csskit

@media(max-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}}
0007
Details

Convert min-height to range syntax

(min-height: 600px) can be shortened to (height&gt;=600px) using Media Queries Level 4 range syntax.

Source

@media (min-height: 600px) {
  a {
    color: red;
  }
}

Expected

@media (height&gt;=600px){a{color:red}}

Outputs

clean-css

@media (min-height:600px){a{color:red}}

csskit

@media(min-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}}
0008
Details

Convert max-height to range syntax

(max-height: 900px) can be shortened to (height&lt;=900px) using Media Queries Level 4 range syntax.

Source

@media (max-height: 900px) {
  a {
    color: red;
  }
}

Expected

@media (height&lt;=900px){a{color:red}}

Outputs

clean-css

@media (max-height:900px){a{color:red}}

csskit

@media(max-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}}
0009
Details

Combined min/max-width to range syntax

(min-width: 768px) and (max-width: 1024px) can be collapsed into a single range condition (768px&lt;=width&lt;=1024px), eliminating the and keyword and one pair of parentheses.

Source

@media (min-width: 768px) and (max-width: 1024px) {
  a {
    color: red;
  }
}

Expected

@media (768px&lt;=width&lt;=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}}

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}}
Subtotal 1 / 9 1 / 9 1 / 9 1 / 9 1 / 9 8 / 9 1 / 9

merging

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Merge adjacent rules with same selector

Adjacent rules sharing the same selector can be merged into one rule with combined declarations.

Source

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}

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}
0002
Details

Merge adjacent rules with same declarations

Adjacent rules with identical declarations can merge their selectors into a comma-separated list.

Source

.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}

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}
0003
Details

Merge adjacent @media with same query

Adjacent @media rules with the same query can be merged into a single @media block.

Source

@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}}

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}}
0004
Details

Remove empty @media rule

An @media rule with no content should be removed entirely.

Source

@media screen {
}
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

@media screen{}a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0005
Details

Partial merge of shared declarations

When adjacent rules share some declarations, the common part can be factored out.

Source

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}

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}
0006
Details

Overridden @keyframes removal

When two @keyframes share the same name, only the last definition is kept.

Source

@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}}

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}}
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.

Source

.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}

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}
0008
Details

Non-adjacent @media merge is unsafe due to cascade

Merging two identical @media (width &gt;= 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.

Source

@media (width &gt;= 1px) {
  a {
    color: red;
  }
}

a {
  color: green;
}

@media (width &gt;= 1px) {
  a {
    background: #0b0;
  }
}

Expected

@media (width&gt;=1px){a{color:red}}a{color:green}@media (width&gt;=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}}

cssnano

@media (width >= 1px){a{color:red}}a{color:green}@media (width >= 1px){a{background:#0b0}}

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}}
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.

Source

.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}

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}
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.

Source

.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}

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}
Subtotal 7 / 10 3 / 10 9 / 10 6 / 10 5 / 10 9 / 10 4 / 10

nesting

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

.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}}

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}
0002
Details

Nested & selector with pseudo-class

&amp;:hover inside a parent rule must be preserved as-is. The &amp; refers to the parent selector and appending :hover creates a compound match.

Source

a {
  color: red;
  &amp;:hover {
    margin: 0;
  }
}

Expected

a{color:red;&amp;:hover{margin:0}}

Outputs

clean-css

a{color:red}

csskit

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}
0003
Details

Nested & with compound class selector

&amp;.bar refines the parent selector, matching elements that match both .foo and .bar. The compound form must be preserved during minification.

Source

.foo {
  color: red;
  &amp;.bar {
    margin: 0;
  }
}

Expected

.foo{color:red;&amp;.bar{margin:0}}

Outputs

clean-css

.foo{color:red}

csskit

.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}
0004
Details

Nested child combinator with implicit &

&gt; .bar inside a parent rule uses a relative selector (implicit &amp;). The child combinator form without an explicit &amp; must be preserved.

Source

.foo {
  color: red;
  &gt; .bar {
    margin: 0;
  }
}

Expected

.foo{color:red;&gt;.bar{margin:0}}

Outputs

clean-css

.foo{color:red}

csskit

.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}
0005
Details

Multi-level nesting

Three levels of nesting (figure &gt; figcaption &gt; p) must all be preserved and minified correctly without flattening.

Source

figure {
  margin: 0;
  &gt; figcaption {
    color: red;
    &gt; p {
      font-size: .9rem;
    }
  }
}

Expected

figure{margin:0;&gt;figcaption{color:red;&gt;p{font-size:.9rem}}}

Outputs

clean-css

figure{margin:0}

csskit

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}
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 (.

Source

.foo {
  display: grid;
  @media (width &gt;= 768px) {
    grid-auto-flow: column;
  }
}

Expected

.foo{display:grid;@media(width&gt;=768px){grid-auto-flow:column}}

Outputs

clean-css

.foo{display:grid}

csskit

.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}}
0007
Details

Redundant & removal

&amp; .bar is equivalent to .bar in a nesting context because a nested selector without &amp; implies a descendant relationship. The explicit &amp; is redundant and can be removed.

Source

.foo {
  color: red;
  &amp; .bar {
    margin: 0;
  }
}

Expected

.foo{color:red;.bar{margin:0}}

Outputs

clean-css

.foo{color:red}

csskit

.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}
0008 ERR
Details

Doubled & selector

&amp;&amp; equals .foo.foo, doubling specificity. Must not collapse to a single &amp;.

Source

.foo {
  color: red;
  &amp;&amp; {
    font-size: 16px;
  }
}

Expected

.foo{color:red;&amp;&amp;{font-size:16px}}

Outputs

clean-css

.foo{color:red}

csskit

.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
0009
Details

& in non-initial position

.parent &amp; matches .parent .foo -- the &amp; is not at the start of the selector. This form must be preserved; it cannot be simplified.

Source

.foo {
  color: red;
  .parent &amp; {
    margin: 0;
  }
}

Expected

.foo{color:red;.parent &amp;{margin:0}}

Outputs

clean-css

.foo{color:red}

csskit

.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}
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.

Source

.nav {
  display: flex;
}
.nav a {
  color: red;
}
.nav a:hover {
  margin: 0;
}

Expected

.nav{display:flex;a{color:red;&amp;: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}

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}
0011
Details

Introduce nesting for pseudo-class

a { ... } a:hover { ... } is shorter when nested as a{...;&amp;:hover{...}}, eliminating the repeated a selector.

Source

a {
  color: red;
}
a:hover {
  margin: 0;
}

Expected

a{color:red;&amp;:hover{margin:0}}

Outputs

clean-css

a{color:red}a:hover{margin:0}

csskit

a{color:red}a: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}
0012
Details

Introduce nesting for child combinator

.card { ... } .card &gt; .title { ... } is shorter when nested using a relative child selector: .card{...;&gt;.title{...}}.

Source

.card {
  padding: 1rem;
}
.card &gt; .title {
  color: red;
}

Expected

.card{padding:1rem;&gt;.title{color:red}}

Outputs

clean-css

.card{padding:1rem}.card>.title{color:red}

csskit

.card{padding:1rem}.card>.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}
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.

Source

a {
  b {
    color: red;
  }
}

Expected

a b{color:red}

Outputs

clean-css

csskit

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}
0014
Details

Flatten child combinator nesting with empty parent

a { &gt; b { color: red } } can be flattened to a&gt;b{color:red} when the parent has no own declarations. The relative child selector resolves to a direct child combinator.

Source

a {
  &gt; b {
    color: red;
  }
}

Expected

a&gt;b{color:red}

Outputs

clean-css

csskit

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}
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.

Source

a {
  + b {
    color: red;
  }
}

Expected

a+b{color:red}

Outputs

clean-css

csskit

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}
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.

Source

a {
  ~ b {
    color: red;
  }
}

Expected

a~b{color:red}

Outputs

clean-css

csskit

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}
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.

Source

nav {
  a {
    color: red;
  }
  &gt; .icon {
    width: 1rem;
  }
  + footer {
    margin: 0;
  }
}

Expected

nav{a{color:red}&gt;.icon{width:1rem}+footer{margin:0}}

Outputs

clean-css

csskit

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}
0018
Details

Flatten &:pseudo from empty parent

a { &amp;:hover { color: red } } can be flattened to a:hover{color:red} when the parent has no own declarations. The &amp; resolves to the parent selector, producing the compound a:hover.

Source

a {
  &amp;:hover {
    color: red;
  }
}

Expected

a:hover{color:red}

Outputs

clean-css

csskit

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}
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.

Source

.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}}

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}
Subtotal 0 / 19 8 / 19 8 / 19 0 / 19 10 / 19 4 / 19 5 / 19

page

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

@page {
  margin: 1in;
}

Expected

@page{margin:1in}

Outputs

clean-css

@page{margin:1in}

csskit

@page{margin:1in}

cssnano

@page{margin:1in}

csso

@page{margin:1in}

esbuild

@page{margin:1in}

lightningcss

@page{margin:1in}

sass

@page{margin:1in}
0002
Details

Named @page pseudo-class

@page :first targets the first page. Whitespace between the pseudo-class and the opening brace can be removed.

Source

@page :first {
  margin: 2in;
}

Expected

@page:first{margin:2in}

Outputs

clean-css

@page :first{margin:2in}

csskit

@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}
0003
Details

Empty @page removal

An empty @page {} rule with no declarations can be removed entirely.

Source

@page {
}

Expected

Outputs

clean-css

csskit

@page{}

cssnano

csso

esbuild

lightningcss

@page{}

sass

@page{}
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.

Source

@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"}}

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"}}
Subtotal 3 / 4 3 / 4 3 / 4 3 / 4 3 / 4 3 / 4 2 / 4

property

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@property whitespace removal

Remove whitespace inside @property while preserving the descriptor values. The syntax string and initial-value must not be altered.

Source

@property --brand-color {
  syntax: "&lt;color&gt;";
  inherits: false;
  initial-value: #000;
}

Expected

@property --brand-color{syntax:"&lt;color&gt;";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}

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}
0002
Details

@property descriptor preservation

All three descriptors (syntax, inherits, initial-value) are required and must not be removed or altered. The syntax string quotes are required. Changing inherits silently breaks inheritance behavior.

Source

@property --size {
  syntax: "&lt;length&gt;";
  inherits: true;
  initial-value: 16px;
}

Expected

@property --size{syntax:"&lt;length&gt;";inherits:true;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}

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}
Subtotal 2 / 2 2 / 2 2 / 2 2 / 2 0 / 2 2 / 2 2 / 2

scope

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

@scope whitespace removal

Basic whitespace removal in @scope. The space between @scope and the opening parenthesis can be removed.

Source

@scope (.card) {
  .title {
    color: red;
  }
}

Expected

@scope(.card){.title{color:red}}

Outputs

clean-css

csskit

@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}}
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.

Source

@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}}

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}}
0003
Details

Empty @scope removal

An empty @scope block with no rules can be removed entirely.

Source

@scope (.card) {
}

Expected

Outputs

clean-css

csskit

@scope (.card){}

cssnano

csso

esbuild

@scope(.card){}

lightningcss

@scope(.card){}

sass

@scope (.card){}
Subtotal 1 / 3 0 / 3 1 / 3 1 / 3 2 / 3 1 / 3 0 / 3

selectors

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

nth-child 2n+1 to odd

2n+1 is the definition of odd. The keyword form is shorter.

Source

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}

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}
0002
Details

nth-child redundant +0 removal

2n+0 simplifies to 2n. The +0 offset is redundant.

Source

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}

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}
0003 ERRERR
Details

nth-child positive sign removal

+5 is the same as 5. The redundant positive sign should be stripped.

Source

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}

cssnano

a:nth-child(+5){color:red}

csso

esbuild

a:nth-child(+ 5){color:red}

lightningcss

Unexpected token WhiteSpace(" ")

sass

Expected "n".
  ╷
1 │ a:nth-child(+ 5) {
  │              ^
  ╵
  - 1:14  root stylesheet
0004
Details

nth-child zero-step simplification

0n+3 simplifies to just 3. The zero-step term is redundant.

Source

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}

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}
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 same length). Minifiers should preserve 2n as-is.

Source

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}

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}
0006
Details

nth-child(1) to first-child

:nth-child(1) is equivalent to :first-child, which is shorter.

Source

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}

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}
0007
Details

nth-last-child(1) to last-child

:nth-last-child(1) is equivalent to :last-child, which is shorter.

Source

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}

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}
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.

Source

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}

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}
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.

Source

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}

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}
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.

Source

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}

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}
0011
Details

Double-colon to single-colon pseudo-element

::before can be shortened to :before for CSS2.1 pseudo-elements.

Source

a::before {
  content: "x";
}

Expected

a:before{content:"x"}

Outputs

clean-css

a::before{content:"x"}

csskit

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"}
0012
Details

Universal selector preservation

The * selector must be preserved as-is, not removed or altered.

Source

* {
  margin: 0;
}

Expected

*{margin:0}

Outputs

clean-css

*{margin:0}

csskit

*{margin:0}

cssnano

*{margin:0}

csso

*{margin:0}

esbuild

*{margin:0}

lightningcss

*{margin:0}

sass

*{margin:0}
0013
Details

Redundant universal selector before ID

*#id is equivalent to #id. The universal selector is implied.

Source

*#id {
  color: red;
}

Expected

#id{color:red}

Outputs

clean-css

#id{color:red}

csskit

*#id{color:red}

cssnano

#id{color:red}

csso

#id{color:red}

esbuild

*#id{color:red}

lightningcss

#id{color:red}

sass

*#id{color:red}
0014
Details

Attribute selector quote removal

Quotes around attribute values can be removed when the value is a valid identifier.

Source

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}

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}
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.).

Source

::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}

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}
Subtotal 10 / 15 3 / 15 11 / 15 6 / 15 9 / 15 14 / 15 4 / 15

selectors-advanced

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

:is( a , b ) {
  color: red;
}

Expected

a,b{color:red}

Outputs

clean-css

:is( a ,b ){color:red}

csskit

: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}
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).

Source

: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}

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}
0003
Details

:has() whitespace removal

Whitespace inside :has() around combinators can be removed, same as regular selector lists.

Source

a:has(&gt; b) {
  color: red;
}

Expected

a:has(&gt;b){color:red}

Outputs

clean-css

a:has(> b){color:red}

csskit

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}
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).

Source

: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}

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}
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.

Source

input:not(:invalid) {
  color: red;
}

Expected

input:valid{color:red}

Outputs

clean-css

input:not(:invalid){color:red}

csskit

input:not(:invalid){color:red}

cssnano

input:not(:invalid){color:red}

csso

input:not(:invalid){color:red}

esbuild

input:not(:invalid){color:red}

lightningcss

input:not(:invalid){color:red}

sass

input:not(:invalid){color:red}
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).

Source

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}

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}
0007
Details

Double :not() cancellation

:not(:not(X)) is logically equivalent to X. The double negation can be removed, producing a shorter selector.

Source

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}

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}
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.

Source

input:not(:enabled) {
  color: red;
}

Expected

input:disabled{color:red}

Outputs

clean-css

input:not(:enabled){color:red}

csskit

input:not(:enabled){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}
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.

Source

input:not(:required) {
  color: red;
}

Expected

input:optional{color:red}

Outputs

clean-css

input:not(:required){color:red}

csskit

input:not(:required){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}
0010
Details

a:not(:link) to a:visited

On hyperlink elements (&lt;a&gt;, &lt;area&gt;, &lt;link&gt; with href) :link and :visited are a complete partition. a:not(:link) can be replaced with the shorter a:visited.

Source

a:not(:link) {
  color: red;
}

Expected

a:visited{color:red}

Outputs

clean-css

a:not(:link){color:red}

csskit

a:not(:link){color:red}

cssnano

a:not(:link){color:red}

csso

a:not(:link){color:red}

esbuild

a:not(:link){color:red}

lightningcss

a:not(:link){color:red}

sass

a:not(:link){color:red}
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).

Source

:is(:link, :visited) {
  color: red;
}

Expected

:any-link{color:red}

Outputs

clean-css

:is(:link,:visited){color:red}

csskit

:is(:link,:visited){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}
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.

Source

: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}

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}
0013
Details

h1-h6 selector list 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. Its specificity is that of a class. No browser implements this yet.

Source

h1, h2, h3, h4, h5, h6 {
  color: red;
}

Expected

:heading{color:red}

Outputs

clean-css

h1,h2,h3,h4,h5,h6{color:red}

csskit

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}
Subtotal 2 / 13 3 / 13 3 / 13 3 / 13 3 / 13 3 / 13 3 / 13

shorthands

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Margin 4-to-1 collapse

margin: 10px 10px 10px 10px collapses to margin:10px when all four values are identical.

Source

a {
  margin: 10px 10px 10px 10px;
}

Expected

a{margin:10px}

Outputs

clean-css

a{margin:10px}

csskit

a{margin:10px 10px 10px 10px}

cssnano

a{margin:10px}

csso

a{margin:10px}

esbuild

a{margin:10px}

lightningcss

a{margin:10px}

sass

a{margin:10px 10px 10px 10px}
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.

Source

a {
  margin: 10px 20px 10px 20px;
}

Expected

a{margin:10px 20px}

Outputs

clean-css

a{margin:10px 20px}

csskit

a{margin:10px 20px 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}
0003
Details

Margin 4-to-3 collapse

margin: 10px 20px 30px 20px collapses to margin:10px 20px 30px when left equals right.

Source

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 20px}

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}
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.

Source

a {
  background: none;
}

Expected

a{background:0 0}

Outputs

clean-css

a{background:0 0}

csskit

a{background:none}

cssnano

a{background:none}

csso

a{background:0 0}

esbuild

a{background:none}

lightningcss

a{background:0 0}

sass

a{background:none}
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.

Source

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}

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}
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.

Source

a {
  inset: 0px 0px 0px 0px;
}

Expected

a{inset:0}

Outputs

clean-css

a{inset:0px 0px 0px 0px}

csskit

a{inset:0 0 0 0}

cssnano

a{inset:0 0 0 0}

csso

a{inset:0 0 0 0}

esbuild

a{inset:0}

lightningcss

a{inset:0}

sass

a{inset:0px 0px 0px 0px}
0007
Details

Padding 4-value to 1-value collapse

padding: 10px 10px 10px 10px collapses to padding: 10px when all four values are identical.

Source

a {
  padding: 10px 10px 10px 10px;
}

Expected

a{padding:10px}

Outputs

clean-css

a{padding:10px}

csskit

a{padding:10px 10px 10px 10px}

cssnano

a{padding:10px}

csso

a{padding:10px}

esbuild

a{padding:10px}

lightningcss

a{padding:10px}

sass

a{padding:10px 10px 10px 10px}
0008
Details

Overflow longhand to shorthand

overflow-x and overflow-y with the same value can be combined into overflow shorthand.

Source

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}

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}
0009
Details

Gap longhand to shorthand

row-gap and column-gap with the same value can be combined into gap shorthand.

Source

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}

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}
0010
Details

Flex longhand to shorthand

flex-grow, flex-shrink, and flex-basis can be combined into flex shorthand.

Source

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}

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}
0011
Details

Outline longhand to shorthand

outline-width, outline-style, and outline-color can be combined into outline shorthand.

Source

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}

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}
0012
Details

Border-radius longhand to shorthand

All four border-*-radius longhands with the same value collapse into border-radius shorthand.

Source

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}

cssnano

a{border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-right-radius:5px;border-bottom-left-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}
0013
Details

Margin longhand to shorthand

Individual margin-top/right/bottom/left declarations collapse into margin shorthand with symmetric 2-value form.

Source

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}

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}
0014
Details

Text-decoration longhand to shorthand

text-decoration-line/style/color collapse into text-decoration shorthand, omitting solid (the default style).

Source

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}

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}
0015
Details

Padding longhand to shorthand

Four padding longhand properties collapse to padding shorthand with value dedup.

Source

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}

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}
0016
Details

Border longhand to shorthand

All 12 border longhand properties collapse to a single border shorthand.

Source

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}

cssnano

a{border:1px solid red}

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}
0017
Details

Padding 4-to-2 value collapse

When top equals bottom and right equals left, padding collapses from 4 values to 2.

Source

a {
  padding: 5px 10px 5px 10px;
}

Expected

a{padding:5px 10px}

Outputs

clean-css

a{padding:5px 10px}

csskit

a{padding:5px 10px 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}
0018
Details

Padding 4-to-3 value collapse

When left equals right, the 4th value can be dropped. Top/bottom differ so 3 values remain.

Source

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 10px}

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}
0019
Details

Border-radius 4-to-2 value collapse

border-radius follows the same top-right-bottom-left collapsing rules as margin/padding.

Source

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 5px 10px}

cssnano

a{border-radius:5px 10px 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}
0020
Details

Border-radius slash syntax collapse

Both horizontal and vertical radii have identical values across all 4 corners, each side collapses to 1 value. The / separator must be preserved.

Source

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 10px 10px 10px/5px 5px 5px 5px}

cssnano

a{border-radius:10px 10px 10px 10px/5px 5px 5px 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}
0021
Details

Inset 4-to-2 value collapse

The inset shorthand follows the same collapsing rules as margin/padding.

Source

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 10px 20px}

cssnano

a{inset:10px 20px 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}
0022
Details

Margin 2-to-1 value collapse

When both values in a 2-value margin are identical, collapse to a single value.

Source

a {
  margin: 10px 10px;
}

Expected

a{margin:10px}

Outputs

clean-css

a{margin:10px}

csskit

a{margin:10px 10px}

cssnano

a{margin:10px}

csso

a{margin:10px}

esbuild

a{margin:10px}

lightningcss

a{margin:10px}

sass

a{margin:10px 10px}
0023
Details

place-items longhand merge (same value)

When align-items and justify-items are the same, they merge into a single-value place-items.

Source

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}

cssnano

a{align-items:center;justify-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}
0024
Details

place-content longhand merge (same value)

When align-content and justify-content are the same, they merge into a single-value place-content.

Source

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}

cssnano

a{align-content:center;justify-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}
0025
Details

place-self longhand merge (same value)

When align-self and justify-self are the same, they merge into a single-value place-self.

Source

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}

cssnano

a{align-self:center;justify-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}
0026
Details

Gap longhand merge (different values)

When row-gap and column-gap differ, they merge into a 2-value gap shorthand (row then column).

Source

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}

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}
0027
Details

Columns longhand merge

column-width and column-count merge into the columns shorthand.

Source

a {
  column-width: auto;
  column-count: 3;
}

Expected

a{columns:auto 3}

Outputs

clean-css

a{column-width:auto;column-count:3}

csskit

a{column-width:auto;column-count:3}

cssnano

a{columns:3}

csso

a{column-width:auto;column-count:3}

esbuild

a{column-width:auto;column-count:3}

lightningcss

a{column-width:auto;column-count:3}

sass

a{column-width:auto;column-count:3}
0028
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.

Source

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}

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}
0029
Details

Background longhand merge with defaults

Background longhands merge into background shorthand. All properties at their default values except background-color are omitted.

Source

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}

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}
0030
Details

Font longhand merge

font-style, font-weight, font-size, line-height, and font-family merge into font shorthand. bold optimizes to 700.

Source

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}

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}
0031
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.

Source

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}

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}
0032
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.

Source

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}

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}
0033
Details

Border default width omission

medium is the default border-width value and can be omitted from the border shorthand.

Source

a {
  border: medium solid red;
}

Expected

a{border:solid red}

Outputs

clean-css

a{border:solid red}

csskit

a{border:medium 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}
0034
Details

Background shorthand default omission

Default background-position (0% 0%), background-repeat (repeat), and background-attachment (scroll) can be omitted from the background shorthand.

Source

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:red url(bg.png) 0% 0% repeat scroll}

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}
0035
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.

Source

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}

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}
0036
Details

Outline shorthand color optimization

Color values within shorthands like outline should still be optimized (#ff0000 -> red).

Source

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}

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}
0037
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.

Source

a {
  margin: 0 auto 0 auto;
}

Expected

a{margin:0 auto}

Outputs

clean-css

a{margin:0 auto}

csskit

a{margin:0 auto 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}
0038
Details

border-color 4-to-1 value collapse

border-color follows the same top-right-bottom-left collapsing rules as margin/padding.

Source

a {
  border-color: red red red red;
}

Expected

a{border-color:red}

Outputs

clean-css

a{border-color:red}

csskit

a{border-color:red red red 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}
0039
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.

Source

a {
  gap: 10px 10px;
}

Expected

a{gap:10px}

Outputs

clean-css

a{gap:10px 10px}

csskit

a{gap:10px 10px}

cssnano

a{gap:10px 10px}

csso

a{gap:10px 10px}

esbuild

a{gap:10px 10px}

lightningcss

a{gap:10px}

sass

a{gap:10px 10px}
0040
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.

Source

a {
  overflow: hidden hidden;
}

Expected

a{overflow:hidden}

Outputs

clean-css

a{overflow:hidden hidden}

csskit

a{overflow:hidden hidden}

cssnano

a{overflow:hidden hidden}

csso

a{overflow:hidden hidden}

esbuild

a{overflow:hidden hidden}

lightningcss

a{overflow:hidden}

sass

a{overflow:hidden hidden}
0041
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.

Source

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}

cssnano

a{border-image:url(border.png) 30 round;border:1px solid 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}
0042
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.

Source

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}

cssnano

a{border:1px solid 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}
0043
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.

Source

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-image:url(border.png) 30 round;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}
0044
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.

Source

a {
  border-image-source: url(border.png);
  border-image-slice: 30;
  border-image-repeat: round;
  border-width: 4px;
  border-style: solid;
  border-color: transparent;
}

Expected

a{border:4px solid transparent;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:4px solid transparent}

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}

cssnano

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border:4px solid #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:transparent}

esbuild

a{border-image-source:url(border.png);border-image-slice:30;border-image-repeat:round;border-width:4px;border-style:solid;border-color:transparent}

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)}
0045
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).

Source

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}

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}
0046
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.

Source

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}

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}
0047
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.

Source

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-variant-numeric:tabular-nums;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}
0048
Details

font longhands with font-kerning longhand (collapse, preserve order)

Font longhands and font-kerning are both expressed as longhands. Minifier should collapse the font longhands into font shorthand, keeping font before font-kerning since font resets font-kerning to auto.

Source

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}

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}
0049
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.

Source

a {
  mask-border: url(mask.png) 25 / 10px round;
  mask-image: linear-gradient(black, transparent);
  mask-size: cover;
  mask-repeat: no-repeat;
}

Expected

a{mask:linear-gradient(#000,transparent) no-repeat/cover;mask-border:url(mask.png) 25/10px round}

Outputs

clean-css

a{mask-border:url(mask.png) 25/10px round;mask-image:linear-gradient(black,transparent);mask-size:cover;mask-repeat:no-repeat}

csskit

a{mask-border:url(mask.png) 25 / 10px round;mask-image:linear-gradient(black,transparent);mask-size:cover;mask-repeat:no-repeat}

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,transparent);mask-size:cover;mask-repeat:no-repeat}

esbuild

a{mask-border:url(mask.png) 25 / 10px round;mask-image:linear-gradient(black,transparent);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(black, transparent);mask-size:cover;mask-repeat:no-repeat}
0050
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.

Source

a {
  mask-image: linear-gradient(black, transparent);
  mask-repeat: no-repeat;
  mask-border: url(mask.png) 25 round;
}

Expected

a{mask:linear-gradient(#000,transparent) no-repeat;mask-border:url(mask.png) 25 round}

Outputs

clean-css

a{mask-image:linear-gradient(black,transparent);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}

csskit

a{mask-image:linear-gradient(black,transparent);mask-repeat: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,transparent);mask-repeat:no-repeat;mask-border:url(mask.png)25 round}

esbuild

a{mask-image:linear-gradient(black,transparent);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(black, transparent);mask-repeat:no-repeat;mask-border:url(mask.png) 25 round}
0051
Details

mask shorthand intentionally resets mask-border

Declaring mask-border then mask resets mask-border to its initial value. The mask-border declaration is dead code and can be removed.

Source

a {
  mask-border: url(mask.png) 25 round;
  mask: linear-gradient(black, transparent);
}

Expected

a{mask:linear-gradient(#000,transparent)}

Outputs

clean-css

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(black,transparent)}

csskit

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(black,transparent)}

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,transparent)}

esbuild

a{mask-border:url(mask.png) 25 round;mask:linear-gradient(black,transparent)}

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(black, transparent)}
0052
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).

Source

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}

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}
0053
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.

Source

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)}

cssnano

a{padding:var(--pt) var(--pr) var(--pb) 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)}
0054
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.

Source

a {
  font-weight: bold;
  font: 14px serif;
}

Expected

a{font:14px serif}

Outputs

clean-css

a{font:14px serif}

csskit

a{font-weight:bold;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}
0055
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.

Source

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)}

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)}
0056
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.

Source

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}

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}
0057
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.

Source

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)}

cssnano

a{padding:var(--pt,10px) var(--pr,20px) var(--pb,10px) 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)}
0058
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.

Source

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))}

cssnano

a{padding:var(--pt,var(--fallback)) var(--pr,var(--fallback)) var(--pb,var(--fallback)) 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))}
0059
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.

Source

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)}

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)}
0060
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.

Source

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)}

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)}
0061
Details

Padding longhand merge safe with @property constraint

When each custom property has an @property rule constraining its syntax to &lt;length&gt;, the value is guaranteed valid at parse time. Invalid values are rejected and fall back to initial-value, so shorthand merge is safe.

Source

@property --pt {
  syntax: "&lt;length&gt;";
  inherits: false;
  initial-value: 0px;
}
@property --pr {
  syntax: "&lt;length&gt;";
  inherits: false;
  initial-value: 0px;
}
@property --pb {
  syntax: "&lt;length&gt;";
  inherits: false;
  initial-value: 0px;
}
@property --pl {
  syntax: "&lt;length&gt;";
  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:"&lt;length&gt;";inherits:false;initial-value:0px}@property --pr{syntax:"&lt;length&gt;";inherits:false;initial-value:0px}@property --pb{syntax:"&lt;length&gt;";inherits:false;initial-value:0px}@property --pl{syntax:"&lt;length&gt;";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)}

cssnano

@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:var(--pt) var(--pr) var(--pb) 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)}
0062
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.

Source

@property --bw {
  syntax: "&lt;length&gt;";
  inherits: false;
  initial-value: 0px;
}
@property --bs {
  syntax: "&lt;custom-ident&gt;";
  inherits: false;
  initial-value: none;
}
@property --bc {
  syntax: "&lt;color&gt;";
  inherits: false;
  initial-value: black;
}
a {
  border-width: var(--bw);
  border-style: var(--bs);
  border-color: var(--bc);
}

Expected

@property --bw{syntax:"&lt;length&gt;";inherits:false;initial-value:0px}@property --bs{syntax:"&lt;custom-ident&gt;";inherits:false;initial-value:none}@property --bc{syntax:"&lt;color&gt;";inherits:false;initial-value:#000}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:black}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:black}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}

cssnano

@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:#000}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:black}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: black;}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:#000}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:#000}a{border-width:var(--bw);border-style:var(--bs);border-color:var(--bc)}
0063
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.

Source

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}

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}
0064
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.

Source

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}

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}
0065
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.

Source

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}

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}
0066
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.

Source

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-top:1px solid blue;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}
0067
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.

Source

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-start:1px solid blue;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}
0068
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.

Source

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-start:10px;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}
0069
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.

Source

a {
  margin-top: 10px;
  margin: 20px;
}

Expected

a{margin:20px}

Outputs

clean-css

a{margin:20px}

csskit

a{margin-top:10px;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}
0070
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.

Source

a {
  border-top-width: 1px;
  border-width: 2px;
}

Expected

a{border-width:2px}

Outputs

clean-css

a{border-width:2px}

csskit

a{border-top-width:1px;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}
0071
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.

Source

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-color:blue;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}
0072
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.

Source

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-start:10px;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}
0073
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.

Source

a {
  transition: all 0s;
}

Expected

a{transition:all}

Outputs

clean-css

a{transition:none}

csskit

a{transition:all 0s}

cssnano

a{transition:all 0s}

csso

a{transition:all 0s}

esbuild

a{transition:all 0s}

lightningcss

a{transition:all}

sass

a{transition:all 0s}
0074
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).

Source

a {
  flex: 0 0 0px;
}

Expected

a{flex:0 0}

Outputs

clean-css

a{flex:0 0 0px}

csskit

a{flex:0 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}
Subtotal 29 / 74 8 / 74 22 / 74 21 / 74 19 / 74 54 / 74 3 / 74

starting-style

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

@starting-style {
  .card {
    opacity: 0;
  }
}

Expected

@starting-style{.card{opacity:0}}

Outputs

clean-css

csskit

@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}}
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.

Source

.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}}

cssnano

.card{transition:opacity .5s,display allow-discrete .5s}@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}}
Subtotal 0 / 2 2 / 2 1 / 2 2 / 2 2 / 2 1 / 2 2 / 2

supports

testclean-csscsskitcssnanocssoesbuildlightningcsssass
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.

Source

@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}}

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}}
0002
Details

Remove empty @supports rule

An @supports rule with no content should be removed entirely, like empty style rules.

Source

@supports (display: grid) {
}
a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

@supports(display:grid){}a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
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.

Source

@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}}

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}}
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.

Source

@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}}

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}}
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.

Source

@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}}

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}}
Subtotal 3 / 5 1 / 5 3 / 5 3 / 5 1 / 5 3 / 5 1 / 5

transforms

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

translate3d to translate

translate3d(x, 0, 0) can be simplified to translate(x) since the Y and Z components are zero.

Source

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)}

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)}
0002
Details

translate to translateY

translate(0, y) can be simplified to translateY(y) when the X component is zero.

Source

a {
  transform: translate(0, 20px);
}

Expected

a{transform:translateY(20px)}

Outputs

clean-css

a{transform:translate(0,20px)}

csskit

a{transform:translate(0,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)}
0003
Details

scale with equal axes

scale(x, x) can omit the second argument when both axes are identical: scale(x).

Source

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)}

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)}
0004
Details

rotateZ to rotate

rotateZ(a) is equivalent to rotate(a) and shorter. The Z axis is the default rotation axis.

Source

a {
  transform: rotateZ(45deg);
}

Expected

a{transform:rotate(45deg)}

Outputs

clean-css

a{transform:rotateZ(45deg)}

csskit

a{transform:rotateZ(45deg)}

cssnano

a{transform:rotate(45deg)}

csso

a{transform:rotateZ(45deg)}

esbuild

a{transform:rotate(45deg)}

lightningcss

a{transform:rotate(45deg)}

sass

a{transform:rotateZ(45deg)}
0005
Details

scale3d to scale

scale3d(x, y, 1) can be simplified to scale(x, y) when the Z component is 1.

Source

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)}

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)}
0006
Details

rotate3d to rotateY

rotate3d(0, 1, 0, a) is equivalent to rotateY(a) and shorter.

Source

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)}

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)}
0007
Details

scale(x, 1) to scaleX

scale(1.5, 1) only scales the X axis, equivalent to scaleX(1.5).

Source

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)}

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)}
0008
Details

translate3d(0,0,z) to translateZ

When X and Y are zero, translate3d simplifies to translateZ.

Source

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)}

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)}
0009
Details

rotate3d(1,0,0,a) to rotateX

Rotation around the X axis only simplifies to rotateX.

Source

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)}

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)}
0010
Details

scale() percentage to number

scale(50%) is equivalent to scale(.5). Per the CSS Transforms spec, scale functions accept &lt;number&gt; or &lt;percentage&gt;, where 100% = 1. Converting percentages to numbers is shorter and avoids the % character.

Source

a {
  transform: scale(50%);
}

Expected

a{transform:scale(.5)}

Outputs

clean-css

a{transform:scale(50%)}

csskit

a{transform:scale(50%)}

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%)}
0011
Details

scale property percentage to number

The individual scale property accepts &lt;number&gt; or &lt;percentage&gt;, where 100% = 1. scale: 200% is equivalent to scale: 2. Converting to a number saves 2 bytes (drops % and avoids the longer percentage representation).

Source

a {
  scale: 200%;
}

Expected

a{scale:2}

Outputs

clean-css

a{scale:200%}

csskit

a{scale:200%}

cssnano

a{scale:200%}

csso

a{scale:200%}

esbuild

a{scale:200%}

lightningcss

a{scale:2}

sass

a{scale:200%}
Subtotal 0 / 11 0 / 11 7 / 11 0 / 11 8 / 11 11 / 11 0 / 11

values

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

font-weight bold to 700

bold is equivalent to 700 and shorter as a numeric value.

Source

a {
  font-weight: bold;
}

Expected

a{font-weight:700}

Outputs

clean-css

a{font-weight:700}

csskit

a{font-weight:bold}

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}
0002
Details

font-weight normal to 400

normal is equivalent to 400 and shorter as a numeric value.

Source

a {
  font-weight: normal;
}

Expected

a{font-weight:400}

Outputs

clean-css

a{font-weight:400}

csskit

a{font-weight:normal}

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}
0003
Details

url() quote removal

Quotes inside url() are optional when the URL contains no special characters. They can be removed to save bytes.

Source

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")}

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")}
0004
Details

Trailing zero removal in dimensions

1.0000px should be normalized to 1px. Trailing fractional zeros are redundant.

Source

a {
  width: 1.0000px;
}

Expected

a{width:1px}

Outputs

clean-css

a{width:1px}

csskit

a{width:1px}

cssnano

a{width:1px}

csso

a{width:1px}

esbuild

a{width:1px}

lightningcss

a{width:1px}

sass

a{width:1px}
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()).

Source

a {
  margin: +1.5px;
}

Expected

a{margin:1.5px}

Outputs

clean-css

a{margin:+1.5px}

csskit

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}
0006
Details

Leading zero padding removal

001 should be normalized to 1. Leading zeros on integer values are redundant and can be safely stripped.

Source

a {
  z-index: 001;
}

Expected

a{z-index:1}

Outputs

clean-css

a{z-index:001}

csskit

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}
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.

Source

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)}

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)}
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.

Source

a {
  content: 'hello world';
}

Expected

a{content:"hello world"}

Outputs

clean-css

a{content:'hello world'}

csskit

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"}
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).

Source

a {
  transition-duration: 500ms;
}

Expected

a{transition-duration:.5s}

Outputs

clean-css

a{transition-duration:.5s}

csskit

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}
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.

Source

a {
  font-size: 12pt;
}

Expected

a{font-size:16px}

Outputs

clean-css

a{font-size:12pt}

csskit

a{font-size:16px}

cssnano

a{font-size:12pt}

csso

a{font-size:12pt}

esbuild

a{font-size:12pt}

lightningcss

a{font-size:12pt}

sass

a{font-size:12pt}
0011
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.

Source

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)}

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)}
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.

Source

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)}

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)}
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.

Source

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)}

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)}
0014
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).

Source

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)}

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)}
0015
Details

Calc constant folding

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

Source

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

Expected

a{width:200px}

Outputs

clean-css

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

csskit

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

cssnano

a{width:200px}

csso

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

esbuild

a{width:200px}

lightningcss

a{width:200px}

sass

a{width:200px}
0016
Details

Calc addition folding

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

Source

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

Expected

a{width:100px}

Outputs

clean-css

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

csskit

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

cssnano

a{width:100px}

csso

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

esbuild

a{width:100px}

lightningcss

a{width:100px}

sass

a{width:100px}
0017
Details

Calc partial reduction

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

Source

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))}

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)}
0018
Details

Trailing zero removal

0.50000 should be reduced to .5 by removing trailing zeros and leading zero.

Source

a {
  opacity: 0.50000;
}

Expected

a{opacity:.5}

Outputs

clean-css

a{opacity:.5}

csskit

a{opacity:.5}

cssnano

a{opacity:.5}

csso

a{opacity:.5}

esbuild

a{opacity:.5}

lightningcss

a{opacity:.5}

sass

a{opacity:.5}
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.

Source

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  ...."}

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  ...."}
0020
Details

Background position center to percentage

center center resolves to 50% -- when both axes are the same, a single value suffices.

Source

a {
  background-position: center center;
}

Expected

a{background-position:50%}

Outputs

clean-css

a{background-position:center center}

csskit

a{background-position:center center}

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}
0021
Details

Calc division with irrational result must keep calc

calc(100% / 3) cannot be statically resolved to a finite decimal without precision loss. Any truncation (e.g. 33.3333%) introduces measurable rounding error. The calc wrapper must be preserved; only whitespace is removable.

Source

a {
  width: calc(100% / 3);
}

Expected

a{width:calc(100%/3)}

Outputs

clean-css

a{width:calc(100% / 3)}

csskit

a{width:calc(100% / 3)}

cssnano

a{width:33.33333%}

csso

a{width:calc(100%/3)}

esbuild

a{width:calc(100% / 3)}

lightningcss

a{width:33.3333%}

sass

a{width:33.3333333333%}
0022
Details

Multi-keyword display to single keyword

display: block flow is the two-value syntax equivalent of display: block.

Source

a {
  display: block flow;
}

Expected

a{display:block}

Outputs

clean-css

a{display:block flow}

csskit

a{display:block flow}

cssnano

a{display:block}

csso

a{display:block flow}

esbuild

a{display:block flow}

lightningcss

a{display:block}

sass

a{display:block flow}
0023
Details

cubic-bezier to linear keyword

cubic-bezier(0, 0, 1, 1) is the functional form of linear.

Source

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)}

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)}
0024
Details

cubic-bezier to ease keyword

cubic-bezier(0.25, 0.1, 0.25, 1) is the functional form of ease.

Source

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)}

cssnano

a{transition:color ease}

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)}
0025
Details

Two-value background-repeat collapse

no-repeat no-repeat collapses to no-repeat when both axes are identical.

Source

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}

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}
0026
Details

Two-value background-repeat to repeat-x

repeat no-repeat is equivalent to the repeat-x keyword.

Source

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}

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}
0027
Details

Turn to degrees conversion

0.25turn equals 90deg. Degrees are shorter for common turn fractions.

Source

a {
  transform: rotate(0.25turn);
}

Expected

a{transform:rotate(90deg)}

Outputs

clean-css

a{transform:rotate(.25turn)}

csskit

a{transform:rotate(.25turn)}

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)}
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.

Source

a {
  min-width: initial;
}

Expected

a{min-width:auto}

Outputs

clean-css

a{min-width:initial}

csskit

a{min-width:initial}

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}
0029
Details

Border shorthand canonical order

border: solid 1px red should be reordered to border: 1px solid red (width style color).

Source

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}

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}
0030
Details

flex-flow canonical order

flex-flow: wrap row should be reordered to flex-flow: row wrap (direction then wrap).

Source

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}

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}
0031
Details

Unquote font-family name

Quotes around font-family names can be removed when the name is a valid CSS identifier sequence.

Source

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"}

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"}
0032
Details

Deduplicate font-family list

Duplicate font names in a font-family list can be removed.

Source

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}

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}
0033
Details

Multi-keyword display: inline flow-root to inline-block

display: inline flow-root is the two-value equivalent of inline-block.

Source

a {
  display: inline flow-root;
}

Expected

a{display:inline-block}

Outputs

clean-css

a{display:inline flow-root}

csskit

a{display:inline flow-root}

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}
0034
Details

steps(1, start) to step-start keyword

steps(1, start) is the functional form of the step-start timing keyword.

Source

a {
  animation: fade steps(1, start);
}

Expected

a{animation:fade step-start}

Outputs

clean-css

a{animation:steps(1,start) fade}

csskit

a{animation:fade steps(1,start)}

cssnano

a{animation:fade step-start}

csso

a{animation:fade steps(1,start)}

esbuild

a{animation:fade steps(1,start)}

lightningcss

a{animation:step-start fade}

sass

a{animation:fade steps(1, start)}
0035
Details

background-position keywords to numeric

left top is equivalent to 0 0 and shorter.

Source

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}

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}
0036
Details

cubic-bezier to ease-in keyword

cubic-bezier(0.42, 0, 1, 1) is the functional form of ease-in.

Source

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)}

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)}
0037
Details

cubic-bezier to ease-out keyword

cubic-bezier(0, 0, 0.58, 1) is the functional form of ease-out.

Source

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)}

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)}
0038
Details

Two-value background-repeat to repeat-y

no-repeat repeat is equivalent to the repeat-y keyword.

Source

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}

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}
0039
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.

Source

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

Expected

a{width:100px}

Outputs

clean-css

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

csskit

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

cssnano

a{width:100px}

csso

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

esbuild

a{width:100px}

lightningcss

a{width:100px}

sass

a{width:100px}
0040
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.

Source

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

Expected

a{width:100px}

Outputs

clean-css

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

csskit

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

cssnano

a{width:100px}

csso

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

esbuild

a{width:100px}

lightningcss

a{width:100px}

sass

a{width:100px}
0041
Details

Calc double negation elimination

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

Source

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

Expected

a{margin-left:10px}

Outputs

clean-css

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

csskit

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

cssnano

a{margin-left:10px}

csso

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

esbuild

a{margin-left:10px}

lightningcss

a{margin-left:10px}

sass

a{margin-left:10px}
0042
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.

Source

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))}

cssnano

a{width:calc(1 / (1 / 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}
0043
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.

Source

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))}

cssnano

a{width:100px}

csso

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

esbuild

a{width:100px}

lightningcss

a{width:100px}

sass

a{width:100px}
0044
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.

Source

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)}

cssnano

a{width:60px}

csso

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

esbuild

a{width:60px}

lightningcss

a{width:60px}

sass

a{width:60px}
0045
Details

Calc distribute multiplication over sum

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

Source

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))}

cssnano

a{width:150px}

csso

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

esbuild

a{width:150px}

lightningcss

a{width:150px}

sass

a{width:150px}
0046
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.

Source

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

Expected

a{width:75%}

Outputs

clean-css

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

csskit

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

cssnano

a{width:75%}

csso

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

esbuild

a{width:75%}

lightningcss

a{width:75%}

sass

a{width:75%}
0047
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.

Source

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)}

cssnano

a{line-height:5}

csso

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

esbuild

a{line-height:5}

lightningcss

a{line-height:5}

sass

a{line-height:5}
0048
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).

Source

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

Expected

a{width:0}

Outputs

clean-css

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

csskit

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

cssnano

a{width:0}

csso

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

esbuild

a{width:0px}

lightningcss

a{width:0}

sass

a{width:0px}
0049
Details

Calc multiply by zero

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

Source

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

Expected

a{width:0}

Outputs

clean-css

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

csskit

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

cssnano

a{width:0}

csso

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

esbuild

a{width:0px}

lightningcss

a{width:0}

sass

a{width:0px}
0050
Details

Calc constant pi resolution

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

Source

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)}

cssnano

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

csso

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

esbuild

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

lightningcss

a{width:314.159px}

sass

a{width:314.159265359px}
0051
Details

Calc constant e resolution

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

Source

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)}

cssnano

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

csso

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

esbuild

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

lightningcss

a{width:271.828px}

sass

a{width:271.8281828459px}
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.

Source

a {
  width: min(10px, 20px);
}

Expected

a{width:10px}

Outputs

clean-css

a{width:min(10px,20px)}

csskit

a{width:min(10px,20px)}

cssnano

a{width:min(10px,20px)}

csso

a{width:min(10px,20px)}

esbuild

a{width:min(10px,20px)}

lightningcss

a{width:10px}

sass

a{width:10px}
0053
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.

Source

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

Expected

a{width:96px}

Validate

Outputs

clean-css

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

csskit

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

cssnano

a{width:1in}

csso

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

esbuild

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

lightningcss

a{width:96px}

sass

a{width:1in}
0054
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.

Source

a {
  width: calc(100px);
}

Expected

a{width:100px}

Outputs

clean-css

a{width:calc(100px)}

csskit

a{width:calc(100px)}

cssnano

a{width:100px}

csso

a{width:calc(100px)}

esbuild

a{width:100px}

lightningcss

a{width:100px}

sass

a{width:100px}
0055
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.

Source

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

Expected

a{width:25px}

Validate

Outputs

clean-css

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

csskit

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

cssnano

a{width:25px}

csso

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

esbuild

a{width:25px}

lightningcss

a{width:25px}

sass

a{width:25px}
0056
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.

Source

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%)}

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%)}
0057
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.

Source

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)}

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)}
0058
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.

Source

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)}

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)}
0059
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.

Source

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}

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}
0060
Details

url() quotes required when URL contains 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 ).

Source

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")}

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")}
0061
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.

Source

a:before {
  content: "";
}

Expected

a:before{content:""}

Outputs

clean-css

a:before{content:""}

csskit

a:before{content:""}

cssnano

a:before{content:""}

csso

a:before{content:""}

esbuild

a:before{content:""}

lightningcss

a:before{content:""}

sass

a:before{content:""}
0062
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.

Source

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)}

cssnano

a{width:clamp(10px,10px,10px)}

csso

a{width:clamp(10px,10px,10px)}

esbuild

a{width:clamp(10px,10px,10px)}

lightningcss

a{width:10px}

sass

a{width:10px}
0063
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.

Source

a {
  width: max(10px, 20px);
}

Expected

a{width:20px}

Outputs

clean-css

a{width:max(10px,20px)}

csskit

a{width:max(10px,20px)}

cssnano

a{width:max(10px,20px)}

csso

a{width:max(10px,20px)}

esbuild

a{width:max(10px,20px)}

lightningcss

a{width:20px}

sass

a{width:20px}
0064
Details

opacity: initial to 1

The initial value of opacity is 1, which is shorter than initial.

Source

a {
  opacity: initial;
}

Expected

a{opacity:1}

Outputs

clean-css

a{opacity:initial}

csskit

a{opacity:initial}

cssnano

a{opacity:1}

csso

a{opacity:initial}

esbuild

a{opacity:initial}

lightningcss

a{opacity:initial}

sass

a{opacity:initial}
0065
Details

display: initial must not shorten

The initial value of display is inline, which is the same length as initial. No replacement saves bytes.

Source

a {
  display: initial;
}

Expected

a{display:initial}

Outputs

clean-css

a{display:initial}

csskit

a{display:initial}

cssnano

a{display:initial}

csso

a{display:initial}

esbuild

a{display:initial}

lightningcss

a{display:initial}

sass

a{display:initial}
0066
Details

z-index: initial to auto

The initial value of z-index is auto, which is shorter than initial.

Source

a {
  z-index: initial;
}

Expected

a{z-index:auto}

Outputs

clean-css

a{z-index:initial}

csskit

a{z-index:initial}

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}
0067
Details

margin: initial to 0

The initial value of margin is 0, which is shorter than initial.

Source

a {
  margin: initial;
}

Expected

a{margin:0}

Outputs

clean-css

a{margin:initial}

csskit

a{margin:initial}

cssnano

a{margin:initial}

csso

a{margin:initial}

esbuild

a{margin:initial}

lightningcss

a{margin:initial}

sass

a{margin:initial}
0068
Details

background-color: initial must not shorten

The initial value of background-color is transparent, which is longer than initial. No replacement saves bytes.

Source

a {
  background-color: initial;
}

Expected

a{background-color:initial}

Outputs

clean-css

a{background-color:initial}

csskit

a{background-color:initial}

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}
0069
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.

Source

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%)}

cssnano

a{width:100px}

csso

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

esbuild

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

lightningcss

a{width:100px}

sass

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

Custom property value can be minified with @property constraint

When @property declares syntax: "&lt;color&gt;", 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.

Source

@property --brand-color {
  syntax: "&lt;color&gt;";
  inherits: false;
  initial-value: #000;
}
a {
  --brand-color: rgb(0 0 0);
}

Expected

@property --brand-color{syntax:"&lt;color&gt;";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)}

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)}
Subtotal 16 / 70 18 / 70 57 / 70 19 / 70 34 / 70 55 / 70 31 / 70

whitespace

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Basic whitespace removal

Remove newlines, indentation, and spaces around braces and colons.

Source

a {
  color: red;
}

Expected

a{color:red}

Outputs

clean-css

a{color:red}

csskit

a{color:red}

cssnano

a{color:red}

csso

a{color:red}

esbuild

a{color:red}

lightningcss

a{color:red}

sass

a{color:red}
0002
Details

Inter-rule whitespace

Blank lines between rules must be collapsed. No separator is needed between } and the next selector.

Source

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}

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}
0003
Details

Tab characters and multi-line values

Values split across lines with tab indentation must collapse correctly.

Source

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}

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}
0004
Details

Selector list whitespace

Spaces around commas in selector lists (a , b) must be removed.

Source

a,
b {
  color: red;
}

Expected

a,b{color:red}

Outputs

clean-css

a,b{color:red}

csskit

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}
0005
Details

Trailing semicolon removal

The semicolon after the last declaration in a rule can be dropped.

Source

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}

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}
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.

Source

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}

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}
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.

Source

@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}}

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}}
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.

Source

@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}}

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}}
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.

Source

@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}}

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}}
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 (.

Source

@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}}

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}}
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.

Source

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)}

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)}
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.

Source

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

Expected

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

Validate

Outputs

clean-css

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

csskit

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

cssnano

a{width:200%}

csso

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

esbuild

a{width:200%}

lightningcss

a{width:200%}

sass

a{width:200%}
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.

Source

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)}

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)}
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.

Source

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}

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}
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.

Source

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}

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}
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.

Source

div p {
  color: red;
}

Expected

div p{color:red}

Validate

Outputs

clean-css

div p{color:red}

csskit

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}
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.

Source

a {
  border: solid #fff;
}

Expected

a{border:solid#fff}

Outputs

clean-css

a{border:solid #fff}

csskit

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}
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.

Source

a {
  color: red !important;
}

Expected

a{color:red!important}

Outputs

clean-css

a{color:red!important}

csskit

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}
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.

Source

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)}

cssnano

a{font-size: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)}
Subtotal 13 / 19 15 / 19 14 / 19 16 / 19 13 / 19 15 / 19 12 / 19

zero-units

testclean-csscsskitcssnanocssoesbuildlightningcsssass
0001
Details

Strip unit on zero length

0px equals 0 for length properties. The unit is redundant.

Source

a {
  margin: 0px;
}

Expected

a{margin:0}

Outputs

clean-css

a{margin:0}

csskit

a{margin:0}

cssnano

a{margin:0}

csso

a{margin:0}

esbuild

a{margin:0}

lightningcss

a{margin:0}

sass

a{margin:0px}
0002
Details

Keep unit on zero time

0s must NOT be simplified to 0. Unitless zero is invalid for &lt;time&gt; values. Stripping the unit breaks transitions and animations.

Source

a {
  transition-delay: 0s;
}

Expected

a{transition-delay:0s}

Outputs

clean-css

a{transition-delay:0s}

csskit

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}
0003
Details

Keep unit on zero percent

0% is not always equivalent to 0 (e.g. in width). The unit must be preserved.

Source

a {
  width: 0%;
}

Expected

a{width:0%}

Outputs

clean-css

a{width:0%}

csskit

a{width:0%}

cssnano

a{width:0}

csso

a{width:0%}

esbuild

a{width:0%}

lightningcss

a{width:0%}

sass

a{width:0%}
0004
Details

Zero-unit strip in padding

padding-block: 0px 10px can strip 0px to 0

Source

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}

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}
0005
Details

Leading zero removal

0.5em can be shortened to .5em.

Source

a {
  margin: 0.5em;
}

Expected

a{margin:.5em}

Outputs

clean-css

a{margin:.5em}

csskit

a{margin:.5em}

cssnano

a{margin:.5em}

csso

a{margin:.5em}

esbuild

a{margin:.5em}

lightningcss

a{margin:.5em}

sass

a{margin:.5em}
0006
Details

Trailing zero removal

1.0 can be shortened to 1. Trailing fractional zeros are redundant.

Source

a {
  opacity: 1;
}

Expected

a{opacity:1}

Outputs

clean-css

a{opacity:1}

csskit

a{opacity:1}

cssnano

a{opacity:1}

csso

a{opacity:1}

esbuild

a{opacity:1}

lightningcss

a{opacity:1}

sass

a{opacity:1}
0007
Details

Drop unit from zero-length flex-basis

flex-basis: 0px can be shortened to flex-basis: 0. Unitless zero is valid for &lt;length&gt; values, and 0px and 0 resolve identically.

Source

a {
  flex-basis: 0px;
}

Expected

a{flex-basis:0}

Outputs

clean-css

a{flex-basis:0px}

csskit

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}
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.

Source

a {
  rotate: 0deg;
}

Expected

a{rotate:0deg}

Outputs

clean-css

a{rotate:0deg}

csskit

a{rotate:0deg}

cssnano

a{rotate:0deg}

csso

a{rotate:0deg}

esbuild

a{rotate:0deg}

lightningcss

a{rotate:0deg}

sass

a{rotate:0deg}
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.

Source

@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}}

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}}
0010
Details

0fr must keep unit

Unitless zero is invalid for &lt;flex&gt; values. 0fr in grid track sizing must not be stripped to 0, which would be parsed as a &lt;length&gt;.

Source

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}

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}
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.

Source

a {
  flex-basis: 0%;
}

Expected

a{flex-basis:0%}

Outputs

clean-css

a{flex-basis:0%}

csskit

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%}
Subtotal 9 / 11 11 / 11 9 / 11 10 / 11 9 / 11 11 / 11 8 / 11