Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Refactoring styles to Aphrodite

Suguru Hirahara edited this page Nov 2, 2017 · 12 revisions

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.

Example

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:

  1. All multi-word properties become camel cased (lineHeight instead of line-height).
  2. Pseudo selectors and media queries can be given their own class or be nested within a selector.

things you should know about Aphrodite

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.

CSS and Stylesheet

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'
  }
})

Refactor time

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
  ...
)} />

Conditional

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)}

globalStyles and commonStyles

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
    ...
  )} />

Thinking different with Aphrodite

While refactoring to Aphrodite, there are some things you should consider:

1. Declared class names changes after rendering

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.

2. Pseudo-state

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'
  }
}

3. No support for descendant selection of pseudo-state

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.

Side notes you must be aware

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.

How can I decide if style should be attached to component / separate file

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:

https://github.com/Khan/aphrodite

Vertical Side Tabs Tab Suspender

Clone this wiki locally