51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

那些年用过的CSS奇妙用法之能用CSS就不用JS技巧系列

嵌套的圆角 {#嵌套的圆角}

在遇到内外两层圆角时,可以通过 CSS 变量动态去计算内部的圆角,看起来会更加和谐

运行效果

核心代码

|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | .parent { --anzhiyu-nested-radius: calc(var(--radius) - var(--padding)); } .nested { border-radius: var(--anzhiyu-nested-radius); } |

《css嵌套的圆角》在线运行1024code

视图转换动画 View Transitions API {#视图转换动画-View-Transitions-API}

我们先从一个简单的例子来认识一下。

|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | <div class="list" id="list"> <div class="item">1</div> <div class="item">2</div> <div class="item">3</div> <div class="item">4</div> <div class="item">5</div> <div class="item">6</div> <div class="item">7</div> <div class="item">8</div> <div class="item">9</div> <div class="item">10</div> </div> |

我们实现一个简单交互,点击每个元素,元素就会被删除

|-------------------|--------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | list.addEventListener('click', function(ev){ if (ev.target.className === 'item') { ev.target.remove() } }) |

用 css修饰一下就变成了以下效果

功能正常,就是有点太过生硬了

现在轮到 View Transitions 出场了!我们只需要在改变状态的地方添加document.startViewTransition,如下

|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | list.addEventListener('click', function(ev){ if (ev.target.className === 'item') { document.startViewTransition(() => { // 开始视图变换 ev.target.remove() }); } }) |

|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | list.addEventListener('click', function(ev){ if (document.startViewTransition) { // 如果支持就视图变换 document.startViewTransition(() => { // 开始视图变换 ev.target.remove() }); } else { // 不支持就执行原来的逻辑 ev.target.remove() } }) |

现在效果如下

加速startViewTransition监听后的效果

删除前后现在有一个淡入淡出的效果了,也就是默认的动画效果,我们可以把这个动画时长设置大一点,如下

|-----------------|----------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | ::view-transition-old(root), /* 旧视图*/ ::view-transition-new(root) { /* 新视图*/ animation-duration: 2s; } |

这两个伪元素我们后面再做介绍,先看效果

延长动画时间

是不是明显感觉过渡变慢了许多?

但是这种动画还是不够舒服,是一种整体的变化,看不出删除前后元素的位置变化。

接下来我们给每个元素指定一个标识,用来标记变化前后的状态,为了方便控制,可以借助 CSS 变量

|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | <div class="list" id="list"> <div class="item" style="--i: a1">1</div> <div class="item" style="--i: a2">2</div> <div class="item" style="--i: a3">3</div> <div class="item" style="--i: a4">4</div> <div class="item" style="--i: a5">5</div> <div class="item" style="--i: a6">6</div> <div class="item" style="--i: a7">7</div> <div class="item" style="--i: a8">8</div> <div class="item" style="--i: a9">9</div> <div class="item" style="--i: a10">10</div> </div> |

这里通过view-transition-name来设置名称

|---------------|--------------------------------------------------| | 1 2 3 | .item{ view-transition-name: var(--i); } |

然后可以得到这样的效果,每个元素在变化前后会自动找到之前的位置,并且平滑的移动过去,如下

是不是非常丝滑?这就是 View Transitions 的魅力!

在动画执行的过程中,还会在页面根节点自动创建以下伪元素

|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | ::view-transition └─ ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root) |

控制台截图

其中,::view-transition-old表示「旧视图」的状态,也就是变化之前的截图,::view-transition-new表示「新视图」的状态,也就是变化之后的截图。

默认情况下,整个页面root都会作为一个状态,也就是上面的::view-transition-group(root),在切换前后会执行淡入淡出动画,如下

|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | :root::view-transition-new(root) { animation-name: -ua-view-transition-fade-in; /*淡入动画*/ } :root::view-transition-old(root) { animation-name: -ua-view-transition-fade-out; /*淡出动画*/ } |

这也是为什么在使用了document.startViewTransition后整个页面会有淡入淡出的效果了

详细的内容这里不再做过多赘述。

最佳实践:《View Transitions API》

另外还有几个有趣的案例

  1. 《css动画拖拽排序》
  2. 《数字过渡动画》
  3. 类似于 APP 的转场动画(多页面跳转)

总的来说,原生的视图转换动画可以很轻松的实现两种状态的过渡,让 web 也能实现媲美原生 APP 的动画体验,下面再来回顾一下整个变化过程:

  1. 调用document.startViewTransition,浏览器会捕捉当前页面的状态,类似于实时截图,或者"快照"

  2. 执行实际的 dom 变化,再次记录变化后的页面状态(截图)

  3. 触发两者的过渡动画,包括透明度、位移等变化,也可以自定义 CSS 动画

  4. 默认情况下是整个页面的淡入淡出变化

  5. ::view-transition-old表示「旧视图」的状态,也就是变化之前的截图,::view-transition-new表示「新视图」的状态,也就是变化之后的截图。

  6. 如果需要指定具体元素的变化,可以给该元素指定view-transition-name

  7. 前后变化不一定要同一元素,浏览器是根据view-transition-name寻找的

  8. 同一时间页面上不能出现两个相同view-transition-name的元素,不然视图变化会失效

另外,视图转换动画应该作为一种「体验增强」的功能,而非必要功能,在使用动画时其实拖慢了页面打开或者更新的速度,并且在动画过程中,页面是完全"冻结"的,做不了任何事情,因此需要衡量好动画的时间,如果页面本身就很慢就更不要使用这些动画了。

参考链接
[1] View Transitions: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API

[2] view-transition sort (juejin.cn): https://code.juejin.cn/pen/7268263402853072931

[3] view-transition sort (codepen.io): https://codepen.io/xboxyan/pen/BavBevP

[4] view-transition-dialog (juejin.cn): https://code.juejin.cn/pen/7268262983178911779

[5] view-transition-dialog (codepen.io): https://codepen.io/xboxyan/pen/WNLeBgY

[6] view transition theme change - (juejin.cn): https://code.juejin.cn/pen/7268257573277532219

[7] view transition theme change (codepen.io): https://codepen.io/xboxyan/pen/poqzmLY

css属性占用 {#css属性占用}

问题的原因,众所周知,CSS 中的transform是一个包含很多值的属性,例如

|---------------|------------------------------------------------------------| | 1 2 3 | div{ transform: translate(-50%, -50%) scale(1.5) } |

但是,这并不是简写,而是就该这么写,这一点和background不一样

|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 | div{ background: url('1xxx') 10px 10px / 20px 20px no-repeat; } /*等同于以下写法*/ div{ background-image: url('1xxx'); background-position: 10px 10px; background-size: 20px 20px; background-repeat: no-repeat; } |

分开写的好处在于,如果只需要改变某一部分就很容易覆盖

|---------------|--------------------------------------------------------------------| | 1 2 3 | div.div1{ background-image: url('2xxx'); /*只改变图片,不改变其他*/ } |

回到前面,如果希望改变transform中的某一部分,就不太行了,必须把没改变的部分也写一遍,而且更改的部分也无法抽离出来作为一个公共的样式

|-----------------------|---------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | div.scale{ transform: scale(2) /*这样不行,会丢失translate*/ } /*必须写完整*/ div.scale{ transform: translate(-50%, -50%) scale(2) } |

那如何巧妙的解决这个问题呢?

  1. 用其他方式来代替 transform
    碰到这种情况,第一感觉可能就是放弃原有transform属性,用其他方式代替。

    比如translate(-50%, -50%)一般是为了实现元素居中效果,可以用flex等其他方式实现

    |---------------------|-----------------------------------------------------------------------------| | 1 2 3 4 5 6 | .parent{ display: flex } div{ margin: auto; /*通过margin:auto实现居中*/ } |

    再比如translate(10px, 10px)这样的偏移,可以用left或者margin-left等方式实现

    |-------------------|-----------------------------------------------------------| | 1 2 3 4 5 | div{ position: relative; left: 10px; top: 10px; } |

    或者干脆再包裹一层父级,也能避免transform被占用的问题。

    |-------------------|------------------------------------------------------------| | 1 2 3 4 5 | <div class="wrap"> <div class="box"> </div> </div> |

    不过,这些方式都是规避方式,其实还有官方解决方案

  2. transform 的单独赋值
    前面提到过transform并不是一个简写属性,所以没有办法像background那样对某一部分进行赋值。为了解决这个问题,从Chrome 104开始,浏览器终于正式支持单独赋值了

    有兴趣的可以参考这篇文章:解放生产力!transform 支持单独赋值改变

    就拿前面那个例子来说

    |---------------|------------------------------------------------------------| | 1 2 3 | div{ transform: translate(-50%, -50%) scale(1.5) } |

    可以写成

    |-----------------|-------------------------------------------------| | 1 2 3 4 | div{ translate: -50% -50%; scale: 1.5 } |

    这样如果需要改变某一部分,就只需要像普通属性一样覆盖就行了

    |---------------|-------------------------------| | 1 2 3 | div.scale{ scale: 2 } |

    不过目前兼容性欠佳,谨慎使用!(适合内部项目尝鲜)

  3. 借助 CSS 变量拆分属性

    前面的transform 的单独赋值虽然很好,但是太新了,无法立刻在项目中使用。下面介绍一个兼容性更好,使用更放心的解决方案。

    还是上面这个例子

    |---------------|------------------------------------------------------------| | 1 2 3 | div{ transform: translate(-50%, -50%) scale(1.5) } |

    通过 CSS 变量,将transform拆分

    |-------------------|------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | div{ --translate: -50%, -50%; --scale: 1.5; transform: translate(var(--translate)) scale(var(--scale)) } |

    经过这样拆分以后,CSS 变量就成了独立属性,如果需要覆盖,只需要修改其中一个就行了,而无需关注--translate是什么样的,这样变化的部分就可以单独作为一个公共的样式了,如下

    |---------------------------|------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | .div1{ --translate: -50%, -50%; } .div1{ --translate: 10px, 10px; } div.scale{ --scale: 2; /*无需关注其他transform,可以作为公共的样式*/ } |

    是不是非常清晰明了?下面是一个演示demo

    |---------------|-------------------------------------------------------| | 1 2 3 | [type="checkbox"]:checked+div{ --scale: 1.5 } |

    效果如下

    完整代码可以查看以下任意链接:
    CSS transform (codepen.io)
    CSS transform - 码上掘金 (juejin.cn)
    CSS transform (runjs.work)

动是动了,但是没有过渡,太生硬了,那如何解决呢?

这就需要用到@property了,是干什么的呢?简单来讲,可以自定义属性,让自定义变量像颜色一样进行过渡和动画,换句话说,现在执行动画的是--scale这个属性,而不再是transform

将这个自定义属性通过@property定义一下就行了,如下

|-------------------|--------------------------------------------------------------------------------------| | 1 2 3 4 5 | @property --scale { syntax: '<number>'; inherits: false; initial-value: 1; } |

关于@property的应用,可以参考以下文章

@property: https://developer.mozilla.org/zh-CN/docs/Web/CSS/@property

其实不至于transform,很多类似的都可以用到这个技巧,比如颜色

|---------------------|---------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | el{ --r: 255; --g: 255; --b: 255; color: rgb(var(--r), var(--g), var(--b)); } |

总结一下

  1. 部分 CSS 属性比较复杂,比如transform,很容易被占用
  2. 通常的解决方式是规避,比如换一种居中方式,或者用left、top等代替
  3. 新出现的transform 的单独属性可以解决这个问题,目前还比较新,仅适合内部项目尝鲜
  4. 借助 CSS 变量可以拆分复杂的属性
  5. 借助 @property 让CSS自定义属性支持过渡动画
  6. 不仅仅是 transform,思维放开,复杂的属性都可以采取这样的思路

CSS scroll-driven animations {#CSS-scroll-driven-animations}

CSS 滚动驱动动画,在Chrome 115中被支持。

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations

可以参考这篇文章学习CSS 滚动驱动动画终于正式支持了~

文章略长,建议收藏后反复查阅

赞(1)
未经允许不得转载:工具盒子 » 那些年用过的CSS奇妙用法之能用CSS就不用JS技巧系列