Styled System 初探 (๑╹◡╹๑)

Styled 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 />
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,但這會有一些缺點

Styled System 依照傳入的屬性值自動設定 media query 的效果,像是自動對照不同 breakpoint 所要的值、屬性縮寫。若改用 Styled System,則可改進

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],
};

當螢幕寬度為

.iJHGrn {
  width: 200px;
}
@media screen and (min-width: 40em) .iJHGrn {
  width: 100px;
}
@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 數,也就是當螢幕寬度為

.ePOXVk {
  width: 200px;
}
@media screen and (min-width: 40em) .ePOXVk {
  width: 300px;
}
@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' />

備註

References


styled-components styled-system CSS in JS css