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
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 webexpo-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:
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:
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.:
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 oftwMerge()
andcslx()
to merge the classes together.
Theme management
You can manage colors, default text sizes, spacing and other theme-related properties in a single place:
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:
@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 valueuseThemeColor()
— 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')
Recommended tooling
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:
// ...
"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
],
// ...