图文并茂总结7个工作中常用的css3案例,带你了解冷门却实用的特性!

1,987 阅读11分钟

前言

最近看了《css 揭秘》这本神书,学到了很多技巧,工作中遇到的一些问题在这本书中得到了很好的解决。这篇文章也不是把书中的内容随便抄一下就拿来给大家说,我会在此基础上向外扩展一些,请求大家理性评论!另外,有几个案例是我工作中遇到过的比较棘手的问题的解决方案,总结出来让大家有个印象,万一哪天你也要实现同样的需求呢?😁

如果对大家有帮助,请各位老爷务必留下你宝贵的 star🌟,这是我的 github/blog


我会从以下 4 个纬度介绍各个案例:

  • 需求描述:客户(产品)就是上帝;
  • 尝试方案:每个人都有第一次;
  • 改进方案:我不敢称其为最佳,万一有更牛的人有更好的实现呢?
  • 在线演示:shut up and show me the code!

DJ, drop the beat! 🎤

使用变量 currentColor 减少重复代码

需求描述

  • 移到按钮上时,改变该元素的 border-colorcolor ,还有一个具有透明度的同色背景。
  • 点击按钮之后,颜色更改为移入按钮时的同种颜色。

1

尝试方案

我相信任何一个前端开发者都能很快实现这个需求,不知道大家怎么样的,我在之前一直都是以下代码快速实现:

index.html 文件 :

<div class='good'>请给我点赞</div>

index.scss 文件 :

.good {
  padding: 3px 6px;
  color: #333;
  background: rgba(#333, 0.1);
  border: 1px solid #333;
  border-radius: 3px;
  cursor: pointer;

  &:hover {
    color: #0069ff;
    background: rgba(#0069ff, 0.1);
    border: 1px solid #0069ff;
  }

  &.good-click {
    color: #0069ff;
    background: rgba(#0069ff, 0.1);
    border: 1px solid #0069ff;
  }
}

index.js 文件 :

const goodBtn = document.querySelector('.good')

goodBtn.addEventListener('click', () => {
  if (goodBtn.classList.contains('good-click')) {
    goodBtn.classList.remove(['good-click'])
    return
  }
  goodBtn.classList.add(['good-click'])
})

是的,就是那么朴实无华,缺点也暴露无遗:

  • 相同的颜色我们使用了多次,比如 #333 和 #0069ff 。如果有一天产品说把这个那个颜色改一下,细心点的你多动动手指也没啥问题,改就改了,但是这种方式很不“程序员”。

针对这个问题我们直接使用预处理器(SASS/LESS)的变量就完事了:

index.scss 文件 :

$color: #333;
$hoverColor: #0069ff;

.good {
  padding: 3px 6px;
  color: $color;
  background: rgba($color, 0.1);
  border: 1px solid $color;
  border-radius: 3px;
  cursor: pointer;

  &:hover {
    color: $hoverColor;
    background: rgba($hoverColor, 0.1);
    border: 1px solid $hoverColor;
  }

  &.good-click {
    color: $hoverColor;
    background: rgba($hoverColor, 0.1);
    border-color: $hoverColor;
  }
}

咋一看已经是很好的实现方式了,但是也有缺点:

  • 有时候(比如我大多数时候)都不想为了某一个特殊的类下的 color 单独设置一个变量,仅仅只有它使用,我还要专门为其定义一个变量就显得代码很臃肿;
  • 在我添加了 good-click 这个类名后,我要把 color 、 border-color 、 background 全都重新设置一遍;

这个时候,css 原生变量 currentColor 即可大显身手了。

改进方案

变量 currentColor 能拿到本元素的 color 属性的值,如果没有显示设置,拿的将会是父元素的 color 属性的值,由此类推。借助这个特性,我们即可优化上述代码:

index.scss 文件 :


.good {
  position: relative;
  padding: 3px 6px;
  color: #333;
  border: 1px solid currentColor;
  border-radius: 3px;
  cursor: pointer;

  &::before {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: currentColor;
    opacity: 0.1;
    content: '';
  }

  &:hover {
    color: #0069ff;
  }

  &.good-click {
    color: #0069ff;
  }
}

现在看起来是不是好多了,我每次要更改颜色,只需要将此元素的 color 属性更改即可,不需要再重新写一堆重复的属性,当然,原生的 css 以及功能强大的 sass/less 都还是无法支持 rgba(currentColor, 0.1) 这种写法,我还去官方提了个 issue ,官方也给了很好的回复,有兴趣的同学可以看看。

所以现在我只能添加一个 ::before 来模拟背景色块,真正做到只改 color 属性,即可改全部颜色。

现在大家就可以在 React 或 Vue 中通过状态来控制改变颜色的类名添加与否并设置 color 属性,以此来完美地进行颜色的快速变换了~

多说一句,如果我们直接使用 ele.style.color = '#fff' 这种操作 dom 的形式来改变字体颜色,在未使用 currentColor 的情况下,我们是没法操作伪元素的,也就改变不了伪元素的 background 、 border-color 等其他与字体颜色一致的属性,所以这时候 currentColor 的优势就更明显了~

在线演示

使用变量 currentColor 减少重复代码 - codepen

完美的带小箭头的聊天框

需求描述

  • 主体功能聊天气泡,需有有边框 border 、背景色 background 、阴影、带边框的小三角箭头。
  • 小三角的边框颜色和阴影颜色与主体框的颜色要一致,小三角的边框有 border-radius 。
4

尝试方案

给该元素加个伪元素,背景色与聊天框背景色一致,再给该伪元素添加上、左同色边框,绝对定位调整位置,再来个 border-top-left-radius: 3px ,最后 transform: rotate(-45deg) 旋转一下,代码如下:

index.html 文件 :

<div class="chat-box">大家好,我是 vortesnail,如果大家喜欢我的文章,对大家有所帮助,麻烦给个小小的赞支持一下,谢谢😊</div>

index.scss 文件 :


.chat-box {
  position: relative;
  max-width: 200px;
  padding: 10px;
  color: #faae43;
  background: #fff9ed;
  border: 1px solid #ffc16b;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(250, 174, 67, 0.8);
}

.chat-box::before {
  position: absolute;
  top: 20px;
  left: -6px;
  width: 10px;
  height: 10px;
  background: #fff9ed;
  border-color: #ffc16b;
  border-style: solid;
  border-width: 1px 0 0 1px;
  transform: rotate(-45deg);
  content: '';
  /* box-shadow: 0 2px 6px rgba(250, 174, 67, 0.8); */
}

可以达到现在下面的效果: 5

细心的你一定发现了,这个小三角指示箭头是没有阴影的,如果我给其加上与主体元素一致的 box-shadow ,又因为这个属性不能像 border-color 一样分别给各边设置为透明,结果就会像下面这样:

6

这已经是无法满足具有相同阴影的要求了,而且大家如过想一下就知道,在我主体元素不设 padding 或设的很小的情况下,小三角的背景色会将我们的文字挡住,这种方案直接宣布失败!

改进方案

针对以上的问题,我们进行一步步改造。

首先,我们考虑到主体元素不设置 padding 的情况,为了防止内容被我们的小三角背景色覆盖,我们可通过加一个伪元素 ::before ,利用 border 来画成一个三角形,代码如下:

.chat-box {
  // 其他样式
  
  &::before {
    position: absolute;
    top: 20px;
    left: -8px; // 注意,这里做了略微调整
    width: 0;width
    height: 0;
    border-color: transparent #fff9ed transparent transparent;
    border-style: solid;
    border-width: 8px 8px 8px 0;
    content: '';
  }
}

现在是这个样子: 7

注意,这里的小三角已经是没有右边部分的了,解决了我们不设置 padding 时导致内容被遮挡的问题。但是这样就没有办法实现边框,毕竟你已经是使用边框做出来的三角形了。

那我们就再使用一个伪元素呗, ::after 安排上了。接下来为大家提供一个思路:采用尝试方案中的方式再画一个正方形做旋转,但是不为其设置背景色,只设置其 border ,调整下位置即可。

.chat-box {
  // 其他样式
  
  &::before {}
  
  &::after {
    position: absolute;
    top: 22px;
    left: -7px;
    width: 10px;
    height: 10px;
    /* border-color: inherit transparent transparent inherit; */
    border-color: transparent;
    border-style: solid;
    border-width: 1px;
    border-top-color: inherit;
    border-left-color: inherit;
    border-top-left-radius: 3px;
    transform: rotate(-45deg);
    content: '';
  }
}

可以看到,代码中我设置上和左的 border-color 为 inherit ,表示继承父级元素的 border-color ,因我注释那部分的写法不被识别,所以我们新增了几行代码实现,利用 inherit 可以在颜色更改时少写颜色值的重复代码,与 currentColor 想要达到的目的是一致的。

现在,越来越接近我们的目标: 8

这里小三角还是没有阴影,因为 box-shadow 并不会作用于伪元素,解决方案就是使用 filter 属性, drop-shadow 接受的参数和 box-shadow 基本一致,我们替代它即可:

// box-shadow: 0 2px 6px rgba(250, 174, 67, 0.8);box-shadow
filter: drop-shadow(0 2px 6px rgba(250, 174, 67, 0.8));

现在已经完美实现~

在线演示

实现一个完美的带小箭头的聊天框 - codepen

利用 grid 实现完美的水平铺满、间隔一致的自适应布局

需求描述

  • 在一个容器元素下,有不确定数量的子元素,要求他们水平铺满,并且在当前行的最左边和最右边的子元素距离父元素左边缘和右边缘都是无缝贴合的。
  • 每个子元素之间的间隔必须一致。
  • 当浏览器窗口大小变动自适应。

2

尝试方案

这个问题从我入职第一份工作之后困扰了我接近半年,我基本还是惯性思维,一眼看过去就是 flex弹性盒子 一把梭,于是我有了以下这种方案:

index.html 文件 :

<body>
  <div class="father">
    <div class="child">Child1</div>
    <div class="child">Child2</div>
    <div class="child">Child3</div>
    <div class="child">Child4</div>
    <div class="child">Child5</div>
    <div class="child">Child6</div>
    <div class="child">Child7</div>
    <div class="child">Child8</div>
    <div class="child">Child9</div>
    <div class="child">Child10</div>
  </div>
</body>

index.scss 文件 :


.father {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: flex-start;
  width: 100%;
  padding: 10px 0 10px 20px;

  .child {
    margin-right: 14px;
    margin-bottom: 14px;
    // 其他卡片样式
  }
}

可以看到,我会为每个子元素都设置 margin-top 以及 margin-right 来固定他们之间的间距,但是因为每一行最右边的子元素也有 margin-right ,为了补偿这个,我就将父元素的 padding-right 去掉了,这样做的坏处太多了,需要自己去计算,做补偿,而且右边有时候容纳不下一个完整的子元素,就会导致换行而留下一大片白。。 3

为了能用弹性盒子做到想要的效果,我已经把阮一峰老师的Flex 布局教程:语法篇看烂了。。根本没法实现最佳最想要的效果,以上只是我多次尝试之后唯一能接受的方案,我就这么个方案用了好多次。

直到有一天,我又遇到了这种布局需求,我辛辛苦苦用 js 去硬算他们之间的间距,算是实现了想要的效果,但是真的非常繁琐,我就受不了了。这个时候我又偶遇了阮一峰老师的CSS Grid 网格布局教程,谢天谢地,采用 Grid 可完美实现以上需求!

改进方案

Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大

首先我们需要给容器指定为 grid 网格布局,就像 flex 一样:

.father {
  display: grid;
}

接着要为其划分列数, grid-template-columns 属性可定义每一列的列宽,假如代码如下,我们将容器划分成 3 列,每列宽度为容器的 100px :

.father {
  grid-template-columns: 100px 100px 100px;
}

但是这个时候我们看到的效果会是下面这样:

3 0

子元素并没有把父元素占满,这显然不是我们想要的效果,幸亏有 repeat() 函数帮助我们简化重复值, 它接受两个参数,第一个参数是重复的次数,第二个参数是所要重复的值 。上面的代码完全可用以下代码代替:

.father {
  grid-template-columns: repeat(3, 100px);
}

当然,这只是第一步,我们还需要借助 auto-fill 关键字,在我们需要容器能尽可能容纳子元素时,就需要用到它,表示自动填充,我的理解是 repeat() 接受了这个 auto-fill 的参数时,会去自动计算容纳的数量,就好像你事先算出来这个容器能容纳多少子元素,然后把这个“多少”传给该函数一样。这时候代码如下:

.father {
  display: grid;
  grid-template-columns: repeat(auto-fill, 100px);
}

现在图形如下,已经越来越接近我们的目标了:

3 1

但是很显然,右边有一个空隙, justify-content 属性拯救我们,它整个内容区域在容器里面的水平位置,当我设置其为 space-between 时,意味着子元素之间的间隔相等,而子元素与容器边框之间没有间隔

不过子元素之间还是没有间隔,简单设置一下属性 gap 即可,它是 column-gap 和 row-gap 的合并简写,分别表示列与列行与行之间的间距,现在代码如下:

.father {
  display: grid;
  grid-template-columns: repeat(auto-fill, 100px);
  justify-content: space-between;
  gap: 14px 4px;
}

由此简单的几行代码就已经完美实现了我们想要的效果:

3 2

不过 grid 网格布局的兼容性不是很好,点此查看支持的浏览器列表~

在线演示

利用 grid 实现完美的水平铺满、间隔一致的自适应布局 - codepen

间距可调整的虚线框

需求描述

  • 实现一个按钮,该按钮边框为虚线,且虚线的每个笔触之间的空隙和长度都是可调的。
  • 能支持有圆角,即可设置 border-radius 。

9

尝试方案

其实我一直很迷惑为什么 css3 不提供一些能调整虚线框的必要属性,默认的 dash-border 经常会和 ui 所需要的虚线框要求会不一致,既然官方不支持,我们只能自己寻找一些解决方案。

这个解决方案看似很多,其实每一种解决方案都会有一定的局限性,选择最合适的就是最好的,具体我列出了以下几条:

  • 利用 border-image 和自定义的图片来进行虚线框的生成,该方案在 stackoverflow 查到的,我个人也尝试了下,但是修改起来特别麻烦,我第一感觉就是不会采用这种方案,有兴趣的可以看下:Brew your own border with border-image
  • 在所需要虚线框的元素的宽高是固定的情况下,可以让 UI 画好这个虚线框就行,弊端很明显,长度若一旦发生变化,虚线比例和原来就不会一致,特别丑。
  • 利用 4 个绝对定位的“伪元素”来模拟,代码示例如下:

index.html 文件 :

<body>
  <div id="box">
    <div class="border-horizontal top"></div>
    <div class="border-vertical right"></div>
    <div class="border-horizontal  bottom"></div>
    <div class="border-vertical left"></div>
    I am vortesnail, now i try to make a custom dashed border!
  </div>
</body>

index.scss 文件 :

$border-color: #ccc;
$border-dashed-unit-width: 8px;
$border-dashed-unit-height: 1px;
$stroke-rate: 50%;

body {
  #box {
    width: 400px;
    background: #fff;
    padding: 10px;
    box-sizing: border-box;
    position: relative;

    .border-horizontal {
      position: absolute;
      width: 100%;
      height: $border-dashed-unit-height;
      left: 0;
      background-image: linear-gradient(
        to right,
        $border-color 0%,
        $border-color $stroke-rate,
        transparent $stroke-rate
      );
      background-size: $border-dashed-unit-width $border-dashed-unit-height;
      background-repeat: repeat-x;
    }

    .border-vertical {
      position: absolute;
      width: $border-dashed-unit-height;
      height: 100%;
      top: 0;
      background-image: linear-gradient(
        to bottom,
        $border-color $stroke-rate,
        $border-color $stroke-rate,
        transparent $stroke-rate
      );
      background-size: $border-dashed-unit-height $border-dashed-unit-width;
      background-repeat: repeat-y;
    }

    .top {
      top: 0;
    }

    .right {
      right: 0;
    }

    .bottom {
      bottom: 0;
    }

    .left {
      left: 0;
    }
  }
}

其实其思想很简单,就是 4 个矩形,每个矩形加上渐变背景,并 repeat 即可模拟虚线效果,其间距、比例可根据我们设定的变量去调整。

但是它的弊端非常大,就是无法调整 border-radius ,即没有圆角!这里向大家展示只是为了抛砖引玉,万一你有更好的想法,或者你不需要圆角,那就可以用这个方案。效果如下:

10

改进方案

其实我们借助 svg 就能比较不错的实现自定义虚线框,如果不想自己写 svg 的朋友可以直接在这个网站进行调整和生成:Customize your CSS Border ,但如果你稍微了解一些 svg 的用法,也完全可以自己实现,如果你想了解一下,可阅读这篇文章:SVG入门—如何手写SVG

代码如下: index.html 文件

<body>
  <div id="box">I am vortesnail, now i try to draw a dashed border box.</div>
</body>

index.scss 文件 :

body {
  #box {
    width: 400px;
    border-radius: 4px;
    padding: 10px;
    background-image: url('data:image/svg+xml,\
    <svg xmlns="http://www.w3.org/2000/svg">\
      <rect width="100%" height="100%" rx="4" ry="4" style="stroke: black; stroke-width: 2px; fill: none; stroke-dasharray: 8px 5px; stroke-dashoffset: 10px;"/>\
    </svg>');
  }
}

可通过 stroke-width 调整虚线框宽度, stroke-dasharray 调整比例及长度、间距, stroke-dashoffset 调整偏移值。

tips:svg 方案在一些比较老的安卓机上会不兼容,即使在新的机型上,也会出现一些表现差异,虽然在 web 端支持 svg 的浏览器上表现是正常的,但若考虑到移动端用户群体时,使用请慎重。

在线演示

实现一个间距可调的虚线框 - codepen

自定义复选框

需求描述

  • 能够完全自定义复选框样式,就像定义一个普通的 div 元素一样。
  • disabled 状态复选框样式也可以自定义。 11

尝试方案

很显然,当我们使用默认的 input.checkbox 方案时,是没有办法改变其样式的,而且在不同浏览器之间,其表现也不一致,代码如下:

index.html 文件 :

<body>
  <div class="checkbox-container">
    <input type="checkbox" id="apple">  
    <label for="apple">苹果</label>
  </div>
  <div class="checkbox-container">
    <input type="checkbox" id="banana" disabled>  
    <label for="banana">香蕉</label>
  </div>
  <div class="checkbox-container">
    <input type="checkbox" id="watermelon">  
    <label for="watermelon">西瓜</label>
  </div>
</body>

index.scss 文件 :

.checkbox-container {
  display: flex;
  align-items: center;
  
  input[type='checkbox'] {
    & + label {
      color: #333;
    }
  }

  input[type='checkbox']:disabled {
    &+ label {
      color: #c6c6c6;
    }
  }
}

在 chrome 下表现为: 12

在 firefox 下表现为: 13

在 safari 下与在 firefox 下一致。

如果任由这种情况的发生,你们 ui 可能会找产品经理和你打一架~🐶

改进方案

我们可以在不改变上面尝试方案中的 html 结构,只需 css 即可做到!给每一个 label 标签添加一个伪元素 ::before 作为复选框!

首先,我们给这个伪元素添加必要样式,使其符合 ui 的设计:

input[type="checkbox"] {
  & + label {
    display: flex;
    align-items: center;
  }

  & + label::before {
    box-sizing: border-box;
    content: "\a0"; /* 不换行空格 */
    width: 13px;
    height: 13px;
    margin-right: 4px;
    border-radius: 2px;
    border: 1px solid #333;
  }
}

现在的拙劣效果如下: 14

我们发现,默认的复选框还是存在的,我们怎么做到将其隐藏而不破坏其可访问性呢(即不能使用 display: none )?

input[type="checkbox"] {
  position: absolute;
  clip: rect(0, 0, 0, 0);
  
  & + label {...}

  & + label::before {...}
}

以上隐藏的方案引用至 css揭秘151页

现在点击我们自定义的复选框是没有任何效果的,接下来借助 css 的相邻兄弟选择器对 checked 状态、disabled 状态分别设置样式即可:

input[type="checkbox"]:checked {
  & + label::before {
    background-color: #1890ff;
    background-image: url("https://s1.ax1x.com/2020/10/11/0cUbi4.png");
    background-repeat: no-repeat;
    background-size: 100% 100%;
    border: none;
  }
}

input[type="checkbox"]:disabled {
  & + label {
    color: #868686;
    cursor: not-allowed;
  }

  & + label::before {
    border-color: #868686;
  }
}

可以看到,经过此番处理后,我们可以完全自主地去设计复选框样式,比如我在上面选中时,呈现了一张对勾的图片: background-image: url("https://s1.ax1x.com/2020/10/11/0cUbi4.png"); ,这极大地方便了我们对其呈现形式的掌控。

除此之外,你还可以设置 input[type="checkbox"]:focus 时的样式哦,赶快试试吧!

在线演示

自定义复选框 - codepen


交互式图片对比效果

需求描述

  • before 和 after 图片对比效果,可拖过拖拽中间的竖形条状进行两张图片的宽度变化。 15 0

尝试方案

css3 中引入了 resize 属性,该属性可以不通过 js 就可以改变设置该属性的元素的宽度 width ,大家一定使用过 textarea 标签把?那个右下角的可拖拽更改长宽的东西就是该属性的功劳。实际上,所有标签都可以设置该属性!

于是,简单的几段代码就可以达到交互式图片对比效果,虽然和我们想要实现的效果有点差异,但如果要求不高的话,就采用它吧:

index.html 文件 :

<div class="image-slider">
  <div class="before-container">
    <img src="https://img3.doubanio.com/view/photo/l/public/p2622600072.webp" alt="before">
  </div>
  <img src="https://img9.doubanio.com/view/photo/l/public/p2380745925.webp" alt="after">
</div>

index.scss 文件 :

.image-slider {
  position: relative;
  
  img {
    display: block;
    width: 720px;
    user-select: none;
  }
  
  .before-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 50%;
    max-width: 100%; /* 防止容器宽度拉长至比图片还宽 */
    overflow: hidden; /* 必须不可见 */
    resize: horizontal; /* 赋予水平宽度可拉伸功能 */
    
    &::before {
      position: absolute;
      right: 0;
      bottom: 0;
      width: 12px;
      height: 12px;
      background: linear-gradient(-45deg, #000 50%, transparent 0);
      background-clip: content-box;
      cursor: ew-resize;
      content: '';
    }
  }
}

我们利用一个伪元素 ::before 来对右下角的拉伸图标进行覆盖,以便于自定义样式,现在效果如下:

15

这个方案弊端就是右下角的可拖拽图标无法更改位置和大小,即使我们利用伪元素去覆盖,但是和我们需求中所需要的效果也相差甚远,于是我们不得不借助 js 了!

改进方案

在上述方案中增加一个 span 标签用于画我们的拖拽竖条,紧接着按照上述方案先将两张照片的位置和大小调好:

index.html 文件 :

<body>
  <div class="image-slider">
    <div class="before-container">
      <img src="https://img3.doubanio.com/view/photo/l/public/p2622600072.webp" alt="before">
    </div>
    <img src="https://img9.doubanio.com/view/photo/l/public/p2380745925.webp" alt="after">
    <span class="handler"></span>
  </div>
</body>

index.scss 文件 :

body {
  .image-slider {
    position: relative;
    
    img {
      display: block;
      width: 520px;
      user-select: none;
      pointer-events: none;
    }
    
    .before-container {
      position: absolute;
      left: 0;
      top: 0;
      width: 50%;
      overflow: hidden;
    }
  }
}

现在效果如下:

16

初步的效果出来了,接下来增加可拖拽改变水平宽度的功能。首先需要先在两张图片交际出添加一个竖形的条状,用于拖拽位置,更改 class='handler' 样式:

.handler {
  position: absolute;
  top: 0;
  left: 50%;
  display: block;
  width: 4px;
  height: 100%;
  background: rgba(0, 0, 0, 0.4);
  transform: translateX(-50%);
  cursor: ew-resize;
}

注意中间的透明竖形条状即是我们可拖拽的位置:

17

接下来写我们的 js 脚本,首先通过原生 js 方法找到三个 dom 节点:

index.js 文件 :

const imageSlider = document.querySelector(".image-slider");
const beforeContainer = document.querySelector(".before-container");
const handler = document.querySelector(".handler");

然后我们还需要获得 image-slider 这个最外层元素相对页面左边的距离,我们定义为变量 leftX ,并在鼠标于 handler 元素上按下时计算该值:

let leftX;

handler.onmousedown = (e) => {
  leftX = e.pageX - handler.offsetLeft;
};

用一张图来解释说明下:

18

然后在给 window 对象添加一个 mousemove 的监听事件,该回调用于改变 handler 位置和 before-image 的宽度:

handler.onmousedown = (e) => {
  leftX = e.pageX - handler.offsetLeft;
  window.addEventListener('mousemove', moveHandler)
};

const moveHandler = e => {
  const beforeWidth = e.pageX - leftX;
  const imageSliderWidth = imageSlider.offsetWidth;

  if (beforeWidth >= 0 && beforeWidth <= imageSliderWidth) {
    handler.style.left = beforeWidth + 'px';
    beforeContainer.style.width = beforeWidth + 'px';
  }
}

目前为止,我们拖拽 handler 已经能实现所需的效果,但是无法停止,我们需要添加一个鼠标按键抬起的监听事件,需要注意的是不能在 handler 上添加,比如: handler.onmouseup ,必须在 window 对象上添加,具体为什么,大家可以动手试试就知道了:

window.onmouseup = (e) => {
  window.removeEventListener('mousemove', moveHandler)
};

到此为止,就已经完美实现了该效果,大家还可以在 handler 上做更多工作,使其用户体验达到更好~

在线演示

交互式图片对比效果 - codepen


透明度渐变层代替滚动条提示

需求描述

  • 一个可滚动列表,在未滚动到顶部之前,需有有一个渐变层代替滚动条作为剩余内容提示,底部同理;
  • 滚到到顶部时,渐变层消失,底部同理;
  • 渐变层未矩形渐变。

19

尝试方案

首先很明确的是,先将内容滚动条搞出来:

index.html 文件 :

<body>
  <div class="content-wrapper">
    <header>目录</header>
    <div class="list-wrapper">
      <ul>
        <li>如何长高</li>
        ...省略
      </ul>
    </div>
  </div>
</body>

index.scss 文件 :

body {
  .content-wrapper {
    width: 248px;
    padding: 20px 0;
    background: #fafafa;

    header {
      padding: 0 20px 0 24px;
      font-weight: 500;
      font-size: 16px;
    }
    
    .list-wrapper {
      ul {
        height: 400px;
        padding: 0 20px 0 24px;
        overflow-y: auto;
        color: #595959;

        li {
          padding: 6px 0;
          font-size: 14px;
          list-style: none;
        }
      }
    }
  }
}

现在效果如下:

20

接下来为滚动范围的顶部和底部都先加上我们所需要的渐变层,通过父容器的 ::before 和 ::after 伪元素来实现,同时动态为 list-wrapper 这个元素增加两个类名,用于控制渐变层的显隐:

.list-wrapper {
  position: relative;
  
  &::before {
    position: absolute;
    right: 0;
    left: 0;
    z-index: 1;
    height: 60px;
    content: '';
    pointer-events: none;
  }
  
  &.top-gradient::before {
    top: 0;
    background: linear-gradient(to bottom,#fafafa,hsla(0,0%,98%,.5) 84%,hsla(0,0%,98%,.13));
  }
  
  &::after {
    position: absolute;
    right: 0;
    left: 0;
    z-index: 1;
    height: 60px;
    content: '';
    pointer-events: none;
  }
  
  &.bottom-gradient::after {
    bottom: 0;    
    background: linear-gradient(to top,#fafafa,hsla(0,0%,98%,.5) 84%,hsla(0,0%,98%,.13));
  }
}

但是我们想要达到的效果是:一旦滚动条不是最顶部,顶部就要有渐变层;一旦滚动条不是最底部,底部就要有渐变层。现在完全是写死在两头,需要通过简单的 js 脚本来判断 ul 元素的滚动条的位置:

index.js 文件 :

const listWrapper = document.querySelector(".list-wrapper");
const ul = document.querySelector("ul");

const onScroll = (e) => {
  // 滚动条是否在顶部
  if (e.target.scrollTop > 0) {
    listWrapper.classList.add("top-gradient");
  } else {
    listWrapper.classList.remove("top-gradient");
  }
  // 滚动条是否在底部
  if (e.target.scrollHeight - e.target.scrollTop !== ul.offsetHeight) {
    listWrapper.classList.add("bottom-gradient");
  } else {
    listWrapper.classList.remove("bottom-gradient");
  }
};

ul.addEventListener("scroll", onScroll);

最后再将原生滚动条隐藏,OK!

ul {
  ...
  scrollbar-width: none;scrollbar-width /* Firefox */
  -ms-overflow-style: none; /* IE10+ */
  
  &::-webkit-scrollbar { 
    display: none; /* Chrome */
  }
}

顺带说一句,在 codepen 中滚动条隐藏不了,本地调试时可以,我也不晓得啥问题~

改进方案

实际上上述方案是我看《css揭秘》之后想到的,在这本书中,讲到了利用两层 background 以及 background-attachment 属性来进行渐变层的实现,但是我按书中实现之后,发现效果并不完美,甚至可以说有很大缺陷!我想了好久还是觉得用 js 方便,css 看起来是无法实现我想要的效果的!

所以上述方案就是最终改进方案,《css揭秘》中的方法我实在不敢认同,不过关于 background-attachment 属性的介绍倒是给我学到了~

在线演示

透明度渐变层代替滚动条提示 - codepen


结语

虽然标题写了是 css3,但是还是难免涉及到了 js,我的目的是希望有同类需求的小伙伴能通过本篇文章得到帮助。欢迎各位理性讨论~如果有更好的方法,请大佬们务必不吝赐教!如果你已经看到此处,干脆点个赞再走吧~

如果对大家有帮助,请各位老爷务必留下你宝贵的 star🌟,这是我的 github/blog

good