现代UI设计技巧

张玥 2026年9月18日 阅读时间 50分钟
现代UI 玻璃拟态 设计令牌 容器查询 Bento Grid
热门CSS技巧之现代UI设计技巧

过去十年,界面设计的话语权从设计软件一路转移到了浏览器。曾经需要切图、标注、导出九宫格阴影的视觉效果,如今几行 CSS 就能实时呈现,还能随主题、随容器、随用户偏好自由变化。现代 UI 设计的分水岭,不在于“好不好看”,而在于“是不是用 CSS 原生能力实现的”。玻璃拟态、新拟态、渐变描边、Bento Grid、流式排版、容器查询、:has() 状态样式——这些听起来很“设计”的词,背后全是可以直接写进样式表的 CSS 技巧。本篇 50 分钟长文,从设计令牌讲到玻璃拟态的光学原理,从流体排版讲到容器查询的自适应哲学,并配有六个可实时调参的实验台。读完它,你不仅能复刻当下最流行的视觉风格,更能理解它们为什么这样写。

现代 UI 的底层逻辑:从“画界面”到“设计系统”

如果把 2015 年的网页和 2026 年的网页放在一起对比,最明显的差异不是配色,也不是字体,而是层次感与秩序感。现代界面几乎都在做同一件事:用有限的几组颜色、圆角、阴影、间距,构建出一套自洽的视觉语言,然后在所有组件里重复使用它。

这套语言在工程上有一个名字:设计令牌(Design Tokens)。它把“主色是什么”“圆角多大”“阴影多深”从设计师的脑子里,搬到了 CSS 自定义属性里。于是换肤、暗色模式、品牌定制,全都变成了改变量值这么简单的事。

现代 UI 技术的四个层次

点击下面每一层,看看它负责什么,以及对应哪些 CSS 能力。

1
Token
设计令牌
颜色、间距、圆角、阴影、字号、动效曲线。全部落在 :root 的 CSS 自定义属性里,是全站唯一的“真相来源”。
2
Material
材质与质感
玻璃拟态(backdrop-filter)、新拟态(双向阴影)、渐变描边(mask 合成)、光晕(多层 box-shadow)。
3
Layout
布局与自适应
Grid、Flex、clamp() 流体尺寸、容器查询 @container、Bento 网格,让同一套组件在不同宽度下都成立。
4
State
状态与交互
:hover、:focus-visible、:has()、prefers-color-scheme、prefers-reduced-motion,让界面回应用户的每一个动作与偏好。

为什么“用原生 CSS 做视觉”比图片更好

  • 可主题化:图片是死的,CSS 是活的。暗色模式只需要改一组变量。
  • 可自适应:一张 2x 切图在 4K 屏幕上会糊,CSS 渐变永远不会。
  • 体积更小:一段 20 行的渐变描边代码,大约 400 字节;一张同等效果的 PNG,至少 20KB。
  • 可访问:CSS 生成的装饰不进入无障碍树,也不会被读屏软件念出来。
  • 可动效:图片没法在 hover 时平滑地改变模糊半径,但 backdrop-filter 可以。

接下来的每一节,都会围绕一个具体的现代 UI 技巧展开,并给出可直接抄走的代码。

设计令牌:让整套视觉系统可切换

设计令牌的第一步,是给颜色起“语义化”的名字,而不是“视觉化”的名字。不要写 --blue-500,要写 --color-primary;不要写 --gray-100,要写 --color-surface。原因很简单:当品牌色从蓝变成紫时,你不需要在两百个文件里搜索替换。

/* ✅ 语义化令牌:名字描述用途,而不是颜色 */
:root {
  /* 品牌 */
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;

  /* 表面与文字 */
  --color-bg: #f1f5f9;
  --color-surface: #ffffff;
  --color-text: #0f172a;
  --color-muted: #64748b;
  --color-border: #e2e8f0;

  /* 圆角 */
  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-full: 999px;

  /* 间距(8pt 栅格) */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 32px;

  /* 阴影层级 */
  --shadow-sm: 0 1px 2px rgba(15,23,42,.06);
  --shadow-md: 0 4px 12px rgba(15,23,42,.08);
  --shadow-lg: 0 16px 40px rgba(15,23,42,.12);
}
primary
accent
success
warning
danger
bg
surface
text

动手试:一键切换主题

下面这个卡片内部的所有颜色,都来自一组局部变量。点击按钮切换 data-theme,你会看到颜色、边框、阴影全部平滑过渡——这就是令牌化的威力。

组件预览
当前:亮色主题
使用语义化令牌的列表项 --color-surface
边框与阴影同步过渡 --color-border
无需修改任何组件代码 --color-primary
/* 主题作用域:同一组变量,两套取值 */
.theme-scope {
  --color-bg: #ffffff;
  --color-text: #0f172a;
  --color-border: #e2e8f0;
}

.theme-scope[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-text: #f8fafc;
  --color-border: #334155;
}

/* 组件只引用变量,不关心当前是什么主题 */
.card {
  background: var(--color-bg);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  transition: background .35s ease, color .35s ease,     border-color .35s ease;
}

令牌化的三个常见误区

  • 命名过于视觉化:--blue-500 在品牌换色后会变成谎言。坚持按用途命名。
  • 层级过深:--color-brand-primary-hover-strong 这种名字没人记得住。控制在两到三层。
  • 全部塞进 :root:只有真正全局的东西才放 :root。组件级变量应该定义在组件自己的作用域里。

玻璃拟态 Glassmorphism:不只是加个模糊

玻璃拟态(Glassmorphism)是近几年最出圈的 UI 风格。它的视觉特征是:半透明背景 + 背景模糊 + 细微的亮色描边 + 柔和的投影。但很多人只写了 backdrop-filter: blur() 就以为完成了,结果做出来的卡片又灰又脏,完全没有玻璃的通透感。

真正决定成败的四个参数

  1. 背景必须有色彩层次:玻璃是“透出后面的东西”。如果背景是纯白,玻璃就是一块灰。必须让玻璃后面有渐变、有光斑、有图案。
  2. backdrop-filter 的模糊半径:太小看不出玻璃感,太大则糊成一团。12~20px 是常见的舒适区间。
  3. 饱和度的补偿:模糊会让颜色发灰,所以要在 backdrop-filter 里加 saturate(160%) 甚至更高,把颜色“救”回来。
  4. 1px 的亮色描边:这是玻璃的“边缘高光”。rgba(255,255,255,0.3) 到 0.5 之间效果最好。

动手试:玻璃拟态实时调参台

拖动下面的滑块,观察卡片的变化,下方的 CSS 代码会同步更新。建议先试试把模糊拉到 0,你会看到玻璃感瞬间消失——这能帮你建立参数与视觉之间的直觉。

Glass Card
半透明 + 背景模糊 + 亮色描边
了解更多

玻璃拟态的完整代码骨架

/* 1. 玻璃卡片本体 */
.glass {
  background: rgba(255, 255, 255, 0.18);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(15, 23, 42, 0.25);
}

/* 2. 玻璃后面必须有内容可透 */
.glass-stage {
  background: linear-gradient(135deg, #6366f1, #ec4899 55%, #f59e0b);
  position: relative;
  overflow: hidden;
}

/* 3. 背景里的彩色光斑(可选,但效果拔群) */
.glass-stage::before {
  content: '';
  position: absolute;
  width: 180px;
  height: 180px;
  border-radius: 50%;
  background: rgba(56, 189, 248, 0.8);
  filter: blur(30px);
  top: 8%;
  left: 6%;
}

/* 4. 文字加一点点阴影,保证可读性 */
.glass {
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
}

玻璃拟态的三个陷阱

陷阱一:对比度不足
白字压在浅色玻璃上,读起来非常吃力。解决方式:加深背景、加文字阴影,或者给玻璃一个深色底。
正解:分层控制
背景层(渐变 + 光斑)、玻璃层(模糊 + 描边)、内容层(深色文字或加阴影的浅色文字),三层各司其职。
陷阱二:大面积滥用
backdrop-filter 需要实时采样并模糊背后内容,面积越大、数量越多,GPU 压力越高。满屏玻璃卡片是性能灾难。
正解:局部点缀
只在导航栏、模态框、少数强调卡片上使用。并且给这些元素加 contain: paint 限制影响范围。
陷阱三:忘记兼容前缀
部分 Safari 版本只认 -webkit-backdrop-filter。不加前缀,用户看到的就是一块纯半透明色块。
正解:双写 + 降级
同时写标准属性与前缀属性,并保证在不支持时,半透明背景本身依然可读。

新拟态 Neumorphism:一束光与一团影

新拟态(Neumorphism)是玻璃拟态的反面:它不透明、不模糊,靠同色系的双向阴影制造“从背景里挤出来”的立体感。左上角一层浅色阴影模拟光源,右下角一层深色阴影模拟投影,元素就像被压进了同一块塑料里。

/* 新拟态的核心公式:两个方向的同色阴影 */
.neu {
  background: #e8edf5;
  border-radius: 22px;
  /* 右下深色 + 左上浅色 = 凸起 */
  box-shadow:
    8px 8px 16px #c9d2e0,
    -8px -8px 16px #ffffff;
}

/* 按下状态:把阴影翻转到内部 = 凹陷 */
.neu:active {
  box-shadow:
    inset 6px 6px 12px #c9d2e0,
    inset -6px -6px 12px #ffffff;
}

/* 深色主题下,阴影颜色必须整体下沉 */
.neu-dark {
  background: #1e293b;
  box-shadow:
    8px 8px 16px #131c2b,
    -8px -8px 16px #29384f;
}

点击任意按钮查看“凹陷”效果

新拟态为什么“叫好不叫座”

新拟态在 Dribbble 上非常好看,但在真实产品里很少大规模使用,原因很实在:

  • 对比度天生不足:文字与背景几乎同色,WCAG 对比度要求很难达标。
  • 状态难以区分:凸起和凹陷的差异非常微妙,用户常常看不出哪里可以点。
  • 极度依赖背景色:换一个背景色,整套阴影就全部作废,必须重算。
  • 不适合信息密集界面:在列表、表格、表单里,新拟态会让层级彻底糊掉。

比较务实的用法是:只把它用在少数装饰性控件上,比如播放器的圆形按钮、智能家居的开关面板,同时配合明确的图标和文字标签来保证可用性。

一个关键细节:阴影的颜色不能是纯黑

/* ❌ 用纯黑做阴影,画面立刻变脏 */
.bad { box-shadow: 8px 8px 16px #000; }

/* ✅ 深色阴影应当是背景色的“加深版” */
/* 背景 #e8edf5 → 深色阴影 #c9d2e0 */
.good { box-shadow: 8px 8px 16px #c9d2e0; }

/* ✅ 用 color-mix() 自动算出加深色 */
.auto {
  --bg: #e8edf5;
  background: var(--bg);
  box-shadow:
    8px 8px 16px color-mix(in srgb, var(--bg) 78%, #000),
    -8px -8px 16px color-mix(in srgb, var(--bg) 72%, #fff);
}

渐变、描边与光效:现代 UI 的“高级感”来源

现代 UI 的“高级感”往往来自三个细节:渐变文字、渐变描边、柔和光晕。它们的共同点是:用极少的代码,制造出远超代码量的视觉重量。

渐变文字:一行 background-clip

/* 渐变文字的标准写法 */
.grad-text {
  background: linear-gradient(90deg, #2563eb, #8b5cf6, #ec4899);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* 注意:color 必须写在 background-clip 之后 */
/* 否则部分浏览器会把文字渲染成透明不可见 */

/* 让渐变“流动”起来 */
.grad-text-animated {
  background-size: 200% 100%;
  animation: gradShift 6s linear infinite;
}
@keyframes gradShift {
  to { background-position: 200% 0; }
}

现代 UI 设计

渐变文字是标题的常客,但请节制使用——满屏渐变等于没有重点。

渐变描边:mask 合成的高级用法

CSS 没有 border-image: linear-gradient() 配合圆角的完美方案(border-image 会忽略 border-radius)。真正能兼顾圆角的做法,是用 mask 做“镂空”。

/* 渐变描边 + 圆角的正确实现 */
.grad-border {
  position: relative;
  border-radius: 14px;
  background: #fff;
}

.grad-border::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* padding 决定描边宽度 */
  padding: 2px;
  background: linear-gradient(135deg, #2563eb, #8b5cf6, #ec4899);

  /* 关键:用两层 mask 相减,只保留边框区域 */
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

原理拆解:第一层 mask 只覆盖 content-box(内容区),第二层 mask 覆盖整个盒子。两者用 xor / exclude 做差集,剩下的正好是一圈边框。这是一种非常通用的“局部着色”技巧,也可以用来做渐变下划线、渐变角标。

网格背景与极光光晕

/* 纯 CSS 网格背景:两组线性渐变叠加 */
.grid-bg {
  background-color: #0f172a;
  background-image:
    linear-gradient(rgba(148,163,184,.22) 1px, transparent 1px),
    linear-gradient(90deg, rgba(148,163,184,.22) 1px, transparent 1px);
  background-size: 22px 22px;
}

/* 极光光晕:大半径 radial-gradient + 缓慢漂移 */
.aurora {
  background:
    radial-gradient(circle at 20% 30%, rgba(56,189,248,.5), transparent 45%),
    radial-gradient(circle at 80% 70%, rgba(236,72,153,.5), transparent 45%),
    #0f172a;
  animation: auroraShift 12s ease-in-out infinite alternate;
}
@keyframes auroraShift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 40px 20px, -30px -20px; }
}
渐变描边卡片
1px 描边由 mask 合成,圆角完美保留

阴影体系:让层级“说得清楚”

现代 UI 里,阴影不是装饰,而是信息架构的一部分。卡片浮在页面上、模态框浮在卡片上、提示浮在模态框上——用户对“谁在上层”的判断,八成来自阴影。

/* 一套四级的阴影体系 */
:root {
  /* L0:贴地,几乎无阴影 */
  --shadow-0: none;

  /* L1:卡片 */
  --shadow-1: 0 1px 2px rgba(15,23,42,.06),     0 2px 6px rgba(15,23,42,.05);

  /* L2:悬浮卡片 / 下拉 */
  --shadow-2: 0 4px 10px rgba(15,23,42,.08),     0 10px 24px rgba(15,23,42,.07);

  /* L3:模态框 */
  --shadow-3: 0 10px 24px rgba(15,23,42,.12),     0 24px 64px rgba(15,23,42,.14);
}

/* 关键:多段阴影叠加,比单段大阴影自然得多 */
/* 近处的小阴影负责“贴地感” */
/* 远处的大阴影负责“高度感” */
L0 页面背景、分隔线,不使用阴影
L1 普通卡片、列表项,2 段浅阴影
L2 悬停态、下拉菜单,阴影扩大 + 上移 2px
L3 模态框、抽屉,阴影最大且带模糊

阴影的颜色也要跟随主题

/* ❌ 暗色模式下用 rgba(0,0,0,.1) 的阴影 */
/* 深色背景上的黑色阴影完全看不见,卡片会“糊”在一起 */
.card-bad { box-shadow: 0 4px 12px rgba(0,0,0,.1); }

/* ✅ 暗色模式下改用“更深的背景色 + 更低的透明度” */
[data-theme="dark"] .card {
  box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
}

/* ✅ 更高级:给暗色模式的卡片加一条顶部高光 */
[data-theme="dark"] .card {
  border-top: 1px solid rgba(255,255,255,.08);
}

在暗色界面里,用亮色描边代替阴影来区分层级,通常比硬堆阴影更干净。这也是当前很多设计系统的选择。

流体排版:一个 clamp() 顶掉十个媒体查询

过去写响应式字号,要写一堆断点:手机 18px、平板 22px、桌面 28px、大屏 32px。现在,clamp(最小值, 弹性值, 最大值) 一行就能覆盖整个区间,而且过渡是连续的、没有跳变的。

/* 语法:clamp(最小, 理想值, 最大) */
/* 理想值通常用 vw / cqi 这类相对单位 */
.title {
  font-size: clamp(1.5rem, 4vw, 3rem);
}

/* 更精细的写法:用斜率控制变化速度 */
/* 视口 320px 时约 20px,视口 1440px 时约 44px */
.hero-title {
  font-size: clamp(20px, 1.5rem + 1.2vw, 44px);
}

/* 间距也可以流体化 */
.section {
  padding-block: clamp(32px, 6vw, 96px);
  gap: clamp(12px, 2.5vw, 28px);
}

/* 圆角、阴影模糊半径同样适用 */
.card {
  border-radius: clamp(10px, 1.2vw, 20px);
}

动手试:容器单位 cqi 驱动的流体字号

下面这段文字使用 font-size: clamp(15px, 8cqi, 40px)。cqi 是“容器内联方向尺寸的 1%”,所以文字大小会跟随容器宽度变化,而不是视口。拖动滑块调整容器宽度,观察文字缩放。

流体排版
font-size: clamp(15px, 8cqi, 40px)

流体排版的两个注意点

  • 一定要设最小值:clamp() 的第一个参数是安全网。没有它,极端窄屏下文字会小到无法阅读。
  • 不要对所有文字都流体化:正文段落用固定字号反而更稳,因为行高与行长需要精确配合。流体化主要用于标题、间距、装饰性尺寸。

容器查询:组件终于能“看自己有多宽”

媒体查询问的是“视口有多宽”,容器查询问的是“我这个容器有多宽”。这个区别看起来很小,实际却是组件化设计的分水岭:一个卡片组件放在主栏里可以是横向布局,放进侧边栏就自动变成纵向布局——同一份代码,两种形态。

/* 1. 在父元素上声明容器 */
.card-wrap {
  container-type: inline-size;
  /* 只关心宽度,不影响高度计算 */
}

/* 也可以给容器起个名字,便于精确匹配 */
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

/* 2. 在子元素里根据容器宽度写规则 */
@container (max-width: 320px) {
  .card {
    flex-direction: column;
    text-align: center;
  }
}

/* 3. 指定容器的写法 */
@container sidebar (min-width: 400px) {
  .card { gap: 20px; }
}

动手试:拖动滑块,看卡片自动变形

自适应卡片
容器宽度大于 320px 时横向排列,小于时自动改为纵向居中——全靠 @container,没有一行 JavaScript。

容器查询 vs 媒体查询:什么时候用哪个

场景 推荐方案 原因
页面整体栅格 @media 页面级布局本来就该响应视口
可复用卡片组件 @container 组件不知道自己会被放在哪一栏
导航栏折叠为汉堡菜单 @media 取决于设备与视口,与容器无关
侧边栏内的小组件 @container 侧边栏宽度由布局决定,不随视口线性变化
字号随视口缩放 clamp() + vw 全局排版,与容器无关

简单记法:页面级用媒体查询,组件级用容器查询。两者不是替代关系,而是分工关系。

:has() 选择器:父级终于能“看见”子级

被戏称为“CSS 的 jQuery”的 :has(),让选择器第一次可以从子元素反向选中父元素。它解决了很多以前必须靠 JavaScript 才能完成的状态样式问题。

/* 经典场景一:表单有内容时,标签变色 */
.field:has(input:not(:placeholder-shown)) label {
  color: var(--primary);
}

/* 场景二:卡片内有勾选时,整张卡片高亮 */
.plan:has(input:checked) {
  border-color: var(--primary);
  background: #eff6ff;
  transform: translateY(-3px);
}

/* 场景三:根据是否有图片,改变布局 */
.card:has(img) { grid-template-rows: auto 1fr; }
.card:not(:has(img)) { grid-template-rows: 1fr; }

/* 场景四:列表里有任何一项被选中,整组加边框 */
.list:has(.item.selected) {
  outline: 2px solid var(--primary);
}
省代码 大量“加个 class 就好了”的 JS 逻辑可以删掉
状态同步 DOM 一变,样式自动跟上,不会出现状态漂移
性能 选择器越具体越好,避免大范围 :has(*)
兼容 Chrome 105+ / Safari 15.4+ / Firefox 121+

动手试:用 :has() 实现套餐选择卡片

点击任意卡片,选中的样式完全由 CSS 的 :has() 控制,没有任何 JavaScript 参与样式切换。

当前选择:基础版

一个必须注意的性能细节

/* ⚠️ 危险:范围过大,任何 DOM 变化都要重新匹配 */
body:has(.loading) .everything { /* ... */ }

/* ✅ 安全:限定在具体的组件容器内 */
.toolbar:has(.menu-open) .toolbar__item { /* ... */ }

/* 经验法则:把 :has() 的作用范围限制在 1~2 层 DOM 之内 */

Bento Grid:用 Grid 排出信息密度

Bento Grid(便当盒布局)的核心是:不同尺寸的卡片拼合成一个规整的矩形区域。它既能承载不同权重的信息,又不会像瀑布流那样参差不齐。实现它只需要 Grid 的 span。

/* Bento Grid 的核心:固定行高 + 跨列跨行 */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 86px;
  gap: 10px;
}

/* 主卡片:占 2×2 */
.bento .c1 {
  grid-column: span 2;
  grid-row: span 2;
}

/* 宽卡片:占 2×1 */
.bento .c2 { grid-column: span 2; }

/* 小卡片:默认 1×1,不需要写任何规则 */

/* 窄屏降级为 2 列 */
@media (max-width: 600px) {
  .bento {
    grid-template-columns: repeat(2, 1fr);
    grid-auto-rows: 78px;
  }
}
层次 大卡片 = 高权重内容,小卡片 = 辅助信息
整齐 所有卡片共享同一套行高与间距
克制 卡片数量控制在 5~9 个,再多就失去重点
降级 窄屏改成 2 列或单列,跨行跨列要重新规划
Bento
Grid
主卡片 · 2×2
span 2 宽卡片 · 2×1
1×1 小卡片
1×1 小卡片
grid-auto-rows 统一行高是整齐的关键
gap 统一间距
span 跨列跨行

Bento Grid 的三个设计要点

  • 卡片数量越少越好:超过 9 个,视觉重心会散掉。宁可拆成两组。
  • 大卡片必须放最重要的内容:用户的视线会自然落在大块区域上,别浪费。
  • 所有卡片共用同一套圆角与间距:这是“便当盒”整齐感的来源,不要给某个卡片单独调圆角。

微交互:让界面“回应”用户

现代 UI 与十年前最大的体感差异,往往不在视觉,而在反馈的即时性与细腻度。按钮按下去有轻微的下沉、卡片悬停有柔和的上浮、鼠标位置带出淡淡的高光——这些细节加起来,就是“精致”与“粗糙”的分界线。

/* 按钮的三态:hover / active / focus-visible */
.btn {
  background: var(--primary);
  color: #fff;
  border-radius: 10px;
  transition:
    transform .18s cubic-bezier(.34,1.56,.64,1),
    box-shadow .25s ease;
}

/* 悬停:上浮 + 阴影扩散 */
.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 20px rgba(37,99,235,.4);
}

/* 按下:回落 + 轻微缩小,模拟物理反馈 */
.btn:active {
  transform: translateY(0) scale(.97);
  box-shadow: 0 2px 8px rgba(37,99,235,.35);
}

/* 键盘聚焦:只有键盘用户才看到外圈 */
.btn:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 3px;
}

/* 鼠标跟随高光:用 CSS 变量接收坐标 */
.btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--mx, 50%) var(--my, 50%),     rgba(255,255,255,.45), transparent 55%);
  opacity: 0;
  transition: opacity .3s ease;
}
.btn:hover::after { opacity: 1; }

鼠标移入试试:按钮会跟随光标位置产生高光

微交互的时长法则

  • 按钮反馈:100~150ms。再长就会觉得“迟钝”。
  • 悬停过渡:150~250ms。太短显得生硬,太长显得黏腻。
  • 弹入效果:250~400ms,配合轻微过冲的缓动曲线。
  • 缓动曲线:微交互推荐 cubic-bezier(0.34, 1.56, 0.64, 1),也就是常说的“回弹曲线”。
/* 一套可直接复用的动效令牌 */
:root {
  --dur-instant: 100ms;
  --dur-fast: 150ms;
  --dur-base: 220ms;
  --dur-slow: 360ms;

  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
  --ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

3D 与透视:一张卡片的“呼吸感”

在现代 UI 里,3D 变换很少用于真正的三维场景,更多是用来制造轻微的纵深感。鼠标移到卡片上,卡片顺着光标方向轻微倾斜,内部元素因为 translateZ 而浮起——这种效果只需要几行 CSS 加十几行 JS。

/* 1. 父级提供透视距离 */
.tilt-wrap {
  perspective: 900px;
}

/* 2. 卡片接受旋转,并保留子元素的 3D 层次 */
.tilt-card {
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
  transition: transform .15s ease-out;
  will-change: transform;
}

/* 3. 内部元素沿 Z 轴浮起 */
.tilt-card .title { transform: translateZ(28px); }
.tilt-card .desc  { transform: translateZ(18px); }

/* 4. JS:根据光标位置算出旋转角度 */
card.addEventListener('pointermove', (e) => {
  const r = card.getBoundingClientRect();
  const px = (e.clientX - r.left) / r.width - 0.5;
  const py = (e.clientY - r.top) / r.height - 0.5;
  card.style.setProperty('--ry', (px * 14) + 'deg');
  card.style.setProperty('--rx', (-py * 14) + 'deg');
});

card.addEventListener('pointerleave', () => {
  card.style.setProperty('--rx', '0deg');
  card.style.setProperty('--ry', '0deg');
});
3D 倾斜卡片
把光标移到卡片上,感受纵深感
preserve-3d
内部元素用 translateZ 分层浮起

三个必须注意的细节

  • 旋转角度别超过 15 度:超过就会显得浮夸,甚至让文字难以阅读。
  • 过渡时间要短:跟手的感觉来自 0.1s~0.2s 的过渡。太长会“拖泥带水”。
  • 只在指针设备上启用:触屏没有 hover,用 @media (hover: hover) 包起来更稳妥。
/* 只在支持悬停的设备上启用 3D 倾斜 */
@media (hover: hover) and (pointer: fine) {
  .tilt-card { /* 3D 相关样式 */ }
}

暗色模式:不是简单地把颜色反过来

暗色模式最常见的错误做法,是把所有颜色做一次 invert()。结果就是:纯白变成纯黑、彩色变成刺眼的高饱和、阴影全部消失。真正专业的暗色模式,需要重新设计一整套取值规则。

/* 1. 不要用纯黑,用深蓝灰 */
/* 纯黑 (#000) 与亮色文字对比过强,长时间阅读会累 */
[data-theme="dark"] {
  --color-bg: #0f172a;   /* 不是 #000 */
  --color-surface: #1e293b;
  --color-text: #e2e8f0;  /* 不是 #fff */
}

/* 2. 主色要提亮、降饱和 */
/* 亮色模式:#2563eb 在深色背景上会显得沉闷 */
/* 暗色模式:#60a5fa 更亮、更清爽 */
[data-theme="dark"] {
  --color-primary: #60a5fa;
}

/* 3. 阴影改为“更深”,或用描边替代 */
[data-theme="dark"] .card {
  box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
  border: 1px solid rgba(255, 255, 255, .08);
}

/* 4. 图片和图标要降低亮度,避免刺眼 */
[data-theme="dark"] img {
  filter: brightness(.88) contrast(1.05);
}
标签

用 data-mode 控制作用域内的令牌,页面其余部分不受影响

跟随系统 + 手动覆盖

/* 默认跟随系统 */
@media (prefers-color-scheme: dark) {
  :root { /* 暗色令牌 */ }
}

/* 用户手动选择优先于系统偏好 */
:root[data-theme="light"] { /* 亮色令牌 */ }
:root[data-theme="dark"]  { /* 暗色令牌 */ }

/* 让表单控件也跟着变色 */
:root { color-scheme: light dark; }

color-scheme: light dark 这一行非常关键:它会让浏览器自带的滚动条、输入框、下拉菜单也切换到暗色样式。少了它,暗色页面里会突兀地冒出一个白色滚动条。

无障碍:现代 UI 的底线而不是加分项

再漂亮的界面,如果有一群人用不了,它就是失败的。现代 UI 技巧里,有几条无障碍规则必须刻进肌肉记忆。

1. 对比度:正文至少 4.5:1

/* WCAG 2.2 对比度要求 */
/* 正文文字:≥ 4.5 : 1 */
/* 大号文字(18.66px 粗体 / 24px 常规):≥ 3 : 1 */
/* UI 组件边界、图标:≥ 3 : 1 */

/* ❌ 常见错误:浅灰文字压在白色卡片上 */
.bad { color: #cbd5e1; } /* 约 1.6:1 */

/* ✅ 安全取值 */
.good { color: #64748b; } /* 约 4.8:1 */
.better { color: #475569; } /* 约 7.5:1 */

2. 焦点可见:永远不要 outline: none

/* ❌ 灾难级写法 */
*:focus { outline: none; }

/* ✅ 正确:只在鼠标点击时隐藏,键盘聚焦时显示 */
.btn:focus { outline: none; }
.btn:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 3px;
}

/* :focus-visible 的智能之处:鼠标用户看不到外圈,键盘用户看得到 */

3. 减少动效:尊重 prefers-reduced-motion

/* 基础方案:整体降速到接近静止 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* 更优雅:保留淡入淡出,去掉位移与旋转 */
@media (prefers-reduced-motion: reduce) {
  .reveal {
    transform: none;
    transition: opacity 150ms linear;
  }
  .tilt-card, .parallax {
    transform: none !important;
  }
}
禁用 大幅度位移、视差滚动、连续循环动画、快速闪烁
慎用 缩放、旋转、3D 倾斜
保留 短时淡入淡出、颜色过渡、焦点圈、状态反馈
原则 减少“位移与旋转”,而不是粗暴地全部关掉

性能与兼容性:漂亮的代价要可控

前面介绍的技巧里,有相当一部分是“昂贵”的。backdrop-filter 要实时采样背景、box-shadow 要额外绘制、大面积渐变要重新光栅化、3D 变换要生成合成层。用对了是加分,用错了就是卡顿。

技巧 主要开销 代价 控制建议
transform / opacity 动画 合成 低 放心用,是现代 UI 动效的基石
CSS 渐变 / 渐变文字 绘制(一次性) 低 静态渐变几乎零成本,动态改变渐变色才需注意
box-shadow 绘制 中 面积越大越贵;动画时改用伪元素 + opacity
border-radius + overflow: hidden 绘制 + 裁剪 中 静态使用没问题,滚动容器内慎用
filter: blur() 绘制 + 合成 中 小面积可用,避免长时动画
backdrop-filter 采样 + 模糊 + 合成 高 数量控制在个位数,加 contain: paint 限制范围
大面积 mask 合成 绘制 + 合成 高 仅用于静态装饰性元素,不要动画 mask
3D 变换 + preserve-3d 合成 + 图层管理 高 只给少数卡片使用,动画结束后移除 will-change

兼容性现状速查

/* 现代 UI 特性的支持情况(2026 年) */
/* CSS 自定义属性       —— 全平台,放心用 */
/* Grid / Flex            —— 全平台,放心用 */
/* clamp()                 —— 全平台,放心用 */
/* :focus-visible          —— Chrome 86+ / Safari 15.4+ */
/* backdrop-filter         —— Chrome 76+ / Safari 9+(需前缀) */
/* :has()                   —— Chrome 105+ / Safari 15.4+ */
/* 容器查询 @container      —— Chrome 105+ / Safari 16+ */
/* 容器单位 cqi            —— 随容器查询一起支持 */
/* color-mix()             —— Chrome 111+ / Safari 16.2+ */
/* transition-behavior      —— Chrome 117+ */
/* @starting-style         —— Chrome 117+ / Safari 17.5+ */
直接使用 自定义属性、Grid、clamp()、transform
加前缀 backdrop-filter 同时写 -webkit- 版本
渐进增强 :has()、容器查询,先保证基础可用
特性检测 用 @supports 或 CSS.supports() 分流
/* 渐进增强的标准写法:基础可用 + 增强可选 */

/* 1. 先写所有浏览器都能用的基础样式 */
.card {
  background: rgba(255, 255, 255, 0.85);
  border-radius: 16px;
  border: 1px solid rgba(255,255,255,.5);
}

/* 2. 支持时才升级为玻璃效果 */
@supports ((backdrop-filter: blur(1px)) or   (-webkit-backdrop-filter: blur(1px))) {
  .card {
    background: rgba(255, 255, 255, 0.18);
    backdrop-filter: blur(16px) saturate(180%);
    -webkit-backdrop-filter: blur(16px) saturate(180%);
  }
}

/* 3. 容器查询的降级:默认用媒体查询的结果 */
.card { flex-direction: row; }
@supports (container-type: inline-size) {
  .card-wrap { container-type: inline-size; }
  @container (max-width: 320px) {
    .card { flex-direction: column; }
  }
}

性能优化的四条铁律

  • 动画只碰 transform 与 opacity:其他属性的动画都可能触发重排或重绘。
  • backdrop-filter 数量要克制:一屏超过 5 个就该重新考虑设计。
  • 避免图层爆炸:不要无脑 will-change: transform,图层会吃显存。
  • 在真机上测:模拟器反映不出 GPU 与内存的真实压力,中低端安卓机才是试金石。

现代 UI 设计技巧最佳实践清单

把全文的核心结论浓缩成一份可以直接贴在工位上的清单。

  • 先建令牌,再写组件:颜色、圆角、间距、阴影全部变量化,主题切换才有意义。
  • 令牌命名按用途:用 --color-surface,不要用 --gray-100。
  • 玻璃拟态必须有色彩背景:纯色背景上的玻璃就是一块灰。
  • backdrop-filter 记得加 saturate():模糊会让颜色发灰,饱和度要补回来。
  • 玻璃卡片数量控制在个位数:这是性能上最容易失控的一项。
  • 新拟态只用于装饰性控件:它的对比度天生不适合承载信息。
  • 阴影用多段叠加:近处小阴影 + 远处大阴影,比单段大阴影自然得多。
  • 暗色模式不要用纯黑:#0f172a 比 #000 舒服得多。
  • 暗色模式记得 color-scheme:否则滚动条和表单控件会突兀地发白。
  • 字号间距用 clamp():一个函数顶掉一串媒体查询。
  • 组件级响应式用 @container:页面级才用 @media。
  • 状态样式优先用 :has():能少写一堆 class 切换的 JS。
  • 微交互时长 150ms 左右:超过 300ms 就会显得迟钝。
  • 3D 倾斜不超过 15 度:并且用 @media (hover: hover) 包起来。
  • 永远不要 outline: none:用 :focus-visible 做精细化处理。
  • 尊重 prefers-reduced-motion:减少位移与旋转,而不是粗暴地全部关掉。
  • 正文对比度至少 4.5:1:这是可读性的硬门槛。
  • 新特性用 @supports 渐进增强:基础体验要在所有浏览器上成立。
/* 一套可直接复用的现代 UI 基线 */

/* 1. 设计令牌 */
:root {
  --color-primary: #2563eb;
  --color-surface: #ffffff;
  --color-text: #0f172a;
  --color-muted: #64748b;
  --color-border: #e2e8f0;

  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 20px;

  --shadow-1: 0 1px 2px rgba(15,23,42,.06), 0 2px 6px rgba(15,23,42,.05);
  --shadow-2: 0 4px 10px rgba(15,23,42,.08), 0 10px 24px rgba(15,23,42,.07);
  --shadow-3: 0 10px 24px rgba(15,23,42,.12), 0 24px 64px rgba(15,23,42,.14);

  --dur-fast: 150ms;
  --dur-base: 220ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  color-scheme: light dark;
}

/* 2. 基础卡片 */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  padding: clamp(14px, 2vw, 22px);
  transition: transform var(--dur-base) var(--ease-standard),     box-shadow var(--dur-base) var(--ease-standard);
}
.card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-2);
}

/* 3. 玻璃层 */
.glass {
  background: rgba(255,255,255,.18);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255,255,255,.35);
  border-radius: var(--radius-lg);
}

/* 4. 焦点与无障碍 */
:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

最后的三个提醒

第一,技巧是手段,不是目的。玻璃拟态、新拟态、Bento Grid 都是工具。如果一个界面需要堆上所有流行风格才显得“现代”,那它大概率不现代。真正的现代 UI,是克制、有秩序、且在每个细节上都为使用者考虑过的。

第二,先跑起来,再谈好看。所有视觉增强都应该建立在“基础可用”的前提上。玻璃效果不支持时,半透明背景依然可读;3D 倾斜不支持时,卡片依然是卡片;容器查询不支持时,媒体查询依然兜底。

第三,去真机上试。这篇文章里的每一个实验台,都建议你在自己的项目里复刻一遍,然后拿到中低端手机上看一眼。你会发现,那些在桌面浏览器上丝滑无比的效果,在真实设备上可能完全是另一回事——而这个发现,比读完十篇文章都有价值。

愿你的界面,既好看,又好用,还很快。