Drop-in replacement of React Native StyleSheet with media-queries, variables, dynamic themes, relative units, percents, math operations, scaling and other styling stuff.
npm i react-native-extended-stylesheet --save
- Define styles using
EStyleSheet.create()
instead ofStyleSheet.create()
:
/* component.js */
import EStyleSheet from 'react-native-extended-stylesheet';
// define extended styles
const styles = EStyleSheet.create({
column: {
width: '80%' // 80% of screen width
},
text: {
color: '$textColor', // global variable $textColor
fontSize: '1.5rem' // relative REM unit
},
'@media (min-width: 350) and (max-width: 500)': { // media queries
text: {
fontSize: '2rem',
}
}
});
// use styles as usual
class MyComponent extends React.Component {
render() {
return (
<View style={styles.column}>
<Text style={styles.text}>Hello</Text>
</View>
);
}
}
- In app entry point call
EStyleSheet.build()
to actually calculate styles:
/* app.js */
import EStyleSheet from 'react-native-extended-stylesheet';
EStyleSheet.build({ // always call EStyleSheet.build() even if you don't use global variables!
$textColor: '#0275d8'
});
[top]
Global variables are passed to EStyleSheet.build()
and available in all stylesheets.
// app entry: set global variables and calc styles
EStyleSheet.build({
$textColor: '#0275d8'
});
// component: use global variables
const styles = EStyleSheet.create({
text: {
color: '$textColor'
}
});
// global variable as inline style or as props to components
<View style = {{
backgroundColor: EStyleSheet.value('$textColor')
}}>
...
</View>
[top]
Local variables can be defined directly in sylesheet and have priority over global variables.
To define local variable just start it with $
:
const styles = EStyleSheet.create({
$textColor: '#0275d8',
text: {
color: '$textColor'
},
icon: {
color: '$textColor'
},
});
Local variables are also available in result style: styles.$textColor
.
[top]
Changing app theme contains two steps:
- re-build app styles
- re-render components tree with new styles
To re-build app styles you can call EStyleSheet.build()
with new set of global variables:
EStyleSheet.build({
$theme: 'light', // required variable for caching!
$bgColor: 'white',
});
Please note that special variable
$theme
is required for proper caching of calculated styles.
Re-rendering whole component tree is currently a bit tricky in React.
One option is to wrap app into component and re-mount it on theme change:
toggleTheme() {
const theme = EStyleSheet.value('$theme') === 'light' ? darkTheme : lightTheme;
EStyleSheet.build(theme);
this.setState({render: false}, () => this.setState({render: true}));
}
render() {
return this.state.render ? <App/> : null;
}
The caveat is that all components loss their state.
In the future it may be possible with forceDeepUpdate()
method (see facebook/react#7759).
The approach is open for discusison, feel free to share your ideas in #22,
#47.
Full theming example is here.
[top]
Media queries allows to have different styles for different screens, platform and orienation.
They are supported as properties with @media
prefix (thanks for idea to @grabbou,
#5).
Media queries can operate with the following values:
- media type:
ios|android
width
,min-width
,max-width
height
,min-height
,max-height
orientation
(landscape|portrait
)aspect-ratio
You can use media queries on:
- global level
- sheet level
- style level
Examples:
// global level
EStyleSheet.build({
'@media ios': {
$fontSize: 12,
},
'@media android': {
$fontSize: 16,
},
});
// sheet level
const styles = EStyleSheet.create({
column: {
width: '80%',
},
'@media (min-width: 350) and (max-width: 500)': {
column: {
width: '90%',
}
}
});
// style level
const styles = EStyleSheet.create({
header: {
'@media ios': {
color: 'green',
},
'@media android': {
color: 'blue',
},
}
});
Any value can contain one of following math operations: *
, /
, +
, -
. Operands can be numbers, variables and percents.
For example, to render circle you may create style:
const styles = EStyleSheet.create({
$size: 20,
circle: {
width: '$size',
height: '$size',
borderRadius: '0.5 * $size'
}
});
[top]
Similar to CSS3 rem unit it allows to define any integer value as relative to the root element. In our case root value is special rem
global variable that can be set in EStyleSheet.build()
. It makes easy to scale app depending on screen size and other conditions. Default rem is 16
.
// component
const styles = EStyleSheet.create({
text: {
fontSize: '1.5rem',
marginHorizontal: '2rem'
}
});
// app entry
let {height, width} = Dimensions.get('window');
EStyleSheet.build({
$rem: width > 340 ? 18 : 16
});
[top]
Percent values are supported natively since React Native 0.43.
EStyleSheet passes them through to original StyleSheet except cases, when you use calculations with percents,
e.g. "100% - 20"
. Percents are calculated relative to screen width/height on application launch.
const styles = EStyleSheet.create({
column: {
width: '100% - 20'
}
});
Percents in nested components
If you need sub-component with percent operations relative to parent component - you can achieve that with variables.
For example, to render 2 sub-columns with 30%/70% width of parent column:
render() {
return (
<View style={styles.column}>
<View style={styles.subColumnLeft}></View>
<View style={styles.subColumnRight}></View>
</View>
);
}
...
const styles = EStyleSheet.create({
$columnWidth: '80%',
column: {
width: '$columnWidth',
flexDirection: 'row'
},
subColumnLeft: {
width: '0.3 * $columnWidth'
},
subColumnRight: {
width: '0.7 * $columnWidth'
}
});
[top]
You can apply scale to components by setting special $scale
variable.
const styles = EStyleSheet.create({
$scale: 1.5,
button: {
width: 100,
height: 20,
marginLeft: 10
}
});
This helps to create reusable components that could be scaled depending on prop:
class Button extends React.Component {
static propTypes = {
scale: React.PropTypes.number
};
render() {
let style = getStyle(this.props.scale)
return (
<View style={style.button}>
</View>
);
}
}
let getStyle = function (scale = 1) {
return EStyleSheet.create({
$scale: scale,
button: {
width: 100,
height: 20,
marginLeft: 10
}
});
}
To cache calculated styles please have a look on caching section.
[top]
Original react-native stylesheets are calculated to integer numbers and original values are unavailable.
But sometimes they are needed. Let's take an example:
You want to render text and icon with the same size and color.
You can take this awesome icon library
and see that <Icon>
component has size
and color
props.
It would be convenient to define style for text and keep icon's size/color in sync.
const styles = EStyleSheet.create({
text: {
fontSize: '1rem',
color: 'gray'
}
});
In runtime styles
created with original react's StyleSheet
will look like:
styles = {
text: 0
}
But extended stylesheet saves calculated values under _text
property:
styles = {
text: 0,
_text: {
fontSize: 16,
color: 'gray'
}
}
To render icon we just take styles from _text
:
return (
<View>
<Icon name="rocket" size={styles._text.fontSize} color={styles._text.color} />
<Text style={styles.text}>Hello</Text>
</View>
);
[top]
Extended stylesheet supports 4 pseudo classes: :first-child
, :nth-child-even
, :nth-child-odd
, :last-child
. As well as in traditional CSS it allows to apply special styling for first/last items or render stripped rows.
To get style for appropriate index you should use EStyleSheet.child()
method.
It's signature: EStyleSheet.child(stylesObj, styleName, index, count)
.
const styles = EStyleSheet.create({
row: {
fontSize: '1.5rem',
borderTopWidth: 1
},
'row:nth-child-even': {
backgroundColor: 'gray' // make stripped
},
'row:last-child': {
borderBottomWidth: 1 // render bottom edge for last row
}
});
...
render() {
return (
<View>
{items.map((item, index) => {
return (
<View key={index} style={EStyleSheet.child(styles, 'row', index, items.length)}></View>
);
})}
</View>
);
}
[top]
For the deepest customization you can specify any value as a function that will be executed on EStyleSheet build. For example, you may darken or lighten color of variable via npm color package:
import Color from 'color';
import EStyleSheet from 'react-native-extended-stylesheet';
EStyleSheet.build({
$buttonColor: 'green'
});
...
const styles = EStyleSheet.create({
button: {
backgroundColor: () => Color(EStyleSheet.value('$buttonColor')).darken(0.1).hexString()
}
});
...
render() {
return (
<TouchableHighlight style={styles.button}>
...
</TouchableHighlight>
);
}
[top]
If you use dynamic styles depending on runtime prop or you are making reusable component with dynamic styling
you may need stylesheet creation in every render()
call. Let's take example from scaling section:
class Button extends React.Component {
static propTypes = {
scale: React.PropTypes.number
};
render() {
let style = getStyle(this.props.scale)
return (
<View style={style.button}>
</View>
);
}
}
let getStyle = function (scale = 1) {
return EStyleSheet.create({
$scale: scale,
button: {
width: 100,
height: 20,
marginLeft: 10
}
});
}
To avoid creating styles on every render you can use lodash.memoize: store result for particular parameters and returns it from cache when called with the same parameters. Updated example:
import memoize from 'lodash.memoize';
let getStyle = memoize(function (scale = 1) {
return EStyleSheet.create({
$scale: scale,
button: {
width: 100,
height: 20,
marginLeft: 10
}
});
});
Now if you call getStyle(1.5)
3 times actually style will be created on the first call
and two other calls will get it from cache.
[top]
It is possible to outline all components that are using EStyleSheet. For that set global $outline
variable:
EStyleSheet.build({$outline: 1});
Note that components without styles will not be outlined, because RN does not support default component styling yet.
To outline particular component set local $outline
variable:
const styles = EStyleSheet.create({
$outline: 1,
column: {
width: '80%',
flexDirection: 'row'
},
...
});
[top]
Hot module reload (HMR) allows you to change code and see live updates without loosing app state. It is very handy for tuning styles. EStyleSheet supports HMR with the following options:
- When you change style of component - the component is updated by HMR automatically without any affort from your side.
- When you change global variable or theme - you should use HMR API
to force style re-calculation:
// app.js EStyleSheet.build({ $fontColor: 'black' }); ... module.hot.accept(() => { EStyleSheet.clearCache(); EStyleSheet.build(); // force style re-calculation });
See full example of HMR here.
[top]
/**
* Creates extended stylesheet object
*
* @param {Object} source style
* @returns {Object} extended stylesheet object
*/
create (source) {...}
[top]
/**
* Calculates all stylesheets
*
* @param {Object} [globalVars] global variables for all stylesheets
*/
build (globalVars) {...}
[top]
/**
* Calculates particular value
*
* @param {*} value
* @param {String} [prop] property for which value is calculated. Needed for example for percent values.
* @returns {*} calculated result
*/
value (value, prop) {...}
[top]
/**
* Returns styles with pseudo classes :first-child, :nth-child-even, :last-child according to index and count
*
* @param {Object} stylesheet
* @param {String} styleName
* @param {Number} index index of item for style
* @param {Number} count total count of items
* @returns {Object|Array} styles
*/
child (styles, styleName, index, count) {...}
[top]
/**
* Subscribe to events. Currently only 'build' event is supported
*
* @param {String} event
* @param {Function} listener
*/
subscribe (event, listener) {...}
This method is useful when you want to pre-render some component on init.
As extended style is calculated after call of EStyleSheet.build()
,
it is not available instantly after creation so you should wrap pre-render
info listener to build
event:
const styles = EStyleSheet.create({
button: {
width: '80%',
}
});
// this will NOT work as styles.button is not calculated yet
let Button = <View style={styles.button}></View>;
// but this will work
let Button;
EStyleSheet.subscribe('build', () => {
Button = <View style={styles.button}></View>;
});
[top]
-
Hot module reload is not supported yet
Hot module reload requires all components to be re-rendered and keep local state. Please follow #16 for updates. -
Dynamic theme change is possible only with loosing components local state
When theme styles are re-calculated - all components should be re-rendered. Currently it can be done via re-mounting components tree, please see #47.Note: it is not issue if you are using state container like Redux
-
Dynamic orientation change is not processed
Please see #9 for more details.
Please see CHANGELOG.md
If you have any ideas or something goes wrong feel free to open new issue.
MIT @ Vitaliy Potapov
[top]
If you love ❤️ JavaScript and would like to track new trending repositories,
have a look on vitalets/github-trending-repos.