-
Notifications
You must be signed in to change notification settings - Fork 974
Refactoring styles to Aphrodite
We are considering other options than Aphrodite. Please keep in mind that on some cases new PRs might not be merged.
We are moving our LESS codebase to a styled-component approach with Aphrodite.
Styled components allow us to have a more consistent style. This way you can inject only the exact styles needed for the render into the DOM.
Please have a look at our style guidelines for more general information about styling with Aphrodite.
Here is an example of a React component styled with Aphrodite.
Aphrodite will automatically create a single <style>
tag in the document <head>
to put its generated styles in.
const React = require('react')
const ImmutableComponent = require('./immutableComponent')
const {StyleSheet, css} = require('aphrodite')
class Button extends ImmutableComponent {
render () {
return <span className={css(styles.browserButton)}
disabled={this.props.disabled}
data-l10n-id={this.props.l10nId}
data-button-value={this.props.dataButtonValue}
onClick={this.props.onClick} />
}
}
const styles = StyleSheet.create({
browserButton: {
cursor: 'default',
display: 'inline-block',
lineHeight: '25px',
width: '25px',
height: '25px',
':hover': {
color: 'black'
},
'@media (min-width: 500px)': {
width: '100px'
}
}
})
module.exports = Button
A few things to note:
- All multi-word properties become camel cased (lineHeight instead of line-height).
- Pseudo selectors and media queries can be given their own class or be nested within a selector.
Styles turns to be plain JS objects, so this:
.parent {
background-color: red;
color: white;
}
becomes this:
parent: {
backgroundColor: 'red',
color: 'white'
}
Notice that value is set as a string, and CSS properties are treated as default object properties.
The two most common functions you'll need to refactor a component to Aphrodite are CSS
and Stylesheet
.
Stylesheet
allows you to create your styles while css()
is what Aphrodite looks to render the style you created.
A simple Aphrodite implementation would be similar to the below:
const {StyleSheet, css} = require('aphrodite/no-important')
...
render () {
return <a className={css(styles.orangeLink)} href='https://brave.com' />
}
...
const styles = StyleSheet.create({
orangeLink: {
color: 'orange',
textDecoration: 'underline'
}
})
The first thing you should look before starting your refactor is which classes does the component requires. We currently use classSet
module (cx
)
to apply style classes. cx
allows us to dynamically set a given class, for example:
const cx = require('../lib/classSet')
...
render () {
return <div className{cx({
tabArea: true, // component will always have tabArea class
isDragging: this.isDragging // if isDragging is true, apply that class.
})} />
}
This is a good first step to check which styles you will need to refactor. The above example would be converted to Aphrodite in the following way:
const {StyleSheet, css} = require('aphrodite/no-important')
...
render () {
<div className={css(
styles.tabArea, // component will always have tabArea class
this.isDragging && styles.isDragging // style for isDragging will only be called if condition is true
)}
}
const styles = StyleSheet.create({
tabArea: {
// ... tabArea styles here
},
isDragging: {
// ... isDragging styles here
}
})
You can use css
and cx
at the same time:
<button className={cx({
fa: true,
'fa-lock': true,
[css(styles.red)]: true
...
)} />
If your style is conditional and has more than one condition, consider putting them in a constant to make it clearer/more readable:
// Bad
className={css(this.navBar && this.braveLogo && styles.braveLogoImage)}
// Good
const navBarBraveLogo = this.navBar && this.braveLogo
className={css(navBarBraveLogo && styles.braveLogoImage)}
While refactoring please bear in mind to avoid using hardcoded values (width, height, size, etc) and consider to import from globalStyles
as far as possible.
Also, please search commonStyles
for the styles you are going to specify. If they were already specified, please import them from it like this way:
const commonStyles = require('./styles/commonStyles')
<button
className={css(
commonStyles.browserButton
...
)} />
While refactoring to Aphrodite, there are some things you should consider:
If you have a component with class .tab
, Aphrodite will change its name to something else that makes sense to Aphrodite itself. It's an important thing to know if you plan to test a new component, you can't rely on class names. In this case, you'll need to create a custom property, like data-test-id='tab'
to make your assertions.
Aphrodite works ok with pseudo-states like :hover
, :active
, :visited
. Unlike other attributes, pseudo-states are treated as strings (quoted):
component: {
background: 'grey',
':hover': {
background: 'orange'
}
}
If your element depends on pseudo-states from parent classes, they'll not work. Consider the below example in LESS:
.closeIcon {
opacity: 0; // Hide closeIcon by default
}
.tab {
&:hover {
.closeIcon {
opacity: 1; // If mouse is hovering tab, show close icon
}
}
}
A way to solve that problem is to manually set a state for the hover state using onMouseEnter
and onMouseLeave
events, which will take a considerable extra step but will ensure that even the hover state can be tested, which is a good thing that Aphrodite API forces that pattern.
Our most used component, <Button />
, is not deprecated in favor of <BrowserButton />
component. If you get catch in a situation that this component is visible (and it's likely is), please remove legacy button and include it.
If the component is small enough (example: publisherToggle button), you're encouraged to display component style on the same file as the component itself. If a given component has too many styles (such as tab) and isn't too dynamic (need properties generated at component level), it's ok to have its own file separated.
For more information regarding Aphrodite, check their (awesome) docs:
insecurity test
Vertical Side Tabs Tab Suspender