OveUI 博客
菜鸟前端笔记

一次搞懂数据大屏适配方案 (vw vh、rem、scale)

lee992阅读(61)

前言

当接到可视化大屏需求时,你是否会有以下疑问👇 如何做一款定制化的数据大屏? 开发可视化数据大屏如何做自适应? vm vh、rem、scale 到底哪种比较好? 时间不够,有没有偷懒的方法?

而解决了适配问题后,后面就只是一个慢工出细活,耗时间的事情了。

适配方案分析

看了网上的各种方案,目前大家采用的大概有 3 种👇

方案实现方式优点缺点
vm vh1.按照设计稿的尺寸,将px按比例计算转为vwvh1.可以动态计算图表的宽高,字体等,灵活性较高 2.当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况1.每个图表都需要单独做字体、间距、位移的适配,比较麻烦
scale1.通过 scale 属性,根据屏幕大小,对图表进行整体的等比缩放1.代码量少,适配简单 2.一次处理后不需要在各个图表中再去单独适配1.因为是根据 ui 稿等比缩放,当大屏跟 ui 稿的比例不一样时,会出现周边留白情况 2.当缩放比例过大时候,字体会有一点点模糊,就一点点 3.当缩放比例过大时候,事件热区会偏移。
rem + vm vh1.获得 rem 的基准值 2.动态的计算html根元素的font-size 3.图表中通过 vm vh 动态计算字体、间距、位移等1.布局的自适应代码量少,适配简单1.因为是根据 ui 稿等比缩放,当大屏跟 ui 稿的比例不一样时,会出现周边留白情况 2.图表需要单个做字体、间距、位移的适配

以上 3 种方案在实际应用中该怎么选择视具体情况而定,也有看到大家说自适应在地图的适配中会有一些兼容问题,我这边还没有实践过。

  • 如果想简单,客户能同意留白,选用 scale 即可
  • 如果需要兼容不同比例的大屏,并且想在不同比例中都有比较好的效果,图表占满屏幕,类似于移动端的响应式,可以采用 vm vh 的方案
  • 至于 rem,个人觉得就是 scale 和 vm vh 的综合,最终的效果跟 scale 差不多

接下来介绍下三种方案的具体实现,方案中的代码都以 vue2.0 和 vue-cli3 搭建的 vue 项目为例,因为是 demo,图表的一些细节就没有过多细致的调整了

方案一:vw vh

上效果

ezgif.com-gif-maker (1).gif

当屏幕的尺寸比例刚好是 16:9 时

vwh02.jpg

当屏幕的尺寸比例大于 16:9 时

vwh03.jpg

当屏幕的尺寸比例小于 16:9 时

vwh09.jpg

实现思路

按照设计稿的尺寸,将px按比例计算转为vwvh,转换公式如下

假设设计稿尺寸为 1920*1080(做之前一定问清楚 ui 设计稿的尺寸)

即:
网页宽度=1920px
网页高度=1080px

我们都知道
网页宽度=100vw
网页宽度=100vh

所以,在 1920px*1080px 的屏幕分辨率下

1920px = 100vw

1080px = 100vh

这样一来,以一个宽 300px 和 200px 的 div 来说,其所占的宽高,以 vw 和 vh 为单位,计算方式如下:

vwDiv = (300px / 1920px ) * 100vw
vhDiv = (200px / 1080px ) * 100vh

所以,就在 1920*1080 的屏幕分辨率下,计算出了单个 div 的宽高

当屏幕放大或者缩小时,div 还是以 vw 和 vh 作为宽高的,就会自动适应不同分辨率的屏幕
复制代码

话不多说,上代码

css 方案 – sass

util.scss

// 使用 scss 的 math 函数,https://sass-lang.com/documentation/breaking-changes/slash-div
@use "sass:math";
​
// 默认设计稿的宽度
$designWidth: 1920;
// 默认设计稿的高度
$designHeight: 1080;
​
// px 转为 vw 的函数
@function vw($px) {
  @return math.div($px, $designWidth) * 100vw;
}
​
// px 转为 vh 的函数
@function vh($px) {
  @return math.div($px, $designHeight) * 100vh;
}

路径配置 只需在vue.config.js里配置一下utils.scss的路径,就可以全局使用了

vue.config.js

const path = require("path");
​
function resolve(dir) {
  return path.join(__dirname, dir);
}
​
module.exports = {
  publicPath: "",
  configureWebpack: {
    name: "app name",
    resolve: {
      alias: {
        "@": resolve("src"),
      },
    },
  },
  css: {
    // 全局配置 utils.scs,详细配置参考 vue-cli 官网
    loaderOptions: {
      sass: {
        prependData: `@import "@/styles/utils.scss";`,
      },
    },
  },
};

在 .vue 中使用

<template>
    <div class="box">
    </div>
</template>
​
<script>
export default{
    name: "Box",
}
</script>
​
<style lang="scss" scoped="scoped">
/*
 直接使用 vw 和 vh 函数,将像素值传进去,得到的就是具体的 vw vh 单位
 */
.box{
    width: vw(300);
    height: vh(100);
    font-size: vh(16);
    background-color: black;
    margin-left: vw(10);
    margin-top: vh(10);
    border: vh(2) solid red;
}
</style>
css 方案 – less

utils.less

@charset "utf-8";
​
// 默认设计稿的宽度
@designWidth: 1920;
​
// 默认设计稿的高度
@designHeight: 1080;
​
.px2vw(@name, @px) {
  @{name}: (@px / @designWidth) * 100vw;
}
​
.px2vh(@name, @px) {
  @{name}: (@px / @designHeight) * 100vh;
}
​
.px2font(@px) {
  font-size: (@px / @designWidth) * 100vw;
}

路径配置vue.config.js里配置一下utils.less

const path = require("path");
​
function resolve(dir) {
  return path.join(__dirname, dir);
}
​
module.exports = {
  publicPath: "",
  configureWebpack: {
    name: "app name",
    resolve: {
      alias: {
        "@": resolve("src"),
      },
    },
  },
  css: {
    // 全局配置utils.scss
    loaderOptions: {
      less: {
        additionalData: `@import "@/styles/utils.less";`,
      },
    },
  },
};

在 .vue 文件中使用

<template>
    <div class="box">
    </div>
</template>
​
<script>
export default{
    name: "Box",
}
</script>
​
<style lang="less" scoped="scoped">
/*
 直接使用 vw 和 vh 函数,将像素值传进去,得到的就是具体的 vw vh单位
 */
.box{
    .px2vw(width, 300);
    .px2vh(height, 100);
    .px2font(16);
    .px2vw(margin-left, 300);
    .px2vh(margin-top, 100);
    background-color: black;
}
</style>
定义 js 样式处理函数
// 定义设计稿的宽高
const designWidth = 1920;
const designHeight = 1080;
​
// px转vw
export const px2vw = (_px) => {
  return (_px * 100.0) / designWidth + 'vw';
};
​
export const px2vh = (_px) => {
  return (_px * 100.0) / designHeight + 'vh';
};
​
export const px2font = (_px) => {
  return (_px * 100.0) / designWidth + 'vw';
};
屏幕变化后,图表自动调整

这种使用方式有个弊端,就是屏幕尺寸发生变化后,需要手动刷新一下才能完成自适应调整

为了解决这个问题,你需要在各个图表中监听页面尺寸变化,重新调整图表,在 vue 项目中,也可以借助element-resize-detector,最好封装个 resize 的指令,在各图表中就只要使用该指令就可以了,毕竟作为程序员,能偷懒就偷懒

  1. 安装 element-resize-detector

npm install element-resize-detector –save

  1. 引入工具包在组件中使用或者在单独的 js 中使用

import resizeDetector from ‘element-resize-detector’

  1. 封装 directive
// directive.js
import * as ECharts from "echarts";
import elementResizeDetectorMaker from "element-resize-detector";
import Vue from "vue";
const HANDLER = "_vue_resize_handler";
function bind(el, binding) {
  el[HANDLER] = binding.value
    ? binding.value
    : () => {
        let chart = ECharts.getInstanceByDom(el);
        if (!chart) {
          return;
        }
        chart.resize();
      };
  // 监听绑定的div大小变化,更新 echarts 大小
  elementResizeDetectorMaker().listenTo(el, el[HANDLER]);
}
function unbind(el) {
  // window.removeEventListener("resize", el[HANDLER]);
  elementResizeDetectorMaker().removeListener(el, el[HANDLER]);
  delete el[HANDLER];
}
// 自定义指令:v-chart-resize 示例:v-chart-resize="fn"
Vue.directive("chart-resize", { bind, unbind });
  1. main.js 中引入
import '@/directive/directive';
  1. html 代码
<template>
  <div class="linechart">
    <div ref="chart" v-chart-resize class="chart"></div>
  </div>
</template>

这里要注意的是,图表中如果需要 tab 切换动态更新图表数据,在更新数据时一定不要用 echarts 的 dispose 方法先将图表移除,再重新绘制,因为 resize 指令中挂载到的图表实例还是旧的,就监听不到新的 chart 元素的 resize 了,更新数据只需要用 chart 的 setOption 方法重新设置配置项即可。

图表字体、间距、位移等尺寸自适应

echarts 的字体大小只支持具体数值(像素),不能用百分比或者 vw 等尺寸,一般字体不会去做自适应,当宽高比跟 ui 稿比例出入太大时,会出现文字跟图表重叠的情况

  • 默认情况下,这里以你的设计稿是 1920*1080 为例,即网页宽度是 1920px (做之前一定问清楚 ui 设计稿的尺寸)
  • 把这个函数写在一个单独的工具文件dataUtil.js里面,在需要的时候调用
  • 其原理是计算出当前屏幕宽度和默认设计宽度的比值,将原始的尺寸乘以该值
  • 另外,其它 echarts 的配置项,比如间距、定位、边距也可以用该函数
  1. 编写 dataUtil.js 工具函数
// Echarts图表字体、间距自适应
export const fitChartSize = (size,defalteWidth = 1920) => {
  let clientWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;
  if (!clientWidth) return size;
  let scale = (clientWidth / defalteWidth);
  return Number((size*scale).toFixed(3));
}
  1. 将函数挂载到原型上
import {fitChartSize} from '@src/utils/dataUtil.js'
Vue.prototype.fitChartFont = fitChartSize;
  1. 这样你可以在.vue文件中直接使用this.fitChartSize()调用
<template>
  <div class="chartsdom" ref="chart" v-chart-resize></div>
</template>
​
<script>
export default {
  name: "dashboardChart",
  data() {
    return {
      option: null,
    };
  },
  mounted() {
    this.getEchart();
  },
  methods: {
    getEchart() {
      let myChart = this.$echarts.init(this.$refs.chart);
      const option = {
        backgroundColor: "transparent",
        tooltip: {
          trigger: "item",
          formatter: "{a} <br/>{b} : {c}%",
        },
        grid: {
          left: this.fitChartSize(10),
          right: this.fitChartSize(20),
          top: this.fitChartSize(20),
          bottom: this.fitChartSize(10),
          containLabel: true,
        },
        calculable: true,
        series: [
          {
            color: ["#0db1cdcc"],
            name: "计划投入",
            type: "funnel",
            width: "45%",
            height: "70%",
            x: "5%",
​
            minSize: "10%",
            funnelAlign: "right",
​
            center: ["50%", "50%"], // for pie
​
            data: [
              {
                value: 30,
                name: "下单30%",
              },
              {
                value: 55,
                name: "咨询55%",
              },
              {
                value: 65,
                name: "点击65%",
              },
              {
                value: 60,
                name: "访问62%",
              },
              {
                value: 80,
                name: "展现80%",
              },
            ].sort(function (a, b) {
              return a.value - b.value;
            }),
            roseType: true,
            label: {
              normal: {
                formatter: function () {},
                position: "inside",
              },
            },
            itemStyle: {
              normal: {
                borderWidth: 0,
                shadowBlur: this.fitChartSize(20),
                shadowOffsetX: 0,
                shadowOffsetY: this.fitChartSize(5),
                shadowColor: "rgba(0, 0, 0, 0.3)",
              },
            },
          },
​
          {
            color: ["#0C66FF"],
            name: "实际投入",
            type: "funnel",
            width: "45%",
            height: "70%",
            x: "50%",
​
            minSize: "10%",
            funnelAlign: "left",
​
            center: ["50%", "50%"], // for pie
​
            data: [
              {
                value: 35,
                name: "下单35%",
              },
              {
                value: 40,
                name: "咨询40%",
              },
              {
                value: 70,
                name: "访问70%",
              },
              {
                value: 90,
                name: "点击90%",
              },
              {
                value: 95,
                name: "展现95%",
              },
            ].sort(function (a, b) {
              return a.value - b.value;
            }),
            roseType: true,
            label: {
              normal: {
                position: "inside",
              },
            },
            itemStyle: {
              normal: {
                borderWidth: 0,
                shadowBlur: this.fitChartSize(20),
                shadowOffsetX: 0,
                shadowOffsetY: this.fitChartSize(5),
                shadowColor: "rgba(0, 0, 0, 0.3)",
              },
            },
          },
        ],
      };
      myChart.setOption(option, true);
    },
  },
  beforeDestroy() {},
};
</script>
​
<style lang="scss" scoped>
.chartsdom {
  width: 100%;
  height: 100%;
}
</style>

方案二:scale

通过 css 的 scale 属性,根据屏幕大小,对图表进行整体的等比缩放,从而达到自适应效果

上效果

sceen-scale.gif

当屏幕的尺寸比例刚好是 16:9 时,页面能刚好全屏展示,内容占满显示器

sceen-demo.jpg

当屏幕的尺寸比例小于 16:9 时,页面上下留白,左右占满并上下居中,显示比例保持 16:9

sceen-demo-02.jpg

当屏幕尺寸比例大于 16:9 时,页面左右留白,上下占满并居中,显示比例保持 16:9

sceen-demo-03.jpg

话不多说,上代码

html 部分

<div className="screen-wrapper">
    <div className="screen" id="screen">
​
    </div>
 </div>

js 部分

<script>
export default {
mounted() {
  // 初始化自适应  ----在刚显示的时候就开始适配一次
  handleScreenAuto();
  // 绑定自适应函数   ---防止浏览器栏变化后不再适配
  window.onresize = () => handleScreenAuto();
},
deleted() {
  window.onresize = null;
},
methods: {
  // 数据大屏自适应函数
  handleScreenAuto() {
    const designDraftWidth = 1920; //设计稿的宽度
    const designDraftHeight = 960; //设计稿的高度
    // 根据屏幕的变化适配的比例
    const scale =
      document.documentElement.clientWidth /
        document.documentElement.clientHeight <
      designDraftWidth / designDraftHeight
        ? document.documentElement.clientWidth / designDraftWidth
        : document.documentElement.clientHeight / designDraftHeight;
    // 缩放比例
    document.querySelector(
      '#screen',
    ).style.transform = `scale(${scale}) translate(-50%, -50%)`;
  },
},
};
</script>

css部分

/*
  除了设计稿的宽高是根据您自己的设计稿决定以外,其他复制粘贴就完事
*/  
.screen-root {
    height: 100%;
    width: 100%;
    .screen {
        display: inline-block;
        width: 1920px;  //设计稿的宽度
        height: 960px;  //设计稿的高度
        transform-origin: 0 0;
        position: absolute;
        left: 50%;
        top: -50%;
    }
}

实现思路

如何缩放

屏幕宽高比 < 设计稿宽高比,我们需要缩放的比例是屏幕宽度 / 设计稿宽度屏幕宽高比 > 设计稿宽高比,我们需要缩放的比例是屏幕高度 / 设计稿高度

const scale = document.documentElement.clientWidth / document.documentElement.clientHeight < designDraftWidth / designDraftHeight ?
            (document.documentElement.clientWidth / designDraftWidth) :
            (document.documentElement.clientHeight / designDraftHeight);

如果我们拿到的设计稿宽高为: 1920 * 960 px ,而我们的屏幕大小是 1440 * 900 px,那么 1440/900 = 1.6,920/960 = 2

因为 1.6 < 2 (当前屏幕宽高比小于设计稿宽高比)

所以我们需要缩放的比例是:屏幕宽度除以设计稿宽度 = 1440/1920 = 0.75

如何居中 首先我们利用 transform:translate(-50%,-50%) ,将动画的基点设为左上角

transform-origin:设置动画的基点(中心点),默认点是元素的中心点

语法

transform-origin: x-axis y-axis z-axis;

然后利用transform:translate(-50%,-50%),将图表沿 x,y 轴移动 50%

image.png

接下来利用绝对定位将图表定位到中间位置

position: absolute;
left: 50%;
top: 50%;
复制代码

偷懒方法-插件

v-scale-screen是使用 css 属性 transform 实现缩放效果的一个大屏自适应组件,通过 scale 进行等比例计算,达到等比例缩放的效果,同时也支持铺满全屏,宽度等比,高度等比,等自适应方案,具体可查大屏自适应终极解决方案

方案三:rem + vw wh

上效果

rem.gif

当屏幕的尺寸比例刚好是 16:9 时,页面能刚好全屏展示,内容占满显示器

rem01.jpg

当屏幕的尺寸比例小于 16:9 时,页面上下留白,左右占满并上下居中,显示比例保持 16:9

rem02.jpg

当屏幕尺寸比例大于 16:9 时,页面左右留白,上下占满并居中,显示比例保持 16:9

rem03.jpg

实现思路

关于 rem rem(font size of the root element),是 css3 中新增的一个大小单位,即相对于根元素 font-size 值的大小。 自适应思路 动态的计算出页面的 fontsize 从而改变 rem 的大小。

  1. 拿 1920 * 1080 的标准屏幕大小为例,将屏幕分为10份,先计算rem 的基准值: 1920 / 10 = 192;
  2. 把所有元素的长、宽、位置、字体大小等原来的 px 单位全部转换成 rem;

因此 rem + vm vh 方案要解决三件事

  1. 获得 rem 的基准值;
  2. 页面内写一段 js 代码,动态的计算html根元素的font-size
  3. 屏幕变化后,图表自动调整和图表字体、间距、位移等的自适应。

实现方案

第一点:获得 rem 的基准值

  1. 首先安装 @njleonzhang/postcss-px-to-rem 这个包
npm i @njleonzhang/postcss-px-to-rem -D
  1. 在项目根目录新建.postcssrc.js配置文件
module.exports = {
  plugins: {
    autoprefixer: {},
    "@njleonzhang/postcss-px-to-rem": {
      unitToConvert: 'px', // (String) 要转换的单位,默认是 px。
      widthOfDesignLayout: 1920, // (Number) 设计布局的宽度。对于pc仪表盘,一般是 1920.
      unitPrecision: 3, // (Number) 允许 rem 单位增长到的十进制数字.
      selectorBlackList: ['.ignore', '.hairlines'], // (Array) 要忽略并保留为 px 的选择器.
      minPixelValue: 1, // (Number) 设置要替换的最小像素值.
      mediaQuery: false // (Boolean) 允许在媒体查询中转换 px.
    }
  }
}
  1. 配置完成后,页面内的 px 就会被转换成 rem 了

第二点:动态的计算html根元素的font-size

  1. 在工具函数文件中新建一个 rem.js 文件,用于动态计算 font-size
(function init(screenRatioByDesign = 16 / 9) {
  let docEle = document.documentElement
  function setHtmlFontSize() {
    var screenRatio = docEle.clientWidth / docEle.clientHeight;
    var fontSize = (
      screenRatio > screenRatioByDesign
        ? (screenRatioByDesign / screenRatio)
        : 1
    ) * docEle.clientWidth / 10;
    docEle.style.fontSize = fontSize.toFixed(3) + "px";
    console.log(docEle.style.fontSize);
  }
  setHtmlFontSize()
  window.addEventListener('resize', setHtmlFontSize)
})()
  1. 在入口文件 main.js 中引入 rem.js 文件
import './utils/rem.js';

至此,页面就已经可以实现 16:9 自适应了。

第三点:屏幕变化,图表自适应 屏幕变化后,图表自动调整字体、间距、位移等,此处参考上面 vm vh 的实现方式即可,在此就不重复赘述了

参考资料

css中好用的clamp()函数

lee992阅读(420)

clamp() 函数的作用是把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用。

语法

clamp() 函数接收三个用逗号分隔的表达式作为参数,按最小值、首选值、最大值的顺序排列。

width: clamp(最小值, 首选值, 最大值)

{
    font-size: clamp(20px, 18px, 40px);
    width: clamp(100px, 100%, 200px);
}
  • 当首选值比最小值要小时,则使用最小值。
  • 当首选值介于最小值和最大值之间时,用首选值。
  • 当首选值比最大值要大时,则使用最大值。

兼容性

https://caniuse.com/?search=clamp
从 caniuse 网站可以看出,不支持IE11

CSS3伪类大全汇总

lee992阅读(449)

所有 CSS 伪元素

选择器例子例子描述
::afterp::after在每个 <p> 元素之后插入内容。
::beforep::before在每个 <p> 元素之前插入内容。
::first-letterp::first-letter选择每个 <p> 元素的首字母。
::first-linep::first-line选择每个 <p> 元素的首行。
::selectionp::selection选择用户选择的元素部分。

所有 CSS 伪类

选择器例子例子描述
:activea:active选择活动的链接。
:checkedinput:checked选择每个被选中的 <input> 元素。
:disabledinput:disabled选择每个被禁用的 <input> 元素。
:emptyp:empty选择没有子元素的每个 <p> 元素。
:enabledinput:enabled选择每个已启用的 <input> 元素。
:first-childp:first-child选择作为其父的首个子元素的每个 <p> 元素。
:first-of-typep:first-of-type选择作为其父的首个 <p> 元素的每个 <p> 元素。
:focusinput:focus选择获得焦点的 <input> 元素。
:hovera:hover选择鼠标悬停其上的链接。
:in-rangeinput:in-range选择具有指定范围内的值的 <input> 元素。
:invalidinput:invalid选择所有具有无效值的 <input> 元素。
:lang(language)p:lang(it)选择每个 lang 属性值以 “it” 开头的 <p> 元素。
:last-childp:last-child选择作为其父的最后一个子元素的每个 <p> 元素。
:last-of-typep:last-of-type选择作为其父的最后一个 <p> 元素的每个 <p> 元素。
:linka:link选择所有未被访问的链接。
:not(selector):not(p)选择所有不是<p>的元素。
:nth-child(n)p:nth-child(2)选择作为其父的第二个子元素的每个 <p> 元素。
:nth-last-child(n)p:nth-last-child(2)选择作为父的第二个子元素的每个<p>元素,从最后一个子元素计数。
:nth-last-of-type(n)p:nth-last-of-type(2)选择作为父的第二个<p>元素的每个<p>元素,从最后一个子元素计数
:nth-of-type(n)p:nth-of-type(2)选择作为其父的第二个 <p> 元素的每个 <p> 元素。
:only-of-typep:only-of-type选择作为其父的唯一 <p> 元素的每个 <p> 元素。
:only-childp:only-child选择作为其父的唯一子元素的 <p> 元素。
:optionalinput:optional选择不带 “required” 属性的 <input> 元素。
:out-of-rangeinput:out-of-range选择值在指定范围之外的 <input> 元素。
:read-onlyinput:read-only选择指定了 “readonly” 属性的 <input> 元素。
:read-writeinput:read-write选择不带 “readonly” 属性的 <input> 元素。
:requiredinput:required选择指定了 “required” 属性的 <input> 元素。
:rootroot选择元素的根元素。
:target#news:target选择当前活动的 #news 元素(单击包含该锚名称的 URL)。
:validinput:valid选择所有具有有效值的 <input> 元素。
:visiteda:visited选择所有已访问的链接。

什么是伪元素?

CSS 伪元素用于设置元素指定部分的样式。

例如,它可用于:

  • 设置元素的首字母、首行的样式
  • 在元素的内容之前或之后插入内容

语法

伪元素的语法:

selector::pseudo-element {
property: value;
}

::first-line 伪元素

::first-line 伪元素用于向文本的首行添加特殊样式。

下面的例子为所有 <p> 元素中的首行添加样式:

实例

p::first-line {
color: #ff0000;
font-variant: small-caps;
}

注意:::first-line 伪元素只能应用于块级元素。

以下属性适用于 ::first-line 伪元素:

  • 字体属性
  • 颜色属性
  • 背景属性
  • word-spacing
  • letter-spacing
  • text-decoration
  • vertical-align
  • text-transform
  • line-height
  • clear

请注意双冒号表示法::first-line 对比 :first-line

在 CSS3 中,双冒号取代了伪元素的单冒号表示法。这是 W3C 试图区分伪类伪元素的尝试。

在 CSS2 和 CSS1 中,伪类和伪元素都使用了单冒号语法。

为了向后兼容,CSS2 和 CSS1 伪元素可接受单冒号语法。

::first-letter 伪元素

::first-letter 伪元素用于向文本的首字母添加特殊样式。

下面的例子设置所有 <p> 元素中文本的首字母格式:

实例

p::first-letter {
color: #ff0000;
font-size: xx-large;
}

注意:::first-letter 伪元素只适用于块级元素。

下面的属性适用于 ::first-letter 伪元素:

  • 字体属性
  • 颜色属性
  • 背景属性
  • 外边距属性
  • 内边距属性
  • 边框属性
  • text-decoration
  • vertical-align(仅当 “float” 为 “none”)
  • text-transform
  • line-height
  • float
  • clear

伪元素和 CSS 类

伪元素可以与 CSS 类结合使用:

实例

p.intro::first-letter {
color: #ff0000;
font-size: 200%;
}

上面的例子将以红色和较大的字体显示 class=”intro” 的段落的首字母。

多个伪元素

也可以组合几个伪元素。

在下面的例子中,段落的第一个字母将是红色,字体大小为 xx-large。第一行的其余部分将变为蓝色,并使用小型大写字母。该段的其余部分将是默认的字体大小和颜色:

实例

p::first-letter {
color: #ff0000;
font-size: xx-large;
}

p::first-line {
color: #0000ff;
font-variant: small-caps;
}

CSS – ::before 伪元素

::before 伪元素可用于在元素内容之前插入一些内容。

下面的例子在每个 <h1> 元素的内容之前插入一幅图像:

实例

h1::before {
content: url(smiley.gif);
}

CSS – ::after 伪元素

::after 伪元素可用于在元素内容之后插入一些内容。

下面的例子在每个 <h1> 元素的内容之后插入一幅图像:

实例

h1::after {
content: url(smiley.gif);
}

CSS – ::selection 伪元素

::selection 伪元素匹配用户选择的元素部分。

以下 CSS 属性可以应用于 ::selection

  • color
  • background
  • cursor
  • outline

下例使所选文本在黄色背景上显示为红色:

实例

::selection {
color: red;
background: yellow;
}

深入研究 CSS 文本换行

lee992阅读(587)

今天来研究一下 CSS 中的文本换行。正常情况下,在固定宽度的盒子中的中文会自动换行。但是,当遇到非常长的英文单词或者很长的URL时,文本可能就不会自动换行,而会溢出所在容器。幸运的是,CSS 为我们提供了一些和文本换行相关的属性;

  • overflow-wrap
  • word-break
  • white-space
  • line-break
  • hyphens

1. overflow-wrap

overflow-wrap用来说明当一个不能被分开的字符串太长而不能填充其包裹盒时,为防止其溢出,浏览器是否允许这样的单词中断换行。其属性值有以下三种:

overflow-wrap:normal;
overflow-wrap:anywhere;
overflow-wrap:break-word;

(1)normal

属性值为 normal 将使浏览器使用系统的默认换行行为。因此,对于英语和其他相关书写系统,换行符将出现在空格和连字符处。

图片

从图中可以看出,段落中有一个很长的单词溢出了容器,这是系统的默认换行行为。

(2)anywhere

使用值 anywhere 将在字符串之间的任意点来进行中断,仅当在其行上显示单词会导致溢出时,浏览器才会中断该单词。如果单词放在其行上时仍然溢出,它将在发生溢出的点处中断该单词。

图片

可以看到,使用overflow-wrap:anywhere将溢出的单词分解成文本块,这样就可以将其放入容器中。这里文本所在的容器宽度是固定的。

该属性会影响其所在元素的min-content属性大小计算。当width设置为min-content时很容易看出来:

.break-word{
width:min-content;
overflow-wrap:break-word;
}

.anywhere{
width:min-content;
overflow-wrap:anywhere;
}

效果如下:

图片

可以看到,带有overflow-wrap:break-word的元素计算出的min-content就像单词没有被破坏一样,因此它的宽度变成了最长单词的宽度。而带有overflow-wrap:anywhere的元素,由于在任何地方都可能发生中断,因此min-content最终成为单个字符的宽度。

注意,这种行为只有为文本所在容器的宽度设置为min-content时才会发挥作用,如果宽度设置为固定的值,那么anywherebreak-word的表现是一致的。

另外需要注意,目前有些浏览器不支持该属性:

图片

(3)break-word

break-word属性表示如果行内没有多余的地方容纳该单词到结尾,则那些正常的不能被分割的单词会被强制分割换行。

图片

可以看到,文本在长单词的某个地方自动换行了。如果文本所在容器设置了固定的宽度,就会在长单词溢出的地方换行。

(4)浏览器兼容性

overflow-wrap属性就是原来的word-wrapword-wrap最初是一个没有前缀的Microsoft扩展。它不是CSS标准的一部分,尽管大多数浏览器都使用word-wrap这个名称来实现它。根据CSS3规范草案,浏览器应将word-wrap视为overflow-wrap属性的遗留名称别名,以确保兼容性。

图片

2. word-break

word-break 属性用于指定怎样在单词内进行断行。我们可以使用该属性在内容发生溢出的确切位置拆分单词并将其换行到下一行。下面是word-break的属性值:

word-break:normal;
word-break:break-all;
word-break:keep-all;
word-break:break-word;

需要注意,break-word 属性值已经被弃用,但是由于遗留原因,浏览器仍然支持它。指定该属性与同时使用word-break: normaloverflow-wrap: anywhere的效果是一样的。

下面就来看看前三个属性:

(1)normal

word-break属性的值设置为normal将应用默认的断行规则:

图片

可以看到,设置为normal时,和不设置word-break时的效果是一样的,这就是浏览器默认的断行行为。

(2)break-all

当属性值为break-all时,对于 non-CJK (CJK 指中文/日文/韩文) 的文本,可在任意字符间断行。

图片

可以看到,长单词在溢出的位置将剩余的文本进行了换行。使用break-all将在英语和其他相关语言系统中发生溢出的确切位置在两个字符之间断开一个单词。但是,它不会对中文、日文和韩文文本应用相同的行为。因为 CJK 书写系统有自己的应用断点规则。

(3)keep-all

如果使用值keep-all,即使内容溢出,浏览器也不会将分词应用于 CJK 文本。应用 keep-all 值的效果与非 CJK 书写系统的正常效果相同。简单来说就是,像英语这种 CJK 文本不会断行,像中文这种 Non-CJK 文本表现同normal

(4)浏览器兼容性

图片

3. white-space

white-space属性是用来设置如何处理元素中的空白。其属性值如下:

white-space:normal;
white-space:nowrap;
white-space:pre;
white-space:pre-wrap;
white-space:pre-line

下面来看看这些属性值都有哪些作用。

(1)nowrap

当我们将white-space的值设置为nowrap时,可以防止文本自动换行

图片

可以看到,这里文本并没有换行,这时文本会在同一行上继续,直到遇到<br>标签为止。

(2)pre

当我们将white-space的值设置为pre时,文本之间的空白会被浏览器保留。其行为方式类似 HTML 中的<pre>标签。

<!--这个HTML的格式会导致额外的空白-->
<p>
What'sworse,ignoranceorapathy?
Idon'tknowandIdon'tcare.
</p>


<p>What'sworse,ignoranceorapathy?
Idon'tknowandIdon'tcare.</p>


<pre>What'sworse,ignoranceorapathy?
Idon'tknowandIdon'tcare.</pre>

样式设置如下:

p{
white-space:pre;
}

pre{
/*<pre>会设置font-family:monospace,这里将其重置*/
font-family:inherit;
}

显示效果如下:

图片

(3)浏览器兼容性

图片

4. line-break

line-break属性可以用来处理如何断开带有标点符号的中文、日文或韩文文本的行。简而言之,该属性可以用来处理过长的标点符号。

(1)anywhere

可以使用line-break: anywhere来使长标点符号进行换行:

图片

可以看到,overflow-wrap: break-wordline-break:anywhere能够保持内容被包含在容器内,但是word-break: break-all在有长标点符号时就会发生溢出。

(2)浏览器兼容性

图片

5.hyphens

hyphens属性告知浏览器在换行时如何使用连字符连接单词。可以完全阻止使用连字符,也可以控制浏览器什么时候使用,或者让浏览器决定什么时候使用。其断字规则由语言决定,因此需要告诉浏览器使用哪种语言。这是通过在 HTML 中指定lang属性来完成的:

<plang="en">Thisisjustabitofarbitrarytexttoshowhyphenationinaction.</p>

(1)auto

hyphens设置为auto时,浏览器可以自由地在适当的断字点自动断词:

p{
-webkit-hyphens:auto;/*用于Safari*/
hyphens:auto;
}

显示效果如下:

图片

(2)浏览器兼容性

图片

6. 总结

  • 当文本所在容器的宽度固定时,可以使用overflow-wrap: break-word;overflow-wrap: anywhere;来实现文本的自动换行;如果容器宽度为min-content,就只能使用overflow-wrap: break-word;实现文本换行;overflow-wrap: break-word;也可以用于长标点符号的换行。
  • word-break: break-all;可以用于文本换行,但是该属性不能使长标点符号换行;
  • white-space: nowrap;可以用于防止文本自动换行;
  • line-break: anywhere可以用于将长标点符号进行换行;
  • hyphens: auto;可以用于使用连字符连接单词。

助你成为 CSS 大师的18个 GitHub 仓库

lee992阅读(625)

将所有资源进一步分类,从学习基础知识到样式指南、最佳实践、有用的技巧和技巧,以及您可以学习的其他资源,以进一步扩展您的 CSS 知识。

学习资源

1.Awesome-css-learning ⭐ GitHub 星数:1k+

最好的 CSS 学习资源列表。

2.CSS-reference ⭐ GitHub 星数:4k+

CSS 参考:最流行的 CSS 属性的免费视觉指南。以cssreference.io的形式提供。

3.Magic-of-css ⭐ GitHub 星数:5k+

一个 CSS 课程,让你变成魔术师。

风格指南和最佳实践

4. CSS-architecture ⭐ GitHub 星数:2k+

8 条简单的规则,用于构建健壮、可扩展的 CSS 架构。

5. Bevacqua-css ⭐ GitHub 星数:800+

CSS:好的部分。

6.CSS-style-guide ⭐ GitHub 星数:700+

Dropbox 的 CSS 创作风格指南。

7.Airbnb-css ⭐ GitHub 星数:6k+

一种最合理的 CSS 和 Sass 方法。

Flexbox 和 Grid

8.Flex-cheatsheet ⭐ GitHub 星数:900+

带有视觉示例的 Flexbox 备忘单。

9. Flexbox30 ⭐ GitHub 星数:1k+

通过 30 条代码花絮在 30 天内学习 Flexbox。

10.Awesome-css-grid ⭐ GitHub 星数:200+

CSS 网格资源的精选列表。

11.Gridgarden ⭐ GitHub 星数:2k+

一款学习 CSS 网格布局的互动游戏。

有用的提示和技巧

12. You-need-to-know-css ⭐ GitHub stars: 4k+

面向 Web 开发人员的 CSS 技巧

13. CSS-protips ⭐ GitHub stars: 21k+

帮助您掌握CSS技能的技巧集

14. 30-seconds-of-css ⭐ GitHub stars: 15k+

满足所有开发需求的简短CSS代码片段。

15. You-dont-need-javascript ⭐ GitHub stars: 17k+

CSS很强大,没有JS你也可以做很多事情。

其他资源

16. Awesome-css-frameworks ⭐ GitHub stars: 4k+

2022年最棒的CSS框架列表.

17. Must-watch-css ⭐ GitHub stars: 4k+

一个有用的关于CSS的必看列表

18. Scalable-css-reading-list ⭐ GitHub stars: 1k+

从 The Quest for Scalable CSS 收集的调度。

浅谈逻辑选择器 is、where、not、has

lee992阅读(657)

在 CSS 选择器家族中,新增这样一类比较新的选择器 —逻辑选择器,目前共有 4 名成员:

  • :is
  • :where
  • :not
  • :has

本文将带领大家了解、深入它们。做到学以致用,写出更现代化的选择器。


:is 伪类选择器

:is()CSS伪类函数将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素。

在之前,对于多个不同父容器的同个子元素的一些共性样式设置,可能会出现如下 CSS 代码:

headerp:hover,
mainp:hover,
footerp:hover{
color:red;
cursor:pointer;
}

而如今有了:is()伪类,上述代码可以改写成:

:is(header,main,footer)p:hover{
color:red;
cursor:pointer;
}

它并没有实现某种选择器的新功能,更像是一种语法糖,类似于 JavaScript ES6 中的 Class() 语法,只是对原有功能的重新封装设计,实现了更容易的表达一个操作的语法,简化了某些复杂代码的写法。

语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言,操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。用比较通俗易懂的方式去理解就是,在之前的某个语法的基础上改变了一种写法,实现的功能相同,但是写法不同了,主要是为了让开发人员在使用过程中更方便易懂。

一图胜前言(引用至New CSS functional pseudo-class selectors :is() and :where()[1]):

图片

支持多层层叠连用

再来看看这种情况,原本的 CSS 代码如下:

<div><i>divi</i></div>
<p><i>pi</i></p>
<div><span>divspan</span></div>
<p><span>pspan</span></p>
<h1><span>h1span</span></h1>
<h1><i>h1i</i></h1>

如果要将上述 HTML 中,<div><p>下的<span><i>的 color 设置为 red,正常的 CSS 可能是这样:

divspan,
divi,
pspan,
pi{
color:red;
}

有了:is()后,代码可以简化为:

:is(div,p):is(span,i){
color:red;
}

结果如下:

图片

这里,也支持:is()的层叠连用。通过:is(div, p) :is(span, i)的排列组合,可以组合出上述 4 行的选择器,达到同样的效果。

当然,这个例子比较简单,看不出:is()的威力。下面这个例子就比较明显,这么一大段 CSS 选择器代码:

ololul,olulul,olmenuul,oldirul,
ololmenu,olulmenu,olmenumenu,oldirmenu,
ololdir,oluldir,olmenudir,oldirdir,
ulolul,ululul,ulmenuul,uldirul,
ulolmenu,ululmenu,ulmenumenu,uldirmenu,
uloldir,ululdir,ulmenudir,uldirdir,
menuolul,menuulul,menumenuul,menudirul,
menuolmenu,menuulmenu,menumenumenu,menudirmenu,
menuoldir,menuuldir,menumenudir,menudirdir,
dirolul,dirulul,dirmenuul,dirdirul,
dirolmenu,dirulmenu,dirmenumenu,dirdirmenu,
diroldir,diruldir,dirmenudir,dirdirdir{
list-style-type:square;
}

可以利用:is()优化为:

:is(ol,ul,menu,dir):is(ol,ul,menu,dir):is(ul,menu,dir){
list-style-type:square;
}

不支持伪元素

有个特例,不能用:is()来选取::before::after两个伪元素。譬如:

注意,仅仅是不支持伪元素,伪类,譬如:focus:hover是支持的。

divp::before,
divp::after{
content:"";
//...
}

不能写成:

divp:is(::before,::after){
content:"";
//...
}

:is选择器的优先级

看这样一种有意思的情况:

<div>
<pclass="test-class"id="test-id">where&istest</p>
</div>
<div>
<pclass="test-class">where&istest</p>
</div>

我们给带有.test-class的元素,设置一个默认的颜色:

div.test-class{
color:red;
}

如果,这个时候,我们引入:is()进行匹配:

div:is(p){
color:blue;
}

此时,由于div :is(p)可以看成div p,优先级是没有div .test-class高的,因此,被选中的文本的颜色是不会发生变化的。

但是,如果,我们在:is()选择器中,加上一个#test-id,情况就不一样了。

div:is(p,#text-id){
color:blue;
}

按照理解,如果把上述选择器拆分,上述代码可以拆分成:

divp{
color:blue;
}
div#text-id{
color:blue;
}

那么,我们有理由猜想,带有#text-id<p>元素由于有了更高优先级的选择器,颜色将会变成blue,而另外一个div p由于优先级不够高的问题,导致第一段文本依旧是green

但是,这里,神奇的是,两段文本都变成了blue

图片

CodePen Demo — the specificity of CSS :is selector[2]

这是由于,:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。我们不能把它们割裂开来看。

对于div :is(p, #text-id)is:()内部有一个 id 选择器,因此,被该条规则匹配中的元素,全部都会应用div #id这一级别的选择器优先级。这里非常重要,再强调一下,对于:is()选择器的优先级,我们不能把它们割裂开来看,它们是一个整体,优先级取决于选择器列表中优先级最高的选择器

:is 的别名 :matches() 与 :any()

:is()是最新的规范命名,在之前,有过有同样功能的选择,分别是:

:is(div,p)span{}
//等同于
:-webkit-any(div,p)span{}
:-moz-any(div,p)span{}
:matches(div,p)span{}

当然,下面 3 个都已经废弃,不建议再继续使用。而到今天(2022-04-27):is()的兼容性已经非常不错了,不需要兼容 IE 系列的话可以考虑开始用起来(配合autoprefixer),看看CanIUse[3]

图片

:where 伪类选择器

了解了:is后,我们可以再来看看:where,它们两个有着非常强的关联性。:where同样是将选择器列表作为其参数,并选择可以由该列表中的选择器之一选择的任何元素。

还是这个例子:

:where(header,main,footer)p:hover{
color:red;
cursor:pointer;
}

上述的代码使用了:where,可以近似的看为:

headerp:hover,
mainp:hover,
footerp:hover{
color:red;
cursor:pointer;
}

这就有意思了,这不是和上面说的:is一样了么?

那么它们的区别在什么地方呢?

:is:where的区别

首先,从语法上,:is:where是一模一样的。它们的核心区别点在于优先级

来看这样一个例子:

<div>
<p>where&istest</p>
</div>

CSS 代码如下:

:is(div)p{
color:red;
}
:where(div)p{
color:green;
}

正常按我们的理解而言,:is(div) p:where(div) p都可以转化为div p,由于:where(div) p后定义,所以文字的颜色,应该是green绿色,但是,实际的颜色表现为color: red红色:

图片

这是因为,:where():is()的不同之处在于,:where()的优先级总是为 0,但是:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。

上述的例子还不是特别明显,我们再稍微改造下:

<divid="container">
<p>where&istest</p>
</div>

我们给 div 添加上一个 id 属性,改造上述 CSS 代码:

:is(div)p{
color:red;
}
:where(#container)p{
color:green;
}

即便如此,由于:where(#container)的优先级为 0,因此文字的颜色,依旧为红色 red。:where()的优先级总是为 0这一点在使用的过程中需要牢记。

组合、嵌套

CSS 选择器的一个非常大的特点就在于组合嵌套。:is:where也不例外,因此,它们也可以互相组合嵌套使用,下述的 CSS 选择器都是合理的:

/*组合*/
:is(h1,h2):where(.test-a,.test-b){
text-transform:uppercase;
}
/*嵌套*/
.title:where(h1,h2,:is(.header,.footer)){
font-weight:bold;
}

这里简单总结下,:is:where都是非常好的分组逻辑选择器,唯一的区别在于:where()的优先级总是为 0,而:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not 伪类选择器

下面我们介绍一下非常有用的:not伪类选择器。

:not伪类选择器用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。

举个例子,HTML 结构如下:

<divclass="a">div.a</div>
<divclass="b">div.b</div>
<divclass="c">div.c</div>
<divclass="d">div.d</div>
div:not(.b){
color:red;
}

div:not(.b)它可以选择除了 class 为.b元素之外的所有 div 元素:

图片

MDN 的错误例子?一个有意思的现象

有趣的是,在 MDN 介绍:not的页面,有这样一个例子:

/*SelectsanyelementthatisNOTaparagraph*/
:not(p){
color:blue;
}

意思是,:not(p)可以选择任何不是<p>标签的元素。然而,上面的 CSS 选择器,在如下的 HTML 结构,实测的结果不太对劲。

<p>p</p>
<div>div</div>
<span>span</span>
<h1>h1</h1>

结果如下:

图片

意思是,:not(p)仍然可以选中<p>元素。我尝试了多个浏览器,得到的效果都是一致的。

CodePen Demo — :not pesudo demo[4]

这是为什么呢?这是由于:not(p)同样能够选中<body>,那么<body>的 color 即变成了blue,由于color是一个可继承属性,<p>标签继承了<body>的 color 属性,导致看到的<p>也是蓝色。

我们把它改成一个不可继承的属性,试试看:

/*SelectsanyelementthatisNOTaparagraph*/
:not(p){
border:1pxsolid;
}
图片

OK,这次<p>没有边框体现,没有问题!实际使用的时候,需要注意这一层继承的问题!

:not 的优先级问题

下面是一些使用:not需要注意的问题。

:not:is:where这几个伪类不像其它伪类,它不会增加选择器的优先级。它的优先级即为它参数选择器的优先级。

并且,在CSS Selectors Level 3[5]:not()内只支持单个选择器,而从 CSS Selectors Level 4[6]开始,:not()内部支持多个选择器,像是这样:

/*CSSSelectorsLevel3,:not内部如果有多个值需要分开*/
p:not(:first-of-type):not(.special){
}
/*CSSSelectorsLevel4支持使用逗号分隔*/
p:not(:first-of-type,.special){
}

:is()类似,:not()选择器本身不会影响选择器的优先级,它的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not(*) 问题

使用:not(*)将匹配任何非元素的元素,因此这个规则将永远不会被应用。

相当于一段没有任何意义的代码。

:not() 不能嵌套 :not()

禁止套娃。:not伪类不允许嵌套,这意味着:not(:not(...))是无效的。

:not() 实战解析

那么,:not() 有什么特别有意思的应用场景呢?我这里列举一个。

W3 CSS selectors-4 规范[7]中,新增了一个非常有意思的:focus-visible伪类。

:focus-visible这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

有了这个伪类,就可以做到,当用户使用鼠标操作可聚焦元素时,不展示:focus样式或者让其表现较弱,而当用户使用键盘操作焦点时,利用:focus-visible,让可获焦元素获得一个较强的表现样式。

看个简单的 Demo:

<button>Test1</button>
button:active{
background:#eee;
}
button:focus{
outline:2pxsolidred;
}

使用鼠标点击:

图片

可以看到,使用鼠标点击的时候,触发了元素的:active伪类,也触发了:focus伪类,不太美观。但是如果设置了outline: none又会使键盘用户的体验非常糟糕。因为当键盘用户使用 Tab 尝试切换焦点的时候,会因为outline: none而无所适从。

因此,可以使用:focus-visible伪类改造一下:

button:active{
background:#eee;
}
button:focus{
outline:2pxsolidred;
}
button:focus:not(:focus-visible){
outline:none;
}

看看效果,分别是在鼠标点击 Button 和使用键盘控制焦点点击 Button:

图片

CodePen Demo — :focus-visible example[8]

可以看到,使用鼠标点击,不会触发:foucs,只有当键盘操作聚焦元素,使用 Tab 切换焦点时,outline: 2px solid red这段代码才会生效。

这样,我们就既保证了正常用户的点击体验,也保证了无法使用鼠标的用户的焦点管理体验,在可访问性方面下了功夫。

值得注意的是,这里为什么使用了button:focus:not(:focus-visible)这么绕的写法而不是直接这样写呢:

button:focus{
outline:unset;
}
button:focus-visible{
outline:2pxsolidred;
}

解释一下,button:focus:not(:focus-visible)的意思是,button 元素触发 focus 状态,并且不是通过 focus-visible 触发,理解过来就是在支持:focus-visible的浏览器,通过鼠标激活:focus的 button 元素,这种情况下,不需要设置outline

为的是兼容不支持:focus-visible的浏览器,当:focus-visible不兼容时,还是需要有:focus伪类的存在。

因此,这里借助:not()伪类,巧妙的实现了一个实用效果的方案降级。

这里有点绕,需要好好理解理解。

:not 兼容性

经历了 CSS Selectors Level 3 & CSS Selectors Level 4 两个版本,到今天(2020-05-04),除去 IE 系列,:not的兼容性已经非常之好了:

图片

:has 伪类选择器

OK。最后到所有逻辑选择器里面最重磅的:has出场了。它之所以重要是因为它的诞生,填补了在之前 CSS 选择器中,没有核心意义上真正的父选择器的空缺。

:has伪类接受一个选择器组作为参数,该参数相对于该元素的:scope[9]至少匹配一个元素。

实际看个例子:

<div>
<p>div--p</p>
</div>
<div>
<pclass="g-test-has">div--p.has</p>
</div>
<div>
<p>div--p</p>
</div>
div:has(.g-test-has){
border:1pxsolid#000;
}

我们通过div:has(.g-test-has)选择器,意思是,选择 div 下存在 class 为.g-test-has的 div 元素。

注意,这里选择的不是:has()内包裹的选择器选中的元素,而是使用:has()伪类的宿主元素。

效果如下:

图片

可以看到,由于第二个 div 下存在 class 为.g-test-has的元素,因此第二个 div 被加上了 border。

:has() 父选择器 — 嵌套结构的父元素选择

我们再通过几个 DEMO 加深下印象。:has()内还可以写的更为复杂一点。

<div>
<span>divspan</span>
</div>

<div>
<ul>
<li>
<h2><span>divullih2span</span></h2>
</li>
</ul>
</div>

<div>
<h2><span>divh2span</span></h2>
</div>
div:has(>h2>span){
margin-left:24px;
border:1pxsolid#000;
}

这里,要求准确选择 div 下直接子元素是 h2,且 h2 下直接子元素有 span 的 div 元素。注意,选择的最上层使用 :has() 的父元素 div。结果如下:

图片

这里体现的是嵌套结构,精确寻找对应的父元素

:has() 父选择器 — 同级结构的兄元素选择

还有一种情况,在之前也比较难处理,同级结构的兄元素选择。

看这个 DEMO:

<divclass="has-test">div+p</div>
<p>p</p>

<divclass="has-test">div+h1</div>
<h1>h1</h1>

<divclass="has-test">div+h2</div>
<h2>h2</h2>

<divclass="has-test">div+ul</div>
<ul>ul</ul>

我们想找到兄弟层级关系中,后面接了<h2>元素的 .has-test元素,可以这样写:

.has-test:has(+h2){
margin-left:24px;
border:1pxsolid#000;
}

效果如下:

图片

这里体现的是兄弟结构,精确寻找对应的前置兄元素

这样,一直以来,CSS 没有实现的父选择器,借由:has()开始,也能够做到了。这个选择器,能够极大程度的提升开发体验,解决之前需要比较多 JavaScript 代码才能够完成的事。

上述 DEMO 汇总,你可以戳这里CodePen Demo — :has Demo[10]

:has() 兼容性,给时间一点时间

比较可惜的是,:has()在最近的Selectors Level 4[11]规范中被确定,目前的兼容性还比较惨淡,截止至 2022-05-04,Safari 和 最新版的 Chrome(V101,可通过开启Experimental Web Platform features体验)

图片

Chrome 下开启该特性需要,1. 浏览器 URL 框输入 chrome://flags,2. 开启 #enable-experimental-web-platform-features

耐心等待,给给时间一点时间,这么好的选择器马上就能大规模应用了。

最后

本文到此结束,希望对你有帮助 🙂

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 —iCSS前端趣闻😄

更多精彩 CSS 技术文章汇总在我的Github — iCSS[12],持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

[1]

New CSS functional pseudo-class selectors :is() and :where():https://web.dev/css-is-and-where/[2]

CodePen Demo — the specificity of CSS :is selector:https://codepen.io/Chokcoco/pen/rNJaGvb[3]

CanIUse:https://caniuse.com/?search=%3Amatches[4]

CodePen Demo — :not pesudo demo:https://codepen.io/Chokcoco/pen/KKZbWjy[5]

CSS Selectors Level 3:https://www.w3.org/TR/selectors-3/[6]

CSS Selectors Level 4:https://www.w3.org/TR/selectors-4/[7]

W3 CSS selectors-4 规范:https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo[8]

CodePen Demo — :focus-visible example:https://codepen.io/Chokcoco/pen/abBbPrE[9]

:scope:https://developer.mozilla.org/zh-CN/docs/Web/CSS/:scope[10]

CodePen Demo — :has Demo:https://codepen.io/Chokcoco/pen/poaJjwm[11]

Selectors Level 4:https://drafts.csswg.org/selectors/#relational[12]

Github — iCSS:https://github.com/chokcoco/iCSS

CSS 父元素选择器详解

lee992阅读(698)

你有没有想过使用CSS选择器来检查父元素中是否存在特定的元素?例如,如果一个卡片组件中有图片,就给它添加一个display:flex。这以前在CSS中是无法实现的,而全新的 CSS 选择器:has就可以帮助我们选择包含特定元素的父元素。下面来看看这个全新的 CSS 选择器吧!

:has 选择器概述

在CSS中,我们无法根据元素中是否存在特定的元素来设置父元素的样式,要想实现这一点,就必须创建CSS类,并根据需要进行类的切换。来看下面的例子:

图片

这里有两种卡片:包含图片和不包含图片。在CSS中需要这样做:

/* 有图片的卡片 */
.card {
display: flex;
align-items: center;
gap: 1rem;
}

/* 没有图片的卡片 */
.card-plain {
display: block;
border-top: 3px solid #7c93e9;
}
<!-- 有图片的卡片 -->
<div class="card">
<div class="card-image">
<img src="awameh.jpg" alt="">
</div>
<div class="card-content">
卡片内容
</div>
</div>

<!-- 没有图片的卡片 -->
<div class="card card-plain">
<div class="card-content">
卡片内容
</div>
</div>

这里创建了一个类card-plain,专门用于没有图片的卡片,在没有图片时就不需要flex布局。如果使用 CSS 中的父选择器 :has 就不需要再写这个类,只需要使用它来检查卡片中是否包含.card-image即可:

.card:has(.card-image) {
display: flex;
align-items: center;
}

根据 CSS 规范,:has 选择器可以检查父元素是否包含至少一个元素,或者一个条件,例如输入是否获取到焦点。

:has 选择器不仅可以检查父元素是否包含特定的子元素,还可以检查一个元素后面是否跟有另一个元素:

.card h2:has(+ p) { }

这将检查 <h2> 元素是否直接跟在 <p> 元素之后。

我们也可以将它与表单元素一起使用来检查输入是否获取到了焦点:

form:has(input:focused) {
background-color: lightgrey;
}

:has 选择器使用示例

下面来看看一些使用:has选择器实现页面效果的案例吧!

1. 标题样式

当处理章节标题时有两种情况,一种是只包含标题,另一种包含标题和链接。

图片

根据是否有链接来定义不同的样式:

<section>
<div class="section-header">
<h2>Latest articles</h2>
<a href="/articles/>See all</a>
</div>
</section>
.section-header {
display: flex;
justify-content: space-between;
}

.section-header:has(> a) {
align-items: center;
border-bottom: 1px solid;
padding-bottom: 0.5rem;
}

这里使用了:has(> a),它表示只选择直接子元素。

2. 卡片布局

有两种类型的卡片操作:一种只有一个操作(链接),另一种具有多个操作(保存、分享等)。

图片

当图片具有多个操作时需要给这些操作添加display: flex,可以这样来实现:

<div class="card">
<div class="card-thumb><img src="cool.jpg"/></div>
<div class="card-content">
<div class="card-actions">
<div class="start">
<a href="#">Like</a>
<a href="#">Save</a>
</div>
<div class="end">
<a href="#">More</a>
</div>
</div>
</div>
</div>
.card-actions:has(.start, .end) {
display: flex;
justify-content: space-between;
}

3. 卡片圆角

根据是否有图片来重置卡片组件的border-radius

图片
.card:not(:has(img)) .card-content {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}

.card img {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}

.card-content {
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}

实现效果如下:

图片

4. 过滤组件

有一个具有多个选项的组件,当它们没有被选中时,不显示重置按钮。当选中其中一个选项时,显示重置按钮。

可以使用 :has选择器轻松实现这个功能:

.btn-reset {
display: none;
}

.multiselect:has(input:checked) .btn-reset {
display: block;
}

5. 显示或隐藏表单元素

有时可能需要根据之前的选择来显示特定的表单字段。在下面的例子中,当下拉框选中“other”字段时,就展示 other reason 输入框:

使用 CSS :has 选择器就可以检查选择菜单是否选择了 other 字段,并在此基础上显示 other reason 输入框:

.other-field {
display: block;
}

form:has(option[value="other"]:checked) .other-field {
display: block;
}

6. 导航栏

有一个带有子菜单的导航栏,当鼠标悬停在菜单项上时展示子菜单:

我们需要做的就是根据是否展示子菜单来显示或隐藏右侧的箭头。可以使用:has 选择器轻松实现这一点,这里只需检查li元素中是否包含ul即可:

li:has(ul) > a:after {
content: "";
}

7. 强制警报

在某些仪表板中,可能需要用户必须注意重要警报。在这种情况下,拥有页内警报可能还不够。例如,在这种情况下,可能会为标题元素添加红色边框和暗红色背景色。这样会增加用户快速注意到警报的可能性。

CSS :has 就可以检查.main元素是否有警报。如果有,将以下样式添加到标题中:

.main:has(.alert) .header {
border-top: 2px solid red;
background-color: #fff4f4;
}

8. 切换配色方案

可以使用 CSS :has 来更改网站的配色方案。例如,如果有多个使用 CSS 变量构建的主题,可以通过<select>菜单来进行切换。

html {
--color-1: #9e7ec8;
--color-2: #f1dceb;
}

当选择另一个主题时,CSS 变量将被更改:

html:has(option[value="blueish"]:checked) {
--color-1: #9e7ec8;
--color-2: #f1dceb;
}

显示效果如下:

9. 带有图标的按钮

有一个默认的按钮样式。当按钮中包含图标时,使用 flexbox 来居中对齐按钮的内容:

.button:has(.c-icon) {
display: inline-flex;
justify-content: center;
align-items: center;
}

10. 多个按钮

有一组操作按钮,如果超过 2 个按钮,则最后一个按钮显示在右侧:

图片

可以使用数量查询来实现这一点。下面的 CSS 将检查按钮的数量是否为 3 或更多。如果是,则最后一个 flex 项目将使用 margin-left: auto

.btn-group {
display: flex;
align-items: center;
gap: 0.5rem;
}

.btn-group:has(.button:nth-last-child(n + 3)) .button:last-child {
margin-left: auto;
}

11. 根据项目数更改网格

使用 CSS grid 布局中,可以使用 minmax() 功能创建真正响应式和自动调整大小的网格项。然而,这可能还不够,我们还想根据项目数量来改变网格。

图片
.wrapper {
--item-size: 200px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--item-size), 1fr));
gap: 1rem;
}

当有 5 个项目时,最后一个将换行:

可以通过检查.wrapper中是否有 5 个或更多项目来解决这个问题。同样,这是使用到了数量查询的概念。

.wrapper:has(.item:nth-last-child(n + 5)) {
--item-size: 120px;
}

实现效果如下:

浏览器支持

目前,:has 选择器已经在 Safari 15.4 和 Chrome Canary 中得到了支持。

图片

可以使用 CSS 中的@supports规则来判断浏览器是否支持该选择器:

@supports selector(:has(*)) {

}

参考:https://ishadeed.com/article/css-has-parent-selector/

CSS中line-height带单位与不带单位的区别

lee992阅读(689)

对于line-height这个样式,相信很多伙伴都知道,但是真正会用的,可能小部分。今天我们来看看它。

line-height的基本概念图片1、行高是指文本行基线baseline之间的垂直距离

2、行距是上一行的底线和下一行的顶线之间的距离。

3、字体大小是同一行的顶线和底线之间的距离。

上面这张图,通过基线之间的距离,然后转到了当前行的font-size + 上下半行距。我们了解了基本信息之后,来看看今天的主题,主要是看给line-height赋值不同而导致的特殊区别。

1、px单位赋值

这个应该是很多伙伴经常用的,当line-height和height一样时候,上下居中。子元素继承父元素行高。

特点会有继承父元素行高,和font-size无关

2、使用百分数或em单位

.parent{
line-height: 2em;
font-size: 30px;
word-break: break-all;
background-color: red;
}
.child{
font-size: 20px;
background-color: orange;
}
图片

可以发现,子元素直接继承了父元素的行高=倍数*font-sise,子元素自己的font-size没有作用。

3、不带单位

.parent{
line-height: 4;
font-size: 30px;
word-break: break-all;
background-color: red;
}
.child{
font-size: 20px;
}
<div class="parent">
parent
<div class="child">child</div>
</div>
图片

可以得到行高= (继承的line-height | 本身line-height) * 自身font-zise。

特点不带单位表示行高为元素字体大小乘以该数字。如果子元素继承父元素的该属性,则只继承了该数字,实际行高由该系数乘以各个元素自己的字体大小而定,可以说,它可以做到我命由我不由天

总结

带单位

px: 直接继承父元素行高

em | %:继承计算过后的值

不带单位

只是继承了line-height。具体大小由自己的font-size决定。所以推荐使用不带单位的这种。

现代 CSS 解决方案:数学函数之 min、max、clamp

lee992阅读(686)

在 CSS 中,其实存在各种各样的函数。具体分为:

  • Transform functions[1]
  • Math functions[2]
  • Filter functions[3]
  • Color functions[4]
  • Image functions[5]
  • Counter functions[6]
  • Font functions[7]
  • Shape functions[8]
  • Reference functions[9]
  • CSS grid functions[10]

本文,将具体介绍其中的 CSS 数学函数(Math functions)中,已经被浏览器大规模支持的 4 个:

  • calc()
  • min()
  • max()
  • clamp()

为什么说是被浏览器大规模支持的?因为除了这 4 个目前已经得到大规模支持的数学函数外,其实规范 CSS Values and Units Module Level 4[11] 已经定义了诸如三角函数相关 sin()cos()tan() 等,指数函数相关 pow()sqrt() 等等数学函数,只是目前都处于实验室阶段,还没有浏览器支持它们,需要给时间一点时间。

上一篇文章我们详细讲述了 calc(),本文我们将探讨一下另外 3 个。关于 calc(),你可以戳这里:

现代 CSS 解决方案:CSS 数学函数之 calc

min()、max()、clamp()

min()、max()、clamp() 适合放在一起讲。它们的作用彼此之间有所关联。

  • max():从一个逗号分隔的表达式列表中选择最大(正方向)的值作为属性的值
  • min():从一个逗号分隔的表达式列表中选择最小的值作为属性的值
  • clamp():把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用

由于在现实中,有非常多元素的的属性不是一成不变的,而是会根据上下文、环境的变化而变化。

譬如这样一个布局:

<div class="container"></div>
.container {
height: 100px;
background: #000;
}

效果如下,.container 块它会随着屏幕的增大而增大,始终占据整个屏幕:

图片

对于一个响应式的项目,我们肯定不希望它的宽度会一直变大,而是当达到一定的阈值时,宽度从相对单位变成了绝对单位,这种情况就适用于 min(),简单改造下代码:

.container {
width: min(100%, 500px);
height: 100px;
background: #000;
}

容器的宽度值会在 width: 100%width: 500px 之间做选择,选取相对小的那个。

在屏幕宽度不足 500px 时候,也就表现为 width: 100%,反之,则表现为 width: 500px

图片

同理,在类似的场景,我们也可以使用 max() 从多个值中,选取相对更大的值。

min()、max() 支持多个值的列表

min()、max() 支持多个值的列表,譬如 width: max(1px, 2px, 3px, 50px)

当然,对于上述表达:

width: max(1px, 2px, 3px, 50px) 其实等于 width: 50px。因此,对于 min()、max() 的具体使用而言,最多应该只包含一个具体的绝对单位。否则,这样的像上述这种代码,虽然语法支持,但是任何情况下,计算值都是确定的,其实没有意义。

配合 calc

min()、max()、clamp() 都可以配合 calc 一起使用。

譬如:

div {
width: max(50vw, calc(300px + 10%));
}

在这种情况下,calc 和相应包裹的括号可以省略,因此,上述代码又可以写成:

div {
width: max(50vw, 300px + 10%);
}

基于 max、min 模拟 clamp

现在,有这样一种场景,如果,我们又需要限制最大值,也需要限制最小值,怎么办呢?

像是这样一个场景,**字体的大小,最小是 12px,随着屏幕的变大,逐渐变大,但是为了避免老人机现象(随着屏幕变大,无限制变大),我们还需要限制一个最大值 20px。

我们可以利用 vw 来实现给字体赋动态值,假设在移动端,设备宽度的 CSS 像素为 320px 时,页面的字体宽度最小为 12px,换算成 vw 即是 320 / 100 = 3.2,也就是 1vw 在 屏幕宽度为 320px 时候,表现为 3.2px,12px 约等于 3.75 vw。

同时,我们需要限制最大字体值为 20px,对应的 CSS 如下:

p {
font-size: max(12px, min(3.75vw, 20px));
}

看看效果:

图片

通过 max()min() 的配合使用,以及搭配一个相对单位 vw,我们成功地给字体设置了上下限,而在这个上下限之间实现了动态变化。

当然,上面核心的这一段 max(12px, min(3.75vw, 20px)) 看上去有点绕,因此,CSS 推出了 clamp() 简化这个语法,下面两个写法是等价的:

p {
font-size: max(12px, min(3.75vw, 20px));
// 等价于
font-size: clamp(12px, 3.75vw, 20px);
}

clamp()

clamp() 函数的作用是把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用。它接收三个参数:最小值、首选值、最大值。

有意思的是,clamp(MIN, VAL, MAX) 其实就是表示 max(MIN, min(VAL, MAX))

使用 vw 配合 clamp 实现响应式布局

我们继续上面的话题。

在不久的过去,移动端的适配方面,使用更多的 rem 适配方案,可能会借助一些现成的库,类似于 flexible.js、hotcss.js 等库。rem 方案比较大的一个问题在于需要一段 JavaScript 响应视口变化,重设根元素的 font-size,并且,使用 rem 多少有点 hack 的感觉。

在现在,在移动端适配,我们更为推崇的是 vw 纯 CSS 方案,与 rem 方案类似,它的本质也是页面的等比例缩放。它的一个问题在于,如果仅仅使用 vw,随着屏幕的不断变大或者缩小,内容元素将会一直变大变小下去,这也导致了在大屏幕下,许多元素看着实在太大了!

因此,我们需要一种能够控制最大、最小阈值的方式,像是这样:

图片

此时,clamp 就能非常好的派上用场,还是我们上述的例子,这一段代码 font-size: max(12px, min(3.75vw, 20px));,就能将字体限制在 12px - 20px 的范围内。

因此,对于移动端页面而言,所有涉及长度的单位,我们都可以使用 vw 进行设置。而诸如字体、内外边距、宽度等不应该完全等比例缩放的,采用 clamp() 控制最大最小阈值。

Modern Fluid Typography Using CSS Clamp[12] 一文中,对使用 clamp() 进行流式响应式布局还有更为深入的探讨,感兴趣的可以深入阅读。

总结而言,对于移动端页面,我们可以以 vw 配合 clamp() 的方式,完成整个移动端布局的适配。它的优势在于

  1. 没有额外 JavaScript 代码的引入,纯 CSS 解决方案
  2. 能够很好地控制边界阈值,合理的进行缩放展示

反向响应式变化

还有一个技巧,利用 clamp() 配合负值,我们也可以反向操作,得到一种屏幕越大,字体越小的反向响应式效果:

p {
font-size: clamp(20px, -5vw + 96px, 60px);
}

看看效果:

图片

这个技巧挺有意思的,由于 -5vw + 96px 的计算值会随着屏幕的变小而增大,实现了一种反向的字体响应式变化。

总结

总结一下,合理运用 min()、max()、clamp(),是构建现代响应式布局的重点,我们可以告别传统的需要 JavaScript 辅助的一些方案,基于 CSS 这些数学函数即可完成所有的诉求。

一些进阶阅读非常好的文章:

  • A guide to the min(), max(), and clamp() CSS functions[13]
  • Modern Fluid Typography Using CSS Clamp[14]

最后

好了,本文到此结束,希望本文对你有所帮助 🙂

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

  1. Transform functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#transform_functions
  2. Math functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#math_functions
  3. Filter functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#filter_functions
  4. Color functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#color_functions
  5. Image functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#image_functions
  6. Counter functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#counter_functions
  7. Font functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#font_functions
  8. Shape functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#shape_functions
  9. Reference functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#reference_functions
  10. CSS grid functions: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#css_grid_functions
  11. CSS Values and Units Module Level 4: https://drafts.csswg.org/css-values/#math
  12. Modern Fluid Typography Using CSS Clamp: https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/
  13. A guide to the min(), max(), and clamp() CSS functions: https://blog.logrocket.com/min-max-clamp-css-functions/
  14. Modern Fluid Typography Using CSS Clamp: https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/

现代 CSS 解决方案:CSS 数学函数之 calc

lee992阅读(687)

在 CSS 中,其实存在各种各样的函数。具体分为:

  • Transform functions[1]
  • Math functions[2]
  • Filter functions[3]
  • Color functions[4]
  • Image functions[5]
  • Counter functions[6]
  • Font functions[7]
  • Shape functions[8]
  • Reference functions[9]
  • CSS grid functions[10]

本文,将具体介绍其中的 CSS 数学函数(Math functions)中,已经被浏览器大规模支持的 4 个:

  • calc()
  • min()
  • max()
  • clamp()

为什么说是被浏览器大规模支持的?因为除了这 4 个目前已经得到大规模支持的数学函数外,其实规范 CSS Values and Units Module Level 4[11] 已经定义了诸如三角函数相关 sin()cos()tan() 等,指数函数相关 pow()sqrt() 等等数学函数,只是目前都处于实验室阶段,还没有浏览器支持它们,需要给时间一点时间。

Calc()

calc() 此 CSS[12] 函数允许在声明 CSS 属性值时执行一些计算。

语法类似于

{
width: calc(100% - 80px);
}

一些需要注意的点:

  • +- 运算符的两边必须要有空白字符。比如,calc(50% -8px) 会被解析成为一个无效的表达式,必须写成calc(8px + -50%)
  • */ 这两个运算符前后不需要空白字符,但如果考虑到统一性,仍然推荐加上空白符
  • 用 0 作除数会使 HTML 解析器抛出异常
  • 涉及自动布局和固定布局的表格中的表列、表列组、表行、表行组和表单元格的宽度和高度百分比的数学表达式,auto 可视为已指定。
  • calc() 函数支持嵌套,但支持的方式是:把被嵌套的 calc() 函数全当成普通的括号。(所以,函数内直接用括号就好了。)
  • calc() 支持与 CSS 变量混合使用

看一个最常见的例子,页面结构如下:

<div class="g-container">
<div class="g-content">Content</div>
<div class="g-footer">Footer</div>
</div>

页面的 g-footer 高为 80px,我们希望不管页面多长,g-content 部分都可以占满剩余空间,像是这样:

图片

这种布局使用 flex 的弹性布局可以轻松实现,当然,也可以使用 calc() 实现:

.g-container {
height: 100vh;
}
.g-content {
height: calc(100vh - 80px);
}
.g-footer {
height: 80px;
}

下面罗列一些 Calc() 的进阶技巧。

Calc 中的加减法与乘除法的差异

注意,calc() 中的加减法与乘除法的差异:

{
font-size: calc(1rem + 10px);
width: calc(100px + 10%);
}

可以看到,加减法两边的操作数都是需要单位的,而乘除法,需要一个无单位数,仅仅表示一个倍率:

{
width: calc(100% / 7);
animation-delay: calc(1s * 3);
}

Calc 的嵌套

calc() 函数是可以嵌套使用的,像是这样:

{
width: calc(100vw - calc(100% - 64px));
}

此时,内部的 calc() 函数可以退化写成一个括号即可 (),所以上述代码等价于:

{
width: calc(100vw - (100% - 64px));
}

也就是嵌套内的 calc(),calc 几个函数字符可以省略

Calc 内不同单位的混合运算

calc() 支持不同单位的混合运算,对于长度,只要是属于长度相关的单位都可以进行混合运算,包含这些:

  • px
  • %
  • em
  • rem
  • in
  • mm
  • cm
  • pt
  • pc
  • ex
  • ch
  • vh
  • vw
  • vmin
  • vmax

这里有一个有意思的点,运算肯定是消耗性能的,早年间,有这样一段 CSS 代码,可以直接让 Chrome 浏览器崩溃 Crash:

<div></div>

CSS 样式如下:

div {
--initial-level-0: calc(1vh + 1% + 1px + 1em + 1vw + 1cm);

--level-1: calc(var(--initial-level-0) + var(--initial-level-0));
--level-2: calc(var(--level-1) + var(--level-1));
--level-3: calc(var(--level-2) + var(--level-2));
--level-4: calc(var(--level-3) + var(--level-3));
--level-5: calc(var(--level-4) + var(--level-4));
--level-6: calc(var(--level-5) + var(--level-5));
--level-7: calc(var(--level-6) + var(--level-6));
--level-8: calc(var(--level-7) + var(--level-7));
--level-9: calc(var(--level-8) + var(--level-8));
--level-10: calc(var(--level-9) + var(--level-9));
--level-11: calc(var(--level-10) + var(--level-10));
--level-12: calc(var(--level-11) + var(--level-11));
--level-13: calc(var(--level-12) + var(--level-12));
--level-14: calc(var(--level-13) + var(--level-13));
--level-15: calc(var(--level-14) + var(--level-14));
--level-16: calc(var(--level-15) + var(--level-15));
--level-17: calc(var(--level-16) + var(--level-16));
--level-18: calc(var(--level-17) + var(--level-17));
--level-19: calc(var(--level-18) + var(--level-18));
--level-20: calc(var(--level-19) + var(--level-19));
--level-21: calc(var(--level-20) + var(--level-20));
--level-22: calc(var(--level-21) + var(--level-21));
--level-23: calc(var(--level-22) + var(--level-22));
--level-24: calc(var(--level-23) + var(--level-23));
--level-25: calc(var(--level-24) + var(--level-24));
--level-26: calc(var(--level-25) + var(--level-25));
--level-27: calc(var(--level-26) + var(--level-26));
--level-28: calc(var(--level-27) + var(--level-27));
--level-29: calc(var(--level-28) + var(--level-28));
--level-30: calc(var(--level-29) + var(--level-29));

--level-final: calc(var(--level-30) + 1px);

border-width: var(--level-final);
border-style: solid;
}

可以看到,从 --level-1--level-30,每次的运算量都是成倍的增长,最终到 --level-final 变量,展开将有 2^30 = 1073741824--initial-level-0 表达式的内容。

并且,每个 --initial-level-0 表达式的内容 — calc(1vh + 1% + 1px + 1em + 1vw + 1cm),在浏览器解析的时候,也已经足够复杂。

混合在一起,就导致了浏览器的 BOOM(Chrome 70之前的版本),为了能看到效果,我们将上述样式赋给某个元素被 hover 的时候,得到如下效果:

当然,这个 BUG 目前已经被修复了,我们也可以通过这个小 DEMO 了解到,一是 calc 是可以进行不同单位的混合运算的,另外一个就是注意具体使用的时候如果计算量巨大,可能会导致性能上较大的消耗。

当然,不要将长度单位和非长度单位混合使用,像是这样:

{
animation-delay: calc(1s + 1px);
}

Calc 搭配 CSS 自定义变量使用

calc() 函数非常重要的一个特性就是能够搭配 CSS 自定义以及 CSS @property 变量一起使用。

最简单的一个 DEMO:

:root {
--width: 10px;
}
div {
width: calc(var(--width));
}

当然,这样看上去,根本看不出这样的写法的作用,好像没有什么意义。实际应用场景中,会比上述的 DEMO 要稍微复杂一些。

假设我们要实现这样一个 loading 动画效果,一开始只有 3 个球:

图片

可能的写法是这样,我们给 3 个球都添加同一个旋转动画,然后分别控制他们的 animation-delay

<div class="g-container">
<div class="g-item"></div>
<div class="g-item"></div>
<div class="g-item"></div>
</div>
.item:nth-child(1) {
animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
animation: rotate 3s infinite -1s linear;
}
.item:nth-child(3) {
animation: rotate 3s infinite -2s linear;
}

如果有一天,这个动画需要扩展成 5 个球的话,像是这样:

图片

我们就不得已,得去既添加 HTML,又修改 CSS。而如果借助 Calc 和 CSS 变量,这个场景就可以稍微简化一下。

假设只有 3 个球:

<div class="g-container">
<div class="g-item" style="--delay: 0"></div>
<div class="g-item" style="--delay: 1"></div>
<div class="g-item" style="--delay: 2"></div>
</div>

我们通过 HTML 的 Style 标签,传入 --delay 变量,在 CSS 中直接使用它们:

.g-item {
animation: rotate 3s infinite linear;
animation-delay: calc(var(--delay) * -1s);
}
@keyframes rotate {
to {
transform: rotate(360deg);
}
}

而当动画修改成 5 个球时,我们就不需要修改 CSS,直接修改 HTML 即可,像是这样:

<div class="g-container">
<div class="g-item" style="--delay: 0"></div>
<div class="g-item" style="--delay: 0.6"></div>
<div class="g-item" style="--delay: 1.2"></div>
<div class="g-item" style="--delay: 1.8"></div>
<div class="g-item" style="--delay: 2.4"></div>
</div>

核心的 CSS 还是这一句,不需要做任何修改:

{
animation-delay: calc(var(--delay) * -1s);
}

完整的 DEMO,你可以戳这里:CodePen Demo — Calc & CSS Variable Demo[13]

calc 搭配自定义变量时候的默认值

还是上述的 Loading 动画效果,如果我的 HTML 标签中,有一个标签忘记填充 --delay 的值了,那会发生什么?

像是这样:

<div class="g-container">
<div class="g-item" style="--delay: 0"></div>
<div class="g-item" style="--delay: 0.6"></div>
<div class="g-item"></div>
<div class="g-item" style="--delay: 1.8"></div>
<div class="g-item" style="--delay: 2.4"></div>
</div>
{
animation-delay: calc(var(--delay) * -1s);
}

由于 HTML 标签没有传入 --delay 的值,并且在 CSS 中向上查找也没找到对应的值,此时,animation-delay: calc(var(--delay) * -1s) 这一句其实是无效的,相当于 animation-delay: 0,效果也就是少了个球的效果:

图片

所以,基于这种情况,可以利用 CSS 自定义变量 var() 的 fallback 机制:

{
// (--delay, 1) 中的 1 是个容错机制
animation-delay: calc(var(--delay, 1) * -1s);
}

此时,如果没有读取到任何 --delay 值,就会使用默认的 1 与 -1s 进行运算。

Calc 字符串拼接

很多人在使用 CSS 的时候,会尝试字符串的拼接,像是这样:

<div style="--url: 'bsBD1I.png'"></div>
:root {
--urlA: 'url(https://s1.ax1x.com/2022/03/07/';
--urlB: ')';
}
div {
width: 400px;
height: 400px;
background-image: calc(var(--urlA) + var(--url) + var(--urlB));
}

这里想利用 calc(var(--urlA) + var(--url) + var(--urlB)) 拼出完整的在 background-image 中可使用的 URL url(https://s1.ax1x.com/2022/03/07/bsBD1I.png)

然而,这是不被允许的(无法实现的)。calc 的没有字符串拼接的能力

唯一可能完成字符串拼接的是在元素的伪元素的 content 属性中。但是也不是利用 calc。

来看这样一个例子,这是错误的:

:root {
--stringA: '123';
--stringB: '456';
--stringC: '789';
}

div::before {
content: calc(var(--stringA) + var(--stringB) + var(--stringC));
}

此时,不需要 calc,直接使用自定义变量相加即可。

因此,正确的写法:

:root {
--stringA: '123';
--stringB: '456';
--stringC: '789';
}
div::before {
content: var(--stringA) + var(--stringB) + var(--stringC);
}

此时,内容可以正常展示:

再强调一下,calc 的没有字符串拼接的能力,如下的使用方式都是无法被识别的错误语法:

.el::before {
// 不支持字符串拼接
content: calc("My " + "counter");
}
.el::before {
// 更不支持字符串乘法
content: calc("String Repeat 3 times" * 3);
}

最后

本文是现代 CSS 解决方案系列文章的第二篇,首发公众号,希望通过一些更易理解的语言、更直观的 DEMO,讲述在如今如何更好的使用 CSS 去提升我们网站的体验,去提高我们的技巧。

好了,本文到此结束,希望对你有所帮助:)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

  1. Transform functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#transform_functions
  2. Math functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#math_functions
  3. Filter functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#filter_functions
  4. Color functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#color_functions
  5. Image functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#image_functions
  6. Counter functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#counter_functions
  7. Font functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#font_functions
  8. Shape functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#shape_functions
  9. Reference functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#reference_functions
  10. CSS grid functions:
    https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#css_grid_functions
  11. CSS Values and Units Module Level 4:
    https://drafts.csswg.org/css-values/#math
  12. CSS: https://developer.mozilla.org/zh-CN/docs/Web/CSS
  13. CodePen Demo — Calc & CSS Variable Demo:
    https://codepen.io/Chokcoco/pen/OJzarJL