DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on

Streamlining Styling in React Native with Unistyles

UNISTYLES is a cross-platform library that enables you to share up to 100% of your styles across all platforms. It combines the simplicity of StyleSheet with the performance of C++.

Unistyles is a superset of StyleSheet similar to how TypeScript is a superset of JavaScript. If you’re familiar with styling in React Native, then you already know how to use Unistyles.

Feature:

As developers, we appreciate powerful libraries that address all our needs in terms of syntax, scalability, and cross-platform support. Unistyles was designed to be the fastest styling solution on the market. It provides low-level functionality for developers who either want full control of styling in their commercial projects or for library authors to build UI Kits on top of it.

It’s also worth mentioning that there won’t be a faster way than passing your objects to StyleSheet.create. Unfortunately, the functionalities of StyleSheet are limited. Building a fully-featured, cross-platform app without some additional help is challenging.

Image description

For further understanding check: benchmarks


INSTALLATION:

yarn add react-native-unistyles && cd ios && pod install && cd ..
Enter fullscreen mode Exit fullscreen mode

CONFIGURATION:

1. Create a scaling util file(optional):

import { Dimensions, PixelRatio } from 'react-native';

const { width, height } = Dimensions.get('window');

const guidelineBaseWidth = 375;
const guidelineBaseHeight = 667;

const scale = (size: number): number => (width / guidelineBaseWidth) * size;
const verticalScale = (size: number): number => (height / guidelineBaseHeight) * size;
const moderateScale = (size: number, factor: number = 0.5): number => size + (scale(size) - size) * factor;

// FONT SCALING
// Usage: nf(16)
const scaleNew = height / 667;
const normalizeFont = (size: number): number => {
  const newSize = size * scaleNew;
  return Math.round(PixelRatio.roundToNearestPixel(newSize));
};

// DYNAMIC DIMENSION CONSTANTS
// Usage: wp(5), hp(20)
const widthPercentageToDP = (widthPercent: string): number => {
  // Convert string input to decimal number
  const elemWidth = parseFloat(widthPercent);
  return PixelRatio.roundToNearestPixel((width * elemWidth) / 100);
};
const heightPercentageToDP = (heightPercent: string): number => {
  // Convert string input to decimal number
  const elemHeight = parseFloat(heightPercent);
  return PixelRatio.roundToNearestPixel((height * elemHeight) / 100);
};

// Usage: wpx(141), hpx(220)
const widthFromPixel = (widthPx: number, w: number = 414): number => widthPx * (width / w);
const heightFromPixel = (heightPx: number, h: number = 896): number => heightPx * (height / h);

export {
  scale,
  verticalScale,
  moderateScale,
  normalizeFont as nf,
  widthPercentageToDP as wp,
  heightPercentageToDP as hp,
  widthFromPixel as wpx,
  heightFromPixel as hpx,
};
Enter fullscreen mode Exit fullscreen mode

2. Create a unistyles setup file✨:

Using typescript(optional) instead of JS will give suggestions during usage.
unistyles/index.ts

import { UnistylesRegistry, UnistylesRuntime } from 'react-native-unistyles';
import { storage } from '../../redux/storage';
import { nf } from '../../utils/Scaling';
import { MMKV } from "react-native-mmkv";

export const storage = new MMKV();

// Add any custom base style
const base = {
  // USAGE: padding: theme.margins.lg
  margins: {
    xs: 2,
    sm: 4,
    md: 8,
    lg: 12,
    xl: 16,
    superLarge: 20,
    tvLike: 24,
  },
  fontSizes: {
    // fontSize: theme.fontSizes.lg
    xxs: nf(10),
    xs: nf(12),
    sm: nf(14),
    md: nf(16),
    lg: nf(18),
    xl: nf(20),
    xxl: nf(24),
  },
  spacing: {
    // USAGE: padding: theme.spacing(1),
    1: 8,
    2: 16,
    3: 24,
    4: 32,
    5: 40,
    6: 48,
    7: 56,
    8: 64,
  },
} as const;

// You can name your breakpoints however you like. The only restriction is that the first breakpoint must start with 0:
//   USAGE:
//    backgroundColor: {
//      xs: 'pink',
//      sm: "skyblue",
//    }
export const breakpoints = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  superLarge: 2000,
  tvLike: 4000,
} as const;

// You can define as many themes as you want. Each theme just needs to have a unique name and the same type. The library has no restrictions on the shape of the theme. You can use nested objects, functions, spread operators, and so on.
export const lightTheme = {
  colors: {
    typography: '#000000',
    background: '#ffffff',
    primary: '#3498db',
    secondary: '#2ecc71',
    accent: '#e74c3c',
    info: '#3498db',
    success: '#2ecc71',
    warning: '#f39c12',
    error: '#7676a7',
    darkGrey: '#333333',
  },
  margins: base.margins,
  fontSizes: base.fontSizes,
  spacing: base.spacing,
} as const;

export const darkTheme = {
  colors: {
    typography: '#ffffff',
    background: '#000000',
    primary: '#3498db',
    secondary: '#2ecc71',
    accent: '#e74c3c',
    info: '#3498db',
    success: '#2ecc71',
    warning: '#f39c12',
    error: '#e74c3c',
    darkGrey: '#333333',
  },
  margins: base.margins,
  fontSizes: base.fontSizes,
  spacing: base.spacing,
} as const;

export const premiumTheme = {
  colors: {
    typography: '#FFD700',
    background: '#001861',
    primary: '#3498db',
    secondary: '#2ecc71',
    accent: '#e74c3c',
    info: '#3498db',
    success: '#2ecc71',
    warning: '#f39c12',
    error: '#2f2d2d',
    darkGrey: '#333333',
  },
  margins: base.margins,
  fontSizes: base.fontSizes,
  spacing: base.spacing,
} as const;

// If you’re using TypeScript, create types for your breakpoints and/or themes. This step is required to achieve perfect Intellisense support across all StyleSheets.
type AppBreakpoints = typeof breakpoints;
type AppThemes = {
  light: typeof lightTheme;
  dark: typeof darkTheme;
  premium: typeof premiumTheme;
};

declare module 'react-native-unistyles' {
  export interface UnistylesBreakpoints extends AppBreakpoints {}
  export interface UnistylesThemes extends AppThemes {}
}

// Use it for saving and displaying the user-selected theme
const app_theme_mmkv = storage.getString('app_theme') as any; 

// The final step is to call UnistylesRegistry to pass your themes, breakpoints and optional config.
UnistylesRegistry.addBreakpoints(breakpoints)
  .addThemes({
    light: lightTheme,
    dark: darkTheme,
    premium: premiumTheme,
  })
  .addConfig({
    // adaptiveThemes: true, // themes based on device color scheme settings
    initialTheme: app_theme_mmkv || UnistylesRuntime.colorScheme,
  });

UnistylesRuntime.setRootViewBackgroundColor('black'); // Changing rootView background color is useful when your app supports different orientations and you want to match the background color with your theme while transitioning.
Enter fullscreen mode Exit fullscreen mode

3. Initialise✨:

In order to run the code don’t forget to import the unistyles.ts file somewhere in your app eg. in the App.tsx file.

import '../unistyles'
Enter fullscreen mode Exit fullscreen mode

4. Usage:

a) A custom Text Component for proper text Font display with FontFamily and FontSize.:

CustomText.tsx(Optional)

import React from "react";
import { Text, TextStyle } from "react-native";
import { FONTS } from "../../constants/Fonts";
import { createStyleSheet, useStyles } from "react-native-unistyles";

interface Props {
  variant?:
  | "h1"
  | "h2"
  | "h3"
  | "h4"
  | "h5"
  | "h6"
  | "h7"
  | "h8"
  | "h9"
  | "body";
  fontFamily?: FONTS;
  fontSize?: number;
  style?: TextStyle;
  children?: React.ReactNode;
  numberOfLines?: number;
  onLayout?: (event: object) => void;
}

const CustomText: React.FC<Props> = ({
  variant = "body",
  fontFamily = FONTS.Regular,
  fontSize,
  style,
  onLayout,
  children,
  numberOfLines,
}) => {
  const { styles, theme, breakpoint, } = useStyles(stylesheet)

  let computedFontSize: number;
  const { fontSizes } = theme
  switch (variant) {
    case "h1":
      computedFontSize = (fontSize || fontSizes.xxl);
      break;
    case "h2":
      computedFontSize = (fontSize || fontSizes.xl);
      break;
    case "h3":
      computedFontSize = (fontSize || fontSizes.lg);
      break;
    case "h4":
      computedFontSize = (fontSize || fontSizes.md);
      break;
    case "h5":
      computedFontSize = (fontSize || fontSizes.sm);
      break;
    case "h6":
      computedFontSize = (fontSize || fontSizes.xs);
      break;
    case "h7":
      computedFontSize = (fontSize || fontSizes.xs);
      break;
    case "h8":
      computedFontSize = (fontSize || fontSizes.xxs);
      break;
      break;
    default:
      computedFontSize = (fontSize || fontSizes.xs);
  }

  const fontFamilyStyle = {
    fontFamily:
      fontFamily === FONTS.Black
        ? "Roboto-Black"
        : fontFamily === FONTS.Bold
          ? "Roboto-Bold"
          : fontFamily === FONTS.Light
            ? "Roboto-Light"
            : fontFamily === FONTS.Medium
              ? "Roboto-Medium"
              : fontFamily === FONTS.Number
                ? "Manrope-Regular"
                : fontFamily === FONTS.NumberSemiBold
                  ? "Manrope-SemiBold"
                  : fontFamily === FONTS.Lato
                    ? "Lato-Regular"
                    : fontFamily === FONTS.Thin
                      ? "Roboto-Thin"
                      : "Roboto-Regular",
  };

  return (
    <Text
      onLayout={onLayout}
      style={[
        styles.text,
        { color: theme.colors.typography, fontSize: computedFontSize },
        fontFamilyStyle,
        style,
      ]}
      numberOfLines={numberOfLines !== undefined ? numberOfLines : undefined}
    >
      {children}
    </Text>
  );
};


const stylesheet = createStyleSheet((theme, rt) => ({
  text: {
    textAlign: "left",
  },
}))

export default CustomText;
Enter fullscreen mode Exit fullscreen mode

b) Sample screen where unistyles is used:✨

HomeScreen.tsx

import React, { useEffect } from 'react'
import { View, Text, Button } from 'react-native'
import { createStyleSheet, mq, UnistylesRuntime, useStyles } from 'react-native-unistyles'
import { storage } from '../../unistyles';
import CustomText from '../../components/global/CustomText';
import { FONTS } from '../../constants/Fonts';
import Toast from 'react-native-toast-message';

const HomeScreen = () => {

  // useInitialTheme("light") // if want different theme at runtime
  // const { theme, breakpoint } = useStyles()
  // or
  // const { styles, theme, breakpoint, } = useStyles(stylesheet)

 // useStyles accepts two optional arguments: stylesheet and variants.
  const { styles, theme, breakpoint, } = useStyles(stylesheet, {
    color: "true", // you can also use strings
    size: 'medium'
  })

  useEffect(() => {
    console.log("UnistylesRuntime", JSON.stringify(UnistylesRuntime, null, 2))
    // Android only supported (statusBar/navigationBar)
    UnistylesRuntime.statusBar.setColor('red')
    // or with alpha channel
    UnistylesRuntime.statusBar.setColor('green', 0.5)
    UnistylesRuntime.navigationBar.setColor('black')
    // or with 8-digit hex value eg. #50000000
    UnistylesRuntime.navigationBar.setColor('pink')
    return () => {
      // set default color
      UnistylesRuntime.statusBar.setColor(undefined)
      UnistylesRuntime.navigationBar.setColor(undefined)
    }
  }, [])

  return (
    <View style={styles.container}>
      {/* MAKE CUSTOM TEXT COMPONENT FOR DYNAMIC FONT */}
      <CustomText
        style={{ color: theme.colors.typography }}
        variant="h5"
        fontFamily={FONTS.Medium}
      >
        My device is using the {UnistylesRuntime.colorScheme} scheme.
      </CustomText>
      <Text style={styles.text(12)}>
        Selected theme is {UnistylesRuntime.themeName}
      </Text>
      <Button title="Change theme" onPress={() => {
        Toast.show({
          type: "successToast",
          props: { msg: 'Dark Mode Set' },
        });
       // changes theme and save user selected theme to storage
        UnistylesRuntime.setTheme('dark'),
          storage.set('app_theme', 'dark')
      }} />
      <Button title="Change theme" onPress={() => {
       // changes theme and save user selected theme to storage
        UnistylesRuntime.setTheme('premium'),
          storage.set('app_theme', 'premium')
      }} />
      <Button title="Change theme" onPress={() => {
       // changes theme and save user selected theme to storage
        UnistylesRuntime.setTheme('light'),
          storage.set('app_theme', 'light')
      }} />

      <View style={styles.boxesWrapper}>
        <View style={styles.boxBreakPoint}><Text>Breakpoint</Text></View>
        <View style={styles.boxBreakOrientation}><Text>Orientation</Text></View>
        <View style={styles.boxBreakMediaQuery}><Text>MediaQuery</Text></View>
        <View style={styles.boxVariant}><Text>Variant</Text></View>
      </View>
    </View>
  )
}

const stylesheet = createStyleSheet((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: "space-evenly",
    alignItems: 'center',
    backgroundColor: theme.colors.background,
    // backgroundColor: {landscape/portrait This only works in mobile app when breakpoint is not defined
    //   landscape: theme.colors.background,
    //   portrait: theme.colors.warning
    // },
    marginTop: UnistylesRuntime.insets.top, // eg. 42
    marginBottom: rt.insets.bottom, // eg. 24
    marginLeft: rt.insets.left, // eg. 0
    marginRight: rt.insets.right, // eg. 0

  },
  text: (size = 24) => ({
    color: theme.colors.typography,
    fontSize: theme.fontSizes.sm || size,
  }),

  boxesWrapper: {
    gap: theme.spacing[2],
    alignItems: 'center',
    flexDirection: rt.orientation === 'landscape' ? 'row' : 'column',
  },
  boxBreakPoint: {
    height: 50,
    width: 50,
    backgroundColor: {
      xs: 'pink',
      sm: "skyblue",
    }
  },
  boxBreakOrientation: {
    height: 50,
    width: 50,
    // backgroundColor: 'red'
    backgroundColor: rt.orientation === 'landscape' ? 'green' : 'yellow',
  },
  boxBreakMediaQuery: {
    height: 50,
    width: 50,
    backgroundColor: {
      [mq.height(500).and.width('sm')]: "lightblue", // // heigh from 500 onwards and width from 'sm' breakpoint onwards
      [mq.width(380).and.height(300)]: "darkblue"
    }
  },
  boxVariant: {
    borderRadius: 20,
    padding: theme.margins.lg,
    variants: {
      color: {
        true: {
          backgroundColor: 'cyan'
        },
        false: {
          backgroundColor: 'transparent'
        }
      },
      size: {
        small: {
          width: 100,
          height: 100
        },
        medium: {
          height: rt.screen.height / 7,
          width: rt.screen.width / 4,
        },
        large: {
          width: 150,
          height: 150
        }
      }
    }
  }
}))

export default HomeScreen
Enter fullscreen mode Exit fullscreen mode

Conclusion:

Unistyles is designed for building React Native applications for streamlined, theme-based styling. This blog integrates Unistyle to manage global styles, dynamic theming, and responsive designs efficiently, making it easier for developers to maintain a clean and scalable codebase. With TypeScript support, it ensures type safety and improved developer experience, while Unistyle provides a utility-first approach to styling, allowing for seamless theme management and platform-specific customizations. Ideal for projects requiring robust styling solutions, this boilerplate accelerates development with a well-structured, scalable foundation.

Complete Source Code: Github

Top comments (0)