您不再需要JavaScript

本页面纯CSS实现,无JavaScript

如今的网络充斥着现代JavaScript框架带来的臃肿问题。React应用加载需数秒之久,NextJS站点频发随机hydration错误,更别提占用硬盘数GB空间的node_modules文件夹。

太糟糕了。你根本不需要这些。

Name Status Type Size Time
app200document153.8 kB51 ms
6920616d20612066-s.p.6f6e7421.woff2200font31.5 kB32 ms
686579206d652074-s.p.6f6f2121.woff2200font28.5 kB116 ms
77687920646f6573.css200stylesheet253 kB47 ms
2074686520646566.js200script648 kB83 ms
61756c74206e6578.js200script166 kB363 ms
746a732074616b65.js200script83.3 kB46 ms
turbopack-20757020302e354d.js200script38.0 kB95 ms
423f207468617427.js200script414 B34 ms
73206d6f72652074.js200script32.6 kB49 ms
68616e206d792065.js200script15.1 kB71 ms
6e7469726520626c.js200script143 kB48 ms
6f6721 hey there!200script4.1 kB103 ms

本文开篇段落实则戏谑,旨在吸引你继续阅读。我怀疑那些与劣质代码交织的兆字节级追踪脚本,才是导致糟糕网站泛滥的真正元凶。Web框架自有其适用场景。尽管我个人厌恶它们,但承认许多团队确实用它们构建出卓越优化的应用。

即便如此,我仍认为抛弃这一切别有韵味——不仅是框架,更是彻底告别JavaScript。并非每个网站都需要JavaScript。或许你的电商网站需要它来实现复杂的购物车和数据可视化仪表盘,但对于绝大多数网站而言,它真的不可或缺吗?

实际上,仅凭HTML和CSS就能实现的效果着实令人惊叹。

那么,你怎么看?

本文旨在分享我对网络技术的见解,并介绍现代HTML/CSS中许多你可能不熟悉的特性。我无意让你放弃JavaScript,只是想展示所有可能的实现方式,最终选择权仍在你手中,让你根据具体项目需求做出最佳决策。

我认为多数网页开发者对CSS存在认知盲区。

而JS常被用于存在更优解的场景。

现在就让我带你探索这些可能性。

“但CSS很糟糕”

我认为对CSS的诸多负面评价源于不掌握其使用方法。许多开发者跳过CSS基础学习,转而钻研更有趣的Java和TypeScript,最终却对这种他们不理解的样式语言抱怨连连。

我怀疑这源于多数人将CSS视为给网页应用添加边框和盒阴影的笨拙第三轮。它被严重低估,常被比作华而不实的蜡笔,而非其真实本质——一种强大的领域特定编程语言。

时至今日,网页开发圈里唯一的CSS梗仍是div居中对齐,这本身就很有深意。

i am a div
body {
display: flex;
flex-direction: rowcolumnrow-reversecolumn-reverse;
flex-wrap: nowrapwrap;
align-content: centerflex-startflex-endspace-aroundspace-betweenstretch;
justify-content: centerflex-startflex-endspace-aroundspace-betweenspace-evenly;
align-items: centerflex-startflex-endstretchbaseline;
}

没错,语法确实不够优雅,但它真的有那么难吗?

况且你的开发工具里, 1 大概自带个有趣的小工具,只需点击就能随意调整弹性盒布局。根本无需记住语法。

我认为CSS本质上并不比JS更难,但若你跳过其中一种的基础知识而只专注另一种,产生这种感觉也就不足为奇了。

“但编写过程太痛苦”

人们鄙视CSS的另一个原因是它过去编写起来确实糟糕透顶。这确实如此,或许正是为何会出现SassTailwind 2 这类工具。

但关键在于,它曾经是糟糕的。

🦊
Rebane@rebane2001
btw u should write css like cool-thing { display: flex; &[shadow] { box-shadow: 1px 1px #0007; } @media (width < 480px) { flex-direction: column; } } and html like <cool-thing shadow>wow</cool-thing> because it's allowed & modern & neat!

❤️ 1.5K

(没错!上述代码完全符合标准规范 3

近几年CSS获得了大量提升开发体验的特性,让许多过去需要预处理器或JavaScript实现的功能变得轻松可控。

嵌套功能绝对是我最喜爱的增强之一!

过去,你不得不编写这样的代码:

:root {
  --like-color: #24A4F3;
  --like-color-hover: #54B8F5;
  --like-color-active: #0A6BA8;
}

.post {
  display: block;
  background: #EEE;
  color: #111;
}

.post .avatar {
  width: 48px;
  height: 48px;
}

.post > .buttons {
  display: flex;
}

.post > .buttons .label {
  font-size: 24px;
  padding: 8px;
}

.post > .buttons .like {
  cursor: pointer;
  color: var(--like-color);
}

.post > .buttons .like:hover {
  color: var(--like-color-hover);
}

.post > .buttons .like:active {
  color: var(--like-color-active);
}

@media screen (max-width: 800px) {
  .post > .buttons .label {
    font-size: 16px;
    padding: 4px;
  }
}

@media (prefers-color-scheme: dark) {
  .post {
    background: #222;
    color: #FFF;
  }
}

没错,这简直糟糕透顶。凡是涉及多重链式选择器的场景,你都得在脑中构建一张父选择器与子选择器关系的思维地图——而CSS越复杂,这张地图就越难梳理。

但尝试使用嵌套结构:

:root {
  --like-color: #24A4F3;
  --like-color-hover: hsl(from var(--like-color) h s calc(l + 10));
  --like-color-active: hsl(from var(--like-color) h s calc(l - 20));
}

.post {
  display: block;
  background: #EEE;
  color: #111;
  @media (prefers-color-scheme: dark) {
    background: #222;
    color: #FFF;
  }
  .avatar {
    width: 48px;
    height: 48px;
  }
  & > .buttons {
    display: flex;
    .label {
      font-size: 24px;
      padding: 8px;
      @media (width <= 800px) {
        font-size: 16px;
        padding: 4px;
      }
    }
    .like {
      cursor: pointer;
      color: var(--like-color);
      &:hover { color: var(--like-color-hover); }
      &:active { color: var(--like-color-active); }
    }
  }
}

这样就清晰多了去读 4 ! !所有相关部分紧邻排列,逻辑关系一目了然。尤其值得称道的是,当看到&:hover&:active紧邻.like按钮时,这种布局效果令人称心。

由于能清晰看到结构——父级选择器“守护”着子级选择器——这使得使用简短的类名(甚至直接引用元素本身)变得更容易。

你可能注意到我在第二个示例中也运用了相对颜色。我认为MDN文章提供了大量精彩示例,其核心要义在于:你可以基于现有颜色,通过多种方式在不同色彩空间中进行修改,并借助color-mix()与其他颜色混合。

/* remove blue from a color */
rgb(from #123456 r g 0);
/* make a color transparent */
rgb(from #123456 r g b / 0.5);
/* make a color lighter */
hsl(from #123456 h s calc(l + 10));
/* change the hue in oklch color space */
oklch(from #123456 l c calc(h + 10));
/* mix two colors in oklab color space */
color-mix(in oklab, #8CFFDB, #04593B 25%);

这些代码片段在需要微调颜色明暗时非常实用,比如按钮悬停效果或匹配边框颜色,比起在JavaScript中进行繁琐的颜色转换,它们的使用体验要好得多。若您勇于尝试,甚至可直接用CSS生成完整配色方案。

100
200
300
400
500
600
700
800
900
-40°
-20°
+20°
+40°
主色
互补色
次要色
成功色
危险色
警告色
信息色
view-source

(没错!上面的颜色选择器仅用CSS编写)

Safari当前处理cqw/cqh单位时存在缺陷,因此上述演示可能无法正常运行。若遇此情况,请改用 Firefox 或 Chrome 浏览器。

CSS 新增了诸多酷炫特性,让编写过程更轻松。例如允许使用 (width <= 768px) 替代(max-width: 768px) @media查询条件中, 与行高匹配的lh单位,解决滚动条相关布局偏移的scrollbar-gutter属性,以及终于能垂直居中元素而无需flex/grid的特性。

Baseline

而这一切都由点睛之笔——基线——完美融合。这确保特定功能可在所有主流浏览器中正常运行 5 ,同时告知您该功能自何时起可用 - 新特性在所有最新浏览器中可用,而广泛可用的特性则支持长达2.5年前的旧版浏览器。Nesting, 例如,嵌套功能自2023年12月起已获得所有浏览器的完整支持,因此将在2026年6月成为广泛可用功能。您可在MDN文档等多种渠道找到基准符号 6

这些仅是现代CSS相较五年前版本更易编写的几个例证。这几乎如同将ES3 7 与ECMAScript 2025相比较——若您习惯前者,我完全理解您的不满。

何必费心?

好吧,CSS确实比以往更注重开发体验。但为何还要选择它而非其他方案?JavaScript不已足够满足所有需求了吗?

You need to disable JavaScript to run this app.

我使用CSS的理由主要有两点:部分用户拒绝使用JavaScript,以及某些场景下CSS确实能实现更优解。

以我的博客为例,它专注于信息安全领域。许多安全研究人员(包括我自己)会采用强化浏览器配置来保护自身安全,这通常意味着默认禁用JavaScript。我认为让用户无需更改安全设置或运行独立沙盒浏览器就能完整体验我的博客,这点很棒。

注重隐私的用户也是如此,这很合理!我做个实验:在启用JavaScript的浏览器中打开一个爱沙尼亚本地新闻网站。你能猜到它加载了多少个js文件吗?(答案见脚注 8 简直疯狂!你肯定不希望这些代码在自己电脑上运行。

但你肯定不是那种在网站上加载两位数分析脚本的恶意开发者吧——还有必要使用CSS吗?

嗯,我认为很多东西用HTML/CSS实现就是更优雅,无论是从开发者还是终端用户的角度,无论是易用性、可访问性还是性能。

按钮悬停效果?提示动画?表单验证?这些功能在CSS里直接就能实现,既不必重复造轮子,也不用塞进几千字节的他人代码。当然某些场景确实需要JavaScript的额外灵活性,但若无需此类功能且CSS实现更简便,何不省去麻烦?

况且CSS的性能优势更为显著!每次JavaScript交互都需经过事件循环处理,这不仅浪费CPU周期、消耗电池电量,还会让整个系统产生细微卡顿。

诚然,从宏观角度看这并非那么糟糕,诸如requestAnimationFrame这类API确实能有效保持流畅性。 但CSS动画在独立的合成器线程中运行,不受事件循环中的卡顿和阻塞影响。

这在低端设备上效果显著,即便在高端设备上也令人愉悦。在我的240Hz显示器上,CSS动画效果惊艳 9 ——JS动画同样出色,但存在细微卡顿影响完美体验,尤其当同时运行其他高负载代码时。

这还意味着你无需过度担心优化问题,因为浏览器承担了更多渲染工作,并在可能时优先调用GPU处理。

专业技巧!仍想通过JS触发动画?使用现代的Web Animations API即可轻松从JS播放流畅的CSS动画。

过渡效果

既然说到这里,我觉得是时候展示实际案例了,而展示这些样式的最佳起点就是起始样式,具体来说就是 @starting-style

过去给元素添加起始动画(比如淡入效果)相当麻烦。要么得单独设置整套CSS动画并搭配专属的@keyframes块,要么就得用JavaScript实现过渡效果——先将元素添加到页面,等待一帧时间,再给元素添加类。

.toast { transition: opacity 1s, translate 1s; opacity: 1; translate: 0 0; @starting-style { opacity: 0; translate: 0 10px; } }
Success!

但这一切都因全新的@starting-styleat-rule而改变!

你只需像往常一样设置属性,将初始过渡状态添加到@starting-style,再将这些属性加入过渡即可。操作非常简单,而且无需触发动画就能正常运行

Lunalover

CSS 另一个闪光点在于主题设计。许多网站需要独立的浅色和深色模式,而现代CSS让处理这些模式变得相当容易。

:root { color-scheme: light dark; --text: light-dark(#000, #FFF); --bg: light-dark(#EEE, #242936); }

hi there!

you are awesome!

通过将color-scheme属性设置为light dark,你便告知浏览器根据用户偏好自动选择主题,随后可借助light-dark ()函数来设置颜色值。

这不仅能自定义颜色,还能调整原生组件的样式——比如默认按钮、表单元素和滚动条。它本质上就是让东西默认就能正常工作,这点很棒!

:root { color-scheme: light dark; &:has(#theme-light:checked) { color-scheme: light; } &:has(#theme-dark:checked) { color-scheme: dark; } }

随后可添加覆盖color-scheme属性的机制,允许用户选择与系统设置不同的主题。此处我采用单选按钮实现该功能。

专业技巧!虽然CSS无法保存主题偏好设置,但仍可采用渐进增强方案:先通过纯CSS实现主题切换功能,再通过JavaScript或服务器端代码添加可选的偏好保存/加载功能。

竖琴与手风琴

“但这些看起来不像单选按钮啊”我听见你这样喊道。

单选按钮和复选框等输入元素是构建其他功能的绝佳基础——上例包含按钮标签和不可见的单选按钮,可通过:checked伪类进行检测。

<radio-picker aria-label="Radio buttons example" role="radiogroup"> <label><input type="radio" name="demo" id="veni" checked>veni</label> <label><input type="radio" name="demo" id="vidi">vidi</label> <label><input type="radio" name="demo" id="vici">vici</label> </radio-picker> <style> radio-picker { display: flex; label { &:has(input:checked) { box-shadow: inset 0px 0px 8px 0px #888; } &:has(input:focus-visible) { outline: 2px solid #000; } box-shadow: inset 0px 0px 1.2px 0px #000; padding: 10px; cursor: pointer; background: #0002; &:hover { background: #0004; } &:active { background: #0006; } } input { /* To allow screen reader to still access these. */ opacity: 0; position: absolute; pointer-events: none; } } </style>

这就是我如何实现前例中的主题选择器。为清晰起见,演示中将单选按钮设置为半透明,但实际应用时通过opacity: 0即可使其不可见。

此处涉及多个技术点,让我们逐层解析。

<radio-picker aria-label="Radio buttons example" role="radiogroup">

首先定义radio-picker元素——此为自定义命名,也可用div替代。为其添加aria-label属性赋予可访问性名称,并通过aria-role=“radiogroup”使其作为单选按钮组生效。

若更符合你的使用场景,也可用 fieldset 元素替代aria角色。

<label><input type="radio" name="demo" id="veni" checked>veni</label>
<label><input type="radio" name="demo" id="vidi">vidi</label>
<label><input type="radio" name="demo" id="vici">vici</label>

接着添加单选按钮及其对应标签——通常需为标签添加for 属性来定义对应元素,但由于input位于label内部,此步骤可省略。

所有type=“radio”输入框还应设置相同的name值,以便将其分组(但仍需使用 10 radio组)。然后你可以随意为它们赋值或设置ID。

label {
  &:has(input:checked) {
    box-shadow: inset 0px 0px 8px 0px #888;
  }
  &:has(input:focus-visible) {
    outline: 2px solid #000;
  }
  box-shadow: inset 0px 0px 1.2px 0px #000;
  padding: 10px;
  cursor: pointer;
  background: #0002;
  &:hover { background: #0004; }
  &:active { background: #0006; }
}

接着按需为标签样式化——通过:hover:active伪类可增强按钮点击趣味性,:has(input:checked) 选择器可定义选中按钮样式,而:has(input:focus-visible)选择器能在用户通过Tab键切换至按钮时添加轮廓线。

:focus:focus-visible的区别在于:前者即使使用鼠标操作也会显示,而后者仅在键盘导航时显现,因此视觉上更推荐使用后者。〖

input {
  opacity: 0;
  position: absolute;
  pointer-events: none;
}

最后,我们让单选按钮输入框存在但不可见。这种做法虽略显技巧性,却是确保控件可被键盘导航和屏幕阅读器访问的关键。

至此我们便获得了炫酷的单选按钮效果!

<radio-tabs> <div tabindex=0 id="tab-veni">veni...</div> <div tabindex=0 id="tab-vidi">vidi...</div> <div tabindex=0 id="tab-vici">vici...</div> </radio-tabs> <style> body:has(#veni:not(:checked)) #tab-veni, body:has(#vidi:not(:checked)) #tab-vidi, body:has(#vici:not(:checked)) #tab-vici { display: none; } </style>
veni
/ˈveɪni/
(intransitive) to come
vidi
/ˈviːdi/
(intransitive) to see
vici
/ˈviːt͡ʃi/
(intransitive) to conquer

现在只需通过CSS检测其:checked状态,即可随心所欲地进行样式设计。此处通过在父元素上使用:has选择器定位当前选中的单选按钮,实现了用独立div容器承载标签页内容的方案。

:has选择器必须应用于同时包含单选按钮和目标元素的父元素——若需覆盖整个页面,可直接使用htmlbody。你绝不应单独使用类似 :has(…) 的写法,因为它会为页面中的每个元素运行选择器,可能导致性能问题(body:has(…) 是可行的)。

<div> <details name="deets"> <summary>What's your name?</summary> My name is Lyra Rebane. </details> <details name="deets"> ... </details> </div> <style> div { border: 1px solid #AAA; border-radius: 8px; /* based on the MDN example */ summary { font-weight: bold; margin: -0.5em -0.5em 0; padding: 0.5em; cursor: pointer; } details { &:last-child { border: none } border-bottom: 1px solid #aaa; padding: 0.5em 0.5em 0; &[open] { padding: 0.5em; summary { border-bottom: 1px solid #aaa; margin-bottom: 0.5em; } } } } </style>
What's your name?My name is Lyra Rebane.
Cool name!I know ^_^
Where can I learn more?On my website, lyra.horse!

最后,在继续之前,我想快速介绍一下 details元素。该元素适用于需要手风琴式菜单的场景,例如常见问题解答板块。每个details元素可独立展开/折叠,但若将名称属性设为相同值,则仅允许同时展开一个。

使用方法很简单:将内容和摘要标签包裹在details标签内,标题则置于summary标签中。上例为增强视觉效果略显复杂,但您真正需要的仅是HTML部分。

details元素极易通过样式调整!你可以根据[open]状态添加动画效果,也可通过为summary设置list-style: none来消除箭头。

此外,Ctrl+F 功能在此同样适用,这在我看来是重大优势!

验证

最后,我想展示 HTML 和 CSS 输入验证的强大功能。

<label for="usrname">Username</label> <input type="text" id="usrname" pattern="\w{3,16}" required> <small>3-16 letters, only alphanum and _.</small> <style> input:valid { border: 1px solid green; } input:invalid { border: 1px solid red; } </style>
3-16 letters, only alphanum and _.

这是使用正则表达式模式验证输入字段的简单示例。若设置如上所述的模式属性,包含该输入框的表单必须确保字段符合模式才能提交。当提交类似电子邮件地址电话号码网址时,使用对应的输入类型比编写正则表达式更合理。

此时CSS的作用在于通过样式提示输入值是否有效。在上例中, 我使用:valid:invalid来设置边框颜色,但缺点是始终会标记输入框,即使用户尚未输入任何内容。

input { border: none; border-radius: 2px; outline: 1px solid #000; &:focus { outline-width: 2px; } &:user-valid { outline-color: green; } &:user-invalid { outline-color: red; } }
3-16 letters, only alphanum and _.

更优解是改用:user-valid:user-invalid伪类——这些样式仅在用户与输入框交互后生效。我在此示例中还用轮廓线替代边框,视觉效果更佳。

有时结合使用:valid:user-invalid更合理。

当然,你还可以使用:has选择器根据输入状态为其他元素添加样式!

The password must:
- be 8-16 characters
- contain at least ⅰ roman numeral
- not end with a letter

这个纯粹是玩玩 ^_-!你赢啦!耶!

需要说明的是,对于日期选择器() 或数据列表 ()这类功能,虽然有现成的内置元素可实现,但你可能会发现它们在某些方面存在局限性。若需实现此类特定要求的输入功能,可能仍需涉猎些许JavaScript。

切勿使用vw/vh

本节内容看似随意,但我想在此强调——许多人常在此处出错,希望更多人掌握正确用法。

CSS中的vw/vh单位分别对应视口宽度和高度的1%,这在桌面浏览器中完全合理。

CB
Signal聊天
感觉加密了吗?
M
Marat
这里洋葱味好重...
bm
blackle mori
你嘴里叼着啥,马莓?
R
Rhynorater
CSS 发出 BRRRRR 声
P
PatTheHyruler
我刚输掉比赛
M
Malk
我等不及要尝尝冰糕了!
🔒
techug.com/blog/
•••

您不再需要JavaScript

叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳叽叽喳喳

在移动设备上则稍显复杂。例如Firefox和Chrome的移动版在页面滚动时会隐藏地址栏。

这使得vw/vh单位略显模糊——它们究竟代表整个可用屏幕区域,仅指包含网址栏的可见区域,还是介于两者之间的某个范围?

若为第一种情况,可能导致按钮或链接超出屏幕范围 11 !若为第二种情况,则可能出现背景div未能覆盖整个背景的情况。

p
pingotux
CSS规范太棒了,我转行啦
m
maya
老婆你好!!
Z
Zvit
哈哈。笑死,sæv。
!!
!!HAND !!
叽叽喳喳叽叽喳喳
J
Jones
Glory to KuK
S
Spax
im goop
e
enscribe
我需要一份工作

lvh

dvh

svh

🔒
莱拉·马/博客/
•••

lvh

svh

dvh

lvh

svh

dvh

lvh

svh

dvh

lvh

svh

dvh

lvh

svh

dvh

您的数值

Unit Value
vh px
lvh px
dvh px
svh px

以上是浏览器报告的数值表——若您使用移动设备,请尝试上下滚动博客文章使地址栏隐藏,观察数值变化。

这些数值需乘以100(例如使用100vh代替1vh)。

解决方案是采用新型响应式视口单位:lvhsvhdvh

lvh代表最大视口高度,适用于需要覆盖整个屏幕且不介意被截断的背景等元素。

svh代表最小视口高度,应用于必须始终完全显示在屏幕上的元素,如按钮和链接。

dvh代表动态视口高度——该值将实时更新为当前视口高度。虽然看似理所当然的选择,但不应将其用于用户滚动页面时不希望调整大小或移动的元素,否则可能会造成相当大的干扰,甚至导致延迟。

当然,相应的lvwsvwdvw单位也存在哦 :)。

键盘猫

默认情况下,视口单位不会考虑键盘覆盖页面区域的情况。

对此有两种处理方式:使用 interactive-widget 属性,或调用 VirtualKeyboard API。

前者在浏览器中广泛支持,无需JS即可生效,需添加至meta viewport标签中。它能使键盘弹出时改变所有视口单位。

<meta name="viewport" content="width=device-width, interactive-widget=resizes-content">

后者目前仅限Chromium内核浏览器支持,需添加一行JavaScript代码:

navigator.virtualKeyboard.overlaysContent = true;

第二种方案的优势在于支持在CSS中使用环境变量获取键盘位置与尺寸,这相当酷炫。

floating-button {
  margin-bottom: env(keyboard-inset-height, 0px);
}

但鉴于其跨浏览器兼容性不足,建议避免采用。

CSS愿望清单

好吧,这部分内容与本文其他部分略有不同,但我想提出一些我希望CSS能实现的功能。这些想法尚未完全成型,其中部分内容肯定不符合现有规范,但至少或许能激发其他创意。

这些只是有趣的想法,不必太当真。

可复用模块

希望CSS能支持嵌套类名,这样就能写出类似这样的代码:

.border {
  border: 2px solid;
  border-radius: 4px;
}

.button {
  @apply border;
}

.card {
  @apply border;
}

这正是Tailwind已实现的功能,令人艳羡。

组合式@media选择器

当前支持嵌套@media查询,同时也可使用多个选择器:

div {
  &.foo, &.bar {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
  @media (width < 480px) {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
}

但无法将两者合并为单一选择器:

div {
  @media (width < 480px), &.foo {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
}

这意味着若需实现此功能,你将不得不重复代码或采用笨拙的变量技巧,二者皆非理想方案。

第n个子元素变量

面对我惯用的诸多CSS技巧,最终常写出如下代码:

div {
  span:nth-child(1) { --nth: 1; }
  span:nth-child(2) { --nth: 2; }
  span:nth-child(3) { --nth: 3; }
  span:nth-child(4) { --nth: 4; }
  span:nth-child(5) { --nth: 5; }
  ...
  span {
    top: calc(--nth * 24px);
    color: hsl(calc(var(--nth) * 90deg) 100 90);
  }
}

若能直接实现以下操作该多好:

div {
  span {
    --nth: nth-child();
    top: calc(--nth * 24px);
    color: hsl(calc(var(--nth) * 90deg) 100 90);
  }
}

n-th字母定位器

CSS本可为文本的::first-letter赋予样式。若能像 ::nth-child那样,拥有 ::nth-letter(…)选择器就太棒了。我猜想未实现的原因在于:::first-letter作为伪元素,若结合nth-letter概念会增加实现复杂度。

/* not a real feature */ p::nth-letter(2) { color: red; }

hi there~

Blackle建议将nth-child()变量与:nth-letter定位结合使用,可实现某些有趣效果,例如将值代入sin()函数生成波浪文本。

div { /* not a real feature */ --nth: nth-child(nth-letter); will-change: transform; translate: 0 calc(sin(var(--nth) * 0.35 - var(--wave) * 3) * 5px); color: color-mix(in oklch, #58C8F2, #EDA4B2 calc(sin(var(--nth) * 0.5 - var(--wave)) * 50% + 50%)); }
u
n
t
u
c
k
n
o
w
q
u
e
e
n

(taphover to play animation)

单位移除

希望能轻松移除数值中的单位,例如通过除法实现。

div {
  /* Turns into:  (no unit) */
  --screen-width: calc(100vw / 1px);
  color: hsl(var(--screen-width) 100, 50);
}

这将允许将视口或容器的尺寸作为数值变量,用于长度以外的其他场景。例如,前文提到的取色器就利用该功能,将取色点的位置转换为数值,用于生成颜色值。

呃,等等?这难道意味着该功能早已存在?

没错,哈哈!我们已经能在CSS中获取无单位值,但需要通过自定义@property实现诸如tan(atan2(var(--vw), 1px))这类hack手段。如果能直接用除法实现就太棒了。

对了,好消息是这个功能可能很快就要来了!

另外,如果你使用类似 calc(1px + sqrt(1px * 1px))这样的表达式,浏览器会崩溃 12

更好的图像函数

虽然存在 image()函数,但目前尚无浏览器实现。它类似于直接使用url(),但增加了很酷的功能,比如备用颜色,以及从大图裁剪小图的图像片段功能(想想精灵图)。

虽然现有的背景属性已能实现备用方案和精灵图功能,但拥有这种优雅的语法会更理想。老实说,比起CSS,我更希望在<img>标签中使用这种语法。

body中的style标签

我在项目中大量使用<body>内的<style>标签。例如在我的博客中,我会将相关CSS写在图形附近,这样用户就能开始阅读博客,而无需等待整页(或整套CSS)加载完毕 13 。 效果超棒!

但遗憾的是,尽管浏览器支持这项功能,主流网站也在使用,它却不符合官方规范。我猜这是为了规避规范中的FOUC陷阱,但实际应用中需要在body中设置样式的场景实在太多,这种限制显然缺乏合理性。

HTML验证工具应当对此发出警告而非报错。

艺术性

我想以这句话结束本文:对我而言,网页开发是门艺术,CSS亦然。我常难以认同那些纯粹为赚钱或创业而做网页开发的人——当你身处团队,被动接受上级任务而非自由创作时,网页开发便截然不同。

这种差异在AI工具 14 这类事物上尤为明显,它们剥夺了我工作的乐趣与创造力。同样适用于构建链工具如代码检查器和压缩器——我的编码方式本身就是艺术,不愿让工具抹杀这份独特性。我甚至不用集成开发环境 15

本文反复强调坚持使用CSS的实用理由之外,还有个不为人知的额外原因:我喜欢用CSS完成所有创作,因为它承载着表达与艺术。艺术未必实用,CSS亦然。但这是我表达自我的方式,也是我坚持创作的初衷。

本文力求以通俗易懂的方式,为所有网页开发者提供实用指南。但CSS还有太多我想探讨的内容,所以敬请期待另一篇关于那些不实用、却酷毙了的功能的文章。我认为CSS是一种编程语言,而我制作了一款游戏来证明这一点

不过这要另开篇幅详述。

后记

距离上次发文已近一年,但愿等待值得 ^_^

一如既往, 本文采用纯HTML文件呈现,不含任何JavaScript、图片或其他外部资源——页面所有内容均为手写HTML/CSS实现,压缩后大小约49kB。这次创作各种小巧的交互组件和视觉效果真是乐趣无穷,感觉比起上次发文时,我的CSS水平提升了不少。

整篇博文最终呈现出一种有趣的混乱感(就像我本人一样!),通篇几乎像是一道混沌的色调渐变,但希望大家阅读时仍能感到有趣且愉悦。

目前有几篇新文章在筹备中:除了之前提到的第二篇CSS专题,还有一篇关于我发现的新型网络漏洞子类,以及一篇跨性别议题的文章。发布时间尚未确定,敬请期待!若感兴趣请务必将我加入RSS阅读器。

我还将参加九月在塔林举办的BSides大会并发表演讲!希望能为下届CCC和Disobey大会准备CSS主题演讲,不过还得看能否通过审核以及是否有差旅预算。

非常感谢您的阅读<3

您太棒了!!(因为您勾选了之前的那个复选框)

若需联系,欢迎通过我的社交账号或邮箱 lyra.horse@gmail.com 随时私信。

讨论本文请前往: twitter, mastodon, lobsters


  1. Chrome开发者工具自带超酷的弹性盒子控件。但不知为何Firefox似乎没有?这让我很困惑,毕竟Firefox在弹性盒子和网格开发方面本就拥有优秀工具,此处缺失显得格外突兀。 ↩︎

  2. 虽然我认为上述说法正确,但 Tailwind 的存在意义远不止于此,其核心理念可参见这篇博文(作者本人撰写)。 ↩︎

  3. 允许自定义元素名称,前提是名称必须包含连字符。除链接中列出的8个现有标签外,没有任何HTML标签包含连字符,未来也不会出现。规范甚至将<math-α><emotion-😍>作为允许名称的示例。在自主定制元素上,允许自定义属性,但对于其他元素(内置或扩展),仅应自定义data-*属性。我在博客中大量使用此方法,使HTML和CSS编写更美观,并避免无意义的div堆砌。 ↩︎

  4. 这样读起来还是不顺眼?我个人对BEM也没好感,但如果你实在不适应我写示例的方式,我强烈建议你还是去研究一下这个规范。另外,我的示例刻意一次性展示了大量语法,但在实际应用中,结构上稍作调整可能更合理。 ↩︎

  5. 基准浏览器包括 Safari(macOS/iOS)、Chrome(桌面/Android)、Edge(桌面)和 Firefox(桌面/Android)。  ↩︎

  6. MDN文档当然也列出了详细的浏览器兼容性,但基准符号能快速提供“没错,我们可以使用它且适用于所有人”的概览。 ↩︎

  7. ES3(1999年)是JavaScript最后一个“经典”版本。2009年迎来首次重大修订ES5,数年后ES2015开启了年度规范更新周期。而ES4被弃用的事实总让我感到遗憾 :c。 ↩︎

  8. 93个文件!!似乎三分之一是功能模块,三分之一是广告,三分之一是分析工具。 禁用JavaScript后网站运行正常——仅评论区和广告无法加载。不知为何,页面也不再卡顿了。 ↩︎

  9. 我觉得x3ctf挑战页面在我电脑上运行得非常流畅——滚动字幕动画和点击挑战项的操作都如丝般顺滑。而且在我这台低端硬件上运行得也相当不错。需注意某些浏览器性能记录工具在处理CSS动画时可能出现异常,使用前请确保工具运行正常。另附:我还制作了其他酷炫的x3ctf网页素材——欢迎查阅存档库。 ↩︎

  10. Chrome存在一个缺陷,需使用fieldset/radiogroup结构才能使单选按钮索引在屏幕阅读器中正常工作。例如当三个单选按钮拥有相同name属性时,选中其中一个应读作“3个单选按钮中的第1个”(Firefox即如此处理),但在Chrome中却会错误读作 “9个单选按钮中的第4个”——若未使用fieldset/radiogroup,浏览器会将页面所有单选按钮合并为单一索引进行朗读。 ↩︎

  11. 某人力资源平台将操作按钮置于100vh容器底部,导致手机端无法显示/交互——请病假时遇到这种麻烦实在令人头疼。这恰恰说明:仅因使用了错误的单位,就可能引发严重的实际可用性问题。↩︎

  12. 嗯,大概不会吧。这是我在撰写本文时发现的一个仅影响Chrome的漏洞,它很可能在正式版发布前就被修复了。更新:拖延太久才发布这篇博文,现在问题已修复。不过撰写过程中我又发现了Chrome的另一个漏洞,挺有意思的。更新二:撰写本文时又发现了第三个Chrome漏洞,这次的漏洞相当诡异,建议阅读详情。 ↩︎

  13. 这对于网络连接缓慢的用户至关重要,例如移动数据差、卫星网络、Tor或等场景。虽然我的博客文章体积很小,但仅CSS文件就可能占用超过TCP往返前14kB的带宽,因此当CSS在头部阻塞加载时,你可能需要额外等待几秒(使用碘网络时甚至可能长达数分钟)才能开始阅读首段内容。当然,这个14kB的数值在现代环境下并非完全准确,但在我的服务器环境(HTTP/2+TLS 1.3)测试中,压缩后的HTML文件约有16kB会在首轮HTTP数据中送达浏览器。↩︎

  14. 此处所指工具包括Copilot、Cursor、聊天机器人等。我理解全情投入的编码状态与单纯使用Tab键存在巨大差异,但我确实不愿使用或交互任何此类工具。请尊重我的选择。 ↩︎

  15. 我所有的代码(和博客文章)都在Sublime Text中编写,对我来说它不过是记事本的豪华版。比起记事本,它能提供语法高亮、多光标、快捷键和更出色的视觉设计。功能虽不多,却恰到好处。它实在太出色,我甘愿付费购买。 ↩︎