Implement Night Mode on Twitter Lite

We have just launched Night Mode on Twitter Lite recently. Night mode is an exciting feature in regards to engineering. It is a highly demanded, visually pleasing and the primary driver for our effort of moving our CSS to CSS-in-JS. Let’s dive into what did we do to bring this feature to life!

DISCLAIMER: The post was written and posted after the end of my employment at Twitter. I tried to recall the details as best as I could, and I apologize beforehand for any inaccuracies.

What is it?

Night mode is an increasingly popular feature that starts to show up on a lot of websites/apps. Most of the websites use a white background which might cause eye strains when used in a dark environment. When users activate night mode, Twitter Lite switch to a dark color theme app-wide.

Styling components

The core of this feature is the ability to dynamically switching the styling of every component on the screen. Our components were styled using CSS. To swap out styling, we would have to build multiple CSS bundles based on a few factors: color theme, and LTR/RTL text direction. It is not a very scalable solution and requires users to download new CSS when switching different combinations. The other option would be switching to CSS variables. It, unfortunately, does not have enough support across the browsers that Twitter Lite intended to support.

Our next option would be to switch to a CSS-in-JS solution. We use react-native-web throughout our internal component library and the website. It has a built-in component called StyleSheet that provides the function.

// A simplifed example of using react-native-web StyleSheet
const styles = StyleSheet.create({
  root: {
    backgroundColor: theme.colors.red
  }
});

const Component = () => <View styles={styles.root}/>;

Runtime-generated Style Sheet

To create a StyleSheet instance, you make a StyleSheet.create call and pass in a JSON object that looks very much like its CSS counterpart. The API returns you an object with the class name mapped to a number representing the registered styles while its styling engine works in the background to generate runtime CSS classes and deduplication. We would need to somehow allow it to:

  1. Rerun the style creation every time we switch to a new theme
  2. Pass in reference to the next theme so we can use the new color palette

We designed a new API wrapping the StyleSheet API, but instead of taking an object, a function (theme) => styleObject is accepted. We store references to all those functions and return an object with dynamic getters. Whenever users requests to switch themes, we would re-run all the style creations with the new theme. The React components can use the same styles object returned from the first API call to render with the new style.

// Updated to support the new API
const styles = StyleSheet.create(theme => ({
  root: {
    // do not use color name directly but name colors by context
    backgroundColor: theme.colors.navigationalBackground
  }
}));

const Component = () => <View styles={styles.root}/>;

Are we all on the same page?

Sounds perfect! New styles are generated, and all the references are updated. The page, however, is not updated. Well, not until some components receives new data. The components are not re-rendering on the spot because we are updating an external variable instead of working with the React component states. We need a way to signal components to re-render.

Theoretically, we would love this part to be as performant as possible to reduce the overhead of switching themes. For example, we could use a higher-order component to keep track of the components and its corresponding styles and use that information to update components on a smaller scale. It turned out to be hard as we would need to wrap around many components and also the components might have some shouldComponentUpdate tricks to prevent themselves from updating, and the children components might also have shouldComponentUpdate functions too. It does work 80% of the time, it is unfortunate that the other 20% stand out very much under a dark theme.

One hacky solution would be to somehow recursively calling forceUpdate() on every mounted component. It would require some meddling with React internals and we eventually decided not to do this. In our first implementation, we used to manually unmount the previous component tree entirely and remount a new one; this caused a considerable delay in theme switching and was working out of React’s lifecycles. We switched to using React.Fragment with the key set to the theme name, allowing React to optimize the operation better and without lifecycle hooking.

class AppThemeRoot extends React.Component {
  state = { theme: 'light' };

  componentDidMount() {
    StyleSheet.onThemeSwitch((theme) => this.setState({ theme: theme.name }));
  }

  render() {
    return (
      <React.Fragment key={this.state.theme}>
        {this.props.children}
      </React.Fragment>
    );
  }
}

The final touch

▲ Night mode smooth transition

Now that we have the basic going, we would like to make it better. Instead of swapping the content out directly, we would like it to be a smooth transition. We have also explored a few different options to implement this.

The first option pops up in my head is to implement a cross-fade. Fading out the old content while fading in the new content. We can create a copy of the old content by doing oldDomNode.cloneNode(true) and insert it back into the DOM. It looked absolutely beautiful, but sadly it did screw up our virtualised list implementation. We had to explore other avenues. The next thing we tried was to fade out and fade in. It looks okay when we do it fast enough so that the transition feels smooth. It, however, would have a brief period of white flashing due to the default page background being full white. We addressed the flash by also fading the document background color to the next background color which makes it feels much more like a cross-fade than a simple fade-out-and-in.

Credit

I hope you enjoyed our journey of exploring the implementation of the Night Mode. Night Mode can’t be made without the team’s collaboration. Thanks to Marius and Sidhu for finding out the best solution to this problem with me. Special call out to Sidhu because he implemented the proposal. Thanks to the whole team very efficiently migrated all of our components out of CSS in two hack days which in turn enables us to switch the theme of the entire website!