css-secrets

32. 通过阴影来弱化背景

难题

很多时候,我们需要通过一层半透明的遮罩层来把后面的一切整体调暗,以便凸显某个特定的 UI 元素,引导用户关注。这个效果最常见的实现方式就是增加一个额外的 HTML 元素用于遮挡背景(.overlay 类):

<style>
  body {
    /* 需要加上这个属性,不然背景还是可滚动 */
    overflow: hidden;
  }
  
  /* 构造页面 */
  .container {
    height: 100000px;
  }

  .overlay { /* 用于遮挡背景 */
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0, 0, 0, .5);
  }

  .lightbox { /* 需要吸引用户注意的元素 */
    position: absolute;
    z-index: 1;

    /* 其余样式 */
    top: 50%;
    left: 50%;
    color: white;
  }
</style>

<div class="container">页面</div>
<div class="lightbox">这里可以弹出一个对话框</div>
<div class="overlay"></div>

这里我们直接显示遮罩层了,通常情况下是需要 js 交互触发才显示的

我们可以看下 bootstrap 的 modal 实现,思路类似,当 modal 出现时,body 元素会加上 modal-open 这个类,其实就是添加 overflow: hidden 样式,使得背景不可滚动(这不是本文重点,只是顺带提一下)

在代码中,.lightbox 需要指定一个更高的 z-index,以便绘制在遮罩层的上层(当然可以有其他办法,比如将 .lightbox 当作 .overlay 的子元素,或者将 .lightbox 当作 .overlay 的后一个兄弟元素,只是设置了 z-index 比较保险,不需要再考虑那些情况)

这个方法需要增加一个额外的 HTML 元素(.overlay,这里不考虑 .lightbox)这意味着该效果无法由 CSS 单独实现,不过,我们有其他办法可以摆脱这个麻烦

伪元素方案

我们可以利用伪元素来消除额外的 HTML 元素:

<body class="dimmend">
<style>
  body {
    /* 需要加上这个属性,不然背景还是可滚动 */
    overflow: hidden;
  }

  body.dimmend:before { /* 用于遮挡背景 */
    /* 注意 content 属性不能忘 */
    content: '';

    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0, 0, 0, .5);
  }

  /* 构造页面 */
  .container {
    height: 100000px;
  }

  .lightbox { /* 需要吸引用户注意的元素 */
    position: absolute;
    z-index: 1;

    /* 其余样式 */
    top: 50%;
    left: 50%;
    color: white;
  }
</style>

<div class="container">页面</div>
<div class="lightbox">这里可以弹出一个对话框</div>
</body>

这样实现有几个问题:

后两个问题是无法解决的,针对第一个问题,我们可以把遮罩层交给这个元素自己的 ::before 伪元素实现:

<style>
  body {
    /* 需要加上这个属性,不然背景还是可滚动 */
    overflow: hidden;
  }

  /* 构造页面 */
  .container {
    height: 100000px;
  }

  .lightbox { /* 需要吸引用户注意的元素 */
    position: absolute;
    z-index: 1;

    /* 其余样式 */
    top: 50%;
    left: 50%;
    color: white;
  }

  .lightbox::before {
    content: '';
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(0, 0, 0, .5);
    z-index: -1;
  }
</style>

<div class="container">页面</div>
<div class="lightbox">这里可以弹出一个对话框</div>

给伪元素设置 z-index: -1,就可以让它出现在元素的背后,但无法对遮罩层的 Z 轴进行细粒度的控制。它可能出现在这个元素之后(我们期望),也可能会出现在这个元素的父元素或祖先元素之后

box-shadow 方案

对于简单的应用场景来说,我们可以利用 box-shadow

<style>
  body {
    /* 需要加上这个属性,不然背景还是可滚动 */
    overflow: hidden;
  }

  /* 构造页面 */
  .container {
    height: 100000px;
  }

  .lightbox { /* 需要吸引用户注意的元素 */
    position: absolute;
    z-index: 1;

    /* 其余样式 */
    top: 50%;
    left: 50%;
    color: white;
    background: red;

    /* 模拟遮罩 */
    box-shadow: 0 0 0 50vmax rgba(0,0,0,.8);
  }
</style>

<div class="container"><a href="https://github.com/hanzichi">页面</a></div>
<div class="lightbox">这里可以模拟弹出一个对话框</div>

该方法有个严重的问题,就是无法阻止用户和页面的其他部分交互

backdrop 方案

<style>
/**
 * Native modal dialog (limited support)
 */

dialog::backdrop {
  background: rgba(0,0,0,.8)
}
</style>

<button onclick="document.querySelector('#modal').showModal()">Click me</button>
<dialog id="modal">
O HAI!
<button onclick="this.parentNode.close()">Close</button>
</dialog>

注意下浏览器兼容性