Restyle

A type-enforced system for building UI components in React Native with Type...

README

@shopify/restyle


RestyleTheme 2020-02-25 17_43_51

The Restyle library provides a type-enforced system for building UI components in React Native with TypeScript. It's a library for building UI libraries, with themability as the core focus.

This library assumes that the UI is built upon a design system that (at the very least) defines a set of colors and spacing constants that lays as a foundation. While the library acknowledges that there can be exceptions to the system by allowing any style to be overridden, it keeps the developer most productive when one-off values are kept to a minimum.

Here's an example of how a view built with Restyle components could look:

  1. ```tsx
  2. import {
  3.   ThemeProvider,
  4.   createBox,
  5.   createText,
  6.   createRestyleComponent,
  7.   createVariant,
  8.   VariantProps,
  9. } from '@shopify/restyle';

  10. // See the "Defining Your Theme" readme section below
  11. import theme, {Theme} from './theme';

  12. const Box = createBox<Theme>();
  13. const Text = createText<Theme>();

  14. const Card = createRestyleComponent<
  15.   VariantProps<Theme, 'cardVariants'> & React.ComponentProps<typeof Box>,
  16.   Theme
  17. >([createVariant({themeKey: 'cardVariants'})], Box);

  18. const Welcome = () => {
  19.   return (
  20.     <Box
  21.       flex={1}
  22.       backgroundColor="mainBackground"
  23.       paddingVertical="xl"
  24.       paddingHorizontal="m"
  25.     >
  26.       <Text variant="header">Welcome</Text>
  27.       <Box
  28.         flexDirection={{
  29.           phone: 'column',
  30.           tablet: 'row',
  31.         }}
  32.       >
  33.         <Card margin="s" variant="secondary">
  34.           <Text variant="body">This is a simple example</Text>
  35.         </Card>
  36.         <Card margin="s" variant="primary">
  37.           <Text variant="body">Displaying how to use Restyle</Text>
  38.         </Card>
  39.       </Box>
  40.     </Box>
  41.   );
  42. };

  43. const App = () => {
  44.   return (
  45.     <ThemeProvider theme={theme}>
  46.       <Welcome />
  47.     </ThemeProvider>
  48.   );
  49. };
  50. ```

Restyle Component Workflow

Installation


Yarn


  1. ``` sh
  2. $ yarn add @shopify/restyle
  3. ```

NPM


  1. ``` sh
  2. $ npm install @shopify/restyle
  3. ```

Usage


Defining Your Theme


Any project using this library should have a global theme object. It specifies set values for spacing, colors, breakpoints, and more. These values are made available to Restyle components, so that you can for example write backgroundColor="cardPrimary" to use the named color from your theme. In fact, TypeScript enforces the backgroundColor property to _only_ accept colors that have been defined in your theme, and autocompletes values for you in a modern editor.

Below is an example of how a basic theme could look. Make sure to read the sections below for more details on how to set up your different theme values.

  1. ```ts
  2. import {createTheme} from '@shopify/restyle';

  3. const palette = {
  4.   purpleLight: '#8C6FF7',
  5.   purplePrimary: '#5A31F4',
  6.   purpleDark: '#3F22AB',

  7.   greenLight: '#56DCBA',
  8.   greenPrimary: '#0ECD9D',
  9.   greenDark: '#0A906E',

  10.   black: '#0B0B0B',
  11.   white: '#F0F2F3',
  12. };

  13. const theme = createTheme({
  14.   colors: {
  15.     mainBackground: palette.white,
  16.     cardPrimaryBackground: palette.purplePrimary,
  17.   },
  18.   spacing: {
  19.     s: 8,
  20.     m: 16,
  21.     l: 24,
  22.     xl: 40,
  23.   },
  24.   breakpoints: {
  25.     phone: 0,
  26.     tablet: 768,
  27.   },
  28. });

  29. export type Theme = typeof theme;
  30. export default theme;
  31. ```

_Note: createTheme doesn't do anything except enforcing the theme to have the same shape as the BaseTheme, but it preserves the types of your user specific values (e.g. what colors the theme has) so you don't lose typesafety as a result of the { [key:string]: any } in BaseTheme_

This theme should be passed to a ThemeProvider at the top of your React tree:

  1. ```tsx
  2. import {ThemeProvider} from '@shopify/restyle';
  3. import theme from './theme';

  4. const App = () => (
  5.   <ThemeProvider theme={theme}>{/* Rest of the app */}</ThemeProvider>
  6. );
  7. ```

Colors


When working with colors in a design system a common pattern is to have a palette including a number of base colors with darker and lighter shades, see for example the Polaris Color Palette.

This palette should preferrably not be directly included as values in the theme. The naming of colors in the theme object should instead be used to assign semantic meaning to the palette, see this example:

  1. ```ts
  2. const palette = {
  3.   purpleLight: '#8C6FF7',
  4.   purplePrimary: '#5A31F4',
  5.   purpleDark: '#3F22AB',

  6.   greenLight: '#56DCBA',
  7.   greenPrimary: '#0ECD9D',
  8.   greenDark: '#0A906E',

  9.   black: '#0B0B0B',
  10.   white: '#F0F2F3',
  11. };

  12. const theme = createTheme({
  13.   colors: {
  14.     mainBackground: palette.white,
  15.     mainForeground: palette.black,
  16.     cardPrimaryBackground: palette.purplePrimary,
  17.     buttonPrimaryBackground: palette.purplePrimary,
  18.   },
  19. });
  20. ```

Taking the time to define these semantic meanings comes with a number of benefits:

- It's easy to understand where and in what context colors are applied throughout the app
- If changes are made to the palette (e.g. the purple colors are changed to a shade of blue instead), we only have to update what the semantic names point to instead of updating all references to purplePrimary throughout the app.
- Even though cardPrimaryBackground and buttonPrimaryBackground point to the same color in the example above, deciding that buttons should instead be green (while cards remain purple) becomes a trivial change.
- A theme can easily be swapped at runtime.

Spacing


Spacing tends to follow multiples of a given base spacing number, for example 8. We prefer using the t-shirt size naming convention, because of the scalability of it (any number of x's can be prepended for smaller and larger sizes):

  1. ```ts
  2. const theme = createTheme({
  3.   spacing: {
  4.     s: 8,
  5.     m: 16,
  6.     l: 24,
  7.     xl: 40,
  8.   },
  9. });
  10. ```

Breakpoints


Breakpoints are defined as minimum widths (inclusive) for different target screen sizes where we want to apply differing styles. Consider giving your breakpoints names that give a general idea of the type of device the user is using. Breakpoints can be defined by either a single value (width) or an object containing both width and height:

  1. ```ts
  2. const theme = createTheme({
  3.   breakpoints: {
  4.     phone: 0,
  5.     longPhone: {
  6.       width: 0,
  7.       height: 812,
  8.     },
  9.     tablet: 768,
  10.     largeTablet: 1024,
  11.   },
  12. });
  13. ```

See the Responsive Values section to see how these can be used.

Accessing the Theme


If you need to manually access the theme outside of a component created with Restyle, use the useTheme hook:

  1. ```tsx
  2. const Component = () => {
  3.   const theme = useTheme<Theme>();
  4.   const {cardPrimaryBackground} = theme.colors;
  5.   // ...
  6. };
  7. ```

By doing this instead of directly importing the theme object, it becomes easy to swap the theme out during runtime to for example implement a dark mode switch in your app.

Predefined Components


This library comes with predefined functions to create a Box and Text component, as seen in action in the introductory example. These come as functions instead of ready-made components to give you a chance to provide the type of your theme object. Doing this will make all props that map to theme values have proper types configured, based on what's available in your theme.

Box


  1. ```tsx
  2. // In Box.tsx
  3. import {createBox} from '@shopify/restyle';
  4. import {Theme} from './theme';

  5. const Box = createBox<Theme>();

  6. export default Box;
  7. ```

The Box component comes with the following Restyle functions:backgroundColor, opacity, visible, layout, spacing, border, shadow, position.

Text


  1. ```tsx
  2. // In Text.tsx
  3. import {createText} from '@shopify/restyle';
  4. import {Theme} from './theme';

  5. const Text = createText<Theme>();

  6. export default Text;
  7. ```

The Text component comes with the following Restyle functions:color, opacity, visible, typography, textShadow, spacing. It also includes a variant that picks up styles under thetextVariants key in your theme:

  1. ```tsx
  2. // In your theme
  3. const theme = createTheme({
  4.   ...,
  5.   textVariants: {
  6.     header: {
  7.       fontFamily: 'ShopifySans-Bold',
  8.       fontWeight: 'bold',
  9.       fontSize: 34,
  10.       lineHeight: 42.5,
  11.       color: 'black',
  12.     },
  13.     subheader: {
  14.       fontFamily: 'ShopifySans-SemiBold',
  15.       fontWeight: '600',
  16.       fontSize: 28,
  17.       lineHeight: 36,
  18.       color: 'black',
  19.     },
  20.     body: {
  21.       fontFamily: 'ShopifySans',
  22.       fontSize: 16,
  23.       lineHeight: 24,
  24.       color: 'black',
  25.     },
  26.   },
  27. });

  28. // In a component
  29. <Text variant="header">Header</Text>
  30. ```

Custom Components


If you want to create your own component similar to Box or Text, but decide
yourself which Restyle functions to use, use the
createRestyleComponent helper:

  1. ```ts
  2. import {
  3.   createRestyleComponent,
  4.   createVariant,
  5.   spacing,
  6.   SpacingProps,
  7.   VariantProps,
  8. } from '@shopify/restyle';
  9. import {Theme} from './theme';

  10. type Props = SpacingProps<Theme> & VariantProps<Theme, 'cardVariants'>;
  11. const Card = createRestyleComponent<Props, Theme>([
  12.   spacing,
  13.   createVariant({themeKey: 'cardVariants'}),
  14. ]);

  15. export default Card;
  16. ```

For more advanced components, you may want to instead use the useRestyle hook:

  1. ```tsx
  2. import {TouchableOpacity, View} from 'react-native';
  3. import {
  4.   useRestyle,
  5.   spacing,
  6.   border,
  7.   backgroundColor,
  8.   SpacingProps,
  9.   BorderProps,
  10.   BackgroundColorProps,
  11.   composeRestyleFunctions,
  12. } from '@shopify/restyle';

  13. import Text from './Text';
  14. import {Theme} from './theme';

  15. type RestyleProps = SpacingProps<Theme> &
  16.   BorderProps<Theme> &
  17.   BackgroundColorProps<Theme>;

  18. const restyleFunctions = composeRestyleFunctions<Theme, RestyleProps>([
  19.   spacing,
  20.   border,
  21.   backgroundColor,
  22. ]);

  23. type Props = RestyleProps & {
  24.   onPress: () => void;
  25. };

  26. const Button = ({onPress, label, ...rest}: Props) => {
  27.   const props = useRestyle(restyleFunctions, rest);

  28.   return (
  29.     <TouchableOpacity onPress={onPress}>
  30.       <View {...props}>
  31.         <Text variant="buttonLabel">{label}</Text>
  32.       </View>
  33.     </TouchableOpacity>
  34.   );
  35. };
  36. ```

Restyle Functions


Restyle functions are the bread and butter of Restyle. They specify how props should be mapped to values in a resulting style object, that can then be passed down to a React Native component. The props support responsive values and can be mapped to values in your theme.

Predefined Restyle Functions


The Restyle library comes with a number of predefined Restyle functions for your convenience. Properties within brackets are aliases / shorthands for the preceding prop name.

RestylePropsTheme
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
backgroundColorbackgroundColorcolors
colorcolorcolors
opacityopacity_none_
visibledisplay_none_
spacingmarginspacing
layoutwidth,_none_
positionposition,_none_
positionzIndexzIndices
borderborderBottomWidth,_none_
borderborderColor,colors
borderborderRadius,borderRadii
shadowshadowOpacity,_none_
shadowshadowColorcolors
textShadowtextShadowOffset,_none_
textShadowtextShadowColorcolors
typographyfontFamily,_none_

Custom Restyle Functions


To define your own Restyle function, use the createRestyleFunction helper:

  1. ```ts
  2. import {createRestyleFunction, createRestyleComponent} from '@shopify/restyle'
  3. const transparency = createRestyleFunction({
  4.   property: 'transparency',
  5.   styleProperty: 'opacity',
  6.   transform: ({value}: {value: number}) => 1 - value,
  7. });

  8. const TransparentComponent = createRestyleComponent([transparency])

  9. <TransparentComponent transparency={0.5} />
  10. ```

Arguments:

- property: The name of the component prop that the function will receive the value of.
- styleProperty: The name of the property in the style object to map to. Defaults to the value of property.
- transform({value, theme, themeKey}): An optional function that transforms the value of the prop to the value that will be inserted into the style object.
- themeKey: An optional key in the theme to map values from, e.g. colors.

Variants


A variant is a form of Restyle function that maps a prop into multiple other props to use with Restyle functions. A variant needs to always map to a key in the theme.

  1. ```ts
  2. // In theme
  3. const theme = createTheme({
  4.   // ...
  5.   spacing: {
  6.     s: 8,
  7.     m: 16,
  8.     l: 24,
  9.   },
  10.   colors: {
  11.     cardRegularBackground: '#EEEEEE',
  12.   },
  13.   breakpoints: {
  14.     phone: 0,
  15.     tablet: 768,
  16.   },
  17.   cardVariants: {
  18.     defaults: {
  19.       // We can define defaults for the variant here.
  20.       // This will be applied after the defaults passed to createVariant and before the variant defined below.
  21.     },
  22.     regular: {
  23.       // We can refer to other values in the theme here, and use responsive props
  24.       padding: {
  25.         phone: 's',
  26.         tablet: 'm',
  27.       },
  28.     },
  29.     elevated: {
  30.       padding: {
  31.         phone: 's',
  32.         tablet: 'm',
  33.       },
  34.       shadowColor: '#000',
  35.       shadowOpacity: 0.2,
  36.       shadowOffset: {width: 0, height: 5},
  37.       shadowRadius: 15,
  38.       elevation: 5,
  39.     }
  40.   }
  41. })

  42. import {createVariant, createRestyleComponent, VariantProps} from '@shopify/restyle'
  43. import {Theme} from './theme';
  44. const variant = createVariant<Theme>({themeKey: 'cardVariants', defaults: {
  45.   margin: {
  46.     phone: 's',
  47.     tablet: 'm',
  48.   },
  49.   backgroundColor: 'cardRegularBackground',
  50. }})

  51. const Card = createRestyleComponent<VariantProps<Theme, 'cardVariants'>, Theme>([variant])

  52. <Card variant="elevated" />
  53. ```

Arguments:

- property: The name of the component prop that will map to a variant. Defaults to variant.
- themeKey: A key in the theme to map values from. Unlike createRestyleFunction, this option _is required_ to create a variant.
- defaults: The default values to apply before applying anything from the values in the theme.

Responsive Values


Any prop powered by Restyle can optionally accept a value for each screen size, as defined by the breakpoints object in the theme:

  1. ```tsx
  2. // In your theme
  3. const theme = createTheme({
  4.   // ...
  5.   breakpoints: {
  6.     phone: 0,
  7.     tablet: 768,
  8.   }
  9. })

  10. // Props always accept either plain values
  11. <Box flexDirection="row" />

  12. // Or breakpoint-specific values
  13. <Box flexDirection={{phone: 'column', tablet: 'row'}} />
  14. ```

If you need to extract the value of a responsive prop in a custom component (e.g. to use it outside of component styles), you can use the useResponsiveProp hook:

  1. ```tsx
  2. import {
  3.   ColorProps,
  4.   createBox,
  5.   useResponsiveProp,
  6.   useTheme,
  7. } from '@shopify/restyle';
  8. import React from 'react';
  9. import {
  10.   ActivityIndicator,
  11.   TouchableOpacity,
  12.   TouchableOpacityProps,
  13. } from 'react-native';

  14. import Text from './Text';
  15. import {Theme} from './theme';

  16. const BaseButton = createBox<Theme, TouchableOpacityProps>(TouchableOpacity);

  17. type Props = React.ComponentProps<typeof BaseButton> &
  18.   ColorProps<Theme> & {
  19.     label: string;
  20.     isLoading?: boolean;
  21.   };

  22. const Button = ({
  23.   label,
  24.   isLoading,
  25.   color = {phone: 'purple', tablet: 'blue'},
  26.   ...props
  27. }: Props) => {
  28.   const theme = useTheme<Theme>();

  29.   // Will be 'purple' on phone and 'blue' on tablet
  30.   const textColorProp = useResponsiveProp(color);

  31.   // Can safely perform logic with the extracted value
  32.   const bgColor = textColorProp === 'purple' ? 'lightPurple' : 'lightBlue';

  33.   return (
  34.     <BaseButton flexDirection="row" backgroundColor={bgColor} {...props}>
  35.       <Text
  36.         variant="buttonLabel"
  37.         color={color}
  38.         marginRight={isLoading ? 's' : undefined}
  39.       >
  40.         {label}
  41.       </Text>
  42.       {isLoading ? (
  43.         <ActivityIndicator color={theme.colors[textColorProp]} />
  44.       ) : null}
  45.     </BaseButton>
  46.   );
  47. };
  48. ```

Overriding Styles


Any Restyle component also accepts a regular style property and will apply it after all other styles, which means that you can use this to do any overrides that you might find necessary.

  1. ```tsx
  2. <Box
  3.   margin="s"
  4.   padding="m"
  5.   style={{
  6.     backgroundColor: '#F00BAA',
  7.   }}
  8. />
  9. ```

Implementing Dark Mode


Of course, no app is complete without a dark mode. Here a simple example of how you would implement it:

  1. ```tsx
  2. import React, {useState} from 'react';
  3. import {Switch} from 'react-native';
  4. import {
  5.   ThemeProvider,
  6.   createBox,
  7.   createText,
  8.   createTheme,
  9. } from '@shopify/restyle';

  10. export const palette = {
  11.   purple: '#5A31F4',
  12.   white: '#FFF',
  13.   black: '#111',
  14.   darkGray: '#333',
  15.   lightGray: '#EEE',
  16. };

  17. const theme = createTheme({
  18.   spacing: {
  19.     s: 8,
  20.     m: 16,
  21.   },
  22.   colors: {
  23.     mainBackground: palette.lightGray,
  24.     mainForeground: palette.black,

  25.     primaryCardBackground: palette.purple,
  26.     secondaryCardBackground: palette.white,
  27.     primaryCardText: palette.white,
  28.     secondaryCardText: palette.black,
  29.   },
  30.   breakpoints: {},
  31.   textVariants: {
  32.     body: {
  33.       fontSize: 16,
  34.       lineHeight: 24,
  35.       color: 'mainForeground',
  36.     },
  37.   },
  38.   cardVariants: {
  39.     primary: {
  40.       backgroundColor: 'primaryCardBackground',
  41.       shadowOpacity: 0.3,
  42.     },
  43.     secondary: {
  44.       backgroundColor: 'secondaryCardBackground',
  45.       shadowOpacity: 0.1,
  46.     },
  47.   },
  48. });

  49. type Theme = typeof theme;

  50. const darkTheme: Theme = {
  51.   ...theme,
  52.   colors: {
  53.     ...theme.colors,
  54.     mainBackground: palette.black,
  55.     mainForeground: palette.white,

  56.     secondaryCardBackground: palette.darkGray,
  57.     secondaryCardText: palette.white,
  58.   },
  59. };

  60. const Box = createBox<Theme>();
  61. const Text = createText<Theme>();

  62. const App = () => {
  63.   const [darkMode, setDarkMode] = useState(false);
  64.   return (
  65.     <ThemeProvider theme={darkMode ? darkTheme : theme}>
  66.       <Box padding="m" backgroundColor="mainBackground" flex={1}>
  67.         <Box
  68.           backgroundColor="primaryCardBackground"
  69.           margin="s"
  70.           padding="m"
  71.           flexGrow={1}
  72.         >
  73.           <Text variant="body" color="primaryCardText">
  74.             Primary Card
  75.           </Text>
  76.         </Box>
  77.         <Box
  78.           backgroundColor="secondaryCardBackground"
  79.           margin="s"
  80.           padding="m"
  81.           flexGrow={1}
  82.         >
  83.           <Text variant="body" color="secondaryCardText">
  84.             Secondary Card
  85.           </Text>
  86.         </Box>
  87.         <Box marginTop="m">
  88.           <Switch
  89.             value={darkMode}
  90.             onValueChange={(value: boolean) => setDarkMode(value)}
  91.           />
  92.         </Box>
  93.       </Box>
  94.     </ThemeProvider>
  95.   );
  96. };

  97. export default App;
  98. ```

Getting Setup With the Shopify Design System


To start using Shopify style assets we can leverage Polaris tokens. You can see all of the tokens here.

Installation


Using npm:

npm install @shopify/polaris-tokens --save

Using yarn:

yarn add @shopify/polaris-tokens

Define Your Theme


  1. ```tsx
  2. // In theme
  3. import tokens from '@shopify/polaris-tokens';
  4. import {createTheme} from '@shopify/restyle';

  5. const pxToNumber = (px: string) => {
  6.   return parseInt(px.replace('px', ''), 10);
  7. };

  8. const theme = createTheme({
  9.   colors: {
  10.     body: tokens.colorBlack,
  11.     backgroundRegular: tokens.colorWhite,
  12.     backgroundSubdued: tokens.colorSkyLighter,

  13.     foregroundRegular: tokens.colorBlack,
  14.     foregroundOff: tokens.colorInkLight,
  15.     foregroundSubdued: tokens.colorInkLightest,
  16.     foregroundContrasting: tokens.colorWhite,
  17.     foregroundSuccess: tokens.colorGreenDark,

  18.     highlightPrimary: tokens.colorIndigo,
  19.     highlightPrimaryDisabled: tokens.colorIndigoLight,

  20.     buttonBackgroundPlain: tokens.colorSky,
  21.     errorPrimary: tokens.colorRed,

  22.     iconBackgroundDark: tokens.colorBlueDarker,
  23.   },
  24.   spacing: {
  25.     none: tokens.spacingNone,
  26.     xxs: pxToNumber(tokens.spacingExtraTight),
  27.     xs: pxToNumber(tokens.spacingTight),
  28.     s: pxToNumber(tokens.spacingBaseTight),
  29.     m: pxToNumber(tokens.spacingBase),
  30.     l: pxToNumber(tokens.spacingLoose),
  31.     xl: pxToNumber(tokens.spacingExtraLoose),
  32.     xxl: 2 * pxToNumber(tokens.spacingExtraLoose),
  33.   },
  34. });

  35. export type Theme = typeof theme;
  36. export default theme;
  37. ```

Now you can easily style your components with Shopify Polaris.

Migrating to restyle v2


Read more about migration to v2 here

Inspiration


Restyle is heavily inspired by https://styled-system.com.

Contributing


For help on setting up the repo locally, building, testing, and contributing
please see CONTRIBUTING.md.

Code of Conduct


All developers who wish to contribute through code or issues, take a look at the

License


MIT, see LICENSE.md for details.