Mobile styles and themes
For styling and theme support we use the ThemedStyles service. This service provides some frequently used styles, for common layouts, colors, font size, and spacing (padding and margin).
Using styles
<Text style={ThemedStyles.style.colorWhite} />
<Text style={ThemedStyles.style.colorPrimaryText} />
Combining styles
Before diving into the different styles that the service provides, it is important to understand how to combine them, that it returns a stable
reference to the styles, and the implications of this.
Combine
With the method combine you can merge many styles into one.
Standard styles
const fontStyle = ThemedStyle.combine("colorWhite", "fontXL", "marginTop");
Custom styles
const myStyle = ThemedStyle.combine(
{ heigh: 20 },
"flexContainer",
"marginTop",
"bgBlack"
);
Stable reference to styles
ThemedStyles returns always a stable reference to the styles, this allows you to use a "global" defined style for a component and still be able to switch themes in runtime. It is optimum because you share the same style with all the instances of the component and you ensure that you are sending always the same style object (preventing unneeded re-renders).
function MyText() {
return <Text style={fontStyle}>Title</Text>;
}
const fontStyle = ThemedStyle.combine(
"colorPrimaryText",
"fontXL",
"marginTop"
);
useStyle hook
If you need a style based on a runtime value you can use the useStyle hook.
function MyText() {
const inset = useSafeAreaInsets();
const fontStyle = useStyle("colorPrimaryText", "fontXL", {
paddingTop: inset.top,
});
return <Text style={fontStyle}>Title</Text>;
}
It returns a stable reference to the style object but in this case you will have one style for each instance of the component.
Styles
Colors
You can use colors for text, backgrounds, borders and shadows using the following style format:
Text
color[ColorName]
<Text style={ThemedStyles.style.colorWhite} />
Background
bg[ColorName]
<View style={ThemedStyles.style.bgPrimaryBackground} />
Border
bg[ColorName]
<View style={ThemedStyles.style.bcolorLink} />
Shadow
bg[ColorName]
<View style={ThemedStyles.style.shadowLink} />
You can check the available colors or add new ones on
src/styles/Colors.ts
.
Spacing
For spacing you can use the margin and padding styles with the following format:
margin(Top|Bottom|Right|Left|Vertical|Horizontal)(n)x
padding(Top|Bottom|Right|LeftVertical|Horizontal)(n)x
Eg: | Style | | |-------------------|---------------------------------------------| | padding | padding of 4 | margin | margin of 4 | padding2x | padding of 8 | margin4x | margin of 16 | marginTop4x | margin of 16 to top the view |paddingVertical2x | padding of 8 into the top and bottom |paddingHorizontal | padding of 4 into left and right
function MyComponent(props) {
return (
<View style={containerStyle}>
<Text style={ThemedStyle.style.marginTop}>{user.me.name}</Text>
<Text style={ThemedStyle.style.marginTop}>{user.me.username}</Text>
</View>
);
}
const containerStyle = ThemedStyle.combine("paddingTop2x", "marginHorizontal");
For now it uses a multiple of 5 but we will introduce a multiplier based on the screen size.
Border
The border styles have the following format:
border(Top|Bottom|Left|Right|Radius)(n)x
function MyComponent(props) {
return <View style={containerStyle} />;
}
const containerStyle = ThemedStyle.combine(
"borderTop",
"borderBottom5x",
"borderRadius3x"
);
For hair width: borderHair, borderLeftHair, borderRightHair, borderTopHair borderTopHair
Sizes
For width and height percentage you can use the following styles
fullWidth
halfWidth
fullHeight
halfHeight
or
width(n)
height(n)
Eg:
function MyComponent(props) {
return (
<View style={ThemedStyle.style.width35}/> // width: 35%
<View style={ThemedStyle.style.height56}/> // height: 56%
<View style={ThemedStyle.style.fullWidth}/> // width: 100%
<View style={ThemedStyle.style.fullHeight}/> // height: 100%
);
}
Font
You can use the following normalized font style for size and weight
Sizing
Normalized font size: **fontS
fontXS
fontM
fontL
fontXL
fontXXL
fontXXXL`
Weight
Normalized font weight: fontHairline
fontThin
fontLight
fontNormal
fontMedium
fontSemibold
fontBold
Align
Normalized font weight: textRight
textLeft
textCenter
textJustify
Other
strikeThrough
function MyComponent() {
return <Text style={fontStyle}>Title</Text>;
}
const fontStyle = ThemedStyle.combine(
"fontL",
"fontLight",
"colorWhite",
"textCenter"
);
Layout
Usually, you will use custom styles in your component to define the main layout, but there are some commonly used layout styles that are available for you in the themed styles service.
Eg: | Style | |-------------------| |flexContainer | |flexContainerCenter | |flexColumn | |flexColumnCentered | |rowJustifyEnd | |rowJustifyCenter | |rowJustifySpaceEvenly | |rowJustifyStart | |rowJustifySpaceBetween | |rowStretch | |justifyCenter | |justifyEnd | |alignCenter | |alignEnd | |alignSelfEnd | |alignSelfStart | |alignSelfCenter | |centered | |positionAbsolute | |positionAbsoluteTopLeft | |positionAbsoluteTopRight | |positionAbsoluteBottomLeft | |positionAbsoluteBottomRight |
Although the styles for colors, borders, spacing and sizes are dynamically generated they are validated using typescript
Creating styled components
Using the useStyleFromProps hook you can easily create component that generate styles from their properties
function MyText(props) {
const style = useStyleFromProps(props);
return <Text style={style}>{children}</Text>
}
<MyText colorWhite bgBlack fontXL marginTop2x>
<MyText colorWhite bgBlack fontXL marginTop="2x">
Any available style from the ThemedStyle service can be used here.
Keep in mind that this is only syntax sugar for the useStyle hook. Defining the styles as a global constant is more optimal for components that are repeated many times (like list items for example)
Extending
The dynamic styles are implemented using a Proxy that creates the required styles on-demand. For this it use generators
functions that convert a style name into a react-native style. Currently we have generators for spacing
, colors
, sizes
, and borders
.
These generators are located on styles/generators/
folder and they are called from the proxy handler.
Initial design system effort: UI tokens and base UI components
The reasoning
The legacy Mobile UI implementation lacks on centralized UI Development/Design definitions and base UI components. This impacts directly the team's development speed, creating a number of small UI/UX inconsistencies.
As an effort to create a clear abstraction of the commonly used elements between Design and Development, we're creating basic UI Token definitions and a set of pure UI components.
References
Selected articles for contextualization and basic understanding behind the standards that are being adapted on the project.
- Building a Design System: Speed, Scale, Collaboration, and Innovation
- The Benefits of Design Systems: Cutting Costs and Creating Competitive Advantage
- Accelerating Time to Launch with a Design System
Spacing and centralized units
For a starting point we're introducing the classic clothing-size model with XXS
, XS
, S
, M
, L
, XL
, XXL
together with an optional 2L
, 2XL
and 2XXL
for avoiding extra calculations on increased common sizes.
The reasoning for not using the common pattern of one unit for every dimension (horizontal/vertical) or component type is to avoid an increased number of similar definitions. Having a different unit for horizontal spacing, vertical spacing, fonts, buttons, etc ends up being expensive for maintain and extend. We're using a 4 point base grid, that's centralized on one unit type.
Unit | ---------- |
---|---|
XXXS | 2 |
XXS | 4 |
XS | 8 |
S | 12 |
M | 16 |
L | 20 |
XL | 24 |
XXL | 28 |
Sizing/type definitions
For creating a base unit abstraction and to improve the ease of communication between teams, we're defining component sizes nomenclature with: micro
, tiny
, small
, medium
, large
and huge
. For the typography model we're using the modified Header1
, Header2
, Header3
, Header4
, for the headers and Body1
, Body2
, Body3
, Body4
for body texts.
Other definitions
Color
For reducing the on screen/feature logic, we're prioritizing the flag definition on top of setting the color option manually. Mainly if a component has a small set of well defined color variations we'd use the active
light
disabled
patterns to help clearing up the logic on the outside world.
Default medium size
Based on the most used pattern, where the most used style defines the default component prop and medium values.
Font-sizes
Font-sizes have a value on it's own realm. As fonts snap to the grid through it's line-height, font-sizes uses typed specific constants. While it's line-height keep the usual inheritance from the centralized UNIT
.
Wrapped component decision
Following the principle of the most used pattern and trying to respect the atomization of each element, so styles and behaviors that are not commonly used throughout the project are delegated to wrapper components. i.e. on the Icon
set we have the IconButton
and IconCircle
wrappers.
Applying the spacer
The space units can be imported on UI components from the ~styles/tokens.ts
file, applied using the Spacer
component as a parent or wrapping the component itself with the withSpacer
HOC.
Eg:
import {Spacer, withSpacer, Icon} from "~ui"
const Component = (
<Spacer top="XXL" left="L">
/* ... */
</Spacer>
}
const IconS = withSpacer(Icon)
Legacy icon replacement example
On the legacy implementation, we were importing different icon families per screen/feature and defining manually their styling and behavior.
import MIcon from 'react-native-vector-icons/MaterialIcons';
import Icon from 'react-native-vector-icons/MaterialIcons';
...
const chevron = <MIcon
size={34}
name="chevron-left"
style={styles.backIcon}
onPress={props.onPressBack}
/>
...
const error = <Icon name="error" size={32} color="white" />
The new strategy consists on abstracting as much as we can to a base component level to avoid the per screen/feature, strengthening the centralization, accelerating the feature development process and avoiding minor UI bugs and inconsistencies.
import { Icon, IconButton } from "~ui/icons";
const chevron = (
<IconButton name="chevron-left" size="small" onPress={onPressBack} />
);
const error = <Icon name="error" />;
Benchmarking components on-screen
We've created a small on-screen benchmarking tool at ~/performance
for analyzing the component's on-screen render performance. It uses React's Profiler
and render a defined number (5k
/10k
/20k
) of components measuring it's render time on mount, update (3x) and unmount. With the option of setting up multiple experiments at a time.
Experiment setup example
const experimentA = () => {
return {
id, // experiment id
name, // experiment name
mount: generateIcons, // generate random props for the initial mount
update: generateIcons, // generate random props for the 2x/3x updates
component: () => IconNext, // return the base component
};
};
Spacer, row, and column
These are the inner structural components. They are a shortcut to easily access common Row
and Column
flex properties. They're useful for replacing the atomic components on the legacy interface.
Replacement example
Checkout the drawer replacement example.
const WITH_BASE = (
<Row left="XL2" right="XL" bottom="XXL">
<Avatar source={avatar} size="medium" onPress={onUserPress} />
<Column flex centerStart horizontal="M">
<H2 onPress={onUserPress} bold>
{name || `@${username}`}
</H2>
{name && (
<B1 flat light onPress={onUserPress}>
@{username}
</B1>
)}
</Column>
<IconButton scale name="account-multi" onPress={onIconPress} />
</Row>
);
Screen, ScreenHeader, ScreenSection
The screen components are aimed to control the outside structure of the screen. Flexing, basic scrolling, safe insets, default background and horizontal spacing.
Storybook
As the component base grows we're implementing an UI Storybook for centralizing the isolated implementation, documentation and visual regression of the library - TBD.