[css] 如果只有一個 div


Posted by vii120 on 2021-01-19

前言

在 11 月的時候,twitter 上有個 #nodivember 的活動,在 CodePen 上也找得到許多作品,這個活動顧名思義就是 "no div",在 html 沒有任何 div 的情況下做出各種樣式,差不多是「給我一個 html,我就給你全世界」的感覺,程式碼大概長這樣:
no-div-code
這裡放幾個大神們的 CodePen:

其他還有許多厲害的作品,都是沒有使用任何 div 完成的,雖然實際工作上不太會被要求做一個沒有 div 的網站,但可以去思考 css 的延伸使用,當作挑戰賽也很有趣 😂,no-div 的作品其實是把 body 當作一個 div,並結合偽元素一起運用,以下會以 "one div" 的概念來分享一個 div 可以做到哪些事情

常用屬性

在只有一個 div 的情況下,較常用來畫圖的屬性有:

  • pseudo element
  • border
  • box-shadow
  • background

# pseudo element 偽元素

在 no-div 的作品中,大部分都有使用偽元素來處理,因為他本身也可以做出區塊,而且有個很好用的 content 屬性,以下介紹幾個 content 的用法:

1. content: ''

當成普通的 div 使用:結合 flexbox 做出文字左右的線
flex-line

<div>hello</div>
div {
  display: flex;
}
div:before, div:after {
  content: '';
  flex: 1;
  border-bottom: 1px solid;
  margin: auto 12px;
}

2. content: '{string}'

像 div 一樣直接寫入字串

div:before {
  content: 'hello';
}

3. content: '{unicode}'

除了字串之外,只要用反斜線加上 unicode 就可以寫出各種文字及特殊符號,可以搜尋 unicode list,找到 U+ 開頭的 code

/* ★: U+2605 */
div:before {
  content: '\2605';
}

另外像是撲克牌的四種花色,也有他們對應的 unicode,可以用此做出撲克牌圖案

4. content: attr()

attr() 可以抓取其父元素上的屬性值,包含 class & id

<div data-title="main title"></div>
div:before {
  content: attr(data-title);
  /* 結果同等於 content: 'main title' */
}

5. content: url()

url() 其實就像 <img> 一樣,可以放圖片路徑,需要注意的是 url 的圖片無法調整大小

div:before {
  content: url(../icon.png);
}

⚠️ 偽元素的動畫依賴性:若對 div 寫動畫,有些會影響到偽元素,使用時需要再注意。例如:在 div 寫了右移的動畫 translateX(100px),則其偽元素也會跟著右移

# border 邊框

除了方方正正的線條外,border 還有其他不同的用途

1. 三角形

如果上網查如何用 css 做三角形,大多數的回答應該都是用 border,因為在沒有寬度的情況下,border 的樣式剛好就是三角形:
border

div {
  width: 0;
  border: 30px solid;
  border-color: red orange gold green;
}

因此可以用此概念做出像是對話框、蝴蝶結等包含三角形的圖案
msg+knot

/* msg box */
.msg:before {
  content: '';
  // ...
  border-style: solid;
  border-width: 16px 8px 0px;
  border-color: seagreen transparent;
}
/* knot */
.knot {
  width: 0px;
  border-style: solid;
  border-color: transparent red;
  border-width: 20px 35px 50px 50px;
}

2. border-radius

border 加上圓角也可以做出許多不同的應用,像是弦月
moon

.moon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  border-right: 30px solid gold;
}

或是畫個瀏海
head

.head {
  width: 130px;
  height: 80px;
  color: tan;
  border-style: solid;
  border-width: 80px 15px 0;
  border-radius: 50%;
  background-color: wheat;
  /* 眼睛是另外加上去的 */
}

同場加映側面照
head2

.head {
  /* css 同上 */
  ...,
  border-width: 80px 0 0 15px;
}

# box-shadow 區塊陰影

說到陰影,好像就是那個只能低調躲在 div 背後的夥伴,但他也有可以當主角的時候!

1. border-radius

陰影搭配 border-radius 也可以做出弦月,這裡使用的是 inset 內陰影,可以控制大小在寬度內
box-shadow-moon

.moon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  box-shadow: inset -30px 0px gold;
}

2. 複製人

你知道 box-shadow 擁有 css 界的複製人技術嗎?在畫複製人之前,先複習一下他的參數:box-shadow(x y blur spread color),分別有 x 和 y 的座標、模糊程度和發散成度,我們先把發散和模糊都拿掉,並且座標設定超過該元素的大小,就可以用 box-shadow 做出另一個相同的區塊了!若有寫 border-radius,則陰影的樣式也會跟著改變哦
box-shadow

div {
  width: 100px;
  height: 100px;
  background-color: orange;
  box-shadow: 110px 0px #fbdf8b; /* 110px -> 大於元素的寬度 100px */
}

3. loading

下面這個 loading 就是用 one-div 加上 box-shadow 做的
loading-gif

利用複製人的概念,我們可以以元素為中心(下圖紅點),先做出上下左右四個點
loading-side

.loading {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: indianred;
  box-shadow:
      50px 0px gold, /* right */
      -50px 0px gold, /* left */
      0px 50px gold, /* bottom */
      0px -50px gold; /* top */
}

接著斜對角的四個點,需要用到直角三角形邊長的計算:45-45-90 的直角三角形斜邊等於圓形的半徑 1,那麼短邊長就會是 1 / (根號 2) ,在此例中半徑是 50,所以短邊長為 50 / 1.414 大約是 35,得出 x y 距離皆為 35,即可畫出斜對角的四個點了!
triangle-calculation

最後再把中心的顏色拿掉,就做出八個點了~之後再利用 animation 讓 box-shadow 每個點輪流變色,就可以做出 loading 的效果了
loading-shape

.loading {
  ...,
  box-shadow:
      ...,
      35px -35px gold,
      35px 35px gold,
      -35px 35px gold,
      -35px -35px gold;
}
@keyframes loading {
  0%, 100% {
      box-shadow: 0px -50px peru, 35px -35px gold, ...;
  }
  12.5% {
      box-shadow: 0px -50px gold, 35px -35px peru, 50px 0 gold, ... ;
  }
  ...
}

4. box-shadow vs. border

剛剛有提到 border 和 box-shadow 都可以做出弦月的樣式,但效果其實還是有些微不同:border 是邊框畫好後,再被 border-radius 切掉;box-shadow 則是在元素被 border-radius 切掉後,再長出的一段(為了把 border 跟 box-shadow 畫在一起,下圖用的是外陰影)
border-shadow

在 border-radius 比例不同時,兩者的效果也有顯著的差異(上面是 box-shadow,下面是 border):
border-shadow-radius
在同為 border-radius: 50% 時,不同邊寬的動畫比較:

# background 背景

background 是在 one div 的情況下畫出各種樣式的主要功臣,大部分會著重在漸層色的應用,包含線性漸層(linear-gradient)、放射漸層(radial-gradient)、錐形漸層(conic-gradient)以及重複漸層(repeating gradient)

1. background-color

說到背景色,最常使用的應該還是 background-color,但他有個缺點,就是無法設定 background-size,大大減少了他的使用彈性,像是如果要做一個小的方形區塊,就無法用 background-color 來完成:
bg-color

.bg-color {
  width: 100px;
  height: 100px;
  background-color: gold;
  background-size: 50px 50px;
  background-repeat: no-repeat;
}

這個情況下,使用 linear-gradient 就可以解決這個問題了~

2. background: linear-gradient

延續上述例子,若要做出一個固定大小的區塊,可將線性漸層的兩個顏色設為相同 linear-gradient(gold, gold);或是將主色邊界設定為 100%,另一個給透明色:

.bg-gradient {
  width: 100px;
  height: 100px;
  background-image: linear-gradient(gold 100%, transparent);
  background-size: 50px 50px;
  background-repeat: no-repeat;
}

要讓漸層色有清楚的界線(等於沒有漸變區),原理就是讓兩色的交接點相同,例如:linear-gradient(red 30%, pink 30%),這樣就會在 30% 的位置有個明顯的分界點,用此方式也可以畫出三角形:
bg-triangle

- google logo

下圖是大家應該都很熟悉的 google 文字配色,若要做出這個樣式,比較直覺的想法應該是切成六個 span,再分別給不同個 class 去設定顏色,但這裡也可以用一個 div 就解決囉
google-logo

作法是用 linear-gradient 加上 background-clip: text 把背景切成文字的形狀,需要注意的是每個字型的字母寬度不太一樣,像這裡使用的字型,每個字母寬度都有小小的差別,就需要找出每個漸層的分界點
google-parse

/* 找出分界點 */
$point1: 17%;
$point2: $point1 + 16.5%;
$point3: $point2 + 16.5%;
$point4: $point3 + 17%;
$point5: $point4 + 16%;
/* 線性漸層:由左至右 */
background-image:
  linear-gradient(
    90deg,
    $blue $point1,
    $red $point1, $red $point2,
    $yellow $point2, $yellow $point3,
    $blue $point3, $blue $point4,
    $green $point4, $green $point5,
    $red $point5
  );
/* 將背景剪裁成文字的前景色 */
background-clip: text;
/* 文字本身要設定透明,才可以顯示出背景 */
color: transparent;
- 擦亮效果

這個亮晶晶的效果用 linear-gradient 就可以做到:

首先做出一個包含白色小區塊的漸層,再把寬度放大,最後做個動畫讓背景位置由左到右移動,就可以做出亮晶晶的效果了~
shiny

.title {
  background-image: 
      linear-gradient(-45deg, indianred 40%, white 50%, indianred 60%);
  background-size: 250% 100%; /* 放大漸層背景 */
  background-clip: text;
  animation: shiny 2.5s both infinite;
}
@keyframes shiny {
  0% {
    background-position: 100% 100%;
  }
}

3. background: radial-gradient

放射狀漸層可以做出圓形或橢圓形,像是三個點的 loading icon 只需要一個 div 加上 radial-gradient 就可以做到

- loading

這裡主要是用 circle 圓形漸層做出三個點,再控制 position 平分每個點:
radial-loading

/* radial-gradient */
.loading {
  width: 90px;
  height: 15px;
  background-image:
    radial-gradient(circle, gold 50%, transparent 50%),
    radial-gradient(circle, gold 50%, transparent 50%),
    radial-gradient(circle, gold 50%, transparent 50%);
  background-size: 15px 15px;
  background-position: 0 50%, 50% 50%, 100% 50%;
  background-repeat: no-repeat;
}

不過除了放射漸層以外,pseudo element 跟 box-shadow 都可以做出這個 loading icon 哦,需要注意的是 box-shadow 需要往上/下/左/右推,所以會有點位移,可以用 margin 或 transform 再把位置移回來

/* pseudo element */
.loading {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background-color: gold;
  margin: auto;
  position: relative;
}
.loading:before,
.loading:after {
  content: '';
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background-color: inherit;
} 
.loading:before { left: 30px; }
.loading:after { right: 30px; }
/* box-shadow */
.loading {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  box-shadow: 30px 0 0 gold, 60px 0 0 gold, 90px 0 0 gold;
  transform: translateX(-20px); /* 把 shadow 位置調整回來 */
}

做出三個點之後,可以再加上動畫做出彈跳效果:

- 無限輪迴

除了 bounce 以外,也可以做這種無限冒出/消失的 loading

在開始之前,可以先思考一下無限輪迴的動畫效果是怎麼做到的,像是常看到的 gif 梗圖,就很容易看出起始點和結束點
friends-gif

但無限輪迴的 gif 就看不太出來,原理是讓起始點與結束點的樣子相同,等於結束時要回到原點
penguin-gif

回到剛剛的 loading 圖,可以先把點分成四個,第一個不斷地由小到大冒出,最後一個則相反,不斷地由大到小最後消失,中間兩個則固定大小,不斷往右移動,就可以做出無限輪迴的效果了
loop-loading-parse

$dot: radial-gradient(circle, $color 50%, transparent 50%);
.loading {
  background: $dot, $dot, $dot, $dot;
  background-repeat: no-repeat;
  animation: move 0.5s both infinite $bezier;
}
@keyframes move {
  from {
    background-position: 0 50%, 0 50%, 50% 50%, 100% 50%;
    background-size: $bg-small, $bg-normal, $bg-normal, $bg-normal;
  }
  to {
    background-position: 0 50%, 50% 50%, 100% 50%, 100% 50%;
    background-size: $bg-normal, $bg-normal, $bg-normal, $bg-small;
  }
}
- 半徑知多少

關於 radial-gradient 的半徑有點不是那麼直觀,所以想來分享一下:以下是一個寬高 300px 的正方形,我們放了一個以 50% 為分界點的 radial-gradient 圓形背景,這個圓形的半徑應該會是多少呢?
radial-demo

.box {
  width: 300px;
  height: 300px;
  background-image: radial-gradient(
    circle at 50% 50%, 
    orange 5%, 
    #ececec 5% 50%, 
    transparent 50%);
  /* orange 5% 是用來做出圓心當作參考 */
}

這邊要介紹一下 radial-gradient 裡有個屬性叫 extent-keyword,用來設定漸層的終點位置,預設值為最遠的角落(farthest-corner),對分界點是 50% 的圓形來說,從中心點到最遠角落的距離,剛好就是他的直徑(下圖綠線),而這裡又出現了 45-45-90 的直角三角形,利用 1:1:根號2 的算法,可以得知半徑為 150 * (根號 2) / 2,大約是 106
radial-radius

extent-keyword 的屬性另外可以設定最近的角落(closest-corner)、最近的邊(closest-side)和最遠的邊(farthest-side),是若希望圓形撐滿整個區塊,則可以設定成 closest-side + 邊界 100%
radial-radius-full

.box {
  ...,
  background-image: radial-gradient(
    closest-side circle at 50% 50%, 
    orange 5%, #ececec 5% 100%, 
    transparent);
}

4. background: conic-gradient

錐形漸層可以用來製作色相環、金屬的反射效果或是 instagram 限時動態的外框
conic-demo

.hue {
  background-image: conic-gradient(
    #ff0000, #ffff00,
    #00ff00, #00ffff,
    #0000ff, #ff00ff,
    #ff0000);
}
.metal {
  background-image: conic-gradient(
    white 0%, silver 10%,
    white 35%, silver 45%,
    white 60%, silver 70%,
    white 80%, silver 95%,
    white 100%);
}
.story {
  background-image: conic-gradient(
    #8a509d 30%, #e14e53 , 
    #fbc66b, #e14e53, #8a509d);
}

也可以做出邊界或改變中心點、起始角度
conic-demo

.box {
  background-image: conic-gradient(
    from 60deg at 20% 50%,
    indianred 25%,
    seagreen 25%, seagreen 50%,
    gold 50%, gold 75%,
    steelblue 75%);
}

5. background: repeating gradient

上述的三種漸層都有重複性可以使用,像是重複線性漸層是 repeating-linear-gradient,但這個重複跟 background-repeat 有什麼不同呢?先看個例子 🌰
barber-light

假設今天要做個懷舊理髮廳的旋轉燈樣式,應該會先想到用 linear-gradient,但寫下去才發現要一條一條刻,重複寫了很多相同的 code(右下角落最後一條還沒刻到 QQ)
barber-linear

/* linear-gradient */
.light {
  width: 100px;
  height: 300px;
  background-image:
    linear-gradient(135deg,
      #fff, #fff 30px, /* white */
      #ec1d25 30px, #ec1d25 60px, /* red */
      #fff 60px, #fff 90px, /* white */
      #015f9f 90px, #015f9f 120px, /* blue */
      #fff 120px, #fff 150px,
      #ec1d25 150px, #ec1d25 180px,
      #fff 180px, #fff 210px,
      #015f9f 210px,#015f9f 240px,
      #fff 240px, #fff 270px);
}

既然寫了重複的東西,就用 background-repeat 來解決吧!我們在寫好第一段顏色之後,加上 size 和 repeat,雖然達到了重複性,但銜接處很突兀,跟理想的樣子還是差了不少
barber-bg-repeat

/* linear-gradient + bg-repeat */
.light {
  width: 100px;
  height: 300px;
  background-image:
    linear-gradient(135deg,
      #fff, #fff 30px,
      #ec1d25 30px, #ec1d25 60px,
      #fff 60px, #fff 90px,
      #015f9f 90px, #015f9f 120px,
      #fff 120px, #fff 150px);
  background-size: 100px 100px;
  background-repeat: repeat-y;
}

這時候就輪到 repeating-linear-gradient 出場了,一樣寫好第一段顏色後,只要加上 repeating 的前綴,就可以讓這個樣式按照同個步調重複畫下去
repeating-linear

/* repeating-linear-gradient */
.light {
  width: 100px;
  height: 300px;
  background-image:
    repeating-linear-gradient(135deg,
      #fff, #fff 30px,
      #ec1d25 30px, #ec1d25 60px,
      #fff 60px, #fff 90px,
      #015f9f 90px, #015f9f 120px);
}

同理可以應用在另外兩種漸層,像是飛天小女警裡會出現的愛心可以用 repeating-radial-gradient
repeating-radial

div {
  background:
    repeating-radial-gradient(
      circle at 50% 50%,
      #e60103 0px, #e60103 30px,
      #de7878 30px, #de7878 60px,
      #eaa7b5 60px, #eaa7b5 90px
    );
}

漫畫裡出現類似爆炸文字的條紋背景則可以用 repeating-conic-gradient
repeating-conic

div {
  background:
    repeating-conic-gradient(
      from 5deg at 30% 30%,
      #f9e4c9 0deg 15deg,
      #ea4445 15deg 30deg
    );
}

6. 漸層背景的小問題

雖然漸層可以做到的事情很多,但還是有幾個要注意的地方:

- safari 上的 bug

一個是透明色的問題:漸層色若有使用到 transparent,在 safari 上會變成黑灰色而非透明色,但如果是有明確分界點的寫法就不會有這個狀況,解決辦法可以使用 rgba(255, 255, 255, 0) 來取代 transparent
repeating-conic

🎃 2022.6.15 更新:safari 15.4 已經修正這個問題了 🎉🎉

另一個是與 flexbox 的衝突:gradient 如果跟 display: flex 並用,在 safari 上會直接消失(?),但反白的話會發現其實文字還在,只是變成透明的了,解決辦法是把 flex 跟 gradient 分在不同層寫
repeating-conic

<!-- failed -->
<div class="flex gradient">gradient with flex</div>
<!-- success -->
<div class="flex">
  <span class="gradient">gradient inside flex</span>
</div>
- transition 轉場問題

漸層色本身沒有動畫的轉場,像是以下的例子雖然有加上 transition,但 hover 時不會有顏色的轉場效果

div {
  background-image: linear-gradient(gold, pink);
  transition: all 0.3s;
}
div:hover {
  background-image: linear-gradient(orange, red);
}

解決方式可以蓋一個隱藏的偽元素,hover 時再控制 opacity 讓偽元素出現:

div:before {
  content: '';
  ...,
  background-image: linear-gradient(orange, red);
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s;
}
div:hover:before {
  opacity: 1;
  visibility: visible;
}

另外也可用 background-size 做一個較大的漸層背景,再用 background-position 控制 hover 時要顯示的位置:

div {
  background-image: linear-gradient(steelblue, pink 50%, gold);
  background-size: 100% 200%; /* 放大背景 */
  transition: all 0.2s;
}
div:hover {
  background-position: 0 100%; /* 移動背景 */
}
- border 邊框問題

這個問題應該較不容易遇到,只有在較寬的 border 且有透明度的情況下,同時使用漸層背景才會發生,理想中的半透明邊框應該會蓋在漸層背景上才對,但下圖的邊框長得很奇特,還包含了漸層色
weird-border

div {
  width: 300px;
  height: 200px;
  border: 20px solid rgba(255, 255, 255, 0.3);
  background-image: linear-gradient(45deg, pink 50%, steelblue 50%);
}

這裡要介紹一個背景的屬性 background-origin,他可以設定背景的起始位置,預設是 padding-box,也就是從 padding 的位置開始(包含 padding),只要改為 border-box 就可以讓背景從邊框開始,達到我們想要的樣式了

weird-border-fixed

div {
  // ...,
  background-origin: border-box;
}

# 小總結

以上介紹了偽元素的 content 應用,利用 border 與 border-radius 畫幾何圖形,box-shadow 的複製人概念及 loading 圖製作,比較 box-shadow 及 border 的差異,還有花了不少篇幅介紹三種漸層背景:線性 (linear)、放射 (radial)、錐形 (conic),及重複漸層的使用場景,也有補充漸層背景會遇到的問題與注意事項,看完後也來玩玩用 one div 畫個圖吧~

# 本篇同步分享於 medium


參考資料


#css #one-div #gradient #nodivember







Related Posts

如何用 ROS Topic 控制機器人移動

如何用 ROS Topic 控制機器人移動

筆記、第十五週網站前後端開發基礎測試

筆記、第十五週網站前後端開發基礎測試

Node.js Advanced Interview Questions for Experienced Professionals

Node.js Advanced Interview Questions for Experienced Professionals


Comments