engineering

Developer Commentary: Peerlist Dark Mode

Developer Commentary: Peerlist Dark Mode

From Design to Deployment: How We Built Dark Mode Without Disrupting Development

Vaishnav Chandurkar

Vaishnav Chandurkar

Jan 06, 2025 7 min read

Introduction

Recently we introduced dark mode to the Peerlist web and mobile app. We wanted to give you a peek under the hood and some engineering stuff in the process of getting to this point.

As with any large codebase, implementing a working solution is just the first step. The greater challenge lies in maintaining consistent development while preventing regressions and ensuring the new feature integrates seamlessly with existing functionality.

So the question for the dark mode was:
How can we keep building dark mode without holding our daily deployments and without accidentally introducing half-backed dark mode to users?

Backstory

At Peerlist, every feature we work on has this common feature development process: After the first design demo, engineers are involved in the design process, giving suggestions, and feedback on the feature's user experience, and when development starts, the designers are involved in the engineering process, keeping a close eye on user interface and experience. This helps us all reduce rework and work on improving features.

After the first Dark mode demo, the engineering team started planning out development steps. While designers were busy with designs we took advantage of this. We decided to ship dark mode in two phases:

  1. Creating new components, addressing the tech debt & refactoring the codebase

  2. The actual dark mode implementation.

Phase I: Creating new components & refactoring codebase

An easy way to ship fast is “Divide and conquer”. We were 3 developers working together to do this. So we decided to split the tasks of developing new components.

Earlier we did not have planned components, so learning from our mistakes we decided to have a fixed component APIs and documentation. Peerlist started as a side project, one big problem was the lack of documentation. This was a good opportunity for us to solve that up to some extent.

For this, we decided to add comments directly in the component file. This gave us ease to maintain it and, we can just open the component to read the documentation.

Refactoring codebase:

Refactoring the codebase is a crucial step, we need to keep a close eye on every change and test everything for regression. On top, we did not want to hold our daily deployment.

Why daily deployment?

3 years back we used to have a feature release on the 1st of every month. It was a pain to the development process. Later we made a release twice a month, then once a week, and for the past 1.5 years, we have pushed changes daily. Even if it's a one-line change, it will go into production. There are lots of features that cannot be completed in one day, and to avoid showing half-backed features we use a feature flag extensively.

So when a new component is developed, we used to start refactoring our codebase. To avoid regression, we kept a new component under the feature flag.

// Input component
const OldInput = () => { ...old input component code }
const NewInput = () => { ...new input component code }

const Input = isNewComponentEnabled() ? NewInput : OldInput;

export default Input;

This allowed us to test new components in staging without blocking daily deployment. When everything looks good, we just need to enable new components in production.

Phase II: Adding Dark mode support

We use Tailwind CSS and follow the official Tailwind CSS color class-name pattern. Literal color names (like grey, green, red, etc.)  and a numeric scale (where 50 is light and 900 is dark) by default.

The design team was trying Figma variables, and semantic naming for color names while they were working on dark mode design. We were reluctant to make this change because the way the code was set, would have caused an entire rewrite. You can read more about the Dark mode design process here.

We needed to find the middle ground. While working with the designer we noticed only the grey color pallet had different colors in dark mode, while other colors remained the same in almost all cases.

We needed the flexibility of Tailwind, which allows us to have different colors in dark mode using dark: variants. but also need to avoid using dark: as much as possible. The solution was to define the gray color pallet as CSS variables and keep using tailwind literals for color names.

/* style.css */
@layer base {
 :root {
   --gray-00: 255 255 255; /* #ffffff; */
       .
       .
       .
   --gray-1k: 13 13 13; /* #0d0d0d; */
  }
 .dark: {
   --gray-00: 23 23 23; /* #171717; */
       .
       .
       .
   --gray-1k: 250 250 250; /* #fafafa; */
  }
}
/* tailwind.config.js */
module.exports = {
  theme: {
    colors: {
      gray: {
        '00': 'rgb(var(--gray-00))',
		...
      },
      green: { ... green color pallet },
      red: { ... red color pallet }
    }
  }
}

You can learn more about using CSS variables with Tailwind CSS.

With this setup, we got enough flexibility to have a completely different color set, based on light/dark mode, reduce rewrite, and avoid color-related regression.

Handling Images and SVG Icons.

SVG's icons were easy to handle, we just need to make sure svg has stroke='currentColor' so that it will auto-pick the foreground i.e. text color of its parent. If we need more control we pass className.

Image handling was specifically limited to images that were used in CTA's or on the landing pages. We have 2 variants of images, one for light and another for dark mode. We create a component that renders the following HTML.

<!-- When the theme is dark, hide this div -->
<div className='block dark:hidden'> <img/> </div>
<!-- When the theme is light, hide this div -->
<div className='hidden dark:block'> <img/> </div>

We found this solution better than conditionally rendering different images.

Mobile Implementation

The mobile version of Peerlist is built with React Native and uses NativeWind for styling. With NativeWind, we can use Tailwind CSS for mobile development. This setup allows us to maintain styling consistency between our web and mobile platforms, using the same familiar Tailwind class patterns.

However, we faced an initial technical hurdle. Our mobile app was running on React Native 0.72.3 and NativeWind v2, but the CSS variable support we needed for our theming system was only available in NativeWind v4. This meant we needed to undertake a two-step upgrade process:

  • Update React Native using React Native Upgrade Helper to ensure a smooth version transition.

  • Upgrade NativeWind to v4 to gain access to the CSS variable functionality

With the technical foundation updated, we could implement dark mode following the same systematic approach we used for the web. We proceeded screen by screen, ensuring each component properly supported both light and dark themes.

Personalized Mode for Better Experience

We want users to be able to choose different modes for both web and mobile. For example, they can use light mode on the web and dark mode on mobile, or the other way around. This gives them more control and a better experience.

We store user preferences for themes separately for each platform, ensuring users have full control over their experience. This means users can manually select their preferred mode (light or dark) for both web and mobile platforms. Once a preference is set, it is saved and consistently applied whenever they use the app on that platform.

Additionally, the app can automatically detect and adapt to the system’s default theme settings if no manual preference is selected. However, once a user sets their preference, it will take priority and override the device’s default settings.

The Final Phase: Rolling Out Dark Mode

For us, Implementing dark mode was more than just a visual update - it was an opportunity to improve our development practices and codebase quality.

Once we had the core implementation stable, we initiated a controlled rollout of dark mode. We first released it to a select group of users, to gather early feedback and identify any edge cases we might have missed. This phased approach allowed us to, Collect real-world usage data and user feedback & fix subtle UI inconsistencies.

After incorporating the feedback and making necessary adjustments, we gradually expanded the rollout to our entire user base. Resulting in a smooth transition that maintained our daily deployment schedule while delivering a polished dark mode experience to our users.

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.