Styling Universal UI

Universal UI

Write and style once, render anywhere

The default UI primitives to use for building Universal Apps are those that react-native comes with. Instead of using <div>, <p>, <span> or <img>, you instead use <View>, <Text> and <Image>

import { View, Text, Image } from 'react-native'
// ☝️ Auto-transformed to 'react-native-web' in Next.js

However, you’ll likely want to introduce tailwind-style className support through Nativewind:

Nativewind Basics

Graph Showing how Nativewind translates to tailwind on web & react-native stylesheet on native with Expo

NativeWind allows you to use Tailwind CSS to style your components in React Native. Styled components can be shared between all React Native platforms, using the best style engine for that platform; CSS StyleSheet on web and StyleSheet.create for native. It’s goals are to provide a consistent styling experience across all platforms, improving Developer UX, component performance and code maintainability.

On native platforms, NativeWind performs two functions. First, at build time, it compiles your Tailwind CSS styles into StyleSheet.create objects and determines the conditional logic of styles (e.g. hover, focus, active, etc). Second, it has an efficient runtime system that applies the styles to your components. This means you can use the full power of Tailwind CSS, including media queries, container queries, and custom values, while still having the performance of a native style system.

On web, NativeWind is a small polyfill for adding className support to React Native Web.

import { View, Text, Image } from '@app/primitives'
// ☝️ Import from 'nativewind' instead of 'react-native'
 
<View className="px-2 max-w-[100px] items-center rounded-md">
 
// Use the 'className' prop like you would with tailwind on the web
 
// ⬇⬇⬇
 
// When rendering on iOS and Android:
 
//  'px-2'          -> { paddingLeft: 8, paddingRight: 8 }
//  'max-w-[100px]' -> { maxWidth: 100 }
//  'items-center'  -> { alignItems: 'center' }
//  'rounded-md'    -> { borderRadius: 6 }
 
// -- vs. --
 
// When rendering on the server or browser:
 
//  'px-2'          -> padding-left: 8px; padding-right: 8px;
//  'max-w-[100px]' -> max-width: 100px;
//  'items-center'  -> align-items: center;
//  'rounded-md'    -> border-radius: 6px;
 
// (uses regular tailwind css stylesheet on web to apply as actual CSS)

Responsive Design

If you’re doing SSR with responsive-design, this becomes real handy to apply media-queries:

<Text className"text-base lg:text-lg">
 
// Will apply the classes from a mobile-first perspective:
 
// ⬇⬇⬇
 
// on screens smaller than the 'lg' breakpoint:
 
//  'text-base'     -> font-size: 16px;
 
// -- vs. --
 
// on screens larger than the 'lg' breakpoint:
 
//  'lg:text-lg'    -> @media (min-width: 1024px) {
//                        .lg\:text-lg {
//                          font-size: 18px;
//                        }
//                     }

Check nativewind.dev and tailwindcss.com for a deeper understanding of Universal Styling and breakpoints

Create your own primitives

Optimized Images

Some primitives like the Image component have optimized versions for each environment:

  • next/image for web
  • expo-image for native

To automatically use the right in the context that it’s rendered, we’ve provided our own universal Image component:

import { Image } from '@green-stack/components/Image'

Which, ofcourse, you might wish to wrap with Nativewind to provide class names to:

styled.tsx
import { Image as UniversalImage } from '@green-stack/components/Image'
// ☝️ Import the universal Image component
import { styled } from 'nativewind'
 
// ⬇⬇⬇
 
export const Image = styled(UniversalImage, '')
// ☝️ Adds the ability to assign tailwind classes

styled()

You could also create other fixed styles for e.g. headings using this same method.

        • styled.tsx ← `@app/primitives`

Prestyling can be done using the styled() util:

styled.tsx
import { Text as RNText } from 'react-native'
import { styled } from 'nativewind'
 
// ... other re-exported predefined styles ...
 
/* --- Typography ------------ */
 
export const P = styled(RNText, 'text-base')
export const H1 = styled(RNText, 'font-bold text-2xl text-primary-100')
export const H2 = styled(RNText, 'font-bold text-xl text-primary-100')
export const H3 = styled(RNText, 'font-bold text-lg text-primary-100')
// ☝️ These styles will always be applied unless overridden by the className prop
 

For convenience, we’ve already set up an @app/primitives alias that points to this styled.tsx file for you.

Styles you apply while using these components can overwrite the predefined styles if you want them to:

import { Image, View, H1, P } from '@app/primitives'
 
// ⬇⬇⬇
 
<View className="w-full lg:max-w-[600px]">
 
    <Image className="rounded-md 2xl:" src="..." />
 
    <H1 className="text-primary-300">
    {/* ☝️ overrides color from predefined, but keeps the other classes */}
 
</View>

Prefilling other style props

Next to just prefilling className, you can also prefill other props by passing them as the third argument to styled(), e.g.:

Checkbox.restyled.tsx
const RestyledCheckbox = styled(Checkbox, 'bg-primary', {
    labelClassName: 'text-secondary', // <- Prefill the 'labelClassName' prop
    iconColor: '#FFFFFF', // <- Prefill the 'iconColor' prop
})

Combining classnames with cn()

import { cn } from '@app/primitives'

If you want to combine multiple classes, you can use the cn() utility:

<View className={cn('bg-primary', someBoolean && 'text-secondary')}>

This will only apply the second class if someBoolean is true.

Underneath, cn() uses a combination of twMerge() and cslx() to merge the classes together.

Theme management

You can manage colors, default text sizes, spacing and other theme-related properties in a single place:

tailwind.theme.js
const universalTheme = {
    // -i- Extend default tailwind theme here
    // -i- Reference this theme in the tailwind.config.js files in apps/expo, apps/next, features/app-core and other package or feature folders
    extend: {
        colors: {
            'background': 'hsl(var(--background))',
            'foreground': 'hsl(var(--foreground))',
            'primary': 'hsl(var(--primary))',
            'primary-inverse': 'hsl(var(--primary-inverse))',
            'secondary': 'hsl(var(--secondary))',
            'secondary-inverse': 'hsl(var(--secondary-inverse))',
            'link': 'hsl(var(--link))',
            'muted': 'hsl(var(--muted))',
            'warn': 'hsl(var(--warn))',
            ...
        },
        borderColor: (theme) => ({
            ...
        }),
        backgroundColor: (theme) => ({
            ...
        }),
        textColor: (theme) => ({
            ...
        }),
        borderWidth: {
            hairline: hairlineWidth(),
        },
        keyframes: ...,
        animation: ...,
    }
}

It’s advised to reuse this universalTheme in all your tailwind.config.js files.

      • tailwind.config.js
      • global.css ← Theme colors are defined here
      • tailwind.theme.js ← Main universal theme
      • tailwind.config.js

Colors and Dark Mode

You main colors can be managed in global.css.

This is where you can define, for example, the colors for light and dark mode:

global.css
 
@tailwind base;
@tailwind components;
@tailwind utilities;
 
/* --- Tailwind Theme -------------------------------------------------------------------------- */
 
@layer base {
    :root {
        --background: 0, 0%, 100%; /* #FFFFFF; /* tailwind: colors.white */
        --foreground: 240, 5%, 10%; /* #18181b; /* tailwind: colors.zinc[900] */
        --primary: 222, 15%, 13%; /* #111827; /* tailwind: colors.gray[900] */
        --primary-inverse: 0, 0%, 100%; /* #FFFFFF; /* tailwind: colors.white */
        --secondary: 217, 13%, 19%; /* #1f2937; /* tailwind: colors.gray[800] */
        --secondary-inverse: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
        --link: 213, 94%, 76%; /* #93c5fd; /* tailwind: colors.blue[300] */
        --muted: 220, 8%, 65%; /* #9ca3af; /* tailwind: colors.gray[400] */
        --warn: 24, 89%, 47%; /* #ea580c; /* tailwind: colors.orange[600] */
        ...
    }
 
    .dark:root {
        --background: 216, 34%, 17%; /* #1e293b; /* tailwind: colors.slate[800] */
        --foreground: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
        --primary: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
        --primary-inverse: 222, 15%, 13%; /* #111827; /* tailwind: colors.gray[900] */
        ...
    }
}

Theme colors as values

You can retrieve theme colors as values by passing their CSS variable name to one of our style utils:

  • getThemeColor() — Statically retrieves the color value
  • useThemeColor() — Dynamically retrieves the color value in a component
import { getThemeColor } from '@app/primitives' 
 
const primaryColor = getThemeColor('--primary')
const secondaryColor = getThemeColor('--secondary')
import { useThemeColor } from '@app/primitives' 
 
// Within a React component:
 
const primaryColor = useThemeColor('--primary')
const secondaryColor = useThemeColor('--secondary')

For hover previews of what tailwind classes do, or hints for which are available, we recommend you install the following VSCode plugins:

We recommend you add the following settings to your VSCode settings.json:

settings.json
// ...
 
"tailwind-fold.autoFold": false,
"tailwind-fold.unfoldIfLineSelected": true,
"tailwindCSS.classAttributes": [
    "class",
    "className",
    "tw",
    "tailwind",
    "style",
],
"tailwindCSS.experimental.classRegex": [
    "cn\\([\\s\\S]*?['\"]([^'\"\\s]*?)['\"]" // matches simple usage within cn function
    "ClassName.*?z\\.string\\(\\).*?\\.default\\('([^']*)'", // tailwind class property default in zod schemas
    "ClassName.*?z\\.string\\(\\).*?\\.eg\\('([^']*)'", // tailwind class property example in zod schemas
    "Classes.*?z\\.string\\(\\).*?\\.default\\('([^']*)'", // tailwind class property default in zod schemas
    "Classes.*?z\\.string\\(\\).*?\\.eg\\('([^']*)'", // tailwind class property example in zod schemas
],
 
// ...

Official docs