Before I start, note that this article is not a tutorial on styling. It's my take on the code and directory structure that supports the styling of the apps I create.
Every React Native developer has their own way of handling styling in their app. There is no right or wrong way, but some approaches have obvious benefits over others. I've built a couple of apps already, and every project is a chance to learn something new and improve on the past mistakes. One thing I feel I've done really good and improved since I started - styling. So I decided to share my journey from the beginning to my current setup - feel free to copy it, or criticize it in the comments below.
When I just started with React Native I came from a web background, but I didn't have much experience in building big web sites. Most of the stuff I built was really simple - single page websites without a lot of complexity. This meant I always kept all my CSS in a single file. That approach, as rudimental as it was, worked for those websites. So naturally when I got into React Native I started by putting all my styles in a single style.js
file which exported styles for all the screens. This worked for a while, but as the app grew the file also grew and navigating through the file was starting to get harder and harder. By the time the project was over, style.js
was enormous and handling changes in the project felt like a chore. This approach clearly had to be changed.
For the next project I thought about a different approach. Instead of putting all styles in a single style.js
file, I created separate style.js
files for every component, and imported the common stuff, like colors, from a global constants.js
file. This worked much better, as now I had the style right next to the components and I knew where to find the style without having to navigate through a giant stylesheet.
This approach solved some of the problems, but after a while I started seeing how I could further improve it. I felt like I was writing a lot of redundant code, especially for spacing components out. I'd have a lot of marginTop
s and marginBottom
s all over the place, and there were no rules to this. Sometimes the component above would have a marginBottom
, and sometimes the component below would have a marginTop
. This meant that when I had to change some spacing I'd have to open the style.js
file, search for the style applied to the 2 components, check which one has the margin set and change it. Sometimes I'd remove some component and forget to remove the margin from the neighboring component and then the spacing was too big. I knew I could improve this somehow. And then it hit me! I created the single most used component in all my projects.
That's right! Nothing! Well... Not exactly nothing. It's actually a <Spacer />
component and all it does is render some empty space, so it's kind of nothing. It can take one of these size props - small
, medium
, large
, extraLarge
, and internally it renders a square <View />
with a fixed spacing read from the global constants.js
file. Here's how it looks like in code:
<View style={S.container}>
<Logo />
<Spacer extraLarge />
<TextInput {...usernameProps} />
<Spacer medium />
<TextInput {...passwordProps} />
<Spacer large />
<Button style={S.submitButton} />
</View>
So now most of my screens and components used <Spacer />
which meant all my style.js
lost the need for most marginTop
s and marginBottom
s. I got a great productivity boost - when creating UI, I didn't have to jump back and forth between the stylesheet and component file all the time just to add some spacing between some elements. I could just add a <Spacer />
here and there and space out the components as needed. Another benefit I got is when I looked at my render() methods I could actually see where the spacing was between components without having to inspect the stylesheet.
I loved the Spacer component, so naturally I started thinking about what could I improve on next. I noticed my <Text />
components had a lot of repeating code, so that was the next thing to fix. After some trial and error, I landed on a component that I was happy with. This is what it looks like:
<Text weightBold sizeSmall>This text is small and bold</Text>
<Text sizeLarge colorLight>This text is large and has a light color</Text>
<Text colorTheme>This text is medium size and has the theme color</Text>
The idea behind it is to have a flexible text component that covers most use cases. When there's extra styling needed, I can always add a style prop and customize the text further. Currently my Text component can take one of 5 size props, one of 7 color props, one of 3 weight props, but it's easy to add these kind of boolean props for other style attributes like line height and spacing.
The same scheme is used for the <TextInput />
component as it shares a lot of props with the native <Text />
component. There are some reasonable defaults set on the TextInput height, border radius, selection color etc., but this is usually adjusted per project needs.
So now I had the 3 components I use on all my projects. Spacer, Text and TextInput. With just these three components the amount of styling code I have to write is dramatically reduced and mostly boils down to screen specific layout.
In addition to the custom components, I've added a lot of useful constants to my constants.js
file. It's mostly colors, spacings, and font sizes, but as the project grows I sometimes add border radius values and shadows.
To help me define the color scheme I use the color
npm package. It enables manipulation of colors so once I pick a theme color the different shades are automatically calculated using the lighten
, darken
and other methods from the color
package.
Here's how it all looks in code:
// constants.js
import { Dimensions } from "react-native";
import Color from "color";
const window = Dimensions.get("window");
export const windowWidth = window.width;
export const windowHeight = window.height;
export const colorBackgroundTheme = "rgb(255, 17, 100)";
export const colorBackgroundLight = "rgba(244, 244, 244, 1)";
export const colorBackgroundDark = "rgba(10, 10, 10, 1)";
export const colorBackgroundThemeSoft = Color(colorBackgroundTheme)
.lighten(0.25)
.rgb()
.string(2);
export const colorBackgroundThemeSofter = Color(colorBackgroundTheme)
.lighten(0.5)
.rgb()
.string(2);
export const colorBackgroundThemeHard = Color(colorBackgroundTheme)
.darken(0.25)
.rgb()
.string(2);
export const colorBackgroundThemeHarder = Color(colorBackgroundTheme)
.darken(0.5)
.rgb()
.string(2);
export const colorBackgroundLightDark = Color(colorBackgroundLight)
.darken(0.25)
.rgb()
.string(2);
export const colorBackgroundLightDarker = Color(colorBackgroundLight)
.darken(0.5)
.rgb()
.string(2);
export const colorBackgroundDarkLight = Color(colorBackgroundDark)
.lighten(0.25)
.rgb()
.string(2);
export const colorBackgroundDarkLighter = Color(colorBackgroundDark)
.lighten(0.5)
.rgb()
.string(2);
export const colorTextTheme = "rgba(216, 0, 75, 1)";
export const colorTextLight = "rgba(255, 255, 255, 0.9)";
export const colorTextDark = "rgba(0, 0, 0, 0.9)";
export const colorTextLightSoft = Color(colorTextLight)
.fade(0.3)
.rgb()
.string(2);
export const colorTextLightSofter = Color(colorTextLight)
.fade(0.5)
.rgb()
.string(2);
export const colorTextDarkSoft = Color(colorTextDark)
.fade(0.3)
.rgb()
.string(2);
export const colorTextDarkSofter = Color(colorTextDark)
.fade(0.5)
.rgb()
.string(2);
export const spacingSmall = 4;
export const spacingMedium = 8;
export const spacingLarge = 16;
export const spacingExtraLarge = 32;
export const fontSizeExtraSmall = 8;
export const fontSizeSmall = 12;
export const fontSizeMedium = 16;
export const fontSizeLarge = 20;
export const fontSizeExtraLarge = 24;
export const fontWeightLight = "100";
export const fontWeightNormal = "500";
export const fontWeightBold = "900";
// Can also export borderRadius values, shadows, etc...
// Spacer.js
import React from "react";
import { View, StyleSheet } from "react-native";
import { constants as C } from "../../style";
const S = StyleSheet.create({
spacingSmall: { width: C.spacingSmall, height: C.spacingSmall },
spacingMedium: { width: C.spacingMedium, height: C.spacingMedium },
spacingLarge: { width: C.spacingLarge, height: C.spacingLarge },
spacingExtraLarge: { width: C.spacingExtraLarge, height: C.spacingExtraLarge }
});
const Spacer = ({ small, medium, large, extraLarge }) => {
let style = S.spacingMedium;
if (small) style = S.spacingSmall;
else if (medium) style = S.spacingMedium;
else if (large) style = S.spacingLarge;
else if (extraLarge) style = S.spacingExtraLarge;
return <View style={style} />;
};
export default Spacer;
// Text.js
import React from "react";
import { Text as RNText } from "react-native";
import { constants as C } from "../../style";
const Text = ({
sizeExtraSmall,
sizeSmall,
sizeMedium,
sizeLarge,
sizeExtraLarge,
colorTheme,
colorDark,
colorDarkSoft,
colorDarkSofter,
colorLight,
colorLightSoft,
colorLightSofter,
weightLight,
weightNormal,
weightBold,
style,
...props
}) => {
let fontSize = C.fontSizeMedium;
if (sizeExtraSmall) fontSize = C.fontSizeExtraSmall;
else if (sizeSmall) fontSize = C.fontSizeSmall;
else if (sizeMedium) fontSize = C.fontSizeMedium;
else if (sizeLarge) fontSize = C.fontSizeLarge;
else if (sizeExtraLarge) fontSize = C.fontSizeExtraLarge;
let color = C.colorTextDark;
if (colorTheme) color = C.colorTextTheme;
else if (colorDark) color = C.colorTextDark;
else if (colorDarkSoft) color = C.colorTextDarkSoft;
else if (colorDarkSofter) color = C.colorTextDarkSofter;
else if (colorLight) color = C.colorTextLight;
else if (colorLightSoft) color = C.colorTextLightSoft;
else if (colorLightSofter) color = C.colorTextLightSofter;
let fontWeight = C.fontWeightNormal;
if (weightLight) fontWeight = C.fontWeightLight;
else if (weightNormal) fontWeight = C.fontWeightNormal;
else if (weightBold) fontWeight = C.fontWeightBold;
return <RNText style={[{ fontSize, color, fontWeight }, style]} {...props} />;
};
export default Text;
// TextInput.js
import React from "react";
import { TextInput as RNTextInput } from "react-native";
import { constants as C } from "../../style";
const TextInput = ({
sizeExtraSmall,
sizeSmall,
sizeMedium,
sizeLarge,
sizeExtraLarge,
colorTheme,
colorDark,
colorDarkSoft,
colorDarkSofter,
colorLight,
colorLightSoft,
colorLightSofter,
weightLight,
weightNormal,
weightBold,
style,
...props
}) => {
let fontSize = C.fontSizeMedium;
if (sizeExtraSmall) fontSize = C.fontSizeExtraSmall;
else if (sizeSmall) fontSize = C.fontSizeSmall;
else if (sizeMedium) fontSize = C.fontSizeMedium;
else if (sizeLarge) fontSize = C.fontSizeLarge;
else if (sizeExtraLarge) fontSize = C.fontSizeExtraLarge;
let color = C.colorTextDark;
if (colorTheme) color = C.colorTextTheme;
else if (colorDark) color = C.colorTextDark;
else if (colorDarkSoft) color = C.colorTextDarkSoft;
else if (colorDarkSofter) color = C.colorTextDarkSofter;
else if (colorLight) color = C.colorTextLight;
else if (colorLightSoft) color = C.colorTextLightSoft;
else if (colorLightSofter) color = C.colorTextLightSofter;
let fontWeight = C.fontWeightNormal;
if (weightLight) fontWeight = C.fontWeightLight;
else if (weightNormal) fontWeight = C.fontWeightNormal;
else if (weightBold) fontWeight = C.fontWeightBold;
return (
<RNTextInput
selectionColor={C.colorBackgroundThemeSofter}
style={[
{
fontSize,
color,
fontWeight,
padding: C.spacingMedium,
margin: 0,
height: 52,
backgroundColor: C.colorBackgroundLightDark,
borderRadius: 26
},
style
]}
{...props}
/>
);
};
export default TextInput;
Top comments (0)