CSS格式化美化

把压成一整行的 CSS 展开成每条声明独占一行、缩进对齐的样子。逐条规则文本级重排,不解析语法树,所以 content 属性、注释和 url() 里的分号与大括号不会被误切。

重排方式

已按 2 空格展开。上面这段示例的输入是 383 字节 / 5 行,输出是 475 字节 / 31 行。

重排由一个单趟状态机完成,识别的保护上下文有四个:块注释 /* */、双引号串、单引号串、未加引号的 url( ... )。

使用方法

  1. 把 CSS 粘进左边「粘贴 CSS」。本页的 CSS格式化美化 会把它重排成缩进对齐的样子。默认已预填一段 383 字节的示例(含 @media 嵌套和 @supports),你直接点重排就能看到效果。
  2. 选缩进档位:在「重排方式」里选展开(每条声明独占一行,默认)或压成一行(每条规则压成一行,适合塞进 HTML 的 style 属性)。缩进宽度在输入框下方的下拉里选 2 空格、4 空格或制表符。
  3. 需要按属性名字典序整理同一规则内的声明时,勾上「声明内按属性名排序」。这个选项只在「压成一行」模式下有效果——展开模式下每条声明已经各占一行,没有可排序的对象。
  4. 点「重排 CSS」。多行输入框里 Enter 是换行,要触发计算请按 Ctrl+Enter(Mac 是 Cmd+Enter)。
  5. 点「复制结果」把重排后的 CSS 带走,或点「导出 CSV」拿到 12 行「片段 → 字节数」对照表,用来看你的 CSS 到底被压缩成了什么样。

这个 CSS格式化美化 能解决什么问题

一是接手别人压过的样式表。前端项目里经过构建工具压缩的 CSS 常是「一整行几百个规则」,人眼根本没法 review。用 CSS格式化美化 展开后,每条声明独占一行、缩进对齐,grep 一个类名就能看到它全部的声明,review 效率完全不同。CSS格式化美化 只动空白和换行,所以你可以放心地把它接进任何项目的本地工作流。

二是定位「我的样式为什么没生效」。压缩过的 CSS 里如果有一条 content:"a;b" 被错误地按分号切断了,展开后你立刻能看到哪一条声明被劈成两半;同理,注释里的 /* {;} */、data URI 里的 url(data:image/svg+xml;base64,...) 也一眼看得出有没有被破坏。

三是统一团队的缩进风格。有人用 2 空格、有人用 4 空格、有人用 Tab,改一次全站对齐比在编辑器里手动调快得多。页面提供 2 空格 / 4 空格 / 制表符三个档位,选定后整份文件按同一规则重排。

四是给压缩前做体检。页面下方那张 12 行对照表逐个列出常见写法的「输入字节 → 输出字节 → 规则数 → 声明数」,你把自己的片段粘进去对照一下,就知道这份 CSS 里哪部分最占体积。

本页不做这些事:不解析 CSS 语法树(不做 AST 重写、不做语义级合并、不删冗余选择器)、不改属性值内部的内容(calc()、var()、clamp() 一律原样保留)、也不做 Lint 查错。要把 CSS 体积真正压小,请用「CSS压缩工具」;要查语法错误和浏览器兼容性,请用「CSS校验」。

原理和变量:一个状态机,四类保护上下文

先说清定位:这是逐规则文本级重排,不是 AST 解析。整个工具只有一个从左到右扫一遍的状态机,靠「花括号配对 + 分号切分 + 缩进计数」把文本重新断开。它不理解选择器优先级、层叠顺序、也不做 Shorthand 展开或重复声明合并。真正的 AST 工具(比如 PostCSS 生态)要先把 CSS 解析成节点树,代价是体积大一个数量级,且必须处理自定义属性、@supports 条件等所有边缘语法。本页刻意选文本级路线,代价是不理解语义,好处是不改一个字符的语义——只动空白和换行。

四个必须保护的地方。这是本页存在的核心理由,也是绝大多数「一键美化」出错的根源:

① 块注释 /* */。注释里出现分号和大括号是合法的,/* a;b;c */ 里的三个分号不是声明分隔符。状态机扫到 /* 就跳到匹配的 */,整段原样吐出,一个字符都不动。

② 双引号和单引号字符串。content:"a;b" 里的分号、content:"{}" 里的两个花括号都是字面量内容,不是语法。扫到引号就找配对的闭引号(中间遇 \ 跳过下一个字符),整段收下。转义引号 content:"\"" 也正确处理——反斜杠让状态机跳过紧跟的那个引号,不会误判为结束。

③ 未加引号的 url( ... )。这是最容易漏掉的一处:background:url(data:image/svg+xml;base64,PHN2Zy8+) 里的分号是 data URI 的一部分,而 CSS 语法允许 url() 内不加引号。状态机识别 url( 后一直读到配对的 ),中间无论出现什么字符都当作字面量。漏掉这一条,图标和内联 SVG 的背景就会全部被切碎。

④ 块注释之外的 // 行注释。这不是合法 CSS,所以工具不识别它——如果你在 CSS 里写了 //,页面会把它当普通字符处理并给出提示。

补分号这条规则。CSS 允许最后一条声明省略分号。重排器在遇到 } 时会检查缓冲区里剩下的内容,如果末尾没有 ; 就补上。所以 a{color:red} 会变成三行的 color:red;。这不是「多加了东西」,而是把省略的语法显式化,让每条声明长得一样。

缩进怎么算。缩进宽度 = 当前花括号深度 × 每层宽度。示例的 @media 块里 .card 的声明深度是 1,所以缩进 2 空格,.card 的选择器本身缩进 0。同一个片段换 4 空格,输出从 475 字节变成 527 字节;换成制表符则降到 449 字节——制表符最省体积,因为 1 个字节就顶 1 层缩进,而 4 空格要 4 个字节。

「压成一行」用的是栈,不是拼接。把一条规则连同它内部所有嵌套规则压成一行时,需要知道每层规则的边界,所以用了帧栈:每遇 { 压一帧({head, decls, kids, ind}),每遇 } 弹一帧、拼成一行、再 push 回父帧。不用栈就会错位——@media 里的规则会跑到 @media 外面去。缩进值随帧一起存(ind),所以收口时不受深度已递减的影响。

一个可以验证的性质:幂等性。把输出再喂回输入,结果应逐字节完全相同。示例片段跑两次都是 475 字节 / 31 行,完全一致。幂等不成立就说明重排器有状态残留 bug(比如缓冲区没清干净),这是最省事的自检方法。compact 模式同样幂等(425 字节两次一致)。

完整示例(数值仅作演示)

示例条件与页面打开时的默认值完全一致:383 字节 / 5 行的压缩片段、展开模式、2 空格缩进、不排序。逐项实测结果如下:

结果框在页面打开时就填着上面这段展开结果(服务端直出),你点一次「重排 CSS」会得到完全一样的内容——这是用来确认状态机没坏的基准值。

输入范围与对照表

输入上限是字符数而不是字节数,因为状态机是逐字符扫描的:超过 200000 字符会先提示,超过 500000 字符直接拒绝并给出具体数字。实测一台普通笔记本上 2000 条规则的片段(81745 字节)单趟重排耗时约 10 毫秒,所以性能不是瓶颈,DOM 才是——几十万行塞进 <pre> 会让标签页卡住。

行常见写法声明数规则数输入字节输出字节(2 空格)输出行数
1a{color:red;background:blue} 基础选择器2128364
2h1,h2,h3{margin:0;font-weight:700} 多选择器组2134424
3a:hover{text-decoration:underline;color:#06c} 伪类2145534
4@media(min-width:768px){.x{display:flex;gap:8px}} 嵌套2249686
5content:"a;b" 含分号2134424
6content:"{}" 含花括号2238464
7/* a;b;c */ 注释含分号2134435
8/* {;} */ 注释含花括号2231405
9:root{--gap:8px;--fg:#333} 自定义属性2126344
10calc() 与 url("a;b.png")2153614
11linear-gradient() 多参数1158643
12url(data:...;base64,) data URI1164703

第 5、7、12 行是本页存在的理由:这三行里的分号如果被当成分隔符,输出会明显不对。你可以在输入框里粘第 12 行那个 data URI 片段试一下,输出里 data:image/svg+xml;base64, 会完整保留、一个字符不少——如果它被切开了,说明保护逻辑被破坏了。

边界与精度

常见问题

格式化会不会改变我的 CSS 行为?

不会改变语义,只改空白和换行——包括补上省略掉的末尾分号(CSS 本来就允许省略,补上等价)。但有三件事它确实会动,你得知道:一是缩进空白,选 2 空格会把原本的 Tab 和 4 空格全换成 2 空格;二是在「压成一行」模式下,原有的换行会被合并成一行,如果你的 HTML 里有 <pre> 包着这段 CSS,显示会变;三是如果你在字符串或注释里手写了换行(多行 data URI、多行注释),压缩成一行会把它们接在一起——这种内容按 CSS 语义来说空白不敏感,但如果你在注释里写了版权信息、指望靠换行排版,那会失效。

为什么不做 AST 解析,那样不是更准吗?

更准,但代价不成比例。AST 方案要引入完整的 CSS 语法分析器,必须处理 @supports 条件、--自定义属性(它的值按 CSS Variables 规范可以是任意 token 序列)、@media 查询里的复杂表达式、以及各种厂商前缀残留;更麻烦的是现代 CSS 语法还在演进,解析器遇到不认识的语法就会抛错,反而更不可靠。文本级路线的核心优势是保真:它不「理解」任何东西,只把不在保护上下文里的分号和花括号当成结构标记,所以你写什么它就格式化什么,绝不会因为「看不懂」而丢弃内容。对「让压缩过的 CSS 能被人 review」这个目标来说,够用了。

我的 data URI 背景图被切碎了,怎么办?

先确认它是不是在未加引号的 url() 里。正确写法是 url(data:image/svg+xml;base64,PHN2Zy8+) 或 url("data:image/svg+xml;base64,PHN2Zy8+"),两者页面都能正确保护——引号串和未加引号 url() 是两个独立的保护上下文。如果你写成了 background:url(data:image/svg+xml;base64,...) no-repeat 这样的简写,中间的 no-repeat 是在 url() 括号外面的,状态机读到 ) 就结束保护,后面的分号仍按声明分隔符处理,这是正确的。真正出问题的写法是自己手写 CSS 时把 base64 串换行了(data URI 允许换行,但换行符要写成 %0A 而不是真的回车)。

注释会被删掉吗?

不会。块注释 /* ... */ 原样保留,占独立一行(前面按当前深度缩进),内部字符一个不改。这和「CSS压缩工具」的行为正好相反——压缩会删掉所有注释以省体积。本页的用途是给人看,所以注释是重要的上下文(比如 /* 覆盖上面的规则,勿删 */)。唯一不被识别的是 // 行注释,因为它不是合法 CSS,浏览器解析时会直接报错,页面遇到它会给出提示。

「声明内按属性名排序」为什么在展开模式下没反应?

因为展开模式下每条声明已经独占一行,没有「一组声明」这个对象可供排序。排序只在「压成一行」模式下有意义:这时一条规则里的多条声明挤在同一行,顺序才有可读性价值。算法是在规则收口时(弹帧那一刻)把这层的声明按冒号前的属性名字典序重排,选择器固定在首位。实测示例的第一条规则排序前后都是 425 字节,顺序从 padding; border-radius; background; box-shadow 变成 background; border-radius; box-shadow; padding。注意排序不改变字节数,只是换个顺序——如果你要的是「按字母序 review 便于对照规范」,这个功能有用;如果你期待它能减小体积,那是误解。

格式化后体积涨了 24%,上线会不会变慢?

会影响,但通常可以忽略,而且正确做法本来就不该直接上线。示例里 383 字节涨到 475 字节,对单次请求来说几 KB 的差异几乎测不出来——真正影响加载的是图片和字体。更重要的是:未压缩的 CSS 会让 gzip/br 的压缩率变差,因为杂乱空白让重复模式(相同的属性组合、相似的选择器)更难被熵编码器识别。经验做法是「格式化用于 review 和维护 → 提交前用构建工具压缩 → 线上跑压缩版」。你可以在本页点「压成一行」看一眼那一版的字节数,它介于两者之间,比展开版小但仍不是最优压缩。

支持 SCSS / Less / Tailwind 语法吗?

部分支持,但不保证。这些预处理器有 CSS 之外的神语法:SCSS 的 $var、嵌套、@mixin,Less 的 @var、.mixin(),Tailwind 的 @apply 和 @layer。其中嵌套和 @mixin 这类花括号块,本页的状态机会当成普通规则处理并正确缩进;但 $var: value 这种以 $ 开头的行不会被识别为声明(因为它没有冒号前的属性名形态),可能出现错位。建议先让构建工具把 SCSS 编译成纯 CSS 再来格式化,这样能得到可靠结果。要把 Tailwind 的 @apply 类名转成实际 CSS 属性,请用「CSS转Tailwind转换器」做反向操作。

我的数据会传到服务器吗?

不会。重排完全在你浏览器里完成,工具只用了 String.prototype.replace、indexOf、slice 这些原生字符串方法,不发任何网络请求,也没有引入任何第三方库(既没有 CDN 脚本,也没有外部 .js 文件)。你在开发者工具的 Network 面板刷新页面,除了 HTML 和本页那个内联 script 之外看不到别的东西。粘进来的 CSS 不写入浏览器存储,刷新即清空。导出的 CSV 是在本地用 Blob 生成的,文件直接进你的下载目录,不经过服务器。

相关工具