前言
之前在《CSS之浮動》中,我當時是想一起說說定位的,因為我在很多地方看到有把float和position放在一起講的,說它們的一些屬性值可以使元素脫離文件流,但是沒想到在準備內容的時候,發現浮動的內容有點多,就先把浮動的內容單獨整了一篇。本文就繼續來說說定位吧。
基本資訊
Name: | position |
---|---|
Value: | static | relative | absolute | sticky | fixed |
Initial: | static |
Applies to: | all elements except table-column-group and table-column |
Inherited: | no |
Percentages: | N/A |
Computed value: | specified keyword |
Canonical order: | per grammar |
Animation type: | discrete |
儘管在規範文件中我們能看到Position可設定的值有5個,但一般我們不太會去手動設定static,static代表按照普通文件流佈局,預設情況下position的值為static。所以我們現在主要關注後面幾個值,relative、absolute、sticky和fixed,他們都是按照某個參照物來定位,區別就在於參照物的不同。因此這四個值都會使選中元素的定位演算法不同於正常文件流的佈局計算;其中sticky是CSS3新增的內容。
relative
從文件中的描述可以看出,relative這個屬性值,表示選中的元素將根據原本自身在普通流中的位置來計算定位,是一個相對於自身的偏移。雖然這個元素的位置發生了偏移,但這只是視覺效果並不會改變其他盒子的尺寸或位置,也就是說假設給一個DIV設定了relative,它後面的盒子在計算位置的時候並不會考慮這個DIV產生的偏移。也就是不管這個DIV是不是設定了relative,都不會影響它後面盒子的位置計算。比如我們來看這個例子:
.nav {
position: relative;
/*top: -50px;*/
width: 500px;
height: 300px;
background: orange;
}
.section {
width: 500px;
height: 300px;
background: aquamarine;
}
<div class="nav"></div>
<div class="section"></div>
const rect = document.querySelector('.section').getBoundingClientRect();
console.log(rect.top);
在指令碼中log出來的值,不管是否給.nav
設定了top: -50px;
,還是不設定postion
這個屬性時,結果都是一樣的。因此可以看出,雖然設定了position: relative;
之後當前元素的定位可以改變,但這個元素原本的幾何屬性還是會影響文件流其他元素的佈局計算。
absolute
absolute屬性值會使元素被移出正常的文件流,因此這個元素對同級和祖先元素的尺寸和位置不會產生影響,這就和relative不一樣了。並且當盒子設定了absolute,它的定位和尺寸將根據它的包含塊來計算。因此盒子的位置和尺寸可以透過top
、right
、bottom
和left
屬性來指定,這些值是相對於包含塊的:
the box is positioned and sized solely in reference to its absolute positioning containing block
包含塊
那麼哪個是絕對定位元素的包含塊呢?假設有以下程式碼:
<div class="container">
<div class="title">我是標題1</div>
<div class="content">我是內容我是內容我是內容。天街小雨潤如酥,草色遙看近卻無,最是一年春好處,絕勝煙柳滿皇都。</div>
</div>
單看HTML的話,我們會覺得.title
這個元素的包含塊是.container
這個盒子建立的,但是如果設定了以下CSS,我們會看到頁面效果和預期的並不一致。
.container {
background: orange;
width: 500px;
}
.title {
position: absolute;
top: 0;
background: aquamarine;
}
如果說.title
的包含塊是.container
,那麼此時.title
這個盒子的頂部應該和.container
齊平,但我們看到的頁面效果並不是這樣的,所以也就是說.title
的包含塊並不是.container
,那麼absolute positioning containing block
是指什麼呢?這在CSS3的文件中直接給出了定義:
Values other than static make the box a positioned box, and cause it to establish an absolute positioning containing block for its descendants.
這句話的意思是,在position的可選屬性值中除了static
之外的其他值,都會使盒子成為一個被定位的盒子,並且為其後代元素建立絕對定位包含塊(absolute positioning containing block)。
因此.title
元素的包含塊是由祖先元素中離.title
最近的一個positioned box所建立的,如果找不到這樣的元素,就會去找初始包含塊,也就是我們在頁面上看到的html
根節點,所以我們就在頁面中看到了以上效果。
因此如果我們想要改變.title
元素的包含塊,就需要給它的祖先元素設定postion
屬性,比如:
.container {
position: relative;
background: orange;
width: 500px;
}
這樣我們就能看到如下效果:
居中佈局
說到這我想到了一個詞——”子絕父相“,寫過居中佈局的小夥伴可能會對這個詞有印象,在這種佈局方案中,子元素的position會被設定為absolute
,父元素的position
會被設定為relative
。在父元素寬度已知(或隱式宣告)或父子元素寬度都已知的情況下,我們可以透過定位相關的屬性,使子元素在父元素內部水平居中。可以來看下面這個例子,首先是父子寬度都已知:
<div class="h-c1">
<div class="h-c1-child">
<span>談笑有鴻儒,往來無白丁。談笑有鴻儒,往來無白丁。</span>
</div>
</div>
我們可以透過以下CSS樣式使子元素居中:
.h-c1 {
position: relative;
width: 500px; /*或者寬度隱式宣告*/
height: 100px;
background: #00bd7e;
}
.h-c1-child {
position: absolute;
width: 300px;
left: 50%; /* 相對於父元素的寬度 */
margin-left: -150px; /* 子元素寬度的一半的負數 */
background-color: #fbf;
}
absolute
元素的位置將根據它的包含塊來計算,關於這個計算可以參考嵌入性屬性inset properties
的說明:
The interpretation of these inset properties varies by positioning scheme:
- for absolute positioning, they represent insets from the containing block.
The inset is a percentage relative to the containing block’s size in the corresponding axis (e.g. width for left or right, height for top and bottom).
所以left: 50%;
最終計算出的值是50% * 500px = 250px
,也就是說如果沒有其他設定的情況下,盒子的左邊緣距離包含塊的左邊緣為250px。為了使盒子居中,我們繼續設定margin-left
屬性值為子元素寬度的一半的負數,使盒子有一個負數的左外邊距,這樣就能使盒子向左挪動寬度的一半的距離,這樣就達到了居中的效果。但這樣的寫法並不夠靈活,每次設定居中時還要去計算盒子寬度的一半,雖然計算量不大,但每次都要計算還是顯得繁瑣,所以就有了另一種寫法:
.h-c1-child {
position: absolute;
left: 50%; /* 相對於父元素的寬度 */
transform: translateX(-50%); /* 相對於自己的寬度 */
background-color: #fbf;
}
在這段樣式中透過使用transform
的translateX
函式計算並設定偏移,就能達到一樣的效果,在translateX
函式中-50%
是相對於盒子自身的寬度,此處盒子寬度未設定,會預設為父元素寬度的一半,這與寬度的計算規則有關。
寬度
重新看回absolute
,除了用於定位的計算,left/right
和top/bottom
的值還能決定盒子的尺寸,關於這點我們可以參考文件上關於寬度width
的說明。
主要關注其中這個等式:
‘left’ + ‘margin-left’ + ‘border-left-width’ + ‘padding-left’ + ‘width’ + ‘padding-right’ + ‘border-right-width’ + ‘margin-right’ + ‘right’ = width of containing block
這個公式是包含塊的寬度等式,文件中分類為三種情況講解了各屬性值的計算:
-
第一種情況是,
left
、width
和right
均為auto
;If all three of 'left', 'width', and 'right' are 'auto'
-
第二種情況是,
left
、width
和right
均不是auto
;If none of the three is 'auto'
-
其他情況屬於第三種。
left/right
決定盒子寬度的情況就屬於第三種情況,也就是left和right已知:
- 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
因此在寬度為auto
時根據已知的left
和right
,我們就能根據上述等式求得盒子的寬度。
溢位
對於絕對定位的元素,如果它的包含塊是由可滾動盒子建立的,也就是說盒子內容有溢位,我們還需要注意的一點,就是這個絕對定位的元素會包含在滾動的溢位區域內。
is included in the scrollable overflow area of the box that generates is containing block.
有時這可能不是我們想要達到的頁面效果。比如以下例子:
.container {
position: relative;
background: orange;
width: 500px;
height: 30px;
overflow: auto;
}
.title {
position: absolute;
bottom: -10px;
background: aquamarine;
}
這是我們在使用absolute時需要考慮的一種情況。
fixed
接著來看fixed,fixed可以說和absolute
是一樣的,除了說它的位置和尺寸是取決於不同的包含塊,也就是文件上所說的fixed positioning containing block
,所以它被認為是絕對定位的子集。
fixed盒子的位置相對於參考矩形(reference rectangle)是固定的:如果盒子附加到視口(viewport),那麼當文件滾動時,盒子不會移動;如果盒子附加到頁面區域(page area),當對文件進行分頁時,盒子會在每頁上覆制。
固定定位包含塊的建立與絕對定位包含塊的建立有所不同:
Properties that can cause a box to establish a fixed positioning containing block include transform, will-change, contain…
根據文件描述,我們可以透過transform
、will-change
和contain
屬性的設定來觸發固定定位包含塊的建立。
如果在祖先元素中沒有建立這類包含塊,分為兩類情況處理:
- 在連續媒體(continuous media)中是佈局視口
- 在分頁媒體(paged media)中是每一頁的頁面區域(固定定位元素相對於頁面盒子而言是固定的)
相容性
僅僅看文件中的描述其實並沒有什麼問題,但在實際運用中fixed
似乎在移動端的iOS上存在一些相容性的問題,因為不同機型表現不同,然後這些相容性問題在網上也有不少解決方案,所以我這裡就不多說了。我們就看一個例子。
假設有以下程式碼:
.container {
height: calc(100vh + 300px);
background: orange;
}
.nav-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #3a8ee6;
color: #fff;
line-height: 50px;
text-align: center;
}
input {
border: 1px solid #ddd;
}
<div class="container">
<input type="text" placeholder="請輸入姓名">
</div>
<div class="nav-bar">提交按鈕</div>
以下是fixed在安卓和iOS中的不同效果,安卓是我目前在用的一加9pro(用的是自帶瀏覽器),蘋果是一部老手機iPhon8(用的是UC瀏覽器)。
我處理fixed這類問題的經驗並不太多,查了一下資料似乎現在有一個API叫做Visual Viewport可以處理這一類可視視口的問題,有興趣的小夥伴可以去研究一下。
然後如果我們按照文件描述的方式去觸發fixed positioning containing box,會發現它的表現變得有點像絕對定位的樣子。
body {
margin: 0;
padding: 0;
transform: translate(0);
}
sticky
最後我們來看sticky,sticky使用較少,因為sticky相對於前面三個是較新的一個值。
sticky類似於相對定位(relative positioning),這裡是規範中給出的定義:
Identical to relative, except that its offsets are automatically adjusted in reference to the nearest ancestor scroll container’s scrollport (as modified by the inset properties) in whichever axes the inset properties are not both auto, to try to keep the box in view within its containing block as the user scrolls. This positioning scheme is called sticky positioning.
關於scrollport
這個詞,這是文件中給出的定義:
The visual “viewport” of a scroll container (through which the scrollable overflow area can be viewed) coincides with its padding box, and is called the scrollport.
翻譯過來的意思是:
滾動容器的可視 "視口"(透過它可以檢視可滾動溢位區域)與其填充盒子一致,稱為滾動口。
我感覺簡單理解就是滾動容器的可視部分,如果在這部分上操作觸發滑鼠滾動事件,可以讓我們看到溢位部分的內容。
sticky會在使用者滾動頁面時產生作用,它的偏移量會自動調整,這是因為在滾動過程中參照點不是固定的,這是與relative、absolute和fixed最大的不同。它的偏移量是相對於最近的滾動容器的滾動口。
在MDN中可以看到,sticky元素的嵌入式屬性不能全是auto
,否則它的效果就相當於relative:
Note: At least one inset property (
top
,inset-block-start
,right
,inset-inline-end
, etc.) needs to be set to a non-auto
value for the axis on which the element needs to be made sticky. If bothinset
properties for an axis are set toauto
, on that axis thesticky
value will behave asrelative
.
我們注意到,在MDN上還有一段描述:
The element is positioned according to the normal flow of the document, and then offset relative to its nearest scrolling ancestor and containing block (nearest block-level ancestor), including table-related elements, based on the values of top, right, bottom, and left.
是關於sticky元素的偏移,在這段內容中描述sticky元素的偏移量是相對於最近的滾動祖先和最近的塊級祖先,那麼問題來了,如果這兩個祖先不是同一個元素,什麼時候相對於滾動祖先,什麼時候相對於塊級祖先呢?我們先來看下面這個例子:
<div class="container">
<div class="header"></div>
<div class="list">
<div class="item">
<div class="title">列表項111</div>
</div>
<div class="item">
<div class="title">列表項222</div>
</div>
<div class="item">
<div class="title">列表項333</div>
</div>
<div class="item">
<div class="title">列表項444</div>
</div>
<div class="item">
<div class="title">列表項555</div>
</div>
<div class="item">
<div class="title">列表項666</div>
</div>
<div class="item">
<div class="title">列表項777</div>
</div>
<div class="item">
<div class="title">列表項888</div>
</div>
</div>
</div>
.container {
background: orange;
height: 100vh;
overflow: scroll;
}
.item {
height: 300px;
background: #00bd7e;
border: 1px solid #333;
}
.title {
position: sticky;
top: 10px;
height: 30px;
line-height: 30px;
background: #8cc5ff;
color: #fff;
}
執行以上程式碼我們很快能發現,在頁面初始階段,只有第一個.title
元素有明顯的偏移,其他的.title
元素似乎並沒有產生效果,而當我們開始滾動頁面後,就會發現,當.title
元素的塊級祖先有一部分進入滾動祖先的滾動溢位區域後,.title
元素的偏移量就變成相對於滾動祖先了。
可以看到,MDN中的描述就是這樣的:
- A stickily positioned element is an element whose computed
position
value issticky
. It's treated as relatively positioned until its containing block crosses a specified threshold (such as settingtop
to value other than auto) within its flow root (or the container it scrolls within), at which point it is treated as "stuck" until meeting the opposite edge of its containing block.
在sticky元素的包含塊在滾動容器內越過一個閾值後,比如上述例子中的top,sticky元素就會進入一個像是“卡住了”的狀態,此時sticky距離滾動祖先的頂部距離就是top指定的值,直到sticky元素遇到這個包含塊的對邊才會結束這種狀態。
因此sticky這個屬性值可以用於實現類似於吸頂的效果。
因為sticky我個人幾乎也沒在實際專案中使用過,所以就先說到這吧。
總結
最後來總結一下,在我的專案經歷中其實定位屬性用的並不算多,absolute用的相對多一點吧,但由於以前我對CSS不太重視的原因,導致為什麼居中佈局是“子絕父相”以及為什麼使用left和right能決定寬度,為什麼能這樣做在有一段時間內其實我並不清楚,我想應該有一些前端小夥伴和我是一樣的情況,那麼我覺得其實還是有必要去了解的,因為前端要解決的問題就是視覺和互動,而在實現視覺效果中CSS是很重要的一環,我們不應該因為UI庫和框架的使用而忽略了CSS,因為只會使用UI庫的話就會被限制在UI庫的效果範圍內,這就和提倡看技術原版書的原因一樣,在搬運或者翻譯的過程中,總會有資訊的丟失,因此建議小夥伴們還是可以自己再去多多閱讀文件。
sticky這個屬性值還比較新,可以在條件允許的情況下(比如不用考慮相容時)做一些探索使用,但由於偏移值是自動調整的,可能會存在效能方面的問題。
另外對於fixed的使用,主要的點還是在於移動端的相容性問題。