24

默认文件1588564373403.png

来源:https://ishadeed.com,作者:Ahmad Shadeed
翻译:公众号《前端外文精选》

如果两个或多个元素很接近,那么用户就会认为它们以某种方式属于彼此。当对多个设计元素进行分组时,用户可以根据它们之间的空间大小来决定它们之间的关系。没有间距,用户将很难浏览页面并知道哪些内容相关而哪些内容无关。

在本文中,我将介绍有关CSS中的间距,实现此间距的不同方法以及何时使用 padding 或 margin 所需的所有知识。

间距类型

CSS中的间距有两种类型,一种在元素外部,另一种在元素内部。对于本文,我将其称为outerinner。假设我们有一个元素,它内部的间距是inner,外部的间距是outer

在CSS中,间距可以如下:

.element {
  padding: 1rem;
  margin-bottom: 1rem;
}

我使用 padding 来填充内部间距,使用 margin 来填充外部间距。很简单,不是吗?但是,当处理具有许多细节和子元素的组件时,这会变得越来越复杂。

margin 外部间距

它用于增加元素之间的间距。例如,在上一个示例中,我添加了 margin-bottom:1rem 在两个堆叠的元素之间添加垂直间距。

由于可以沿四个不同的方向(top、right、 bottom、left)添加margin,因此在深入研究示例和用例之前,一定要阐明一些基本概念,这一点很重要。

margin 折叠

简而言之,当两个垂直元素具有margin,并且其中一个元素的margin大于另一个元素时,发生边距折叠。在这种情况下,将使用更大的margin,而另一个将被忽略。

在上面的模型中,一个元素有 margin-bottom,另一个元素有 margin-top,边距较大的元素获胜。

为避免此类问题,建议按照本文使用单向边距。此外,CSS Tricks还在页边距底部和页边距顶部之间进行了投票。61%的开发者更喜欢 margin-bottom 而不是 margin-top

请在下面查看如何解决此问题:

.element:not(:last-child) {
  margin-bottom: 1rem;
}

使用 :not CSS选择器,您可以轻松地删除最后一个子元素的边距,以避免不必要的间距。

另一个与边距折叠相关的例子是子节点和父节点。让我们假设如下:

<div class="parent">
  <div class="child">I'm the child element</div>
</div>
.parent {
  margin: 50px auto 0 auto;
  width: 400px;
  height: 120px;
}

.child {
  margin: 50px 0;
}

请注意,子元素固定在其父元素的顶部。那是因为它的边距折叠了。根据W3C,以下是针对该问题的一些解决方案:

  • 在父元素上添加 border
  • 将子元素显示更改为 inline-block

一个更直接的解决方案是将 padding-top 添加到父元素。

负margin

它可以与四个方向一起使用以留出余量,在某些用例中非常有用。让我们假设以下内容:

父节点具有 padding:1rem,这导致子节点从顶部、左侧和右侧偏移。但是,子元素应该紧贴其父元素的边缘。负margin可以助你一臂之力。

.parent {
  padding: 1rem
}

.child {
  margin-left: -1rem;
  margin-right: -1rem;
  margin-top: -1rem;
}

如果您想更多地挖负margin,建议阅读这篇文章。

padding 内部间距

如前所述,padding在元素内部增加了一个内间距。它的目标可以根据使用的情况而变化。

例如,它可以用于增加链接之间的间距,这将导致链接的可点击区域更大。

必须提出的是,垂直方向的padding对于那些具有 display:inline 的元素不适用,比如 <span><a>。如果添加了内边距,它不会影响元素,内边距将覆盖其他内联元素。

这只是一个友好的提醒,应该更改内联元素的 display 属性。

.element span {
  display: inline-block;
  padding-top: 1rem;
  padding-bottom: 1rem;
}

CSS Grid 间隙

在CSS网格中,可以使用 grid-gap 属性轻松在列和行之间添加间距。这是行和列间距的简写。

.element {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 16px; /* 为行和列都增加了16px的间隙。 */
}

gap属性可以使用如下:

.element {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-row-gap: 24px;
  grid-column-gap: 16px;
}

CSS Flexbox 间隙

gap 是一个提议的属性,将用于CSS Grid和flexbox,撰写本文时,它仅在Firefox中受支持。

.element {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

CSS 定位

它可能不是直接的元素间距方式,但在一些设计案例中却起到了一定的作用。例如,一个绝对定位的元素需要从其父元素的左边缘和上边缘定位 16px

考虑以下示例,带有图标的卡片,其图标应与其父对象的左上边缘隔开。在这种情况下,将使用以下CSS:

.category {
  position: absolute;
  left: 16px;
  top: 16px;
}

用例和实际示例

在这一节中,你将回顾一下在日常工作中,你在处理CSS项目时,会遇到的不同用例。

header 组件

在这种情况下,标题具有logo,导航和用户个人资料。你能猜出CSS中的间距应该如何设置吗?好吧,让我为你添加一个骨架模型。

<header class="c-header">
  <h1 class="c-logo"><a href="#">Logo</a></h1>
  <div class="c-header__nav">
    <nav class="c-nav">
      <ul>
        <li><a href="#">...</a></li>
      </ul>
    </nav>
    <a href="#" class="c-user">
      <span>Ahmad</span>
      <img class="c-avatar" src="shadeed.jpg" alt="">
    </a>
  </div>
</header>

Header的左侧和右侧都有padding,这样做的目的是防止内容物紧贴在边缘上。

.c-header {
  padding-left: 16px;
  padding-right: 16px;
}

对于导航,每个链接在垂直和水平侧均应具有足够的填充,因此其可单击区域可以很大,这将增强可访问性。

.c-nav a {
  display: block;
  padding: 16px 8px;
}

对于每个项目之间的间距,您可以使用 margin 或将 <li>display 更改为 inline-block。内联块元素在它的兄弟元素之间添加了一点空间,因为它将元素视为字符。

.c-nav li {
  /* 这将创建你在骨架中看到的间距 */
  display: inline-block;
}

最后,头像(avatar)和用户名的左侧有一个空白。

.c-user img,
.c-user span {
  margin-left: 10px;
}

请注意,如果你要构建多语言网站,建议使用如下所示的CSS逻辑属性。

.c-user img,
.c-user span {
  margin-inline-start: 1rem;
}

请注意,分隔符周围的间距现在相等,原因是导航项没有特定的宽度,而是具有padding。结果,导航项目的宽度基于其内容。以下是解决方案:

  • 设置导航项目的最小宽度
  • 增加水平padding
  • 在分隔符的左侧添加一个额外的margin

最简单,更好的解决方案是第三个解决方案,即添加 margin-left

.c-user {
  margin-left: 8px;
}

网格系统中的间距:Flexbox

网格是间隔最常用的情况之一。考虑以下示例:

间距应在列和行之间。考虑以下HTML标记:

<div class="wrapper">
  <div class="grid grid--4">
    <div class="grid__item">
      <article class="card"><!-- Card content --></article>
    </div>
    <div class="grid__item">
      <article class="card"><!-- Card content --></article>
    </div>
    <!-- And so on.. -->
  </div>
</div>

通常,我更喜欢将组件封装起来,并避免给它们增加边距。由于这个原因,我有 grid__item元素,我的card组件将位于其中。

.grid--4 {
  display: flex;
  flex-wrap: wrap;
}

.grid__item {
  flex-basis: 25%;
  margin-bottom: 16px;
}

使用上述CSS,每行将有四张卡片。这是在它们之间添加空格的一种可能的解决方案:

.grid__item {
  flex-basis: calc(25% - 10px);
  margin-left: 10px;
  margin-bottom: 16px;
}

通过使用CSS calc() 函数,可以从 flex-basis 中扣除边距。如你所见,这个方案并不是那么简单。我比较喜欢的是下面这个办法。

  • 向网格项目添加 padding-left
  • 在网格父节点上增加一个负值 margin-left,其 padding-left 值相同。

几年前,我从CSS Wizardy那里学到了上述解决方案(我忘记了文章标题,如果您知道,请告诉我)。

.grid--4 {
  display: flex;
  flex-wrap: wrap;
  margin-left: -10px;
}

.grid__item {
  flex-basis: 25%;
  padding-left: 10px;
  margin-bottom: 16px;
}

我之所以用了负 margin-left,是因为第一张卡有 padding-left,而实际上不需要。所以,它将把 .wrapper 元素推到左边,取消那个不需要的空间。

另一个类似的概念是在两边都添加填充,然后边距为负。这是Facebook故事的一个示例:

.wrapper {
  margin-left: -4px;
  margin-right: -4px;
}

.story {
  padding-left: 4px;
  padding-right: 4px;
}

网格系统中的间距:CSS Grid

现在,到了激动人心的部分!使用CSS Grid,你可以很容易地使用 grid-gap 添加间距。此外,你不需要关心网格项的宽度或底部空白,CSS Grid 为你做者一切!

.grid--4 {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-gap: 1rem;
}

就是这样!难道不是那么容易和直接吗?

按需定制

我真正喜欢CSS Grid 的地方是 grid-gap 只在需要的时候才会被应用。考虑下面的模型。

没有CSS网格,就不可能拥有这种灵活性。首先,请参见以下内容:

.card:not(:last-child) {
  margin-bottom: 16px;
}

@media (min-width: 700px) {
  .card:not(:last-child) {
    margin-bottom: 0;
    margin-left: 1rem;
  }
}

不舒服吧?这个如何?

.card-wrapper {
  display: grid;
  grid-template-columns: 1fr;
  grid-gap: 1rem;
}

@media (min-width: 700px) {
  .card-wrapper {
    grid-template-columns: 1fr 1fr;
  }
}

完成了!容易得多。

处理底部margin

假设以下组件堆叠在一起,每个组件都有底边距。

注意最后一个元素有一个空白,这是不正确的,因为边距只能在元素之间。

可以使用以下解决方案之一解决此问题:

解决方案1-CSS :not 选择器

.element:not(:last-child) {
  margin-bottom: 16px;
}

解决方案2:相邻兄弟组合器

.element + .element {
  margin-top: 16px;
}

虽然解决方案1具有吸引力,但它具有以下缺点:

  • 它会导致CSS的特异性问题。在使用 :not 选择器之前不可能覆盖它。
  • 万一设计中有不止一列,它将无法正常工作。参见下图。

关于解决方案2,它没有CSS特异性问题。但是,它只能处理一个列栈。

更好的解决方案是通过向父元素添加负边距来取消不需要的间距。

.wrapper {
  margin-bottom: -16px;
}

它用一个等于底部间距的值将元素推到底部。注意不要超过边距值,因为它会与同级元素重叠。

Card组件

Oh,如果我想把所有细节的Card组件间距都写进去的话,最后可能会出现书本上的内容。我就突出一个大概的模式,看看间距应该如何应用。

你能想到此卡片在哪里使用间距吗?参见下图。

<article class="card">
  <a href="#">
    <div class="card__thumb"><img src="food.jpg" alt=""></div>
    <div class="card__content">
      <h3 class="card__title">Cinnamon Rolls</h3>
      <p class="card__author">Chef Ahmad</p>
      <div class="card__rating"><span>4.9</span></div>
      <div class="card__meta"><!-- --></div>
    </div>
  </a>
</article>
.card__content {
  padding: 10px;
}

上面的 padding 将向其中的所有子元素添加一个偏移量。然后,我将添加所有边距。

.card__title,
.card__author,
.card__rating {
  margin-bottom: 10px;
}

对于评分和 .car__meta 元素之间的分隔线,我将添加它作为边框。

.card__meta {
  padding-top: 10px;
  border-top: 1px solid #e9e9e9;
}

糟糕!由于对父元素 .card__content 进行了填充,因此边框没有粘在边缘上。

是的,你猜对了!负边距是解决办法。

.card__meta {
  padding-top: 10px;
  border-top: 1px solid #e9e9e9;
  margin: 0 -10px;
}

糟糕,再次!出了点问题。内容粘在边缘!

为了解决这个问题,内容应该从左右两边加垫(呵呵,看来加垫是个新词)。

.card__meta {
  padding: 10px 10px 0 10px;
  border-top: 1px solid #e9e9e9;
  margin: 0 -10px;
}

文章内容

我相信这是一个非常非常普遍的用例。由于文章内容来自CMS(内容管理系统),或者是由Markdown文件自动生成的,因此无法为元素添加类。

考虑下面的示例,其中包含标题,段落和图像。

<div class="wrapper">
  <h1>Spacing Elements in CSS</h1>
  <p><!-- content --></p>
  <h2>Types of Spacing</h2>
  <img src="spacing-1.png" alt="">
  <p><!-- content --></p> 
  <p><!-- content --></p> 
  <h2>Use Cases</h2>
  <p><!-- content --></p> 
  <h3>Card Component</h3> 
  <img src="use-case-card-2.png" alt="">
</div>

为了使它们看起来不错,间距应保持一致并谨慎使用。我从type-scale.com借了一些样式。

h1, h2, h3, h4, h5 {
  margin: 2.75rem 0 1.05rem;
}

h1 {
  margin-top: 0;
}

img {
  margin-bottom: 0.5rem;
}

如果一个 <p> 后面有一个标题,例如“Types of Spacing”,那么 <p>margin-bottom 将被忽略。你猜到了,那是因为页边距折叠。

Just In Case Margin

我喜欢把这个叫做 "Just in case" margin,因为这就是字面意思。考虑一下下面的模型图。

当元素靠近的时候,它们看起来并不好看。我是用flexbox搭建的。这项技术称为“对齐移位包装”,我从CSS Tricks中学到了它的名称。

.element {
  display: flex;
  flex-wrap: wrap;
}

当视口尺寸较小时,它们的确以新行结尾。见下文:

需要解决的是中间设计状态,即两件物品仍然相邻,但两件物品之间的间距为零的设计状态。在这种情况下,我倾向于向元素添加一个 margin-right,这样可以防止它们相互接触,从而加快 flex-wrap 的工作速度。

CSS 书写模式

根据MDN:

writing-mode CSS属性设置了文本行是水平还是垂直排列,以及块的前进方向。

你是否曾经考虑过将边距与具有不同 writing-mode 的元素一起使用时应如何表现?考虑以下示例。

.wrapper {
  /* 使标题和食谱在同一行 */
  display: flex;
}

.title {
  writing-mode: vertical-lr;
  margin-right: 16px;
}

标题被旋转了90度,在它和图像之间应该有一个空白区。结果表明,基于 writing-mode 的页边距工作得非常好。

我认为这些用例就足够了。让我们继续一些有趣的概念!

组件封装

大型设计系统包含许多组件。向其直接添加边距是否合乎逻辑?

考虑以下示例。

<button class="button">Save Changes</button>
<button class="button button-outline">Discard</button>

按钮之间的间距应在哪里添加?是否应将其添加到左侧或右侧按钮?也许你可以如下使用相邻同级选择器:

.button + .button {
  margin-left: 1rem;
}

这是不好的。如果只有一个按钮的情况怎么办?或者,当它垂直堆叠时在移动设备上将如何工作?很多很多的复杂性。

使用抽象组件

解决上述问题的一种方法是使用抽象的组件,其目标是托管其他组件,就像Max Stoiber所说的那样,这是将管理边距的责任移到了父元素上,让我们以这种思维方式重新思考以前的用例。

<div class="list">
  <div class="list__item">
    <button class="button">Save Changes</button>
  </div>
  <div class="list__item">
    <button class="button button-outline">Discard</button>
  </div>
</div>

注意,我添加了一个包装器,并且每个按钮现在都包装在其自己的元素中。

.list {
  display: flex;
  align-items: center;
  margin-left: -1rem; /* 取消第一个元素的左空白 */
}

.list__item {
  margin-left: 1rem;
}

就是这样!而且,将这些概念应用到任何JavaScript框架中都相当容易。例如:

<List>
  <Button>Save Changes</Button>
  <Button outline>Discard</Button>
</List>

你使用的JavaScript工具应该将每个项包装在自己的元素中。

间隔组件

是的,你没看错。我在这篇文章中讨论了避免margin的概念,并使用间隔组件来代替它们。

让我们假设一个区域需要从左到右24px的空白,并记住这些限制:

  • margin不能直接用于组件,因为它是一个已经构建的设计系统。
  • 它应该是灵活的。间距可能在X页上,但不在Y页上。

我在检查Facebook的新设计CSS时首先注意到了这一点。

那是一个 <div>,内联样式宽度:16px,它唯一的作用是在左边缘和包装器之间增加一个空白空间。

引述这本React游戏手册中的内容。

但在现实世界中,我们确实需要组件之外的间距来合成页面和场景,这就是margin渗入组件代码的地方:用于组件的间距组合。

我同意。对于大型设计系统,不断向组件添加margin是不可伸缩的。这将最终导致一个令人毛骨悚然的代码。

间隔组件的挑战

现在你了解了间隔组件的概念,让我们深入研究使用它们时遇到的一些挑战。这是我想到的一些问题:

  • 间隔组件如何在父级内部取其宽度或高度?在水平布局和垂直布局中,它将如何工作?
  • 我们是否应该根据其父项的显示类型(Flex,Grid)对它们进行样式设置

让我们一一解决上述问题。

调整间隔组件的大小

可以创建一个接受不同变化和设置的间隔。我不是JavaScript开发人员,但我认为他们将其称为Props。考虑来自styled-system.com的以下内容:

我们在一个header和一个 section之间有一个隔板。

<Header />
    <Spacer mb={4} />
<Section />

虽然这个有点不一样,一个间隔器在logo和导航之间建立一个自动间隔。

<Flex>
  <Logo />
  <Spacer m="auto" />
  <Link>Beep</Link>
  <Link>Boop</Link>
</Flex>

你可能会认为,通过添加 justify-content:space-between,使用CSS做到这一点相当容易。

如果设计上需要改一下怎么办?那么,如果是这样的话,样式就应该改了。

见下文,你看到那里的灵活性了吗?

<Flex>
  <Logo />
  <Link>Beep</Link>
  <Link>Boop</Link>
  <Spacer m="auto" />
  <Link>Boop</Link>
</Flex>

那么,如果是这样的话,就应该改变样式。你看出来有什么灵活性了吗?对于尺寸调整部分,可以根据其母体的尺寸调整间隔的尺寸。

对于上面的内容,也许你可以做一个叫 grow 的prop,可以计算成 flex-grow:1 在CSS中。

<Flex>
  <Spacer grow="1" />
</Flex>

使用伪元素

我考虑过的另一个想法是使用伪元素创建间隔符。

.element:after {
    content: "";
    display: block;
    height: 32px;
}

也许我们可以选择通过一个伪元素而不是一个单独的元素来添加间隔器?例如:

<Header spacer="below" type="pseudo" length="32">
  <Logo />
  <Link>Home</Link>
  <Link>About</Link>
  <Link>Contact</Link>
</Header>

直到今天,我还没有在项目中使用间隔组件,但是我期待可以使用它们的用例。

CSS数学函数:Min(),Max(),Clamp()

有可能有动态的边距吗?例如,根据视口宽度设置具有最小值和最大值的空白。答案是肯定的!我们可以。最近,Firefox 75支持CSS数学函数,这意味着根据CanIUse在所有主流浏览器中都支持CSS数学函数。

让我们回想一下Grid用例,以了解如何在其中使用动态间距。

.wrapper {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: min(2vmax, 32px);
}

下面是 min(2vmax,32px) 的意思:使用一个等于 2vmax 的间隙,但不能超过 32px

拥有这样的灵活性确实令人惊讶,并且为我们提供了构建更多动态和灵活布局的许多可能性。


文章首发《前端外文精选》微信公众号

subscribe2.png

继续阅读其他高赞文章



杭州程序员张张
11.8k 声望6.7k 粉丝

Web/Flutter/独立开发者/铲屎官