Créer ses composants avec styled-system et styled-components

par

Hugo

Hugo Foyart

9 minutes de lecture

Créer ses composants avec styled-system et styled-components

Chez Premier Octet comme vous le savez peut-être, nous sommes de grands fans de Chakra UI, une librairie de composants dont le fonctionnement repose grandement sur la librairie styled-system. Nous allons dans cet article plonger dans la découverte de cet outil qui, combiné à styled-components s'avère très puissant de part sa simplicité d'utilisation et sa capacité à mettre en place un design system sur une application de manière rapide et efficace.

Qu'est-ce qu'un design system ?

Un design system regroupe l'ensemble des règles à respecter dans le but de réaliser les différents composants qui constituent le ou les écrans d'une ou plusieurs applications. On y retrouvera notamment une palette de couleurs, les différentes styles de typographies, les espacements, des tailles à respecter qui permettront ainsi la constitution de composants d'une application. Le but au final est que l'ensemble des applications utilisant ce design system aient une identité commune.

Qu'est-ce que styled-system ?

styled-system est une librairie qui, combiné à une autre librairie de CSS-in-JS telle que styled-components ou emotion, nous permet de styliser nos composants directement via les props de ceux-ci. Autrement dit, finies les classes CSS ou les styles inline. Il est aussi possible d'associer les valeurs passées en props à celle d'un thème, c'est aussi une des forces de la librairie qui, en plus de ça, supporte le responsive.

Initialisation

Commençons par installer le nécessaire.

yarn add styled-system styled-components

Si vous utilisez Typescript, il faut ajouter les librairies de typages.

yarn add -D @types/styled-system @types/styled-components

Nous sommes maintenant prêt à créer nos premiers composants.

Notre premier composant

Créons le composant le plus basique possible que nous appellerons Box :

// src/components/Box.ts
import styled from 'styled-components';
import {
  background,
  BackgroundProps,
  border,
  BorderProps,
  color,
  ColorProps,
  flexbox,
  FlexboxProps,
  grid,
  GridProps,
  layout,
  LayoutProps,
  position,
  PositionProps,
  shadow,
  ShadowProps,
  space,
  SpaceProps,
} from 'styled-system';

export type BoxProps = LayoutProps &
  BorderProps &
  ColorProps &
  FlexboxProps &
  GridProps &
  BackgroundProps &
  SpaceProps &
  PositionProps &
  ShadowProps;

const Box = styled.div<BoxProps>`
  ${layout}
  ${space}
  ${color}
  ${flexbox}
  ${grid}
  ${background}
  ${border}
  ${position}
  ${shadow}
`;

export default Box;

Explication

Nous avons ici créé une div stylisée via styled-component grâce à styled.div. Au niveau du style, on y retrouve un ensemble de variables importées depuis styled-system. Il s'agit en fait de fonctions qui vont associer nos props à un style CSS, puis y appliquer si possible une valeur provenant de notre thème. Il est possible de retrouver la liste des fonctions, leurs styles CSS associés ainsi que les valeurs du thèmes associées sur la documentation de la librairie.

Le thème

Créons du coup notre thème :

// src/theme.ts
import { Theme } from 'styled-system';

const theme: Theme = {
  colors: {
    darkblue: '#0B3954',
    lightblue: '#BFD7EA',
    red: '#FF6663',
    lime: '#E0FF4F',
    white: '#FEFFFE',
  },
  sizes: {
    sm: '5rem',
    md: '10rem',
    lg: '20rem',
  },
  space: {
    sm: '1rem',
    md: '2rem',
    lg: '5rem',
    xl: '8rem',
  },
  radii: {
    sm: '0.5rem',
    md: '0.8rem',
    lg: '1.5rem',
    round: '50%',
  },
};

export default theme;

Nous avons ici créé une palette de couleurs, ainsi qu'un ensemble d'alias pour des tailles, des espacements et des rayons de bordures. On pourra encore une fois retrouver les différentes clés possibles de notre thème sur la documentation.

Application du thème

Appliquons maintenant notre thème à notre composant Box créé plus tôt :

// src/App.tsx
import { ThemeProvider } from 'styled-components';
import Box from './components/Box';
import theme from './theme';

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <Box size="lg" borderRadius="round" bg="red" my="sm" />
    </ThemeProvider>
  );
}

Super, nous avons nos Box qui utilisent les valeurs de notre thème. Ajoutons-y un peu de texte. Pour cela, nous allons créer un composant Text :

// src/components/Text.ts
import styled from 'styled-components';
import { color, ColorProps, space, SpaceProps, typography, TypographyProps } from 'styled-system';

export type TextProps = TypographyProps & SpaceProps & ColorProps;

const Text = styled.p<TextProps>`
  ${typography}
  ${space}
  ${color}
`;

export default Text;

Ajoutons quelques éléments à notre thème par la même occasion :

// src/components/theme.ts
fontSizes: {
  sm: "0.7rem",
  md: "1.2rem",
  lg: "1.8rem",
  xl: "2.2rem"
},
fonts: {
  title: "Comic Sans MS",
  body: "Helvetica"
}

Puis ajoutons notre nouveau composant à notre interface :

// src/App.tsx
import { ThemeProvider } from 'styled-components';
import Box from './components/Box';
import Text from './components/Text';
import theme from './theme';

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <Text fontFamily="title" fontSize="xl" as="h1">
        Mon titre génial
      </Text>
      <Text fontFamily="body" fontSize="md">
        Mon rond :
      </Text>
      <Box size="lg" borderRadius="round" bg="red" my="sm" />
    </ThemeProvider>
  );
}

La propriété as permet de changer le composant de base utilisé par styled-components. Ainsi dans le cas présent, notre texte utilisera une balise h1 au lieu d'une balise p.

Les variantes

Comme nous l'avons fait ci-dessus, notre titre est un composant Text ayant une police et une taille précise. Pour rendre cela réutilisable en réécrivant le moins de code possible, il est possible d'utiliser un système de variante qui va regrouper un ensemble de styles au sein d'un seul objet nommé. Créons des variantes pour notre composant Text :

// src/components/Text.ts
import styled from 'styled-components';
import { color, ColorProps, space, SpaceProps, typography, TypographyProps, variant } from 'styled-system';

export type TextVariant = 'title' | 'body';

export type TextProps = TypographyProps & SpaceProps & ColorProps & { variant?: TextVariant };

const Text = styled.p<TextProps>`
  ${typography}
  ${space}
  ${color}
  ${variant({
    prop: 'variant',
    variants: {
      title: {
        fontFamily: 'title',
        fontSize: 'xl',
      },
      body: {
        fontFamily: 'Helvetica',
        fontSize: 'md',
      },
    },
  })}
`;

export default Text;

Puis utilisons nos variantes sur nos textes :

// src/App.tsx
import { ThemeProvider } from 'styled-components';
import Box from './components/Box';
import Text from './components/Text';
import theme from './theme';

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <Text variant="title" as="h1">
        Mon titre génial
      </Text>
      <Text variant="body">Mon rond :</Text>
      <Box size="lg" borderRadius="round" bg="red" my="sm" />
    </ThemeProvider>
  );
}

Bien que nous aurions pu créer un composant pour le titre, le but des variantes est justement de nous permettre de ne pas avoir besoin de faire cela. Un exemple de composant Title que nous aurions pu faire en alternative aurait pu ressembler à cela :

const Title = ({ children }) => (
  <Text fontFamily="title" fontSize="xl">
    {children}
  </Text>
);

Bien que cela fasse sens, cela veut dire que nous aurions aussi dû créer un composant pour un sous-titre par exemple, ou pour un paragraphe tout simple. Cette manière de faire nous permet donc en quelque sorte de créer des composants via des props.

Il est tout à fait possible de styliser des pseudo classes au sein de nos variantes :

// src/components/Text.ts
title: {
  fontFamily: "title",
  fontSize: "xl",
  position: "relative",
  "&:hover": {
    color: "red",
    "&::after": {
      content: '" "',
      height: 10,
      bg: "red",
      position: "absolute",
      left: 0,
      right: 0,
      bottom: 0
    }
  }
},

Gestion du responsive

styled-system propose une gestion du responsive au sein des valeurs que l'on passe en props de nos composants. Pour cela, définissons nos breakpoints dans le thème.

// src/theme.ts
breakpoints: ['40em', '52em', '64em', '80em'];

La définition des breakpoints dans le thème est optionnelle. En effet, styled-system propose des breakpoints par défaut.

Appliquons maintenant ça à une de nos Box. Les styles responsives doivent être appliqués sous la forme d'un array dans le même ordre que les breakpoints appliqués à notre thème :

// src/App.tsx
<Box size={['sm', 'sm', 'md']} bg={['lightblue', 'lightblue', 'red']} my="sm" gridColumn="1" />

En redimensionnant l'écran, on constatera que la taille de notre Box ainsi que sa couleur change. Les styles responsives peuvent s'appliquer à toutes les props gérées par styled-system.

Utilisation avec React Native

Du fait que styled-components propose un support pour React Native, il est donc possible d'utiliser styled-system pour React Native. Il suffit de changer notre import de la variable styled là où elle est utilisée :

import styled from 'styled-components/native';

Ainsi pour notre composant Box, nous utiliserons styled.View au lieu de styled.div. Pour le composant Text, on remplacera styled.p par styled.Text.

Etant donné que React Native ne propose pas de notion de pseudo éléments, ceux-ci ne fonctionneront donc pas. Cela pourra donc être un peu plus trivial pour gérer par exemple un cas où on aura un bouton en état disabled. Nous n'aurons pas non plus de gestion de responsive étant donné que styled-system gère cela via des media queries, non supportées sur React Native.

De manière générale, l'utilisation reste très similaire à ce que l'on a en web

Style props customisées

Il est possible d'ajouter nos propres props et de les associer ou non à un style CSS particulier grâce à la fonction system.

Prenons le cas d'une View en React Native, qui accepte un style elevation permettant de gérer les ombres sur Android. Ce style n'est pas géré par styled-components, nous allons donc faire en sorte que l'on puisse passer ce style en prop et y associer une valeur. Nous y ajouterons une autre prop edgesInsets permettant de définir le placement d'une vue positionnée de manière absolue par rapport aux bords de son parent relatif.

// src/components/Box.ts
import styled from 'styled-components';
import {
  background,
  BackgroundProps,
  border,
  BorderProps,
  color,
  ColorProps,
  flexbox,
  FlexboxProps,
  grid,
  GridProps,
  layout,
  LayoutProps,
  position,
  PositionProps,
  shadow,
  ShadowProps,
  space,
  SpaceProps,
  system,
} from 'styled-system';

export type BoxProps = LayoutProps &
  BorderProps &
  ColorProps &
  FlexboxProps &
  GridProps &
  BackgroundProps &
  SpaceProps &
  PositionProps &
  ShadowProps & { elevation?: number; edgesInsets?: number };

const Box = styled.div<BoxProps>`
  ${layout}
  ${space}
  ${color}
  ${flexbox}
  ${grid}
  ${background}
  ${border}
  ${position}
  ${shadow}
  ${system({
    elevation: true,
    edgesInsets: {
      properties: ['top', 'left', 'right', 'bottom'],
    },
  })}
`;

export default Box;

Et voilà ! Il est possible maintenant de passer les props elevation et edgeInsets à notre Box. Le fait d'avoir assigné un booléen à elevation signifie que l'on renvoie la valeur brute sans forcément l'assigner à une propriété de notre thème ou à un style CSS. Dans le cas de edgesInsets, on assigne la valeur passée à la prop aux styles décrits dans properties.

Le responsive est géré pour ces nouvelles props, tant que l'on reste dans un contexte web bien entendu. Tous les détails sur l'utilisation de la fonction system sont expliqués sur la documentation.

Storybook

Afin d'avoir une vue d'ensemble de nos composants, il est tout à fait possible d'utiliser Storybook. Pour l'implémentation, je ne peux que vous rediriger vers un article précédemment créé abordant le sujet.

En conclusion

Pour les fans de librairies CSS-in-JS, la combinaison styled-system + styled-components se veut extrêmement efficace. Simple d'utilisation, elle nous permet de mettre en place très rapidement des composants basiques suivants les règles d'un design system. Si vous n'aimez pas styled-components, styled-system fonctionne aussi avec emotion. Vous pourrez retrouver ci-dessous le code final de nos composants et vous amuser à changer certaines propriétés. Bon code !

Continuer la discussion sur Twitter

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub

Naviguez à vue