Restyle

repository·master·Indexed 25 days ago

https://github.com/shopify/restyle

A type-enforced theming system for React Native built with TypeScript. Restyle provides a constraint-based system for building consistent UI libraries using design system tokens for colors, spacing, and typography. It includes utilities like createTheme, ThemeProvider, and helpers for creating Box and Text components, as well as support for responsive values via breakpoints.

Tokens
13.3K
Snippets
30
Records
54
Agent score
83%

What's inside @shopify/restyle

  1. Overview of Restyle

    master
    Restyle is a type-enforced system for building UI components in React Native with TypeScript. It is designed specifically for building UI libraries with a core focus on themability. The library assumes a design system foundation (such as colors and spacing constants) and encourages productivity by minimizing one-off style values, though it still allows for style overrides.
  2. Quickstart with Restyle components

    master

    Restyle provides a type-enforced system for building themed UI components. To use it, you typically define a theme, create base components like Box and Text using createBox and createText, and wrap your application in a ThemeProvider.

    import {
      ThemeProvider,
      createBox,
      createText,
      createRestyleComponent,
      createVariant,
      VariantProps,
    } from '@shopify/restyle';
    
    // Import your custom theme and its type
    import theme, {Theme} from './theme';
    
    // Create base components tied to your theme type
    const Box = createBox<Theme>();
    const Text = createText<Theme>();
    
    // Create a custom component with variants
    const Card = createRestyleComponent<
      VariantProps<Theme, 'cardVariants'> & React.ComponentProps<typeof Box>,
      Theme
    >([createVariant({themeKey: 'cardVariants'})], Box);
    
    const Welcome = () => {
      return (
        <Box
          flex={1}
          backgroundColor="mainBackground"
          paddingVertical="xl"
          paddingHorizontal="m"
        >
          <Text variant="header">Welcome</Text>
          <Box
            flexDirection={{
              phone: 'column',
              tablet: 'row',
            }}
          >
            <Card margin="s" variant="secondary">
              <Text variant="body">This is a simple example</Text>
            </Card>
            <Card margin="s" variant="primary">
              <Text variant="body">Displaying how to use Restyle</Text>
            </Card>
          </Box>
        </Box>
      );
    };
    
    const App = () => {
      return (
        <ThemeProvider theme={theme}>
          <Welcome />
        </ThemeProvider>
      );
    };
  3. Implement dark mode with Restyle

    master

    To implement dark mode in Restyle, create a base theme using createTheme and then define a secondary theme object that overrides specific color tokens. You can then toggle between these themes by passing the active theme to the ThemeProvider component.

    Key steps:

    1. Define your base theme with createTheme.
    2. Create a type for your theme using typeof theme to ensure type safety when defining the dark theme.
    3. Create a dark theme object by spreading the base theme and overriding the colors property.
    4. Use a state variable to track the current mode and pass the corresponding theme to <ThemeProvider theme={...} />.
    import React, {useState} from 'react';
    import {Switch} from 'react-native';
    import {
      ThemeProvider,
      createBox,
      createText,
      createTheme,
    } from '@shopify/restyle';
    
    export const palette = {
      purple: '#5A31F4',
      white: '#FFF',
      black: '#111',
      darkGray: '#333',
      lightGray: '#EEE',
    };
    
    const theme = createTheme({
      spacing: {
        s: 8,
        m: 16,
      },
      colors: {
        mainBackground: palette.lightGray,
        mainForeground: palette.black,
    
        primaryCardBackground: palette.purple,
        secondaryCardBackground: palette.white,
        primaryCardText: palette.white,
        secondaryCardText: palette.black,
      },
      textVariants: {
        defaults: {},
        body: {
          fontSize: 16,
          lineHeight: 24,
          color: 'mainForeground',
        },
      },
      cardVariants: {
        defaults: {},
        primary: {
          backgroundColor: 'primaryCardBackground',
          shadowOpacity: 0.3,
        },
        secondary: {
          backgroundColor: 'secondaryCardBackground',
          shadowOpacity: 0.1,
        },
      },
    });
    
    type Theme = typeof theme;
    
    const darkTheme: Theme = {
      ...theme,
      colors: {
        ...theme.colors,
        mainBackground: palette.black,
        mainForeground: palette.white,
    
        secondaryCardBackground: palette.darkGray,
        secondaryCardText: palette.white,
      },
    };
    
    const Box = createBox<Theme>();
    const Text = createText<Theme>();
    
    const App = () => {
      const [darkMode, setDarkMode] = useState(false);
      return (
        <ThemeProvider theme={darkMode ? darkTheme : theme}>
          <Box padding="m" backgroundColor="mainBackground" flex={1}>
            <Box
              backgroundColor="primaryCardBackground"
              margin="s"
              padding="m"
              flexGrow={1}
            >
              <Text variant="body" color="primaryCardText">
                Primary Card
              </Text>
            </Box>
            <Box
              backgroundColor="secondaryCardBackground"
              margin="s"
              padding="m"
              flexGrow={1}
            >
              <Text variant="body" color="secondaryCardText">
                Secondary Card
              </Text>
            </Box>
            <Box marginTop="m">
              <Switch
                value={darkMode}
                onValueChange={(value: boolean) => setDarkMode(value)}
              />
            </Box>
          </Box>
        </ThemeProvider>
      );
    };
    
    export default App;
  4. Use responsive values in Restyle props

    master

    Any prop powered by Restyle can accept either a plain value or an object containing breakpoint-specific values. The available breakpoints are defined in the breakpoints object of your theme.

    To use responsive values, pass an object where keys correspond to your theme's breakpoint names (e.g., phone, tablet) and values correspond to the desired prop value for that screen size.

    // 1. Define breakpoints in your theme
    const theme = createTheme({
      // ...
      breakpoints: {
        phone: 0,
        tablet: 768,
      }
    })
    
    // 2. Use plain values for all screen sizes
    <Box flexDirection="row" />
    
    // 3. Use breakpoint-specific values for responsiveness
    <Box flexDirection={{phone: 'column', tablet: 'row'}} />
  5. Run the Restyle fixture app

    master

    The fixture app is a playground used to learn Restyle, prototype ideas, or test library changes. To run the app locally, follow these steps:

    1. Install dependencies: yarn up
    2. Start the Metro bundler: yarn start
    3. Launch the app on your preferred platform:
      • iOS: yarn run-ios
      • Android: yarn run-android

    Note: Your local iOS simulator or Android emulator should open automatically. If they do not, ensure your React Native development environment is correctly configured.

    yarn up
    yarn start
    yarn run-ios
    # or
    yarn run-android
  6. Create a Text component

    master

    Use createText from @shopify/restyle to generate a Text component. Passing your theme type as a generic argument (e.g., createText<Theme>()) ensures that theme-mapped props are correctly typed.

    The Text component includes the following Restyle functions:

    • color
    • textDecorationColor
    • opacity
    • visible
    • typography
    • textShadow
    • spacing
    • layout

    Additionally, the Text component supports the variant prop, which can be used to apply styles defined under the textVariants key in your theme.

    // In Text.tsx
    import {createText} from '@shopify/restyle';
    import {Theme} from './theme';
    
    const Text = createText<Theme>();
    
    export default Text;
  7. Define semantic colors in your theme

    master

    Instead of using raw palette colors (like purplePrimary) directly in your components, you should map a color palette to semantic names within your theme object. This allows you to assign meaning to colors (e.g., mainBackground, buttonPrimaryBackground) rather than just their visual appearance.

    Benefits of semantic naming:

    • Contextual Clarity: It is easier to understand where and why a color is used.
    • Maintainability: If the underlying palette changes, you only update the theme mapping, not every component reference.
    • Flexibility: You can easily change the color of specific elements (e.g., changing buttons to green while keeping cards purple) without affecting other elements using the same palette color.
    • Runtime Swapping: Enables easy implementation of features like dark mode by swapping themes.
    const palette = {
      purpleLight: '#8C6FF7',
      purplePrimary: '#5A31F4',
      purpleDark: '#3F22AB',
    
      greenLight: '#56DCBA',
      greenPrimary: '#0ECD9D',
      greenDark: '#0A906E',
    
      black: '#0B0B0B',
      white: '#F0F2F3',
    };
    
    const theme = createTheme({
      colors: {
        mainBackground: palette.white,
        mainForeground: palette.black,
        cardPrimaryBackground: palette.purplePrimary,
        buttonPrimaryBackground: palette.purplePrimary,
      },
    });
  8. Define a theme using Shopify Polaris tokens

    master

    You can integrate Shopify's design system by using createTheme from @shopify/restyle and mapping Polaris tokens to your theme object. Since Polaris tokens are often provided as pixel strings (e.g., '16px'), you may need a helper function to convert them to numbers for Restyle's spacing scale.

    import tokens from '@shopify/polaris-tokens';
    import {createTheme} from '@shopify/restyle';
    
    // Helper to convert '16px' strings to numbers
    const pxToNumber = (px: string) => {
      return parseInt(px.replace('px', ''), 10);
    };
    
    const theme = createTheme({
      colors: {
        body: tokens.colorBlack,
        backgroundRegular: tokens.colorWhite,
        backgroundSubdued: tokens.colorSkyLighter,
    
        foregroundRegular: tokens.colorBlack,
        foregroundOff: tokens.colorInkLight,
        foregroundSubdued: tokens.colorInkLightest,
        foregroundContrasting: tokens.colorWhite,
        foregroundSuccess: tokens.colorGreenDark,
    
        highlightPrimary: tokens.colorIndigo,
        highlightPrimaryDisabled: tokens.colorIndigoLight,
    
        buttonBackgroundPlain: tokens.colorSky,
        errorPrimary: tokens.colorRed,
    
        iconBackgroundDark: tokens.colorBlueDarker,
      },
      spacing: {
        none: tokens.spacingNone,
        xxs: pxToNumber(tokens.spacingExtraTight),
        xs: pxToNumber(tokens.spacingTight),
        s: pxToNumber(tokens.spacingBaseTight),
        m: pxToNumber(tokens.spacingBase),
        l: pxToNumber(tokens.spacingLoose),
        xl: pxToNumber(tokens.spacingExtraLoose),
        xxl: 2 * pxToNumber(tokens.spacingExtraLoose),
      },
    });
    
    export type Theme = typeof theme;
    export default theme;
  9. Create custom components with createRestyleComponent

    master

    Use the createRestyleComponent helper to build new components that integrate with your Restyle theme. This is ideal for components that primarily map theme properties (like spacing or variants) directly to a base component. You pass a list of Restyle functions to the helper to define which theme properties the component will support.

    import {
      createRestyleComponent,
      createVariant,
      spacing,
      SpacingProps,
      VariantProps,
    } from '@shopify/restyle';
    import {Theme} from './theme';
    
    type Props = SpacingProps<Theme> & VariantProps<Theme, 'cardVariants'>;
    const Card = createRestyleComponent<Props, Theme>([
      spacing,
      createVariant({themeKey: 'cardVariants'}),
    ]);
    
    export default Card;
  10. Define a theme using createTheme

    master

    Create a global theme object to specify values for spacing, colors, breakpoints, and text variants. Using createTheme ensures your theme adheres to the BaseTheme shape while preserving the specific types of your custom values (like specific color names) for TypeScript autocompletion and type safety in Restyle components.

    Common theme keys include:

    • colors: A mapping of names to color values.
    • spacing: A mapping of names to numeric spacing values.
    • textVariants: A mapping of names to text style objects (e.g., fontWeight, fontSize, lineHeight).
    import {createTheme} from '@shopify/restyle';
    
    const palette = {
      purpleLight: '#8C6FF7',
      purplePrimary: '#5A31F4',
      purpleDark: '#3F22AB',
    
      greenLight: '#56DCBA',
      greenPrimary: '#0ECD9D',
      greenDark: '#0A906E',
    
      black: '#0B0B0B',
      white: '#F0F2F3',
    };
    
    const theme = createTheme({
      colors: {
        mainBackground: palette.white,
        cardPrimaryBackground: palette.purplePrimary,
      },
      spacing: {
        s: 8,
        m: 16,
        l: 24,
        xl: 40,
      },
      textVariants: {
        header: {
          fontWeight: 'bold',
          fontSize: 34,
        },
        body: {
          fontSize: 16,
          lineHeight: 24,
        },
        defaults: {
          // We can define a default text variant here.
        },
      },
    });
    
    export type Theme = typeof theme;
    export default theme;
  11. Create advanced custom components with useRestyle

    master

    For complex components that require custom logic or wrapping multiple elements, use the useRestyle hook. This allows you to compute Restyle style props manually and spread them onto specific sub-elements within your component. You typically use composeRestyleFunctions to combine multiple Restyle functions into a single set of props.

    import {TouchableOpacity, View} from 'react-native';
    import {
      useRestyle,
      spacing,
      border,
      backgroundColor,
      SpacingProps,
      BorderProps,
      BackgroundColorProps,
      composeRestyleFunctions,
    } from '@shopify/restyle';
    
    import Text from './Text';
    import {Theme} from './theme';
    
    type RestyleProps = SpacingProps<Theme> &
      BorderProps<Theme> &
      BackgroundColorProps<Theme>;
    
    const restyleFunctions = composeRestyleFunctions<Theme, RestyleProps>([
      spacing,
      border,
      backgroundColor,
    ]);
    
    type Props = RestyleProps & {
      onPress: () => void;
      label: string;
    };
    
    const Button = ({onPress, label, ...rest}: Props) => {
      const props = useRestyle(restyleFunctions, rest);
    
      return (
        <TouchableOpacity onPress={onPress}>
          <View {...props}>
            <Text variant="buttonLabel">{label}</Text>
          </View>
        </TouchableOpacity>
      );
    };
  12. Define a spacing scale in your theme

    master

    Restyle uses a spacing scale to manage layout gaps and margins. It is recommended to use a base number (e.g., 8) and follow a t-shirt size naming convention (s, m, l, xl, etc.). This allows for scalable spacing values by prepending x to the keys for different sizes.

    const theme = createTheme({
      spacing: {
        s: 8,
        m: 16,
        l: 24,
        xl: 40,
      },
    });