banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

React Native Practice: Colors

Started writing Follow Mobile a month ago, and I think it's time to solidify some things.

This article will first discuss the color system of Follow Mobile.

Before we begin, it’s important to know that Follow Mobile is developed using React Native and utilizes the Expo framework.

Preparation#

Since React Native does not officially support the className syntax in web, to adapt to the convenient TailwindCSS atomic class names in web, we need to use the NativeWind tool. This is a compiler that allows some TailwindCSS capabilities to be used in React Native apps. It wraps the basic components in React Native through a Babel plugin, translating className into React Native style objects at runtime to achieve similar effects.

NativewindCSS also relies on TailwindCSS for translation, and the configuration for TailwindCSS is basically the same as in web.

We install NativewindCSS.

pnpm install nativewind tailwindcss

Configure Babel and Metro.

// babel.config.js
module.exports = function (api) {
  api.cache(true)
  return {
    presets: [
      ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
      'nativewind/babel',
    ],
  }
}
const { withNativeWind } = require('nativewind/metro')
module.exports = withNativeWind(config, { input: './src/global.css' })

Create a PostCSS style entry.

/* src/global.css  */
@tailwind base;
@tailwind components;
@tailwind utilities;

After starting the dev server, the nativewind-env.d.ts file will be automatically generated to provide type support. At this point, the preparation work is complete.

Choosing a Color System#

At the initial stage of an app, a complete design specification may be essential. Color definitions are also crucial for an app. When starting to develop the app, I wanted to create something that feels very Apple-like.

Unfortunately, React Native is a cross-platform development framework and does not provide many native components and styles. Most of the time, we can only simulate native styles or rely on community native modules. This is a later discussion for components; here I will first talk about colors.

Apple has a very standardized color system, as seen in Color | Apple Developer Documentation. Using this definition, we can transfer it to NativeWind.

It may not be easy to see all the color values in the documentation; Apple provides Figma, where you can also see most of the colors used by native components.

https://www.figma.com/community/file/1385659531316001292/ios-18-and-ipados-18

Here, I have extracted the relevant colors for your reference:

const lightPalette = {
  red: '255 59 48',
  orange: '255 149 0',
  yellow: '255 204 0',
  green: '52 199 89',
  mint: '0 199 190',
  teal: '48 176 190',
  cyan: '50 173 200',
  blue: '0 122 255',
  indigo: '88 86 214',
  purple: '175 82 222',
  pink: '255 45 85',
  brown: '162 132 94',
  gray: '142 142 147',
  gray2: '172 172 178',
  gray3: '199 199 204',
  gray4: '209 209 214',
  gray5: '229 229 234',
  gray6: '242 242 247',
}
const darkPalette = {
  red: '255 69 58',
  orange: '255 175 113',
  yellow: '255 214 10',
  green: '48 209 88',
  mint: '99 230 226',
  teal: '64 200 244',
  cyan: '100 210 255',
  blue: '10 132 255',
  indigo: '94 92 230',
  purple: '191 90 242',
  pink: '255 55 95',
  brown: '172 142 104',
  gray: '142 142 147',
  gray2: '99 99 102',
  gray3: '72 72 74',
  gray4: '58 58 60',
  gray5: '44 44 46',
  gray6: '28 28 30',
}

export const lightVariants = {
  // UIKit Colors

  placeholderText: '199 199 204',
  separator: '84 84 86 0.34',
  opaqueSeparator: '84 84 86 0.34',
  nonOpaqueSeparator: '198 198 200',
  link: '0 122 255',

  systemBackground: '255 255 255',
  secondarySystemBackground: '242 242 247',
  tertiarySystemBackground: '255 255 255',

  // Grouped
  systemGroupedBackground: '242 242 247',
  secondarySystemGroupedBackground: '255 255 255',
  tertiarySystemGroupedBackground: '242 242 247',

  // System Colors
  systemFill: '120 120 128 0.2',
  secondarySystemFill: '120 120 128 0.16',
  tertiarySystemFill: '120 120 128 0.12',
  quaternarySystemFill: '120 120 128 0.08',

  // Text Colors
  label: '0 0 0',
  text: '0 0 0',
  secondaryLabel: '60 60 67 0.6',
  tertiaryLabel: '60 60 67 0.3',
  quaternaryLabel: '60 60 67 0.18',
}
export const darkVariants = {
  // UIKit Colors

  placeholderText: '122 122 122',
  separator: '56 56 58 0.6',
  opaqueSeparator: '56 56 58 0.6',
  nonOpaqueSeparator: '84 84 86',
  link: '10 132 255',
  systemBackground: '0 0 0',
  secondarySystemBackground: '28 28 30',
  tertiarySystemBackground: '44 44 46',

  // Grouped
  systemGroupedBackground: '0 0 0',
  secondarySystemGroupedBackground: '28 28 30',
  tertiarySystemGroupedBackground: '44 44 46',

  // System Colors
  systemFill: '120 120 128 0.36',
  secondarySystemFill: '120 120 128 0.32',
  tertiarySystemFill: '120 120 128 0.24',
  quaternarySystemFill: '120 120 128 0.19',

  // Text Colors
  label: '255 255 255',
  text: '255 255 255',
  secondaryLabel: '235 235 245 0.6',
  tertiaryLabel: '235 235 245 0.3',
  quaternaryLabel: '235 235 245 0.18',
}

These correspond to the normal colors and system variable colors under light and dark modes.

Using NativeWind Variable Injection#

NativeWind has a feature that allows for similar CSS variables as in web.

https://www.nativewind.dev/api/vars

For example:

<View style={vars({ '--brand-color': 'red'})}>
  { // style: { color: 'red' } }
  <Text className="text-[--brand-color]" />
</View>

With this feature, we can pass the above color definitions from the top level in this way.


// @ts-expect-error
const IS_DOM = typeof ReactNativeWebView !== 'undefined'

const varPrefix = '--color'

const buildVars = (_vars: Record<string, string>) => {
  const cssVars = {} as Record<`${typeof varPrefix}-${string}`, string>
  for (const [key, value] of Object.entries(_vars)) {
    cssVars[`${varPrefix}-${key}`] = value
  }

  return IS_DOM ? cssVars : vars(cssVars)
}

The above function is to maintain compatibility with react-native-web; if you don't need it, you can omit it.

const mergedLightColors = {
  ...lightVariants,
  ...lightPalette,
}
const mergedDarkColors = {
  ...darkVariants,
  ...darkPalette,
}
const mergedColors = {
  light: mergedLightColors,
  dark: mergedDarkColors,
}

export const colorVariants = {
  light: buildVars(lightVariants),
  dark: buildVars(darkVariants),
}
export const palette = {
  // iOS color palette https://developer.apple.com/design/human-interface-guidelines/color
  light: buildVars(lightPalette),
  dark: buildVars(darkPalette),
}

export const getCurrentColors = () => {
  const colorScheme = Appearance.getColorScheme() || 'light'

  return StyleSheet.compose(
    colorVariants[colorScheme],
    palette[colorScheme],
  ) as StyleProp<ViewStyle>
}

Then wrap a View at the top level. For example:

export const RootProviders = ({ children }: { children: ReactNode }) => {
  useColorScheme() // To listen for light/dark mode
  const currentThemeColors = getCurrentColors()!

  return <View style={[styles.flex, currentThemeColors]}>{children}</View>
}

This way, any child component can directly use the relevant variables. However, using them is still inconvenient. We also need to configure the colors in TailwindCSS.

Since we used the prefix --color above, I can write a wrapper function for the tailwindcss config.

import { Config } from 'tailwindcss'

const configColors = {
  // Palette colors
  red: 'rgb(var(--color-red) / <alpha-value>)',
  orange: 'rgb(var(--color-orange) / <alpha-value>)',
  yellow: 'rgb(var(--color-yellow) / <alpha-value>)',
  green: 'rgb(var(--color-green) / <alpha-value>)',
  mint: 'rgb(var(--color-mint) / <alpha-value>)',
  teal: 'rgb(var(--color-teal) / <alpha-value>)',
  cyan: 'rgb(var(--color-cyan) / <alpha-value>)',
  blue: 'rgb(var(--color-blue) / <alpha-value>)',
  indigo: 'rgb(var(--color-indigo) / <alpha-value>)',
  purple: 'rgb(var(--color-purple) / <alpha-value>)',
  pink: 'rgb(var(--color-pink) / <alpha-value>)',
  brown: 'rgb(var(--color-brown) / <alpha-value>)',
  gray: {
    DEFAULT: 'rgb(var(--color-gray) / <alpha-value>)',
    2: 'rgb(var(--color-gray2) / <alpha-value>)',
    3: 'rgb(var(--color-gray3) / <alpha-value>)',
    4: 'rgb(var(--color-gray4) / <alpha-value>)',
    5: 'rgb(var(--color-gray5) / <alpha-value>)',
    6: 'rgb(var(--color-gray6) / <alpha-value>)',
  },

  // System colors

  'placeholder-text': 'rgb(var(--color-placeholderText) / <alpha-value>)',
  separator: 'rgb(var(--color-separator) / <alpha-value>)',
  'opaque-separator': 'rgba(var(--color-opaqueSeparator))',
  'non-opaque-separator': 'rgba(var(--color-nonOpaqueSeparator))',
  link: 'rgb(var(--color-link) / <alpha-value>)',

  // Backgrounds
  'system-background': 'rgb(var(--color-systemBackground) / <alpha-value>)',
  'secondary-system-background':
    'rgb(var(--color-secondarySystemBackground) / <alpha-value>)',
  'tertiary-system-background':
    'rgb(var(--color-tertiarySystemBackground) / <alpha-value>)',
  'system-grouped-background':
    'rgb(var(--color-systemGroupedBackground) / <alpha-value>)',
  'secondary-system-grouped-background':
    'rgb(var(--color-secondarySystemGroupedBackground) / <alpha-value>)',
  'tertiary-system-grouped-background':
    'rgb(var(--color-tertiarySystemGroupedBackground) / <alpha-value>)',
  // System fills
  'system-fill': 'rgba(var(--color-systemFill))',
  'secondary-system-fill': 'rgba(var(--color-secondarySystemFill))',
  'tertiary-system-fill': 'rgba(var(--color-tertiarySystemFill))',
  'quaternary-system-fill': 'rgba(var(--color-quaternarySystemFill))',

  // Text colors
  label: 'rgb(var(--color-text) / <alpha-value>)',
  text: 'rgb(var(--color-text) / <alpha-value>)',
  'secondary-label': 'rgba(var(--color-secondaryLabel))',
  'tertiary-label': 'rgba(var(--color-tertiaryLabel))',
  'quaternary-label': 'rgba(var(--color-quaternaryLabel))',
}
export const withUIKit = (config: Config) => {
  config.theme = config.theme || {}
  config.theme.extend = config.theme.extend || {}
  config.theme.extend.colors = config.theme.extend.colors || {}
  config.theme.extend.colors = {
    ...config.theme.extend.colors,
    ...configColors,
  }
  return config
}

Then use it directly in tailwind.config.ts.

export default withUIKit(config)

This way, these colors can be directly used in tailwindcss.

Usage#

In components, you can set colors like this:

<View className={'bg-secondary-system-grouped-background'} />

However, there are times when we cannot directly use class names and need the actual variables, such as when doing color transition animations.

Let's write a hook to get the corresponding color for the current theme.

export const useColor = (color: keyof typeof mergedLightColors) => {
  const { colorScheme } = useColorScheme()
  const colors = mergedColors[colorScheme || 'light']
  return useMemo(() => rgbStringToRgb(colors[color]), [color, colors])
}

Usage:

const redColor = useColor('red')

Postscript#

This solution has been extracted from the Follow Mobile project into a general library, welcome to use it.

This article was synchronized and updated to xLog by Mix Space. The original link is https://innei.in/posts/tech/react-native-uikit-colors

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.