Styled System 初探 (๑╹◡╹๑)
08 Nov 2019Styled System 是一包工具函式,用來輔助建立 styled component,它的優點主要是可以讓元件建立統一的 API、更便利的撰寫 repoinsive 樣式等。
以下是我的學習筆記。
Install
由於要在 JS 中撰寫 inline-style,因此要安裝一個 CSS-in-JS 的 library,這裡選用 styled-components。
yarn add styled-system styled-components
How it works & usage
幾乎所有 CSS-in-JS 函式庫在建立 styled component 時,都可接受函式(function)作為參數、並代入 props 來動態決定樣式,styled-components
也是。
如下所示,color 的值是由一個 function 決定的,其中 function 會取得 props 的 color,回傳單一一個 CSS string。
import styled from 'styled-components';
const Box = styled.div`
margin: 15px 0;
padding: 15px;
color: ${(props) => props.color};
background: tomato;
border-radius: 10px;
`;
export default Box;
<Box color='#fff'>Hello World</Box>
改成以下寫法會更簡單明瞭。
import styled from 'styled-components';
const getColor = ({ color }) => `color: ${color}`;
const Box = styled.div`
margin: 15px 0;
padding: 15px;
${getColor};
background: tomato;
border-radius: 10px;
`;
export default Box;
承上,若有多個樣式要設定,是不是要一個個單獨設定呢?
其實,我們也可以將很多個樣式的指令組成一個物件放到 styled component 中,而不是一個個單獨設定,如下,getStyles 回傳一個樣式物件,裡面依照元件動態設定的 props 來產生的樣式。
import styled from 'styled-components';
const getStyles = ({ color, bg }) => ({
color,
background: bg,
});
const Box = styled.div`
margin: 15px 0;
padding: 15px;
${getStyles};
border-radius: 10px;
`;
export default Box;
<Box color='#fff' bg='tomato'>
Hello World
</Box>
對照 Styled System 的寫法如下。
import styled from 'styled-components';
import { color } from 'styled-system';
const Box = styled.div`
${color}
margin: 15px 0;
padding: 15px;
border-radius: 10px;
`;
export default Box;
這就是 Styled System 簡單來說想要做的事情-取得 props 來回傳樣式物件-也就是開一個比較大洞、用比較有彈性、有效率的方式來填進樣式。當然也會幫我們做指令縮寫的 mapping 還有提供讓我們寫起來更順手的工具,讓我們的程式碼寫起來更簡潔、更具一致性。
Theme Provider
Example 2:Theme Provider
利用 <ThemeProvider>
和包裝好主題色的物件,就可以擁有樣式更一致的元件,原理是 <ThemeProvider>
利用 React Context(註一)傳遞樣式的設定值。
如下,利用 <ThemeProvider>
當作 root 節點,並將 theme 當成 props 傳進去,之後的元件(例如 <Box>
)指定的樣式就會在 theme 這個物件查找來做設定。
index.js
<Box>
的 white 與 tomato 的色碼會到 theme.js 查找相對應的 key,得到 white 為 #fefefe
,tomato 為 tomato
,注意,若查找不到會自動降級(fallback)為瀏覽器所支援的 CSS color name,即 white 為 #fff
。
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'styled-components';
import Box from './box';
import theme from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
<Box color='white' bg='tomato'>
Hello World
</Box>
</ThemeProvider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
box.js
import styled from 'styled-components';
import { color } from 'styled-system';
const Box = styled.div`
${color}
padding: 15px;
border-radius: 10px;
`;
export default Box;
theme.js
export default {
colors: {
white: '#fefefe',
},
bg: {
tomato: 'tomato',
},
};
Example 3:Variants
元件也可以設定自己的主題樣式,或來自 global 的 theme 設定。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'styled-components';
import Box from './box';
import theme from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
<div className='App'>
<Box variant='primary'>Primary</Box>
<Box variant='secondary'>Secondary</Box>
</div>
</ThemeProvider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
box.js
import styled from 'styled-components';
import { variant } from 'styled-system';
const Box = styled('div')(
variant({
scale: 'boxes',
variants: {
primary: {},
secondary: {},
},
}),
);
export default Box;
依照 scale 的值來查找,也就是「boxes」。
theme.js
export default {
boxes: {
primary: {
color: 'white',
bg: 'tomato',
},
secondary: {
color: 'white',
bg: 'black',
},
},
};
Exmaple 4:真實範例
這裡有一個萬年曆的元件,其中標題 <CalendarTitle>
會隨著不同的檢視狀態而有不同的樣式。在使用 Styled System 前稱為「Before」,利用 Styled System 修改後稱為「After」。
- Before:
<CalendarTitle>
使用屬性 noHover 來決定是否要有 cursor pointer 和 hover 樣式。若 noHover 為 true,則無 cursor pointer(即為 default)且無 hover 背景色(即為白色)。 - After:為
<CalendarTitle>
設定元件的主題樣式 primary 與 secondary,secondary 為 cursor default 且無 hover 背景色。
Before。
<CalendarTitle noHover />
export const CalendarTitle = styled.div`
width: 200px;
padding: 5px 0;
font-weight: bold;
text-align: center;
border-radius: 5px;
cursor: ${({ noHover }) => (noHover ? 'default' : 'pointer')};
:hover {
background: ${({ noHover }) => (noHover ? white : gray)};
}
`;
After。
改為當需要有 hover 狀態時才加入背景和 cursor pointer。由於 Styled System 沒有支援 cursor 因此要用 system 幫忙擴充,並在 hover 樣式中使用 color function 來填入 hover 時的背景色。這樣的修改似乎非常零碎、無系統化。
<CalendarTitle bg='gray' cursor='pointer' />
import { color, system } from 'styled-system';
export const CalendarTitle = styled.div`
${system({
cursor: true,
})}
width: 200px;
padding: 5px 0;
font-weight: bold;
text-align: center;
border-radius: 5px;
:hover {
${color}
}
`;
再次修改如下,將有無 hover 當成是該元件的不同狀態,就可在 theme 或 variant 定義該元件不同狀態的主題樣式,在此用 variant,相較以上的例子來說,比起散落各個指令依照 props 來修改,variant 就更模組化,也更容易了解。
primary 有 hover 狀態。
<CalendarTitle variant='primary' />
secondary 無 hover 狀態。
<CalendarTitle variant='secondary' />
export const CalendarTitle = styled('div')(
{
width: '200px',
padding: '5px 0',
fontWeight: 'bold',
textAlign: 'center',
borderRadius: '5px',
},
variant({
variants: {
primary: {
cursor: 'pointer',
bg: 'white',
'&:hover': {
bg: 'gray',
},
},
secondary: {
cursor: 'default',
'&:hover': {
bg: 'white',
},
},
},
}),
);
原始碼。
Responsive Styles
無論是傳統上撰寫 CSS 或 styled component 的 responsive styles 方式是針對不同 breakpoint 來寫 media query,但這會有一些缺點
- 可能是因為沒有 guideline 或沒有遵守 guideline,而導致 media query 寫法不統一(例如:間隔的定義、巢狀結構的層次),後續維護不易。
- 重複撰寫大量 media query 相關語法。
Styled System 依照傳入的屬性值自動設定 media query 的效果,像是自動對照不同 breakpoint 所要的值、屬性縮寫。若改用 Styled System,則可改進
- Styled System 提供 API 可強制規範讓開發者用一致的方式撰寫。
- 不需要重複撰寫大量 media query 相關語法,只要在 props 傳入針對 breakpoint 指定的數值即可,簡單易懂。
- Styled System 提供縮寫,可讓程式碼更簡潔,例如:
padding-left
可簡寫為pl
。
Example 5
設定元件 <Box>
的寬度,依照不同的 breakpoints 區間以陣列傳入寬度。
// default breakpoints 為 ['40em', '52em', '64em']
<Box width={[1 / 3, 1 / 2, 1]} />
當螢幕寬度小於 40em 時。
.dtQrEl {
width: 33.33333333333333%;
}
當螢幕寬度介於 40em ~ 52em 時。
media screen and (min-width: 40em) .dtQrEl {
width: 50%;
}
當螢幕寬度介於 52em ~ 64em 時。
@media screen and (min-width: 52em) .dtQrEl {
width: 100%;
}
Example 6
若陣列設定的數字小於或等於 theme 的 index 個數,則會當成 index 查找 sizes 陣列。
<Box width={[1, 0, 2]}>
// default breakpoints 為 ['40em', '52em', '64em']
export default {
sizes: [100, 200, 300],
};
當螢幕寬度為
- 小於 40em 時,Box 寬度為 200px
.iJHGrn {
width: 200px;
}
- 介於 40em ~ 52 em 時,Box 寬度為 100px
@media screen and (min-width: 40em) .iJHGrn {
width: 100px;
}
- 介於 52em ~ 64 em 時,Box 寬度為 300px
@media screen and (min-width: 52em) .iJHGrn {
width: 300px;
}
Example 7
若陣列設定的數字大於 theme 的 index 個數,則會轉換為數字,單位是 px。
<Box width={[200, 300, 400]}>
// default breakpoints 為 ['40em', '52em', '64em']
export default {
sizes: [100, 200, 300],
};
由於 200、300、400 皆大於 sizes 陣列的長度,無法當成 index 來取值,因此就直接當成 px 數,也就是當螢幕寬度為
- 小於 40em 時,Box 寬度為 200px
.ePOXVk {
width: 200px;
}
- 介於 40em ~ 52 em 時,Box 寬度為 300px
@media screen and (min-width: 40em) .ePOXVk {
width: 300px;
}
- 介於 52em ~ 64 em 時,Box 寬度為 400px
@media screen and (min-width: 52em) .ePOXVk {
width: 400px;
}
Example 8
設定元件 <Box>
的左右 padding、上下 padding,其中 index 的 3 是指 16,依此類推。
<Box px={[3, 4]} py={[5, 6]} />
// default breakpoints 為 ['40em', '52em', '64em']
const themes = {
space: [0, 4, 8, 16, 32, 64, 128, 256], // margin and padding
};
當螢幕寬度小於 40em 時。
.gyhZND {
padding-left: 16px;
padding-right: 16px;
padding-top: 64px;
padding-bottom: 64px;
}
當螢幕寬度介於 40em ~ 52em 時。
@media screen and (min-width: 40em) .gyhZND {
padding-left: 32px;
padding-right: 32px;
padding-top: 128px;
padding-bottom: 128px;
}
Custom Style Props
若 CSS 指令沒有被 Styled System 支援(註二),則可利用客製化樣式屬性 system 來擴充 Styled System 建立客製化的 style function。
Exmaple 9
system 接受一組物件當作設定,並回傳一個函式。
如下,Styled System 並沒有支援 text-decoration
,可用 system 來擴充。
const Box = styled.div`
${system({
textDecoration: true,
})}
`;
<Box textDecoration='underline' />
備註
- [註一] React Context:可當成一種用來和整串元件樹分享的全域資料,例如:當前使用者和語言、主題樣式,可參考這裡、範例。
- [註二] 這裡的不支援是指 Styled System 沒有支援,也就是 Styled System 沒有挖洞給這個 CSS 指令做設定,而不是瀏覽器的支援度。