“位置”在css里的细节

位置是个怎样的概念

哎,这个元素怎么跑那里去了?

回想一下,在我们觉得“样式崩了”,“页面出bug了”的时候,是不是会有相当一部分情况都可以描述成上面这句话呢?

我们在写css的时候,就会经常考虑“位置”这个事。理念就是,所有的页面元素都应该被安排在为它预定的位置上。毕竟按照计划预定的来,才能有条不紊,不容易出错。

就像一本杂志的编辑,即便文稿都已准备好,但具体哪篇放在第几页的哪里,是要认真考虑的。好的杂志应该有好的排版。

下面,本文将介绍一些会影响到“位置”,但一般不太会知道的要点。

盒模型的再认识

盒模型你一定很熟悉:

盒模型

contentpaddingbordermargin这些区域之外,请注意图中的edge,也就是分隔盒模型各区域的边。在确定页面元素的准确位置时,需要细致地参考这些边。它们按照范围从小到大分别是:

  • content edge,也叫做inner edge(注意和JavaScript的innerWidth等区别开)。它围成的区域代表内容区,一般由元素内的具体内容决定。它确定的范围叫做content box,也是css属性box-sizing的默认值content-box的范围。
  • padding edge。它确定padding box的范围。尽管w3c规范已经从box-sizing移除了这个值,但很容易类比理解。
  • border edge。它确定border box的范围。
  • margin edge,也叫做outer edge。它确定margin box的范围。box-sizing也没有这个值。

为什么需要了解这些边呢?想象一下你想要放置一个元素到你为它安排的位置上,但是,你拿的元素并不是一个点(比如物理学里的质点),而是一个盒子。一个占据一定空间的盒子,如果没有边作为参照,你能清楚地知道应该怎样去对齐吗?

背景的位置

背景是很常用的样式,但有好多地方可能不太会注意。

背景分为背景图(background-image)和背景色(background-color),渐变(css gradients)也属于背景图。其中,背景图位于背景色之上。如果使用多个背景图(multiple backgrounds),声明靠前的位于上方。

background-clip之外的其他背景属性,如background-positionbackground-size等,都只作用于背景图,对背景色没有作用。这是可以理解的,因为背景色很单纯,它没有起点和终点,总是铺满整个空间,要么有,要么没有。

现在,一个元素div.bg-element定义了这样的css:

.bg-element{
    width: 100px;
    height: 100px;
    margin: 20px;
    padding: 20px;
    border: 5px dashed #386365;
    background: #2aace9 url(pattern.png) no-repeat;
}

这是一个同时有背景图和背景色的元素,而且有内外边距及边框,其效果是:

默认情况的背景范围

可以看出,在默认情况下,背景图的起始点为padding box的左上角,而背景色没有起始点,将铺满整个border box(为了看到这一点,特意用了dashed边框)。虽然这个例子中的背景图好像限制在padding box范围内,但实际上,它和背景色的可见范围是一样的,都是border box,用background-positionbackground-repeat稍作修改即可验证:

背景可见范围

这个背景可见范围的概念,对应了css3新增属性background-clip。这个属性的默认值就是border-box。如果更改它,会是这样:

变更可见范围

无论怎么修改可见范围,这个例子中的背景图的起始点都是padding box左上角,这就是背景图起始点的概念。它对应的是css3中新增的background-origin,其默认值正是padding-box。如果修改了background-origin,那么属性background-position产生的位置偏移,包括rightbottom等关键字的情况,都会对应地改变参考的边。

外边距区域不会有背景。前文的盒模型的图里,margintransparent一起也是这个意思。

自设容器进行定位

你一定用过这样的定位搭配:position: relative;的父元素,加上position: absolute;的子元素。

这比较像建立一个xy平面坐标系,然后把元素放置在自己想要的坐标位置上。但是,x轴、y轴、原点、坐标点,它们分别在哪里呢?

请看这个例子:

<div class="pos-container">
    <div class="pos-element"></div>
</div>
.pos-container{
    position: relative;
    width: 140px;
    height: 140px;
    margin: 20px;
    padding: 20px;
    border: 5px dashed #789;
}
.pos-element{
    position: absolute;
    width: 70px;
    height: 70px;
    margin: 10px;
    padding: 20px;
    border: 5px dashed #a74;
    background: #e5c5a5;
    left: 0;
    top: 0;
}

得到的结果是:

定位组合示例

由于边框是明显的有视觉效果的部分,因此border edge的位置很容易确定。注意两个元素都有完整的内外边距和边框,而此时div.pos-element的坐标是(0, 0),图中间距为10px。经过分析可以得知,这个10px间距来自div.pos-elementmargin。所以可以得到什么结论呢?结论如下:

定位平面坐标系

首先,网页的平面坐标系和通常的数学平面直角坐标系不同,y轴的正方向是朝下的。事实上,包括Photoshop、Flash等平面设计在内的界面,都使用这样的坐标系。

这种搭配的情况下,构成坐标系xy轴的是用作容器的元素的padding edge。其中padding edge的左上角即为坐标系的原点。

绝对定位的元素设置的lefttop所形成的坐标位置点,位于该元素的margin edge的左上角。也就是,定位元素是用margin edge的左上角这个点来对齐坐标的。

此外,构成定位参考坐标系的是包含块,而不一定是直接父元素。

包含块

包含块containing block)是css规范的视觉格式化模型(visual formatting model)中的概念,它是一个逻辑的矩形框,用于css的定位及尺寸的计算,并不限制内部元素的位置,可以溢出。它和盒模型的关系可以这样描述:一个DOM元素对应一个盒模型,盒模型的某一条边(这不是固定的)将标明该DOM元素创建的包含块范围。

一般情况下,一个DOM元素的子元素(以及这些子元素的盒模型)以这个DOM元素的content edge作为包含块的边界。

特别地,在这种position: relative;的父元素及position: absolute;的子元素的搭配中,绝对定位的子元素以父元素的padding edge作为自己的包含块的边界。

更具体的包含块的边界判定可以参考W3C的说明

普通流、浮动与绝对定位的三方纠葛

在确定一个DOM元素的位置时,我们需要考虑它的定位方案positioning schemes)。一个DOM元素可以选择以下三个方案:

  • 普通流normal flow)。如果你对这个元素什么也没干,那就是它了。
  • 浮动floats)。当元素的float属性设置了leftright后。
  • 绝对定位(absolute positioning)。当元素的position属性设置了absolutefixed后。

如果一个DOM元素既设置了浮动又设置了绝对定位,那么它是绝对定位(float会被重置为none的计算值)。

你也许听说过“脱离文档流”这样的词汇,它在css里是确实存在的概念,原词是out of flow。如果一个元素的定位方案是浮动或绝对定位,又或者这个元素是根元素(root element),就称这个元素是脱离文档流(out of flow)的。

文档流的问题

同一层级的兄弟元素,同时有普通流、浮动、绝对定位这三种定位方案时,它们之间的相互关系是怎样的?

要理清这个相互关系,需要理解脱离文档流具体是什么意思。

css规范中这样描述绝对定位:

In the absolute positioning model, a box is removed from the normal flow entirely and assigned a position with respect to a containing block.

请注意这里的entirely,这是在说,绝对定位是完全脱离文档流的。为什么要强调完全呢?

因为,脱离文档流是一个比较暧昧的概念,还有不完全的。请看浮动的描述:

In the float model, a box is first laid out according to the normal flow, then taken out of the flow and shifted to the left or right as far as possible.

前文已经说过,浮动被归类为out of flow,也就是脱离文档流,但这里却提到了先基于文档流取得一次位置,然后再向左或向右移动。所以,浮动不是完全脱离文档流的

浮动的特性

之前看到过别人的这样一个提问:

位于浮动之前的块元素

对应html代码(css省略):

<div class="scheme-container">
    <div class="scheme-element-normal">normal</div>
    <div class="scheme-element-float">float</div>
</div>

按照传统的“浮动元素是脱离文档流的”的理解,为什么这个右浮动元素只是浮动到了它所在行的右边,而不是整个容器的右上角呢?

答案就是,浮动不是完全脱离文档流的。这有两方面的意思。

一方面,浮动可以影响文档流。你一定见过把一段文字里的某一个图片浮动,来制造文字环绕图片效果的用法:

文字环绕图片

这些位于文档流内的文字,仍然会为浮动元素留出空间,而并非互不相干。这其实是浮动元素影响行框(line box)的宽度的结果。

另一方面,浮动会受到文档流的影响。规范里列出的浮动元素的精确特性规则中有这样一条:

The outer top of a floating box may not be higher than the outer top of any block or floated box generated by an element earlier in the source document.

这里的outer top就是margin edge (outer edge)的top edge。意思是,浮动元素不可以高于任何在源文档之前的块元素或浮动元素。我们很熟悉浮动元素是会一个接一个地寻找空间排列的,但这一条却告诉我们,如果前面还有块元素,那么它们也会影响浮动元素的上边缘位置。

请看这个例子:

位于浮动之前的块元素及浮动元素

如果一个浮动元素之前还有其他的浮动元素,那么会考虑到它们,而可能排列到一个更靠下的位置。那么,如果之前还有块元素,而且占据了更大的位置呢?从上图可以看到,浮动元素同样会将其考虑在内。

这就是前面的提问的详细解释了。

绝对定位

绝对定位大部分时候并不像浮动这样让人困惑。显然,它被定义为“完全脱离文档流”,就是说它和文档流的关联很弱,一般只根据包含块和坐标来确定位置就可以了。

完全脱离文档流,也仍然不是说和文档流毫不相干,各自为政。它的意思是:这个元素在参与布局时,不会影响在它之后的兄弟元素。绝对定位元素,在lefttop等定位属性取值auto的时候,仍然会根据文档流取得一个可用的计算值。

三者的覆盖关系

如果有重叠,就要考虑覆盖关系了。它们的位置从下到上依次是:

  • z-index为负的定位元素。
  • 普通流(normal flow)的非行内元素。
  • 浮动元素。
  • 普通流的行内元素。
  • z-indexauto0的定位元素。
  • z-index为正值的定位元素。

结语

想要让元素稳定地待在为它预定的位置上,还是有很多功课要做的。本文只介绍了一部分有关位置的细节知识,如果你也曾对这些内容有所困惑,那么希望能有所帮助。

把鼠标、触摸屏、触控笔统一起来,Pointer Events介绍 探究Gulp的Stream