Restyle
repository·master·Indexed 25 days ago
https://github.com/shopify/restyleA 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.
What's inside @shopify/restyle
- 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.
Quickstart with Restyle components
masterRestyle provides a type-enforced system for building themed UI components. To use it, you typically define a theme, create base components like
BoxandTextusingcreateBoxandcreateText, and wrap your application in aThemeProvider.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> ); };Implement dark mode with Restyle
masterTo implement dark mode in Restyle, create a base theme using
createThemeand then define a secondary theme object that overrides specific color tokens. You can then toggle between these themes by passing the active theme to theThemeProvidercomponent.Key steps:
- Define your base theme with
createTheme. - Create a type for your theme using
typeof themeto ensure type safety when defining the dark theme. - Create a dark theme object by spreading the base theme and overriding the
colorsproperty. - 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;- Define your base theme with
Use responsive values in Restyle props
masterAny prop powered by Restyle can accept either a plain value or an object containing breakpoint-specific values. The available breakpoints are defined in the
breakpointsobject 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'}} />Run the Restyle fixture app
masterThe fixture app is a playground used to learn Restyle, prototype ideas, or test library changes. To run the app locally, follow these steps:
- Install dependencies:
yarn up - Start the Metro bundler:
yarn start - Launch the app on your preferred platform:
- iOS:
yarn run-ios - Android:
yarn run-android
- iOS:
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- Install dependencies:
Create a Text component
masterUse
createTextfrom@shopify/restyleto generate aTextcomponent. Passing your theme type as a generic argument (e.g.,createText<Theme>()) ensures that theme-mapped props are correctly typed.The
Textcomponent includes the following Restyle functions:colortextDecorationColoropacityvisibletypographytextShadowspacinglayout
Additionally, the
Textcomponent supports thevariantprop, which can be used to apply styles defined under thetextVariantskey in your theme.// In Text.tsx import {createText} from '@shopify/restyle'; import {Theme} from './theme'; const Text = createText<Theme>(); export default Text;Define semantic colors in your theme
masterInstead 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, }, });Define a theme using Shopify Polaris tokens
masterYou can integrate Shopify's design system by using
createThemefrom@shopify/restyleand 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;Create custom components with createRestyleComponent
masterUse the
createRestyleComponenthelper 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;Define a theme using createTheme
masterCreate a global theme object to specify values for spacing, colors, breakpoints, and text variants. Using
createThemeensures your theme adheres to theBaseThemeshape 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;Create advanced custom components with useRestyle
masterFor complex components that require custom logic or wrapping multiple elements, use the
useRestylehook. This allows you to compute Restyle style props manually and spread them onto specific sub-elements within your component. You typically usecomposeRestyleFunctionsto 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> ); };Define a spacing scale in your theme
masterRestyle 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 prependingxto the keys for different sizes.const theme = createTheme({ spacing: { s: 8, m: 16, l: 24, xl: 40, }, });