阅读视图

发现新文章,点击刷新页面。

最全Scss语法,赶紧收藏起来吧

Sass/SCSS 的嵌套

作用

  • 减少重复:不再处处写 .card 前缀。
  • 表达结构:一眼看出父子/状态/修饰的关系。
  • 就地维护:媒体查询、状态样式和主体样式放在一起,改组件更集中。
  • BEM 更顺手:&__el&--mod&.is-xxx 自然生成。

下面我们来看下它嵌套编译的效果

.card {
  .title { color: #333; }        // 后代
  &:hover { box-shadow: ...; }   // 父的伪类
  .actions > .btn { ... }        // 组合器保留
}

编译后:

.card .title{color:#333}
.card:hover{box-shadow:...}
.card .actions > .btn{...}

变量与类型

$size: 16px;
$brand: #0b81ff;
$title: "Hello";
$on: true;
$none: null;

// 列表 & 映射
$spaces: 4px 8px 16px;
$palette: (primary: #0b81ff, success: #22c55e);
  • null 的属性会被跳过。
  • map.get($palette, primary) 取值。

选择器嵌套与父选择器 &

.card {
  padding: 16px;
  &--active { box-shadow: 0 4px 12px rgba(0,0,0,.08); } // BEM 修饰
  &:hover { transform: translateY(-1px); }               // 伪类
  .title { font-weight: 600; }                           // 子元素
}

嵌套尽量 ≤ 3 层,避免选择器过长。

插值 #{}(拼接变量)

$base: btn;
$radius: 8;
.#{$base} { border-radius: #{$radius}px; } // → .btn { border-radius: 8px }

Mixin / Include(可复用片段)

@mixin flex-center($gap: 8px) {
  display: flex; align-items: center; justify-content: center; gap: $gap;
}
.toolbar { @include flex-center(12px); }

// 带内容插槽
@mixin layer($z: 1) { position: relative; z-index: $z; @content; }
.badge { @include layer(10) { pointer-events: none; } }

Function(返回计算值)

@use "sass:math";
@function px2rem($px, $base: 16) { @return math.div($px, $base) * 1rem; }
.title { font-size: px2rem(20); }

Dart Sass 用 math.div 代替 / 除法。

控制指令

$theme: dark;

@if $theme == dark { body { background:#0f172a; color:#e2e8f0; } }
@else { body { background:#fff; color:#111; } }

@each $gap in (4px, 8px, 12px) { .gap-#{$gap} { gap: $gap; } }

@for $i from 1 through 3 { .col-#{$i} { width: (100%/3)*$i; } }

$i: 0;
@while $i < 3 { .ring-#{$i} { outline-width: $i+1px; } $i: $i + 1; }

占位选择器与继承(谨慎用)

%btn-base { font: inherit; padding: .5em 1em; border: 1px solid transparent; }
.primary { @extend %btn-base; background: #0b81ff; color: #fff; }

@extend 可能引发“选择器膨胀”,组件库更推荐用 mixin 复用样式。

模块化导入(现代写法)

/* tokens/_color.scss */
$primary: #0b81ff !default;
@mixin btn() { padding: 8px 12px; }

/* design/_index.scss —— 聚合出口 */
@forward "../tokens/color";

/* app.scss —— 使用与配置 */
@use "./design/index" as d with ($primary: #0052d9);
.button { color: d.$primary; @include d.btn(); }
  • @use:有命名空间、成员只读;用 with 配置带 !default 的变量。
  • @forward:做“总入口”(barrel),对外转发变量/函数/mixin。
  • 旧式 @import 已弃用;@import url(...)CSS 导入(不会带来 Sass 变量)。

调试与错误

@use "sass:meta";
@debug meta.type-of((a: b));
@warn "Deprecated var, will be removed.";
@error "Invalid token";

全局方法(旧写法)

颜色相关

写法 说明 示例/结果
darken(#fff, 10%) 调暗亮度 10%(不是透明度) #fff → #e6e6e6
lighten(#fff, 30%) 调亮亮度 30%(纯白几乎无变化;也不是透明度) #fff → #fff
hsl(0, 100%, 50%) 色相/饱和度/亮度 构造颜色(红) #ff0000
adjust-hue(#fff, 180deg) 色相旋转 180°(对白/灰无可见变化) #fff
saturate(#fff, 10%) 提高饱和度(对白/灰无效) #fff
desaturate(#fff, 10%) 降低饱和度 #fff
transparentize(#fff, 0.1) 增加透明度(α 减 0.1) rgba(255,255,255,0.9)
opacify(#fff, 0.1) 增加不透明度(α 加 0.1) rgba(255,255,255,1)

注:transparentize/opacify 的第二个参数是 0–1 的无单位数,不是百分比。

计算

写法 说明 示例/结果
abs(-10px) 绝对值 10px
ceil(-12.5px) 向上取整(朝 +∞) -12px
round(12.3px) 四舍五入 12px
floor(12.8px) 向下取整 12px
percentage(650px / 1000px) 比例转百分比 65%(因为 px/px → 无单位 0.65)
min(1, 2, 3) 取最小值(编译期) 1
max(1, 2, 3) 取最大值(编译期) 3

提醒:min/max 比较带单位时需同维度可换算(如 pxin);px vs rem/vw 等不同维度请用 CSSmin()/max()/clamp() 在浏览器端计算。

字符串相关

写法 说明 示例/结果
to-upper-case("hello") 转大写 "HELLO"
to-lower-case("HELLO") 转小写 "hello"
str-length("hello") 字符串长度 5
str-index("hello", "h") 返回首次出现位置(1 基;找不到为 null 1
str-insert("hello", "world", 5) 在索引处插入子串(1 基;负数为倒数) "hellworldo"

内置模块Api(新写法)

这里主要介绍 sass:color、sass:math、sass:string,其他有需要的可以自行去了解

sass:color(调色/透明度/混色)

@use "sass:color";
$brand: #1677ff;

/* 1) 按比例靠近极值(推荐,变化更自然) */
.bg-hover  { background: color.scale($brand, $lightness: 10%); }   // 变亮
.bg-active { background: color.scale($brand, $lightness: -12%); }  // 变暗
.bg-ghost  { background: color.scale($brand, $alpha: -30%); }      // 更透明

/* 2) 加/减固定量(严格步长) */
.more-sat  { background: color.adjust($brand, $saturation: 15%); }
.rotate    { background: color.adjust($brand, $hue: 30deg); }      // 旋转色相

/* 3) 设定绝对值(直接锁定到目标) */
.fixed-a   { background: color.change($brand, $alpha: 0.6); }      // α=0.6
.fixed-l   { background: color.change($brand, $lightness: 40%); }  // L=40%

/* 4) 混色(做浅/深阶) */
.light-1   { background: color.mix(#fff, $brand, 20%); }           // 更浅
.dark-1    { background: color.mix(#000, $brand, 15%); }           // 更深

/* 5) 反相(生成对比色) */
.invert    { color: color.invert($brand, 100%); }

小抄:

  • 变亮/变暗 → scale($lightness: ±x%);透明度 → scale($alpha: ±x%)
  • 固定步长 → adjust;锁定目标值 → change;两色过渡 → mix

sass:math(数值/单位运算)

@use "sass:math";

/* 1) 除法一定用 math.div */
.title { font-size: math.div(20, 16) * 1rem; }  // 1.25rem

/* 2) 取整/比较/钳制(编译期) */
.box  { margin: math.round(4.6px); }            // 5px
.maxw { max-width: math.min(960px, 1in); }      // 单位可换算才行
/* 混单位(px vs rem/vw)请用 CSS 的 min()/clamp() 在运行时算 */

/* 3) 常用函数 */
$pi: math.$pi;                 // 3.14159…
$len: math.hypot(3, 4);        // 5(向量长度)
$angle: math.atan2(1, -1);     // 135deg(可直接放到 rotate())

/* 4) 小工具:px→rem */
@function px2rem($px, $base: 16) { @return math.div($px, $base) * 1rem; }
.btn { padding: px2rem(10) px2rem(14); }

要点:

  • min/max/clamp 仅比较同维度可换算单位(px↔in 等);混单位用 CSSmin()/max()/clamp()
  • 乘除尽量“有单位 × 无单位”,避免产生非法复合单位。

sass:string(拼接/切片/大小写/引号)

@use "sass:string";

/* 1) 拼接请用插值 #{} */
$ns: "app"; $block: btn;
.selector { content: "#{$ns}-#{$block}"; }   // "app-btn"

/* 2) 长度/索引/切片/插入(索引从 1 开始,负数从尾部数) */
@debug string.length("hello");                    // 5
@debug string.index("btn--primary", "--");        // 4
@debug string.slice("abcdef", 2, 4);              // "bcd"
@debug string.insert("color", "-primary", 6);     // "color-primary"

/* 3) 大小写/引号 */
@debug string.to-upper-case("btn");               // "BTN"
@debug string.quote(btn);                         // "btn"
@debug string.unquote("bold");                    // bold(变标识符)

/* 4) 唯一 id(避免命名冲突) */
$uid: string.unique-id();                         // 比如 "u5ab9"
@keyframes fade-#{$uid} { from{opacity:0} to{opacity:1} }
.fade { animation: fade-#{$uid} .2s ease; }

小抄:

  • 串联字符串 → **插值 #{}**;结构化处理再用 length/index/slice/insert
  • quote/unquote 控制是否带引号;unique-id() 做不冲突的 keyframes/变量名。

Scss 的四种导入方式你都知道吗

想把样式拆模块、做主题、又不想全局变量乱飞?这篇把 SCSS 的四种“导入/组织”方式一次讲透:@use@forward、Sass 旧式 @import、以及 CSS 的 @import url(...)。含示例、对比表、迁移步骤与避坑清单。


@import "xxx":Sass 旧式导入(已弃用

作用
编译期把被导入的 SCSS 内容“直接拼接”到当前位置,变量/混入进入全局,容易重复加载、顺序踩坑。Dart Sass 已弃用,推荐使用@use/@forward

示例

@import "./tokens";   // 编译时把 tokens.scss 的内容贴进来(非 CSS 运行时)

@import url(...)CSS 的导入

作用
浏览器在运行时请求外部 CSS 文件;不会引入 Sass 的变量/混入/函数。

示例

@import url("/base.css");           /* 或者 @import "/print.css" print */

位置限制
必须在样式表最前面(在任何普通规则前;前面最多有 @charset / 其它 @import)。影响性能,一般不建议使用。


@use:模块化导入(推荐

作用
将另一个 SCSS 文件当作“模块”加载,默认通过命名空间访问变量/函数/混入;只加载一次、不污染全局,避免命名冲突。

示例

/* tokens/_color.scss */
$primary: #0b81ff !default;
@mixin btn { padding: 8px 12px; border-radius: 8px; }

/* app.scss */
@use "./tokens/color" as c; // 起别名 c

.button {
  color: c.$primary;
  @include c.btn();
}

配置默认变量(with + !default)

@use "./tokens/color" as c with (
  $primary: #0052d9   // 仅能配置标了 !default 的顶层公开变量
);

要点

  • 命名空间:@use "x" as x;不建议as *(去掉前缀,易冲突)。
  • 变量只读:c.$primary 不能在引入方直接赋值,只能用 with加载时配置。

@forward:聚合/转发导出

作用
把若干模块“转发”出去,做一个统一出口(barrel/index)。自己文件里不能直接使用转发来的成员;若要自用,再 @use 一次。

示例

/* design/_index.scss —— 聚合出口 */
@forward "../tokens/color" show $primary, btn;  // 只暴露想给外部用的成员
@forward "../tokens/spacing" as space-*;        // 导出时加前缀

/* app.scss —— 使用统一出口 */
@use "./design/index" as d;

.card {
  color: d.$primary;
  margin: d.$space-lg;
}

在出口处预配置主题(with)

/* design/_index.scss */
@forward "../tokens/color" with ($primary: #333);

注意:一个模块只能被配置一次。上游已通过 @forward ... with 配过,下游再配会报错。


五分钟速查表

能力/特性 @use @forward Sass @import "x" CSS @import url(...)
发生时机 编译期 编译期 编译期 运行时(浏览器请求)
命名空间 有(可 as * 去掉) 对外导出,不给本文件用 无(全局拼接)
去重加载 ❌ 可能重复 N/A(浏览器请求)
变量可配置 通过 with 配置 !default 通过 with 预配置并转发 通过“先定义后导入”覆盖(老做法)
全局污染
位置限制 任意 任意 任意(编译期) 顶部(在普通规则前)
适用场景 模块化使用 统一出口/SDK 旧项目遗留 引入纯 CSS(不推荐)

常见误区 & 快速排坑

  1. @import url('./variable.scss') 用后 $color 未定义

    • 这是 CSS 导入,不会带来 Sass 变量。
    • 用:@use "./variable" as v;color: v.$color;
  2. 我在文件里写 $primary: red,为啥 @use 来的变量没变?

    • @use 是模块隔离;你改的是当前文件$primary,不是模块里的。
    • 正确:@use "./tokens" with ($primary: red); 且源变量需 !default
  3. 去命名空间 as *@import 一样吗?

    • 不是。as * 只是省掉前缀,仍是模块系统(只加载一次、不污染全局)。
    • 有冲突风险:多个模块导出同名变量会报错。
  4. @forward 文件里用不了转发来的变量?

    • 对。@forward 只是出口;若当前文件要用,@use 一次
  5. 位置问题:为啥我的 @import 被忽略?

    • CSS 的 @import 必须在最顶部(普通规则之前)。Sass 旧式 @import 不限位置,但已经弃用。

迁移指北:从 @import@use/@forward

目录建议

styles/
  tokens/
    _color.scss     // 变量均加 !default
    _spacing.scss
  mixins/
    _typography.scss
  design/
    _index.scss     // 聚合出口(只 forward)
  app.scss          // 项目入口(只 use design)

改造步骤

  1. 给需要对外可配的变量都加 !default

  2. 建“总出口”:

    /* design/_index.scss */
    @forward "../tokens/color";
    @forward "../tokens/spacing";
    @forward "../mixins/typography";
    
  3. 入口使用:

    /* app.scss */
    @use "./design/index" as d with (
      $primary: #0052d9,
      $space-lg: 24px
    );
    
    .btn { color: d.$primary; margin: d.$space-lg; }
    
  4. 删除旧式 @import,逐步把业务文件改为 @use 指向 design/_index.scss


结语

  • 新项目:只用 @use + @forward
  • 做 SDK/设计系统:@forward 聚合成一个总入口,外部只 @use 它。
  • 变量可配:源头加 !default,引入处用 with
  • 避免 @import 与 CSS 的 @import url(...)(性能差、易踩坑)。

需要我按你的项目目录,直接给一份可跑的 @use/@forward 模板吗?把目录贴过来我就给你落地版。

咖啡冒气

前言

这次趁着周末休息的时间,又弄了一个新的动画效果,用CSS实现的一个咖啡冒气效果。整个实现过程比较简单,大家可以先看到下面的效果预览部分,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。

效果预览

最终实现的相关效果如下。

HTML部分

首先我们看到HTML部分,相关代码如下。

 <div class="coffee">
      <div class="vapor">
        <span></span>
        <span></span>
        <span></span>
        <span></span>
        <span></span>
      </div>
      <div class="cup"></div>
      <div class="plate"></div>
    </div>

这里创建了一个带有热气动画的咖啡杯视觉效果

CSS部分

首先我们看到类名为coffee的样式,相关代码如下。

  .coffee {
      display: flex;
      flex-direction: column;
      align-items: center;
      height: calc(9em + 1em + 2em);
      position: relative;
    }
    .coffee .cup {
      width: 10em;
      height: 9em;
      background-color: white;
      border-radius: 0 0 1.5em 1.5em;
      position: relative;
    }

在.coffee容器中使用Flexbox布局,通过flex-direction: column让子元素垂直排列,通过align-items: center让子元素水平居中,最后为内部绝对定位元素提供定位上下文。

在.coffee .cup杯子样式中,设置杯子宽度和高度以及杯子的颜色,然后再给杯子设置底部的圆角,最后通过position: relative为伪元素提供定位上下文。

接下来便是上面提到的伪元素的样式,相关代码如下。

   .coffee .cup::before {
      content: "";
      position: absolute;
      width: 100%;
      height: 2em;
      background-color: chocolate;
      border: 0.5em solid white;
      box-sizing: border-box;
      border-radius: 50%;
      top: -1em;
      box-shadow: inset 0 0 1em rgba(0, 0, 0, 0.5);
    }
    .coffee .cup::after {
      content: "";
      position: absolute;
      width: 3em;
      height: 3.5em;
      border: 0.8em solid white;
      border-radius: 50%;
      top: 20%;
      left: 80%;
    }

这里主要使用伪元素 ::before::after 来绘制杯子的两部分。在.coffee .cup::before中,这个伪元素创建了杯子的杯身部分,杯身颜色为巧克力色,设置了白色边框,通过top: -1em向上偏移,使杯身部分露出,最后定义一个内部阴影,创造深度感。在.coffee .cup::after中,这个伪元素创建了杯子的把手部分,设置把手尺寸以及白色边框,形成环形把手,通过边框和圆形组合,创建出一个咖啡杯的环形把手。

综上所述,这里会创建一个具有巧克力色杯身、白色边框和右侧环形把手的咖啡杯视觉效果。

.coffee .vapor {
      width: 7em;
      height: 2em;
      display: flex;
      justify-content: space-between;
    }
    .coffee .vapor span {
      width: 0.1em;
      min-width: 1px;
      background-color: white;
      animation: evaporation 2s linear infinite;
      filter: opacity(0);
    }

这里创建了咖啡杯上方蒸汽飘动的动画效果。在.coffee .vapor中,设置了蒸汽容器的宽度和高度,并且使用flex布局让子元素均匀分布,两端对齐。

在.coffee .vapor span中,每个span代表一缕蒸汽,在这里应用名为 "evaporation"的动画,持续时间为2秒,速度曲线为匀速,并且无限循环。最后通过filter: opacity(0)让初始透明度为0(完全透明)。

这样就创造了咖啡杯上方热气腾腾的视觉效果。

最后我们看到这个效果的核心实现部分,相关代码如下。

.coffee .vapor span:nth-child(1) {
      animation-delay: 0.5s;
    }

    .coffee .vapor span:nth-child(2) {
      animation-delay: 0.1s;
    }

    .coffee .vapor span:nth-child(3) {
      animation-delay: 0.3s;
    }

    .coffee .vapor span:nth-child(4) {
      animation-delay: 0.4s;
    }

    .coffee .vapor span:nth-child(5) {
      animation-delay: 0.2s;
    }
    @keyframes evaporation {
      from {
        transform: translateY(0);
        filter: opacity(1) blur(0.2em);
      }
      to {
        transform: translateY(-4em);
        filter: opacity(0) blur(0.4em);
      }
    }

这里实现了咖啡蒸汽的动画效果,通过为每个蒸汽条设置不同的延迟和定义关键帧动画,创造出逼真的蒸汽飘散效果。

首先为5个蒸汽条(span元素)设置了不同的动画延迟,通过错开延迟让每个蒸汽条在不同的时间开始动画(0.1s到0.5s之间)。

然后在evaporation动画中,蒸汽从原始位置向上移动4em单位,从完全可见(opacity:1)渐变到完全透明(opacity:0),然后从轻微模糊(blur:0.2em)到更加模糊(blur:0.4em)吗,最终在模拟蒸汽上升过程中逐渐扩散、变淡直至消失的自然现象。

所以整体工作流程就是5个白色细条水平均匀分布,每个细条在不同时间开始动画(通过不同的animation-delay)最终通过动画创造出连续不断的蒸汽效果。

总结

以上就是整个效果的实现过程了,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~

手把手教你实现一个写作翻页

前言

借着上篇三本书的动画灵感,这次我们通过CSS来实现一个写作翻页的效果。整个实现过程比较简单,大家可以先看到下面的效果预览部分,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。

效果预览

最终实现的相关效果如下。

HTML部分

首先我们看到HTML部分,相关代码如下。

<div class="book">
<div class="page">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>

这里创建一个书本容器,右侧有一个翻页效果,通过设计多个span元素模拟书页上的文字行,然后在CSS中使用动画实现文字逐行显示效果。

CSS部分

首先我们看到类名book的样式,相关代码如下。

.book {
    --sw: 0.5em; /* stroke width */
    width: 15em;
    height: 10em;
    background-color: lightyellow;
    box-sizing: border-box;
    border: var(--sw) solid cadetblue;
    border-radius: var(--sw);
    font-size: 20px;
    position: relative;
}

.book .page {
    height: inherit;
    width: calc(50% + var(--sw) + var(--sw) / 2);
    background-color: inherit;
    box-sizing: border-box;
    border: inherit;
    border-radius: inherit;
    position: absolute;
    top: calc(-1 * var(--sw));
    right: calc(-1 * var(--sw));
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 8% 5%;
    transform-origin: left;
    transform-style: preserve-3d;
    animation: flip 4s linear infinite;
}

.book .page span {
    display: block;
    width: 100%;
    border-top: var(--sw) solid cadetblue;
    border-radius: inherit;
    transform-origin: left;
    transform: scaleX(0);
    backface-visibility: hidden;
    animation: 4s linear infinite;
}

在这里,通过--sw: 0.5em;定义CSS变量"stroke width",表示边框宽度,使用box-sizing: border-box确保边框宽度包含在元素总尺寸内。接着使用Flexbox列布局,内容均匀分布,设置变换原点在左侧,保留3D效果,并且应用一个flip动画,持续4秒,线性,无限循环,该动画在后面会提到。这里的每个span代表书页上的一行文字(用横线表示),初始状态:宽度缩放为0(不可见),设置一个动画,持续4秒,线性,无限循环,最后通过backface-visibility: hidden来防止元素背面在3D变换时可见。

综上所述,这里给翻页添加了样式,丰富了整个效果,接着我们来看到剩下的动画实现,相关代码如下。

.book .page span:nth-child(1) {animation-name: stroke-1;}
.book .page span:nth-child(2) {animation-name: stroke-2;}
.book .page span:nth-child(3) {animation-name: stroke-3;}
.book .page span:nth-child(4) {animation-name: stroke-4;}
.book .page span:nth-child(5) {animation-name: stroke-5;}
@keyframes stroke-1 {
    0% {transform: scaleX(0);}
    10%, 100% {transform: scaleX(1);}
}
@keyframes stroke-2 {
    0%, 10% {transform: scaleX(0);}
    20%, 100% {transform: scaleX(1);}
}
@keyframes stroke-3 {
    0%, 20% {transform: scaleX(0);}
    30%, 100% {transform: scaleX(1);}
}
@keyframes stroke-4 {
    0%, 30% {transform: scaleX(0);}
    40%, 100% {transform: scaleX(1);}
}
@keyframes stroke-5 {
    0%, 40% {transform: scaleX(0);}
    50%, 100% {transform: scaleX(1);}
}
@keyframes flip {
    55% {
        transform: rotateY(0) translateX(0) skewY(0);
    }
    70% {
        transform: rotateY(-90deg) translateX(calc(-1 * var(--sw) / 2)) skewY(-20deg);
    }
    80%, 100% {
        transform: rotateY(-180deg) translateX(calc(-1 * var(--sw))) skewY(0);
    }
}

这里实现了书本翻页和文字逐行显示的动画效果,是整个效果实现的核心所在。

这五条规则为每个span元素分配不同的动画,创建逐行显示的效果。在stroke这个逐行显示的动画效果中,每行从宽度为0开始(scaleX(0)),然后每行在前一行完成后开始显示,最后每行显示后保持可见状态。

在翻页动画中,0-55%的时候页面保持原始位置。55-70%的时候页面开始翻转,旋转到90度,同时有一些位移和倾斜。在70-80%的时候,页面完成180度翻转。在80-100%的时候,页面保持翻转状态。

总结

以上就是整个效果的实现过程了,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~

手把手教你实现三本前端书籍

前言

这次趁着周末休息的时间,弄了一个新的动画效果,用CSS实现了三本书,分别是HTMLCSSJS三本书,也算是回忆过去的学习经历了。整个实现过程比较简单,大家可以先看到下面的效果预览部分,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。

效果预览

最终实现的相关效果如下。

HTML部分

首先我们看到HTML部分,相关代码如下。

<div class="books">
<div class="book html">
<p>HTML<span>&lt;devolopment /&gt;</span></p>
</div>
<div class="book css">
<p>CSS<span>.devolopment::</span></p>
</div>
<div class="book js">
<p>JavaScript<span>{ devolopment }</span></p>
</div>
</div>

这里创建了一个包含三本书的书籍展示区,每本书代表一种前端开发技术。当与相应的CSS结合后,这段HTML会呈现出三本倾斜的3D书本效果,每本书有独特的颜色标识(通常HTML橙色、CSS蓝色、JS黄色),以及鼠标悬停时有上浮动画效果。这种设计巧妙地展示了前端开发的三种核心技术。

CSS部分

首先我们看到类名books的样式,相关代码如下。

.books {
display: flex;
width: calc(12rem * 3 + 3rem * 2);
justify-content: space-between;
margin-top: 6rem;
}

这里定义了一个名为.books的容器样式,用于包含多个书本元素。将元素设置为Flex容器,使其子元素(书本)按照Flexbox规则布局,默认情况下,子元素会水平排列在一行中。

通过以上的方式实现了一个水平排列的书本容器,书本之间有3rem的间距,容器总宽度为42rem,书本在容器内均匀分布,首尾书本贴边。

这种布局方式确保了书本在不同屏幕尺寸下保持正确的相对位置和间距。

然后我们看到类名为book的样式,相关代码如下。

.book {
width: 12rem;
height: 18rem;
background: linear-gradient(navy, deeppink, tomato);
transform: skewY(-10deg);
box-shadow: -10px 5px 30px rgba(0, 0, 0, 0.5);
position: relative;
transition: 0.3s;
}
.book:hover {
margin-top: -1.5rem;
}

这里针对上面的样式继续优化书本的特点,加入鲜艳的渐变背景,设置倾斜的3D效果以及逼真的阴影,最后通过一个平滑的悬停动画(鼠标放上去时书上浮)来结束。

这种设计让静态的书本元素具有交互性和动态感,增强了用户体验。

最后我们看到剩下的样式,相关代码如下。

.book::before {
background: linear-gradient(navy, deeppink, tomato);
}
.book::after {
content: '';
position: absolute;
width: 100%;
height: 1.5rem;
background: white;
top: -1.5rem;
left: 0;
transform-origin: bottom;
transform: skewX(45deg);
filter: brightness(0.9);
}
.book p {
position: absolute;
width: inherit;
height: 8rem;
top: 3rem;
box-sizing: border-box;
padding-top: 2rem;
color: whitesmoke;
font-size: 2.2rem;
font-family: sans-serif;
text-align: center;
text-shadow: -2px 2px 10px rgba(0, 0, 0, 0.3);
}

这里使用伪元素和文本样式来增强书本的3D效果和内容展示。::before 伪元素创建书本的左侧厚度效果,使用与书本相同的渐变背景,保持视觉一致性。

::after 伪元素 创建书本的顶部厚度效果,模拟书脊,使其脱离文档流,相对于书本定位,通过transform: skewX(45deg)沿X轴倾斜45度,形成斜面效果,最后通过filter: brightness(0.9) 稍微降低亮度,模拟阴影效果。

综上所述这里通过伪元素和文本样式增强了书本的3D效果,::before 创建左侧厚度,::after 创建顶部厚度,完成3D立体效果,文本样式确保书本内容清晰可读且视觉吸引人,这些样式共同创建了逼真的3D书本效果。

总结

以上就是整个效果的实现过程了,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~

关于 Grid 布局的一种响应式应用

前言

最近在做一个需求,要在页面上做一个图片墙,要求是展示不同大小的图片,但又要排成一个固定的模板样子。

大概像这样:

企业微信截图_1756794007605.png

一看到这种不规则拼贴的效果,我第一反应就是用 CSS Grid。Grid 在处理二维布局的时候是真的好用,不光能精确控制行和列,还能用 grid-template-areas 直接把布局语义化。尤其是遇到响应式场景时,Grid 简直就是开发效率和界面一致性的神器。


实现思路

常规的 Grid 写法就是在样式里给每个子元素设置它占几行几列、行高列宽多少。但问题是:如果要搞一堆不同的模板,那每次都要写一堆样式,维护起来挺麻烦。而且要是整个模块需要做缩放(比如响应式适配),光靠设置行列宽高也不好用,就算用 clamp 也还是怪怪的。

像这种情况,直接用 grid-template-areas 会舒服很多。

比如我们把这个布局拆成最小的“方格单元”,最后抽象成一个 6 列 × 4 行 的网格。格子之间留固定间隙(比如 0.89rem),要求每个小格子保持正方形,并且能随容器宽度自适应。

模板可以这样定义:

const GRID_AREAS_TEMPLATE = {
  ONE: [
    ["A","A","A","B","C","C"],
    ["A","A","A","D","C","C"],
    ["A","A","A","E","F","F"],
    ["G","H","I","J","F","F"],
  ],
};

不同的字母就代表不同的区域,子元素通过 gridArea 对应。这样一来,我们可以提前准备多个模板,按场景动态切换。


React 示例

比如这里我直接写了一个 Demo:

<div
  style={{
    gridTemplateColumns: "repeat(6, 1fr)",
    gridTemplateRows: "repeat(4, 1fr)",
    gridTemplateAreas: template.map(row => `"${row.join(" ")}"`).join(" "),
    gap: "0.89rem",
  }}
>
  {imgsEls.map((el, index) => {
    const area = String.fromCharCode(65 + index); // A,B,C...
    return (
      <div
        key={index}
        style={{ gridArea: area }}
        className="flex items-center justify-center"
      >
        {el}
      </div>
    );
  })}
</div>

打开控制台就能看到 Grid 的区域分布了。

企业微信截图_17567938641448.png

不过问题来了:小格子不是正方形,行高和列宽有 1~2px 的偏差。


为什么会有偏差?

原因其实很简单:

  • 列宽计算时,会减去所有横向 gap,然后平均分给列
  • 行高的 1fr 是“分剩下的空间”,没考虑纵向 gap

结果就是算出来有点对不齐,如果展示图片,可能会导致图片模糊。


解决办法:计算容器的正确高度

本质问题就是 Grid 容器的高度没算准。要让每个格子保持正方形比例,我们可以自己用 padding-top 把高度撑出来。

公式大概是这样的:

容器高度 = 行数 × 每列宽度 + (行数 - 1) × gap
每列宽度 = (100% - (列数 - 1) × gap) / 列数

所以最后写法是:

paddingTop: `calc(${rows} * ((100% - ${columns - 1} * ${gap}) / ${columns}) + ${rows - 1} * ${gap})`

实战代码

const columns = 6;
const rows = 4;
const gap = "0.89rem";
const gridTemplateAreas = GRID_AREAS_TEMPLATE.ONE.map(row => `"${row.join(" ")}"`).join(" ");

return (
  <div
    className="relative w-full"
    style={{
      paddingTop: `calc(${rows} * ((100% - ${columns - 1} * ${gap}) / ${columns}) + ${rows - 1} * ${gap})`,
    }}
  >
    <div
      className="absolute inset-0 grid h-full w-full"
      style={{
        gridTemplateColumns: `repeat(${columns}, 1fr)`,
        gridTemplateRows: `repeat(${rows}, 1fr)`,
        gridTemplateAreas,
        gap,
      }}
    >
      {imgsEls.map((el, index) => {
        const area = String.fromCharCode(65 + index); // A, B, C...
        return (
          <div
            key={index}
            style={{ gridArea: area }}
            className="flex items-center justify-center"
          >
            {el}
          </div>
        );
      })}
    </div>
  </div>
);

这样就能保证:

  • 容器高度和宽度匹配,格子永远是正方形
  • 布局用 grid-template-areas 来控制,清晰直观
  • 想换模板的时候只改配置就行

其他可选方案

除了上面这种 padding-top 算高度的办法,其实 Grid 还有不少响应式写法:

  1. auto-fit / auto-fill 自动列
  2. aspect-ratio 直接控制比例
  3. 媒体查询切列数
  4. grid-auto-rows + minmax 动态行高
  5. 固定 px + auto-fill 保证无偏差

具体写法我就不在这啰嗦了,反正都能解决不同场景的问题。


总结

这次主要踩的坑就是 格子不是正方形,最后靠算高度搞定了。

整个过程下来,我觉得几个要点特别有用:

  1. 模板化:用 GRID_AREAS_TEMPLATE 来抽象布局,换场景只改配置
  2. 自适应padding-top 算高度,保证格子比例
  3. 多方案结合:Grid 其实提供了很多玩法(auto-fit、aspect-ratio、minmax…),要灵活用

所以无论是做图片墙、仪表盘,还是游戏地图,都能找到适合的 Grid 方案。

手把手教你实现飞机上的开窗关窗效果

前言

接着上个小飞机的效果,在这个动画的基础上,用CSS实现的一个在飞机上开窗关窗的效果。整个实现过程比较简单,大家可以先看到下面的效果预览部分,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。

效果预览

最终实现的相关效果如下。

HTML部分

首先我们看到HTML部分,相关代码如下。

  <input type="checkbox" class="toggle">
    <figure class="window">
        <div class="curtain"></div>
        <div class="clouds">
            <span></span>
            <span></span>
            <span></span>
            <span></span>
            <span></span>
        </div>
    </figure>

首先创建一个隐藏的复选框输入元素,用作交互触发器,通过CSS的:checked伪类实现状态切换。然后使用<figure>语义化标签作为窗户的主要容器,作为窗帘和云朵背景的父元素,通常通过CSS设置窗户的边框、阴影和整体样式,curtain代表窗户的窗帘部分,通过CSS实现窗帘的样式和动画效果。最后创建了多个<span>元素,每个代表一朵云。

CSS部分

首先我们看到类名toggle的样式,相关代码如下。

.toggle {
    position: absolute;
    filter: opacity(0);
    width: 25em;
    height: 35em;
    font-size: var(--font-size);
    cursor: pointer;
    z-index: 3;
}

这里定义了一个隐藏但可点击的切换开关元素。通过视觉隐藏 (filter: opacity(0)),使用滤镜将元素透明度设为0(完全透明),比opacity: 0visibility: hidden更好的选择,因为它可以保持元素完全可点击和可聚焦,不影响布局(元素仍占据空间),不会阻止鼠标事件。

配合CSS选择器如:checked,可以实现复杂的交互效果而无需JavaScript。

然后是窗户和窗帘的CSS部分,相关代码如下。

.toggle:checked ~ .window .curtain {
    top: -90%;
}
.window .curtain::before {
    content: '';
    position: absolute;
    width: 40%;
    height: 0.8em;
    background-color: #808080;
    left: 30%;
    bottom: 1.6em;
    border-radius: 0.4em;
}
.window .curtain::after {
    content: '';
    position: absolute;
    width: 1.6em;
    height: 0.8em;
    bottom: 1.6em;
    background-image: radial-gradient(orange, orangered);
    left: calc((100% - 1.6em) / 2);
    border-radius: 0.4em;
}
.toggle:checked ~ .window .curtain::after {
    background-image: radial-gradient(lightgreen, limegreen);
}

这里实现了窗帘的交互效果和样式细节。首先是窗帘开合动画控制,当复选框被选中时,将窗帘向上移动90%的高度,:checked是匹配被选中的复选框,~是通用兄弟选择器,选择.toggle之后的所有.window元素,最终选择.toggle之后.window内的.curtain元素。

所以最终的动画效果是当隐藏的复选框被点击时,窗帘会移动到顶部-90%的位置,实现"打开"效果。

当复选框被选中时,将把手颜色变为绿色渐变,当窗帘打开时,把手颜色从橙红色变为绿色,提供视觉反馈,表示状态已改变。

这里展示了如何通过纯CSS创建具有状态反馈的交互式组件,利用CSS的:checked伪类实现交互,使用::before::after添加装饰元素,无需额外HTML,通过颜色变化提供状态指示。

最后是窗外的云的CSS样式,相关代码如下。

.window .clouds {
    position: relative;
    width: 20em;
    height: 30em;
    background-color: deepskyblue;
    left: calc((100% - 20em) / 2);
    top: calc((100% - 30em) / 2);
    border-radius: 7em;
    box-shadow: 0 0 0 0.4em #808080;
    overflow: hidden;
}
.clouds span {
    position: absolute;
    width: 10em;
    height: 4em;
    background-color: white;
    top: 20%;
    border-radius: 4em;
    animation: move 4s linear infinite;
}
@keyframes move {
    from {left: -150%;}
    to {left: 150%;}
}

这里创建了一个天空背景和飘动的云朵动画效果:

精确定位

  • 使用calc()函数实现天空区域的完美居中
  • left: calc((100% - 20em) / 2) 水平居中
  • top: calc((100% - 30em) / 2) 垂直居中

云朵形状

  • 通过border-radius: 4em创建椭圆形云朵
  • 高度(4em)是宽度(10em)的40%,形成扁平椭圆形状

动画设计

  • 使用linear线性动画确保匀速移动
  • infinite无限循环创造持续飘动效果
  • 从-150%到150%确保云朵完全穿过可见区域

视觉层次

  • 天蓝色背景与白色云朵形成鲜明对比
  • 灰色边框定义天空区域边界
  • overflow: hidden确保动画元素不会破坏布局

这里创建了一个视觉上吸引人的天空背景,云朵从左侧进入,横穿天空区域,然后从右侧离开,形成连续循环的飘动效果。

总结

以上就是整个效果的实现过程了,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~

手把手教你实现一个小飞机

前言

这次趁着周末休息的时间,弄了一个用 CSS实现的直升机的动画效果。整个实现过程比较简单,大家可以先看到下面的效果预览部分,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。

效果预览

最终实现的相关效果如下。

HTML部分

首先我们看到HTML部分,相关代码如下。

 <div class="plane">
        <div class="fans">
            <span></span>
            <span></span>
            <span></span>
            <span></span>
        </div>
        <div class="wheels">
            <span class="wheel left"></span>
            <span class="wheel right"></span>
        </div>
    </div>

这里创建了一个飞机动画的基本结构,包含三个主要部分,首先看到<div class="plane">,这里是飞机的主体容器,紧接着是螺旋桨部分 (<div class="fans">),包含4个<span>元素,代表螺旋桨的叶片,每个<span>会通过CSS样式形成螺旋桨的各个叶片。

最后是起落架部分 (<div class="wheels">),包含左右两个轮子 (<span class="wheel left"><span class="wheel right">),每个轮子元素后续会通过CSS样式形成飞机的轮子和支撑结构。

CSS部分

接着我们看到CSS部分,首先我们看到飞机容器的样式,相关代码如下。

.plane {
    font-size: 10px;
    width: 28em;
    height: 13em;
    display: flex;
    justify-content: center;
    position: relative;
    color: black;
    /* wings */
    background: 
        linear-gradient(currentColor, currentColor)
            no-repeat center top / 100% 0.5em,
        linear-gradient(currentColor, currentColor)
            no-repeat center 4em / 90% 0.4em;
    animation: 
        plane-rotating 10s infinite,
        fly 5s infinite;
}

使用flex布局,水平居中内容,使用线性渐变创建出机翼样式,并且设置一些动画效果,旋转动画:持续10秒,无限循环;飞行动画:持续5秒,无限循环。

接着看到设置的飞机动画效果,相关代码如下。

@keyframes plane-rotating {
    10%, 30%, 50% {transform: rotate(0deg);}
    20% {transform: rotate(-4deg);}     
    80% {transform: rotate(8deg);}          
    100% {transform: rotate(-1turn);}
}
@keyframes fly {
    10%, 50%, 100% {top: 0;}    
    25% {top: 1em;}            
    75% {top: -1em;}            
}

这里创造了两个具有逼真飞行效果的飞机动画,包含旋转和上下浮动两种运动模式。在plane-rotating动画中,在10%、30%、50%时间点保持水平,在20%时间点向左倾斜4度,在80%时间点向右倾斜8度,在100%时间点完成一整圈旋转(逆时针)。

fly动画中,在10%、50%、100%时间点保持在原始位置,在25%时间点向下移动1em(10px),在75%时间点向上移动1em(10px)。

接着我们看到四叶螺旋桨的样式,相关代码如下。

.fans {
    width: 11em;
    height: 11em;
    background: radial-gradient(currentColor 2.5em, transparent 2.5em);
}
.fans span {
    width: inherit;
    height: 50%;
    background-color: hsla(100, 100%, 100%, 0.4);
    border-radius: 50% 50% 0 0 / 100% 100% 0 0;
    transform-origin: bottom;
    transform: rotate(calc((var(--n) - 1) * 90deg));
    animation: fans-rotating 0.8s linear infinite;
    animation-delay: calc(var(--n) * 0.1s);
}
@keyframes fans-rotating {
    to {transform: rotate(-1turn);}
}

从代码中可以看出,半圆形叶片是使用特殊border-radius语法border-radius: 50% 50% 0 0 / 100% 100% 0 0创建出来的半圆形顶部。使用CSS变量(--n) 控制每个叶片的角度和动画延迟,需要配合额外的CSS为每个span设置不同的--n值。使用transform-origin: bottom确保叶片围绕底部中心旋转。使用calc((var(--n) - 1) * 90deg)将4个叶片均匀分布(0°, 90°, 180°, 270°)。最后使用calc(var(--n) * 0.1s)为每个叶片设置不同的延迟,创建连续旋转效果。

综上所述,这里创建了一个四叶片螺旋桨,每个叶片从底部中心点旋转出来,并通过CSS动画实现连续旋转效果,模拟飞机螺旋桨的工作状态。

最后我们看到轮子的样式,相关代码如下。

.wheels {
    width: 16em;
    height: 2em;
    bottom: 0;
    display: flex;
    justify-content: space-between;
}
.wheel {
    position: relative;
    width: 1em;
    height: inherit;
    background-color: currentColor;
    border-radius: 0.5em;
    display: flex;
    justify-content: center;
}
.wheel::before {
    width: 0.2em;
    height: 8em;
    background-color: currentColor;
    transform-origin: bottom;
    bottom: 1em;
    z-index: -1;
}

对于整体的结构,使用display: flexjustify-content: space-between让两个轮子平均分布在起落架容器两端,使用::before伪元素创建轮子的支撑杆,无需额外HTML元素。position: relative为轮子创建定位上下文,position: absolute让支撑杆相对于轮子定位,bottom: 1em将支撑杆底部定位在轮子内部。

综上所述,这里创建了一个完整的飞机起落架结构,包含两个轮子和它们的支撑杆,形成完整的V形起落架结构。

总结

以上就是整个效果的实现过程了,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~

🔥 z-index明明设了999999还是不生效呢

🎯 学习目标:深入理解CSS层叠上下文机制,掌握z-index失效的根本原因和解决方案

📊 难度等级:中级-高级
🏷️ 技术标签#CSS #层叠上下文 #z-index #前端布局
⏱️ 阅读时间:约8分钟


🌟 引言

在日常的前端开发中,你是否遇到过这样的困扰:

  • z-index设置了999999,元素还是被其他元素遮挡
  • 弹窗明明z-index很高,却显示在页面元素下方
  • 同样的z-index值,在不同位置表现完全不同
  • 层级关系看起来很简单,实际效果却让人摸不着头脑

今天分享5个层叠上下文的核心陷阱和解决方案,让你彻底理解z-index的工作机制,再也不用盲目地把z-index设置成天文数字!


💡 核心技巧详解

1. 层叠上下文的创建条件:不只是z-index

🔍 应用场景

很多开发者以为只有设置了z-index才会创建层叠上下文,这是最大的误区。

❌ 常见问题

只关注z-index,忽略了其他创建层叠上下文的属性

/* ❌ 以为只有这样才创建层叠上下文 */
.modal {
  position: relative;
  z-index: 9999;
}

✅ 推荐方案

了解所有创建层叠上下文的条件

/**
 * 创建层叠上下文的完整条件
 * @description 以下任一条件都会创建新的层叠上下文
 */

/* 1. 根元素html */
html { /* 自动创建根层叠上下文 */ }

/* 2. position + z-index */
.positioned {
  position: relative; /* absolute, fixed, sticky */
  z-index: 1; /* 任何非auto值 */
}

/* 3. opacity小于1 */
.transparent {
  opacity: 0.99; /* 创建层叠上下文 */
}

/* 4. transform不为none */
.transformed {
  transform: translateZ(0); /* 任何3D变换 */
}

/* 5. filter不为none */
.filtered {
  filter: blur(1px); /* 任何滤镜效果 */
}

/* 6. isolation: isolate */
.isolated {
  isolation: isolate; /* 强制创建层叠上下文 */
}

/* 7. will-change包含创建层叠上下文的属性 */
.optimized {
  will-change: transform; /* 性能优化同时创建层叠上下文 */
}

💡 核心要点

  • 隐式创建:很多CSS属性会意外创建层叠上下文
  • 嵌套规则:子层叠上下文永远无法超越父层叠上下文
  • 检测方法:使用开发者工具查看Layers面板

🎯 实际应用

检测元素是否创建了层叠上下文的工具函数

/**
 * 检测元素是否创建了层叠上下文
 * @description 通过计算样式判断元素是否创建层叠上下文
 * @param {HTMLElement} element - 要检测的DOM元素
 * @returns {boolean} 是否创建了层叠上下文
 */
const hasStackingContext = (element) => {
  const styles = getComputedStyle(element);
  
  // 检查各种创建层叠上下文的条件
  const conditions = [
    styles.position !== 'static' && styles.zIndex !== 'auto',
    parseFloat(styles.opacity) < 1,
    styles.transform !== 'none',
    styles.filter !== 'none',
    styles.isolation === 'isolate',
    styles.mixBlendMode !== 'normal',
    styles.willChange.includes('transform') || 
    styles.willChange.includes('opacity') ||
    styles.willChange.includes('filter')
  ];
  
  return conditions.some(condition => condition);
};

// 使用示例
const modal = document.querySelector('.modal');
console.log('Modal创建层叠上下文:', hasStackingContext(modal));

2. 层叠上下文的比较规则:同级才能比较

🔍 应用场景

当z-index看起来很高,但元素仍然被遮挡时,通常是层叠上下文层级问题。

❌ 常见问题

不同层叠上下文的元素直接比较z-index

<!-- ❌ 错误理解:认为modal会在最上层 -->
<div class="container" style="transform: translateZ(0); z-index: 1;">
  <div class="content">内容区域</div>
</div>
<div class="modal" style="position: fixed; z-index: 9999;">
  弹窗内容
</div>
/* ❌ modal的z-index再高也无效 */
.container {
  transform: translateZ(0); /* 创建层叠上下文 */
  z-index: 1;
}

.modal {
  position: fixed;
  z-index: 9999; /* 无效!因为不在同一层叠上下文 */
}

✅ 推荐方案

确保需要比较的元素在同一层叠上下文中

/**
 * 正确的层叠上下文管理
 * @description 将需要比较层级的元素放在同一层叠上下文中
 */

/* 方案1: 提升modal到根层叠上下文 */
.container {
  /* 移除创建层叠上下文的属性 */
  /* transform: translateZ(0); */
}

.modal {
  position: fixed;
  z-index: 1000; /* 现在有效了 */
}

/* 方案2: 使用isolation创建明确的层叠上下文 */
.app-root {
  isolation: isolate; /* 创建根层叠上下文 */
}

.content-layer {
  z-index: 1;
}

.modal-layer {
  z-index: 1000;
}

💡 核心要点

  • 同级比较:只有同一层叠上下文中的元素才能比较z-index
  • 父子关系:子元素永远无法超越父元素的层叠上下文
  • 层级管理:建立清晰的层级管理体系

🎯 实际应用

层叠上下文可视化调试工具

/**
 * 可视化显示页面中的层叠上下文
 * @description 为所有创建层叠上下文的元素添加边框和标签
 */
const visualizeStackingContexts = () => {
  const allElements = document.querySelectorAll('*');
  let contextCount = 0;
  
  allElements.forEach(element => {
    if (hasStackingContext(element)) {
      contextCount++;
      
      // 添加可视化边框
      element.style.outline = '2px solid red';
      element.style.position = 'relative';
      
      // 添加标签
      const label = document.createElement('div');
      label.textContent = `层叠上下文 ${contextCount}`;
      label.style.cssText = `
        position: absolute;
        top: -20px;
        left: 0;
        background: red;
        color: white;
        padding: 2px 6px;
        font-size: 12px;
        z-index: 10000;
      `;
      element.appendChild(label);
    }
  });
  
  console.log(`发现 ${contextCount} 个层叠上下文`);
};

// 开发环境下使用
if (process.env.NODE_ENV === 'development') {
  visualizeStackingContexts();
}

3. transform和opacity的隐藏陷阱:意外的层叠上下文

🔍 应用场景

使用CSS动画或透明效果时,经常会意外创建层叠上下文,导致z-index失效。

❌ 常见问题

不知道transform和opacity会创建层叠上下文

/* ❌ 意外创建层叠上下文的动画 */
.card {
  transition: transform 0.3s;
}

.card:hover {
  transform: scale(1.05); /* 创建了层叠上下文! */
}

.tooltip {
  position: absolute;
  z-index: 999; /* 可能被card遮挡 */
}

✅ 推荐方案

合理规划动画和层级关系

/**
 * 避免意外层叠上下文的动画方案
 * @description 使用不创建层叠上下文的属性或合理规划层级
 */

/* 方案1: 使用不创建层叠上下文的属性 */
.card {
  transition: box-shadow 0.3s;
}

.card:hover {
  box-shadow: 0 10px 30px rgba(0,0,0,0.2); /* 不创建层叠上下文 */
}

/* 方案2: 明确管理层叠上下文 */
.card-container {
  isolation: isolate; /* 明确创建层叠上下文 */
}

.card {
  z-index: 1;
  transition: transform 0.3s;
}

.card:hover {
  transform: scale(1.05);
  z-index: 2; /* 在同一层叠上下文中提升 */
}

.tooltip {
  z-index: 10; /* 确保在card之上 */
}

💡 核心要点

  • 动画陷阱:CSS动画经常意外创建层叠上下文
  • 透明度陷阱:opacity < 1 就会创建层叠上下文
  • 预防措施:使用isolation明确管理层叠上下文

🎯 实际应用

安全的动画组件封装

/**
 * 创建不影响层叠上下文的动画组件
 * @description Vue3组件,提供安全的动画效果
 * @param {Object} props - 组件属性
 * @returns {Object} Vue组件
 */
const SafeAnimationCard = {
  props: {
    elevation: {
      type: Number,
      default: 1
    }
  },
  
  setup(props) {
    const cardRef = ref(null);
    const isHovered = ref(false);
    
    /**
     * 鼠标悬停处理
     * @description 使用box-shadow而非transform避免层叠上下文
     */
    const handleMouseEnter = () => {
      isHovered.value = true;
    };
    
    const handleMouseLeave = () => {
      isHovered.value = false;
    };
    
    /**
     * 计算阴影样式
     * @description 根据悬停状态计算box-shadow
     * @returns {string} CSS box-shadow值
     */
    const shadowStyle = computed(() => {
      const baseElevation = props.elevation;
      const hoverElevation = baseElevation + 2;
      const elevation = isHovered.value ? hoverElevation : baseElevation;
      
      return `0 ${elevation * 2}px ${elevation * 4}px rgba(0,0,0,0.1)`;
    });
    
    return {
      cardRef,
      handleMouseEnter,
      handleMouseLeave,
      shadowStyle
    };
  },
  
  template: `
    <div 
      ref="cardRef"
      class="safe-card"
      :style="{ boxShadow: shadowStyle }"
      @mouseenter="handleMouseEnter"
      @mouseleave="handleMouseLeave"
    >
      <slot></slot>
    </div>
  `
};

4. 固定定位的层叠陷阱:position: fixed的特殊性

🔍 应用场景

使用position: fixed创建全屏遮罩或固定导航时,经常遇到层级问题。

❌ 常见问题

fixed元素被其他元素遮挡,即使z-index很高

/* ❌ fixed元素可能被遮挡 */
.sidebar {
  transform: translateX(0); /* 创建层叠上下文 */
  z-index: 100;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 9999; /* 仍然可能被sidebar遮挡 */
}

✅ 推荐方案

为fixed元素创建独立的层叠上下文

/**
 * 固定定位元素的正确层级管理
 * @description 确保fixed元素在正确的层叠上下文中
 */

/* 方案1: 使用CSS自定义属性管理z-index */
:root {
  --z-dropdown: 1000;
  --z-sticky: 1020;
  --z-fixed: 1030;
  --z-modal-backdrop: 1040;
  --z-modal: 1050;
  --z-popover: 1060;
  --z-tooltip: 1070;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: var(--z-modal-backdrop);
}

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: var(--z-modal);
}

/* 方案2: 使用Portal模式 */
.portal-root {
  position: relative;
  z-index: 9999;
  pointer-events: none; /* 不阻挡下层交互 */
}

.portal-content {
  pointer-events: auto; /* 恢复交互 */
}

💡 核心要点

  • 独立管理:fixed元素应该有独立的层级管理
  • Portal模式:使用Portal将fixed元素渲染到特定容器
  • z-index规范:建立统一的z-index命名规范

🎯 实际应用

Vue3 Portal组件实现

/**
 * Portal组件 - 将内容渲染到指定容器
 * @description 解决fixed元素的层叠上下文问题
 */
import { Teleport, defineComponent } from 'vue';

const Portal = defineComponent({
  name: 'Portal',
  props: {
    to: {
      type: String,
      default: 'body'
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  
  setup(props, { slots }) {
    /**
     * 确保目标容器存在
     * @description 如果目标容器不存在则创建
     */
    const ensureContainer = () => {
      if (props.to === 'body') return;
      
      let container = document.querySelector(props.to);
      if (!container) {
        container = document.createElement('div');
        container.id = props.to.replace('#', '');
        container.className = 'portal-container';
        container.style.cssText = `
          position: relative;
          z-index: 9999;
          pointer-events: none;
        `;
        document.body.appendChild(container);
      }
    };
    
    onMounted(() => {
      ensureContainer();
    });
    
    return () => {
      if (props.disabled) {
        return slots.default?.();
      }
      
      return h(Teleport, { to: props.to }, slots.default?.());
    };
  }
});

// 使用示例
const Modal = {
  setup() {
    const visible = ref(false);
    
    return {
      visible
    };
  },
  
  template: `
    <Portal to="#modal-container">
      <div v-if="visible" class="modal-overlay">
        <div class="modal">
          <h2>弹窗标题</h2>
          <p>弹窗内容</p>
        </div>
      </div>
    </Portal>
  `
};

5. 层叠上下文的调试技巧:快速定位问题

🔍 应用场景

当遇到复杂的层级问题时,需要快速定位问题根源。

❌ 常见问题

盲目调整z-index值,没有系统的调试方法

/* ❌ 盲目增加z-index */
.element {
  z-index: 999999999; /* 治标不治本 */
}

✅ 推荐方案

使用系统的调试方法和工具

/**
 * 层叠上下文调试工具集
 * @description 提供完整的层叠上下文调试功能
 */
class StackingContextDebugger {
  constructor() {
    this.contexts = new Map();
  }
  
  /**
   * 分析元素的层叠上下文路径
   * @description 从元素向上遍历,找到所有层叠上下文
   * @param {HTMLElement} element - 要分析的元素
   * @returns {Array} 层叠上下文路径
   */
  getStackingPath = (element) => {
    const path = [];
    let current = element;
    
    while (current && current !== document.documentElement) {
      if (hasStackingContext(current)) {
        const styles = getComputedStyle(current);
        path.push({
          element: current,
          zIndex: styles.zIndex,
          position: styles.position,
          transform: styles.transform,
          opacity: styles.opacity,
          filter: styles.filter
        });
      }
      current = current.parentElement;
    }
    
    return path.reverse(); // 从根到叶子的顺序
  };
  
  /**
   * 比较两个元素的层级关系
   * @description 分析为什么一个元素会遮挡另一个元素
   * @param {HTMLElement} element1 - 第一个元素
   * @param {HTMLElement} element2 - 第二个元素
   * @returns {Object} 比较结果
   */
  compareElements = (element1, element2) => {
    const path1 = this.getStackingPath(element1);
    const path2 = this.getStackingPath(element2);
    
    // 找到分叉点
    let commonAncestorIndex = 0;
    while (
      commonAncestorIndex < Math.min(path1.length, path2.length) &&
      path1[commonAncestorIndex].element === path2[commonAncestorIndex].element
    ) {
      commonAncestorIndex++;
    }
    
    const result = {
      element1: element1,
      element2: element2,
      path1: path1,
      path2: path2,
      commonAncestorIndex: commonAncestorIndex - 1,
      winner: null,
      reason: ''
    };
    
    // 如果有共同的层叠上下文父元素
    if (commonAncestorIndex < Math.max(path1.length, path2.length)) {
      const context1 = path1[commonAncestorIndex];
      const context2 = path2[commonAncestorIndex];
      
      if (context1 && context2) {
        const z1 = parseInt(context1.zIndex) || 0;
        const z2 = parseInt(context2.zIndex) || 0;
        
        if (z1 > z2) {
          result.winner = element1;
          result.reason = `元素1的层叠上下文z-index(${z1}) > 元素2的z-index(${z2})`;
        } else if (z2 > z1) {
          result.winner = element2;
          result.reason = `元素2的层叠上下文z-index(${z2}) > 元素1的z-index(${z1})`;
        } else {
          result.reason = '相同z-index,按DOM顺序决定层级';
        }
      }
    }
    
    return result;
  };
  
  /**
   * 生成调试报告
   * @description 生成详细的层叠上下文调试报告
   * @param {HTMLElement} element - 要调试的元素
   */
  generateReport = (element) => {
    const path = this.getStackingPath(element);
    
    console.group(`🔍 层叠上下文调试报告: ${element.tagName}.${element.className}`);
    
    console.log('📍 层叠上下文路径:');
    path.forEach((context, index) => {
      console.log(`  ${index + 1}. ${context.element.tagName}.${context.element.className}`);
      console.log(`     z-index: ${context.zIndex}`);
      console.log(`     position: ${context.position}`);
      if (context.transform !== 'none') console.log(`     transform: ${context.transform}`);
      if (context.opacity !== '1') console.log(`     opacity: ${context.opacity}`);
      if (context.filter !== 'none') console.log(`     filter: ${context.filter}`);
    });
    
    console.log('💡 建议:');
    if (path.length > 3) {
      console.log('  - 层叠上下文嵌套过深,考虑简化结构');
    }
    
    const hasHighZIndex = path.some(ctx => parseInt(ctx.zIndex) > 1000);
    if (hasHighZIndex) {
      console.log('  - 发现过高的z-index值,建议使用统一的z-index管理');
    }
    
    console.groupEnd();
  };
}

// 全局调试工具
window.stackingDebugger = new StackingContextDebugger();

// 使用示例
// stackingDebugger.generateReport(document.querySelector('.modal'));
// stackingDebugger.compareElements(element1, element2);

💡 核心要点

  • 路径分析:分析元素的完整层叠上下文路径
  • 比较工具:系统比较两个元素的层级关系
  • 可视化调试:使用浏览器开发者工具的Layers面板

🎯 实际应用

开发环境下的自动检测

/**
 * 开发环境下的层叠上下文监控
 * @description 自动检测和报告潜在的层叠问题
 */
const setupStackingMonitor = () => {
  if (process.env.NODE_ENV !== 'development') return;
  
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          checkStackingIssues(node);
        }
      });
    });
  });
  
  /**
   * 检查潜在的层叠问题
   * @description 检测常见的层叠上下文问题
   * @param {HTMLElement} element - 要检查的元素
   */
  const checkStackingIssues = (element) => {
    const styles = getComputedStyle(element);
    const zIndex = parseInt(styles.zIndex);
    
    // 检查过高的z-index
    if (zIndex > 10000) {
      console.warn(`⚠️ 发现过高的z-index: ${zIndex}`, element);
    }
    
    // 检查意外的层叠上下文
    if (hasStackingContext(element) && !element.hasAttribute('data-stacking-context')) {
      console.info(`ℹ️ 意外创建的层叠上下文:`, element);
      console.log('创建原因:', getStackingContextReason(element));
    }
  };
  
  /**
   * 获取创建层叠上下文的原因
   * @description 分析元素创建层叠上下文的具体原因
   * @param {HTMLElement} element - 要分析的元素
   * @returns {string} 创建原因
   */
  const getStackingContextReason = (element) => {
    const styles = getComputedStyle(element);
    const reasons = [];
    
    if (styles.position !== 'static' && styles.zIndex !== 'auto') {
      reasons.push(`position: ${styles.position} + z-index: ${styles.zIndex}`);
    }
    if (parseFloat(styles.opacity) < 1) {
      reasons.push(`opacity: ${styles.opacity}`);
    }
    if (styles.transform !== 'none') {
      reasons.push(`transform: ${styles.transform}`);
    }
    if (styles.filter !== 'none') {
      reasons.push(`filter: ${styles.filter}`);
    }
    if (styles.isolation === 'isolate') {
      reasons.push('isolation: isolate');
    }
    
    return reasons.join(', ');
  };
  
  observer.observe(document.body, {
    childList: true,
    subtree: true
  });
};

// 启动监控
setupStackingMonitor();

📊 技巧对比总结

技巧 使用场景 优势 注意事项
层叠上下文检测 调试z-index问题 快速定位问题根源 需要了解所有创建条件
同级比较原则 元素层级管理 避免无效的z-index设置 确保元素在同一层叠上下文
避免意外创建 CSS动画和效果 减少意外的层级问题 谨慎使用transform和opacity
Portal模式 固定定位元素 独立的层级管理 需要额外的组件封装
调试工具 复杂层级问题 系统化的问题分析 仅在开发环境使用

🎯 实战应用建议

最佳实践

  1. 建立z-index规范:使用CSS自定义属性统一管理z-index值
  2. 使用isolation属性:明确创建层叠上下文,避免意外情况
  3. Portal模式:将固定定位元素渲染到独立容器中
  4. 调试工具:开发环境下使用自动检测工具
  5. 文档化管理:记录项目中的层级规则和约定

性能考虑

  • 减少层叠上下文数量:过多的层叠上下文会影响渲染性能
  • 合理使用will-change:避免不必要的硬件加速
  • 优化动画属性:优先使用不创建层叠上下文的属性

💡 总结

这5个层叠上下文的核心技巧在日常开发中能帮你彻底解决z-index问题,掌握它们能让你的CSS布局:

  1. 准确理解层叠机制:知道什么时候会创建层叠上下文
  2. 正确比较元素层级:确保在同一层叠上下文中比较z-index
  3. 避免意外的层级问题:谨慎使用会创建层叠上下文的CSS属性
  4. 独立管理固定元素:使用Portal模式管理fixed定位元素
  5. 系统化调试层级问题:使用专业工具快速定位问题

希望这些技巧能帮助你在CSS开发中告别z-index的困扰,写出更清晰、更可维护的样式代码!


🔗 相关资源


💡 今日收获:掌握了5个层叠上下文的核心技巧,这些知识点在实际开发中非常实用。

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀

CSS大汇总

概述

上一篇文章讲述了HTML作为名词(noun)的基本用法与汇总,这篇文章来讲述CSS作为形容词(adj)的基本概念与常见用法。

基本形式

    body {
      font-family: 'Poppins', sans-serif;
      color: #444;
      background-color: #f3f3f3;
      height: 100vh;
      padding: 2rem;
    }

上述代码就是一个针对body元素的css代码块,包括选择器和声明块

具体解释

CSS的作用就是通过具体的选择器来选择组件/元素,然后自定义声明块的内容改变具体的元素/组件的表现形式

1,书写位置

具体分为:内联CSS,内部CSS,外部CSS

内联CSS:书写在元素内部(非常不推荐)
内部CSS:在 <head> 标签内创建 <style> 标签
外部CSS:另创一个CSS文件,在head内部用<link href="..." rel="...">进行连接(最常使用)

2,选择器

类型:全局、类名、id、类/伪类、元素,例如"*"、".login-btn"、“#submit-btn”、“li:hover”、“p”

Addition:

1,伪类:形如a:hover,li:last-child形式的选择器称为伪类,作用有选择一个元素的不同状态,选择多个元素中的某个单独元素等

2,优先级:目的是为了实现“先全局,后细节”的思想,具体的顺序为:关键字->内联CSS->id->类/伪类->元素

关键字:

image.png

!important 会强制覆盖所有优先级规则,若多个样式同时加 !important,仍会按原优先级排序;且滥用会导致样式调试困难,仅建议用于‘修复第三方样式冲突’等极端场景。

3,继承
解释:HTML中有父元素和子元素,在CSS中对父元素进行选择修改后,子元素也会随之发生隐式修改,但是如若子元素显示声明其格式,那么使用显示声明的格式

4,组合选择器
组合选择器中常使用的有后代选择器和兄弟选择器

组合选择器:形如nav p,选择第一个元素之后的元素,目的是为了具体确定所选元素的位置,实现具体进行样式修改
兄弟选择器(相邻兄弟选择器):形如 p+span,选择与 p 共享同一父元素’且‘紧跟在 p 后的第一个 span 元素

3,声明块

内容:字体大小,内容填充,边缘间距,颜色,字体风格等

Addition:
声明块中的字体大小,字体风格,边缘间距,内容填充,颜色等大部分都是由UI设计师提供的,前端开发人员只需要设计布局等

其他重要知识点

1,CSS盒子模型

image.png

如上图所示,盒子模型包括内容,内边距(填充),边框,外边距(边缘间距)。因为这个模型对于组件设计时过于复杂,所以实际应用设计时往往使用下图所示的代码块

* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
}

html {
  font-size: 62.5%;
  box-sizing: border-box;
}

通过 html { box-sizing: border-box } 定义根元素的盒模型为 border-box,再通过 * { box-sizing: inherit } 让所有元素继承该属性,确保全局统一使用 border-box 盒模型,避免尺寸计算偏差。

2,元素类型

元素类型分为内联元素、块级元素、内联块元素

  1. 内联元素:
  • width/height 完全无效,尺寸由内容决定;
  • 竖直方向 margin 无效(不显示且不影响布局);
  • 竖直方向 padding 会显示(如背景延伸),但不会推开上下元素(可能覆盖其他内容);
  • 仅水平方向 margin/padding 有效(显示且影响布局)。
  1. 块级元素:元素会占据一行,多个块级元素默认垂直排列(上下分布)
  2. 内联块元素:元素不会占据一行,可以修改width,height等,多个内联块元素串行放置

Addition:
1,常见的内联元素:<span><a><em><i>等,块级元素:<p><div><h1>等,内联块元素:<img><input>
2,内联元素,块级元素,内联块元素之间可以相互转换,通过display:block/inline/inline-block

3,布局设计

所谓布局设计,就是将HTML上的各种组件按照需求确定其位置,CSS中的布局设计包括浮动布局,flexbox布局,grid布局

由于浮动布局可能会出现元素上下间距消失的现象(解决方法为clearfix技术,比较繁琐),这门技术已经有点过时,在这里小编不再详细讲述

1,Flexbox

image.png

具体的Flexbox如图所示,分为Flex container和Flex items,主轴(main axis)和交叉轴(cross axis)

使用Flexbox步骤:
1,如下图所示,确定选定的元素,设置display:flex

nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 2rem;
}

2,使用Flexbox性质来实现整体容器和各个项目的布局
justify-content:将整体的Flexbox容器在主轴方向确定具体位置,如space-between(两端贴边,中间间距相等),center(整体居中对齐,间距手动设置)等
align-items:控制所有 Flex 项目在交叉轴方向的对齐方式(如主轴水平时,交叉轴为垂直方向):

  • flex-start:项目靠交叉轴起点对齐(如垂直顶部);
  • center:项目在交叉轴居中对齐(如垂直居中);
  • stretch(默认):项目拉伸至与 Flex 容器的交叉轴高度一致。

gap:手动设置具体项目之间的间距
align-self:单独设置某个项目对齐最长项目的位置
orderorder 取值为整数(正负均可,如 order: -1order: 2),数值越小越靠前,默认值为 0;若多个项目 order 相同,按 HTML 文档流顺序排列。 flex: flex-grow flex-shrink flex-basis:这里三个参数含义如下:

  1. flex-grow:控制 Flex 项目在容器有剩余空间时的 “分配比例”,初始值为 0(默认不分配剩余空间):
  • 若所有项目设为 flex-grow: 1,且 flex-basis 相同(或为 0),剩余空间会被所有项目平均分配,实现均匀分布;
  • 若项目 flex-grow 比例为 2:1(如 A 为 2,B 为 1),则 A 分配的剩余空间是 B 的 2 倍。
  1. flex-shrink:表示项目能否自动缩小,初始值为1,表示能自动缩小,如需禁止需设为0,之后项目的宽度大于等于初始长度
  2. flex-basis:表示项目初始值宽度大小,初始值为auto,由自身内容确定

Notice:

  1. 在使用align-items之前会将所有项目的初始长度沿交叉轴占据整个项目
  2. Flexbox主轴方向可以改变,用于手风琴设计
  3. 使用Flexbox会自动将元素变为块级元素,如果是内联块元素不会进行修改

2,Grid

image.png

具体的Grid如上图所示,分为Grid container、Grid content 和 Grid items,水平轴(Rows axis)和垂直轴(Column axis)

使用Grid具体步骤如下:
1,如下图所示,确定选定的元素,设置display:grid

.form {
  display: grid;
  grid-template-columns: 2.5fr 2.5fr 1fr;
  grid-template-rows: auto auto;
  gap: 0.4rem 1rem;
}

2,使用Grid性质来实现整体容器以及各个项目的布局
grid-template-columns:设置容器内项目的列数
grid-template-rows:设置容器内项目的行数
column-gap:设置列间距
row-gap:设置行间距
justify-content:当 Grid 容器的主轴尺寸 > 网格整体的主轴尺寸时(如容器水平宽度大于网格总宽),控制 “网格整体在容器主轴方向(默认水平)的对齐方式”,例如 center 让网格在容器水平居中。
align-content:当 Grid 容器的交叉轴尺寸 > 网格整体的交叉轴尺寸时(如容器垂直高度大于网格总高),控制 “网格整体在容器交叉轴方向(默认垂直)的对齐方式”,例如 center 让网格在容器垂直居中。 justify-items:将所有项目在网格内沿水平方向进行设置,例如center 设为网格水平中心
align-items:将所有项目在网格内沿竖直方向进行设置,例如center设为网格竖直中心
justify-self:单独设置某个项目在单元格内的水平方向,例如start设为单元格的水平起始位置
align-self:单独设置某个项目在单元格内的竖直方向,例如end设为单元格的竖直最末位置
grid-column:单独设置某个项目在竖直方向上的占据空间,形式为start/end
grid-row:单独设置某个项目在水平方向上的占据空间,形式为start/end

Notice:

  1. 在实际前端开发设计中,往往只设置列数而不设置行数,因为行数会随列数自动确定
  2. Grid主轴方向也可以改变,但实际一般不改变
  3. fr 是 Grid 专属单位,代表‘剩余空间的比例’,2.5fr 2.5fr 1fr 表示‘将容器剩余空间按 2.5:2.5:1 的比例分配给 3 列’,适合自适应布局。

4,绝对定位

执行步骤:

  1. 确定放置位置的父元素,设置position:relative
  2. 对子元素进行操作:
.element {
  position: absolute; /* 开启绝对定位 */
  top: 20px;    /* 距离定位基准顶部 20px */
  left: 50px;   /* 距离定位基准左侧 50px */
  /* right: 30px; 距离定位基准右侧 30px */
  /* bottom: 10px; 距离定位基准底部 10px */
  
  transform: translate(0, -50%); /* 将移动的组件在竖直方向上移动一半组件的距离*/
}

解释:首先进行大致的定位,即将整个组件移动至大体位置,然后通过transform:translate(...,...)进行细致调整,具体如下图所示

image.png

5,响应式布局

目的:为了让Web在不同的尺寸屏幕上实现布局的合理性以及好的视觉效果

实现手段:媒体查询

实现技巧:

  1. 设置字体大小时使用rem而非px,rem是根元素大小,目的是为了将所有的字体大小设置都有一个基准,具体代码如下:
html {
  font-size: 62.5%;
  box-sizing: border-box;
}

其中百分数62.5%得来的过程是--基准/16px=62.5%(16px是台式电脑下的默认字体大小值)--基准根元素大小为10px

  1. 父容器宽度设置为max-width:具体rem,子元素width:百分数%
    max-width:设定的是容器最大宽度,当视口宽度大于容器最大宽度时,两边会留有空白;当视口宽度小于容器最大宽度时,容器宽度会随着视口宽度的改变而改变
    将父容器使用max-width会将其宽度动态化而非固定不变,同理子容器用百分数也是如此

  2. 在不同的尺寸下查看各个元素/组件的排版问题,如果不合适则需要进行调整,通过改变根元素font-size,用flexbox、grid进行布局调整

最后

前端开发之路任重而道远,在未来仍有很多知识需要学习,小编也是一个初学者,让我们一起加油!如果这篇文章能帮到你的话,荣幸之至;如果有错误的话并指出来的话,小编将不胜感激。

CSS 布局中的各种“间距”

欢迎关注我的公众号:前端侦探

介绍CSS布局中一些设置间隔的小技巧(纯经验,没有新特性)。

一、水平列表

首先是这种,居中的两个按钮,假设间隔是20px(下同)

image-20250831110153174

这种可以设置一半的margin就行了

.item {
  margin: 0 10px; /*一半的间隔*/
}

或者只给第二个元素设置左偏移,也就是除去第一个元素都设置左偏移(推荐✅)

.item+.item{ /*相邻元素的下一个,也就是第二个以后得所有元素*/
  margin-left: 20px;
}
/*或者*/
.item:not(:first-child) {
  margin-left: 20px;
}

然后是这种场景,撑满整行宽度的元素

image-20250831113636275

如果设置左右margin,那么父级容器左右padding需要减少一半

.wrap {
  display: flex;
  padding: 20px 10px;
}
.item {
  margin: 0 10px;
  flex:1;
}

当然,如果是只给除去第一个元素都设置左偏移就无需设置父容器了

除了这种方式,grid布局也是很好的选择

.wrap {
  displaygrid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 20px;
}

注意,这里用的是grid-gap,这是一个早期随grid出现的属性,兼容性已经非常好了

image-20250831120139298

移动端几乎可以放心使用。

现在flex布局也支持gap属性了,不过兼容性差点,酌情使用

image-20250831120550890

二、纵向列表

然后是这种列表结构

image-20250831121044991

如果是普通布局,其实可以直接用margin实现,无需设置一半的偏移,反而更方便

.item {
  margin: 20px;
}

因为margin在垂直方向上会“自动合并”,哪个大取哪个(现在应该没多少人知道了吧😂)

如果是flex布局,就没有合并的问题了,可以用排除第一个元素的方法,如下

.list {
  display: flex;
  flex-direction: column;
  padding: 20px;
}
.item+.item{ /*相邻元素的下一个,也就是第二个以后得所有元素*/
  margin-top: 20px;
}
/*或者*/
.item:not(:first-child) {
  margin-top: 20px;
}

最简单的还是grid布局,使用grid-gap也不错(我目前几乎都是用这种方式了,推荐✅)

默认就是垂直方向,代码量最少,结构最清晰

.list {
  display: grid;
  grid-gap: 20px;
  padding: 20px;
}

三、网格布局

网格布局就推荐直接用 grid 布局了

image-20250831122903362

和上面的实现基本一致

.list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 20px;
  padding: 20px;
}

其实,在grid出现之前,我们一般都是通过margin来实现,但是这种在不确定一排有多少个的情况下,就无法针对某些元素去取消它的margin了,我们可以给每个元素设置右、下两个方向上的margin

.item {
  margin: 0 20px 20px 0;
}

这样的话,右边和下边肯定会多出来20px

image-20250831123830260

没关系,我们可以给父级的右、下padding都取消掉

.list {
  padding: 20px 0 0 20px;
}

还有一种方式,嵌套一层父级

<div class="list">
  <div class="inner">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
</div>

然后给inner设置一个负的margin,这样也能把右下的多余margin抵消掉

.inner{
  margin: 0 -20px -20px 0;
}

这样就可以了,虽然有点 hack,但是也能实现

四、多行标签列表

还有一种布局,像这种不定宽度的标签,如下

image-20250831125258881

这种如果不考虑兼容性的话,flexgrid是最好选择

.list{
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

但是,如果考虑兼容性的话,那就只能用margin实现,和上面一样,设置右、下两个方向的margin

.item{
  margin: 0 20px 20px 0;
}

这样的话,右侧也会出现一个多余的margin,可能会导致某些本来可以放在上一行的被挤到了下一行

image-20250831125812426

最好的实现就是嵌套一层,然后给这层标签设置负的margin,如下

.inner{
  margin: 0 -20px -20px 0;
}

就这些了,如果你也碰到这些间距,可以尝试一下这些方法。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

❌