可能被忽略的"按鈕元件"細節

ziven27發表於2019-01-30

可能被忽略的"按鈕元件"細節

對於元件的開發,我們首推的是在開始寫程式碼之前一定要和設計師溝通。看看設計師對於我們元件的擴充是怎麼思考的。以規避程式設計師和設計師擴充方式的不一致導致的設計師一小改程式設計師一大改的成本問題

通常我們需要做一個按鈕的時候,無非就從之前或者別人那裡把程式碼拷貝過來,然後根據設計稿,改改顏色,字號什麼之類的就可以了。

但隨著專案擴張按鈕形態的增加,如果我們一開始就不是全域性的角度去設計元件,到後面就需要花更多的時間去思考按鈕的擴充性

按鈕命名

舉個例子比如我們有一個名為 .btn 的基礎藍色按鈕。然後設計又給到了一個紅色放到 header 區域中的下載按鈕。你會如何擴充這個樣式呢?

  1. .btn-header :基於位置
  2. .btn-red :基於顏色
  3. .btn-download :基於功能

以上三種是我們通常會用到的比較快速的解決方案。這三個方案之所以快速,是因為他們都是基於當下場景去思考的。我們只是用程式碼描述了這個按鈕和其它按鈕的區別而已。

可是關鍵問題來了,既然不同點有這三個,我應該選擇哪一個呢?如果要在這三個裡選擇一種,我們會推薦顏色

可能被忽略的"按鈕元件"細節

  • 首先,如果我們 footer 裡面也需要放一個紅色的下載按鈕,我們顯然不能使用 .btn-header 這個名字來擴充我們的按鈕,而應該使用 .btn-footer
  • 再者,如果我們在 header 裡面需要新增一個紅色的上傳按鈕,此時顯然我們 .btn-upload 更滿足我們的需求;

可是這樣就達不到我們要複用樣式的目的了。如果是基於顏色擴充的話,你依然可以使用 .btn-red 這個樣式來表示在 footer中的紅色下載按鈕。因為我們要複用的是樣式,而紅色本身就是一種樣式,自然擴充性會更好一點。再者按照我們的實際開發的經驗,顏色的改動明顯是遠遠低於我們位置和功能的。甚至有很多的專案,一旦主題色定下之後,基本上是不會改的。基於這個點推薦大家可以看一下張鑫旭老師的基於CSS color屬性的靜態UI元件重構策略的這篇文章。

如果你的專案追求的是易用性,那麼我們是非常推薦基於顏色命名,這個理解成本是最簡單的。但是如果非要站在一些理論的至高點,在這個方案上挑毛病的話主要有以下三點:

可能被忽略的"按鈕元件"細節

  1. 大家對於同一個顏色的理解不一樣
  2. 不同的專案之間主色是不一樣的,也就是說不能用一套方案適用所有專案,這比較不符合元件化的思維
  3. 有可能會出現,一個專案中兩個紅色,那此時你會怎麼命名另一個紅色的按鈕?.btn-red2?

當然這三個點都是有點吹毛求疵的了,在實際開發中幾乎不會遇到這樣的問題。

按鈕設計應該回歸設計

此時就引出了另外一個問題,我們按鈕的 UI 是設計師給的,所以對於按鈕的擴充方式我們是需要和設計師溝通的。就像上面的問題,如果設計師給到了兩個紅色的按鈕,那麼可以和設計討論是否考慮用其它的顏色替代。

既然是要溝通,就不能所有的擴充邏輯都是完全基於設計同學的思維方式來。我們得拿出我們的方案。好在的是在當下的環境中,已經有比較好的最佳實踐了。這其中首推的是目前最火的也是火了很多年的前端 UI 框架 Bootstrap。他們對於按鈕的封裝,也幾乎成為了國際通用的擴充規則。

這邊基於Bootstrap 按鈕元件,結合我們實際的經驗帶大家看看,按鈕的封裝和擴充邏輯。文中的程式碼是為了解釋原理的虛擬碼,實際開發會為了減少程式碼量,不會寫得這麼囉嗦。

程式碼結構

_var.scss      // 引數
_base.scss     // 基礎樣式 
_theme.scss    // 主題
_size.scss     // 尺寸  
_shape.scss    // 形狀
_status.scss   // 狀態
index.scss     // 引用以上所有檔案
複製程式碼

對於 _var.scss 這個檔案其實是沒有的,因為在實際的專案之中,整個專案會有統籌全域性樣式的一個引數檔案。所以這個檔案往往是引用的全域性的引數檔案。

按鈕基礎樣式 「 _base.scss 」

.btn {
      /* a 連結預設為inline元素,但也有可能顯示為按鈕所以設定 inline-block 屬性 */
      display: inline-block; 
      
      /* 按鈕文字居中,特別是當我們給了按鈕一個固定寬度的時候 */
      text-align: center;
      
      /* 按鈕文字不換行 */
      white-space: nowrap; 
      
      /* 去掉可以用滑鼠選中按鈕上的文字功能,沒有這個屬性選中的時候會出現一個比較難看的半透明框 */
      user-select: none;  
      
      /* 為非可選標籤,新增滑鼠手型。大多數瀏覽器對於 a 標籤和 button 標籤預設是有這個屬性的,但其它標籤就不一定了 */
      cursor: pointer;
      
      /* 按鈕和文字混排的時候近似垂直居中 */      
      vertical-align: middle; 
      
      /* 讓padding 和 border 的寬度不影響按鈕大小,IE8 和 IE8 以上才相容這個屬性 */
      box-sizing: border-box;
      
      /* 按鈕需要設計字型,這裡為了保持統一建議和全站主字型保持一致 */
      /* 但是通常我們在 css reset 中會做這一步的重置,所以這裡不需要了 */
      /* font-family:inherit; */
      
      /* 以下樣式根據實際設計情況來編輯 */
     
      /* 按鈕圓角 */
      border-radius: 3px; 
      
      /* 去掉按鈕的預設邊框 */
      border: none 0; 
}          
複製程式碼

對於按鈕基礎樣式,因為沒有涉及到按鈕的擴充性,所以大家的樣式基本都大同小異。

分類

按鈕基礎樣式設定完成之後,我們要做的就是思考按鈕的擴充了。要擴充按鈕,首先要先看看按鈕的分類有哪些。

型別 型別細分
按鈕主題 主按鈕 primary, 次按鈕 secondary, 成功按鈕 success, 危險按鈕 danger, 警告按鈕 warning
按鈕大小 比大更大 largex, 大按鈕 large, 預設按鈕 default, 中號按鈕 middle, 小按鈕 small, 比小更小 smallx
按鈕形狀 連結按鈕 link, 幽靈按鈕 ghost, 膠囊按鈕 capsule, 塊狀按鈕 block
按鈕狀態 禁用 disabled, 滑鼠移入 hover, 滑鼠按下 active, 獲取焦點 focus, 載入 loading

基本上我們按鈕主要可以分為以上四大類,而以上的幾大類又可以互相的排列組合。

比如 disabled, warning, ghost, large 可以表示一個禁用狀態下的警告幽靈大按鈕。

注:這裡的分類,只是為了拆分程式碼用,比如你要修改或者擴充主題就去theme.scss 這個檔案當中修改。因為在實際使用可以擴充的時候,我們希望開發者和使用方其實弱化分類這個概念。

原生CSS

<button type="button" disabled class="btn _warning _ghost _large">warning按鈕</button>
<a href="javascript:;" class="btn _warning _disabled _ghost _large">warning按鈕</a>
複製程式碼

在 CSS 規範 中有提到通過是用下滑線作為字首的命名規則。

元件化框架

<Button warning disabled ghost large>React按鈕</Button>
複製程式碼

在類似 React 和 VUE 的場景中我們推薦直接使用單屬性的方式擴充我們們的元件,當然元件內部的實現可以採用和原生 CSS 一樣的邏輯。

看到這裡有的同學可能會對於我們的擴充方式感到某些疑惑,因為從可讀性來說以下的方式顯然更加的優雅。

<Button theme="warning" status="disabled" shape="ghost" size="large">按鈕</Button>
複製程式碼

對於這個問題我們內部也有一些分歧,一般當團隊當中有分歧的時候我們可以參考一下目前已有的優秀的團隊是怎麼處理的。

 <!-- 我們的方案 -->
 <Button danger disabled ghost large>大的紅色的disabled狀態的幽靈按鈕</Button>
 
 <!-- ant design: github star 42k+ -->
 <Button type="danger" disabled ghost size="large">大的紅色的disabled狀態的幽靈按鈕</Button>
 
 <!-- material-ui: github star 44k+ -->
 <Button variant="outlined" color="secondary" disabled size="large">大的紅色的disabled狀態的幽靈按鈕</Button>
  <!--在 material-ui 中 variant="outlined" 代表幽靈按鈕 -->
 
 <!-- react-bootstrap : github star 14k+ -->
 <Button variant="outline-danger" size="lg" disabled>大的紅色的disabled狀態的幽靈按鈕</Button>
 <!-- 在 material-ui 中 variant="outline-danger" 代表紅色的幽靈按鈕 -->
 
 <!-- react.semantic-ui : github star 9k+ -->
 <Button basic color='red' fluid>大的紅色的disabled狀態的幽靈按鈕</Button>
 <!--在 react.semantic-ui 中 basic 代表幽靈按鈕 -->
複製程式碼

看到這裡大家可能會比較凌亂,各個場的 API 風格真是百花齊放。我想說的是各個團隊有各個團隊自己的考慮,沒有絕對的好與壞,只能說適合自己的就是最好的。我這邊說說的我們團隊這套 bool屬性 方案的優缺點:

優點

  1. 原始: 因為我們在還沒有接觸元件開發的時候我們就是使用的單 class 去控制的我們的樣式( <a class="btn btn-danger btn-disabled btn-ghost btn-large">大的紅色的disabled狀態的幽靈按鈕</a>),在元件開發中我們只是把 class 挪到了屬性當中;
  2. 乾淨: 同樣的功能,卻只需要更少的程式碼;
  3. 方便使用方: 我們只需要知道屬性名,而不需要記住它的分類。(比如我問 _ghost 這個按鈕應該是什麼分類?我相信對於不熟悉我們按鈕的分類的同學其實是很難反應的出它是屬於我們 shape 這個分類的);
  4. 方便開發者: 對於開發者來說,上面提到的不用糾結分類的問題同樣是適用。並且在自己想要擴充一些自定義按鈕樣式的時候,也不需要去考慮它應該屬於什麼分類,簡單的說,我們的這套邏輯在程式碼層已經弱化了分類的概念;
  5. 邏輯簡單: 我們按鈕的不同屬性之間是可以排列組合的。但是如果採用的 鍵值 的方的話,我們很難實現同分類下的按鈕屬性組合。(比如我們想同時使用 shape 的這個分類當中的 blockghost 這兩個狀態,當然有同學可能會反駁說這兩個本身就不應該在一個分類中);

缺點

  1. 因為全是 bool 型別的屬性,寫校驗邏輯相對複雜;
  2. 對於某些不能排列組合的樣式容易寫錯,比如(<Button primary danger>按鈕</Button>), 這個很難被發現;
  3. 不太符合某些同學的主觀意識,因為好像大家腦海裡對於顏色應該就是有一個分類的;
  4. 因為沒有了分類的概念,所以屬性值會非常多,相當於佔用了很多的屬性全域性變數。
  5. 如果按鈕的主題,是來自於一個 themeProvider ,使用這種扁平化的方式就有點行不通。

當然我們只是主要推薦使用 bool屬性 的方式去擴充我們的按鈕,如果實在某些場景需要用到分類,我們也不強制。

按鈕主題 「 _theme.scss 」

可能被忽略的"按鈕元件"細節

按鈕主題其實是按照功能區分,只是設計師通常用顏色區分功能,所以主題也近似可以看作是顏色的區分。

Bootstrap 是一個沒有特定產品的通用基礎框架,即使在按鈕設計極致收斂的情況下,仍然有 Primary,Secondary,Success,Danger,Warning,Info, Light,Dark, Link 九種主題(在我們看來 link 狀態的按鈕也有 primary 的作用,所以不同於 Bootstrap 我們把 Link 歸類到了形狀shape這個分類中)。

這裡我之前說法有誤,Bootstrap 本身沒有提過主題分類這個概念。官網原文是這樣說的:

Bootstrap includes several predefined button styles, each serving its own semantic purpose, with a few extras thrown in for more control.

修正:Bootstrap 是一個沒有特定產品的通用基礎框架,即使在按鈕設計極致收斂的情況下,仍然有 Primary,Secondary,Success,Danger,Warning,Info, Light,Dark, Link 九種預定義樣式

對於我們自己的產品來說,這麼多的分類是不推薦的。我們期望的是用更少的主題適應更多的場景,要達到這一點,也是需要多和設計師溝通的。以我們的經驗, 主按鈕 primary, 次按鈕 secondary, 成功按鈕 success, 危險按鈕 danger, 警告按鈕 warning 這5種主題已經能涵蓋很大一部分場景了。

._primary{
    background-color:$c_primary;
    color:$c_primary;
}
.btn{ 
    color:#fff; 
}
.btn._ghost{
    background-color:transparent;
    border-color:1px solid;
}
.btn._link{
    background-color:transparent;
}
複製程式碼

按鈕的主題色,在實際開發中我們的顏色應該是基於全域性的顏色引數去獲取的。對於全域性顏色引數的命名,我們推薦使用 c_ 字首。

按鈕大小 「 _size.scss 」

可能被忽略的"按鈕元件"細節

比大更大 largex大按鈕 large預設按鈕 default中號按鈕 middle小按鈕 small比小更小 smallx...

在大小的數量上和主題邏輯是一樣的,建議使用更少的大小,適配更多的場景,我們推薦使用大,中,小,加預設共計四種樣式。

當然如果要擴充大話,我們建議通過類似衣服尺碼 xs, xl 新增 x 的方式進行擴充 _largex。

對於按鈕尺寸是設定邏輯,我們建議遵從 Metiral Design 的 8 point 規則(尺寸控制在 8 畫素的倍數,實在不能滿足也應該至少是 4 的倍數)。

.btn{
	height: 40px;
	font-size: 16px;
	line-height: 24px;
	padding: 8px 16px;
}

.btn._middle{
	height: 32px;
	font-size: 14px;
	line-height: 24px;
	padding: 4px 12px;
}
複製程式碼

對於大小,應該不只是按鈕的高寬的變化,同時應該需要考慮到按鈕字號的變化,這樣才會更加的協調。

按鈕形狀 「 _shape.scss 」

可能被忽略的"按鈕元件"細節

實心按鈕 fill, 連結按鈕 link, 幽靈按鈕 ghost, 膠囊按鈕 capsule, 塊狀按鈕 block...

按鈕的形狀,基本上業界常用的是以上五種方式,當然也不排除設計有定製的需求。

實心按鈕 fill

qq20181228-135519 2x
  .btn._fill{
      color:#fff;
  }    
複製程式碼

背景是主題色,文字是白色的按鈕,因為太常用所以一般作為預設按鈕的樣式,所以在實際開發種我們不會另起一個fill的屬性。

連結按鈕 link

qq20181228-135634 2x
  .btn._link{
      background-color: transparent;
  }    
複製程式碼

文字是主題色,背景為透明的按鈕,雖然看起來是文字,但是它和其它按鈕佔據同樣大小的空間。

幽靈按鈕 ghost

qq20181228-135527 2x
  .btn._ghost{
      background-color: transparent;
      border:1px solid;
      /* 相容邊框增加引起的文字偏移 */
      line-height: 24px - 1px ;
  } 
複製程式碼

文字和邊框是主題色,背景為透明的按鈕。

border會預設使用文字的邊框顏色,這裡因為給按鈕設定了邊框,但是因為按鈕高度是寫死的,那麼意味著,這裡的文字會被往下推 1 個畫素,這邊需要對於不同的按鈕做一個相容。

膠囊按鈕 capsule

qq20181228-135602 2x
  .btn._capsule{
    border-radius:100px;
  }    
複製程式碼

左右兩邊是圓角的按鈕。

塊狀按鈕 block

qq20181228-135703 2x
  .btn._block{
    display:block;
    width:100%;   
    
    /* 如果沒有給按鈕設定 box-sizing:border-box; 屬性,此處還應該去掉按鈕左右間距。 */
    /* padding-left:0; */
    /* padding-right:0; */
  }    
複製程式碼

佔一行的按鈕。

按鈕狀態 [_status.scss]

可能被忽略的"按鈕元件"細節

:disabled 禁用狀態, :hover 滑鼠移入, :active 滑鼠按下, :focus 獲取焦點, ._loading 載入狀態...

按鈕處於一些臨界點的時候需要有一些特殊的狀樣式告知使用者,按鈕通常有以上的五個狀態。

  /* 偷懶但簡潔 */ 
  .btn{
    transition:200ms;
  } 
  
  /* 繁瑣但效能更好 */
  .btn{
    transition:opacity 200ms, background-color 200ms, color 200ms;
  }  
複製程式碼

因為按鈕的狀態切換,通常是從一個狀態到另一個狀態,為了讓這個狀態過度的更加自然,建議新增上 transition屬性。

:disabled 禁用狀態

qq20181228-153338 2x
.btn:disabled, .btn._disabled {
  /* 用css的方式讓元素不可被選中,不支援該屬性的需要用 js 阻止事件提交 */
  pointer-events: none; 
   
  /* 修改滑鼠手型為不允許 */
  cursor: not-allowed; 
  
  /* 修改透明度 */
  opacity: 0.5; 
}
複製程式碼

按鈕不可用狀態,通常是某些只執行一次的操作,在操作完成之後的狀態。或者是需要某些特定觸發條件才能啟用按鈕。 禁用狀態一般比正常按鈕看起來要更弱一點。最簡單的做法是降低透明度,這樣的好處是不用給每個主題單獨去設定一個禁用狀態的顏色。 當然每個主題單獨設定的視覺效果會更好。

並且 button 標籤如果有 disalbed 屬性還可以阻止表單的提交。

:hover 滑鼠移入

qq20181228-153722 2x
/* 滑鼠移入狀態 */
.btn._primary:hover{
      background-color: darken($c_priamry,10%);
      color: darken($c_priamry,10%);
}
複製程式碼

滑鼠移入的狀態和 disabled 的效果會有點相反,通常會讓按鈕變得更重一點。為了統一,我們建議使用,css 前處理器的 darken 函式,來讓我們的主題色,加深 10%

:active 滑鼠按下

qq20181228-154404 2x
/* 滑鼠移入狀態 */
.btn:active{
   transform:scale(0.98);
}
複製程式碼

active 是緊接著 hover 的一個狀態,所以偷懶的話我們忽略這個狀態,直接沿用 hover 的狀態。但是像做得更好的 Metiarl Design 他們採用的是漣漪水波的效果。而我們這邊採用了更簡單的按下變小的邏輯「 感覺很像你拿手把按鈕壓扁了 」。這個效果可以和設計師溝通,權衡一下收益。

:focus 獲取焦點

button{ 
  outline:none;
}
複製程式碼

focus 也是容易被大家忽略,甚至是為了視覺效果而被捨棄。因為主流瀏覽器預設的 focus 狀態一般會是一個藍色漸變的陰影,通常來說設計師會認為不好看。於是我們經常會被要求用以上的程式碼去掉瀏覽器預設的行為。

對於這一點我們是非常不推崇的,因為focus 對於無障礙訪問是非常重要的時候,當你的滑鼠不能使用的時候,在有focus 的狀態下,別人也知道當前焦點的位置,從而也能進行操作。偷懶的做法是我們什麼都不做,沿用瀏覽器的預設行為,如果設計師說不好看,那請麻煩設計師拿出好看的替代方案,而不是直接去掉。

:loading 載入狀態

Loading

除了以上 4 個預設的瀏覽器按鈕狀態。通常情況下,在按鈕按下之後等待 Ajax 請求結果的這段時間,我們會通過一個額外的 Loading 狀態來告訴使用者此時正在載入。並且在這段時間,一般不允許使用者的二次操作,所以我們此時的狀態建議是基於 disabled 的擴充。

此時採用 GIF 動圖會是一個體驗比較好的方案,但是因為我們有多種主題和其它的狀態,我們就需要對不同狀態做不同的 GIF 這難免有點繁瑣。所以我們通常建議是使用CSS 動畫來處理這部分的邏輯。

在我們專案裡,我們採用了兩個小圈的效果「 類似於大白眨眼睛的效果 」。因為我們能簡單操控的偽元素,有 before, after 所以在和設計師溝通的時候我們希望只操作兩個元素。

高階按鈕

以上的按鈕都是我們國際通用的標準化的形態。當然在我們實際的工作狀態中,我們不可能只有這麼單一形態的按鈕。很多時候我們會有基於這些按鈕擴充出的新的高階按鈕。

可能被忽略的"按鈕元件"細節

對於這樣的按鈕,我們不推薦直接在以上的標準的按鈕中相容這些邏輯,哪怕只是在這個基礎按鈕中就加一行程式碼就實現了擴充。而應該是繼承這個按鈕擴充出一個新的按鈕。

<Button small warning>基礎按鈕</Button>
<ButtonPop small warning popNum="3">高階氣泡按鈕</ButtonPop>
複製程式碼
  • 程式碼膨脹率 隨著我們按鈕形態的豐富,基礎按鈕程式碼膨脹率的也不會不受控的增長;
  • 程式碼耦合 我要刪除一個高階按鈕,我只需要直接刪除這個高階元件按鈕就好,而不需要在基礎按鈕樣式中去找關於這個高階按鈕邏輯的程式碼;

END

因為按鈕已經有了很多最佳實踐,我們很容易進入拿來主義的誤區。然後在開發到後期的時候發現,這個最佳實踐和我們實際專案不一定是 Match ,然後進入到縫縫補補的狀態。所以最後給大家的建議是前期還是儘量多花一點時間和設計溝通,和組內其它同學溝通。就是適合自己的就是最好的。

參考文章

  1. 基於CSS color屬性的靜態UI元件重構策略
  2. bootstrap 按鈕元件
  3. ant design 按鈕元件
  4. material-ui 按鈕元件
  5. react-bootstrap 按鈕元件

相關文章