Stealth-mode North Star — Rebranding in secret with feature flags

Welcome, everyone! I’m Kathleen McMahon and I’m here today to show how we rolled out a product rebrand — in secret — using feature flags

Before we begin, let’s get some details out of the way.

https://noti.st/resource11

My presentation will be posted on Notist, that’s https://noti.st/resource11, after my talk.

@resource11 Twitter | Instagram | GitHub

You can follow me at resource11 on Twitter, Instagram, and GitHub.

Let’s back up so I can introduce myself better…

Kathleen McMahon | Engineer • Designer • Speaker

So I’m an engineer and a designer and I like to speak about things…

image: Me on a cyclocross bike, hopping over barriers and riding through mud.

And… race cyclocross. Very badly.

image: Me on a cyclocross bike, in Spam, Ninja Turtle, Nerds candy box, Medusa costumes

Mostly you’ll see me in costume, racing two laps to your six, at the back of the pack, on a singlespeed, — with a bunch of onlookers in the crowd laughing at me, toasting with their beverages.

image: Stanley Spadowski shouts "OPEN WIDE!" and douses boy with a fire hose

Now I think y’all agree these past couple of years have been intense. Whew! Let’s get that out of our system.

image: Group of penguins gathered on an iceberg with a few swimming and jumping in the ocean. One penguin attempts to jump atop an iceberg to join their penguin friends — and fails — splashing back into the water in ungraceful fashion.

See the penguin in the middle of the screen? The one that failed to jump on the iceberg? That was me. Burning out. So, I took a sabbatical these past few months.

image: a wide-screen view of the colorful lamps and lighting strips adorning my home office desk and bookcases

I spent my time getting crafty with lighting setups…

image: Thor, my floofy beige-striped cat, rests in his bed in my colorfully-lit office

Photographing my cats Thor…

image: Thor, resting on the windowsill of my kitchen window, enjoying the warm summer breeze and sunshine on his soft beige fur and dreamy blue eyes.

image: Otis — my silky black haus panther — nestled between my purple bed pillows, gazing at me with calm golden eyes.

And Otis…

image: Otis and Thor — in perfect light and dark fur contrast — nestle together atop a soft red flannel throw.

Who seem to always be fighting. It’s a problem. And Otis…

image: Otis — black cat of magic — lovingly gazing at me with golden eyes while lying upside down. Paws outstretched.

…is especially needy.

image: A gathering of green aventurine, sodalite, rose quarts towers, and quarts diamond on the kitchen table

See, during lockdown before burnout hit peak, I also started a heck of a crystal collection. And attempted…

image: Otis, the black wonder cat, steps into the frame of my crystal collection

…to get good photos. But Otis was having none of that.

image: Otis' paws walk through my crystal arrangement on the kitchen table

None…

image: grouping of citrine, amethyst, and celestite glowing in the sunshine-washed windowsill

…of it.

image: Otis — sassy black cat — sits exactly on top of all my windowsill crystals, seeking attention.

None of it. He has needs.

image: Otis looking very cute as he lays upside down on my bed, gazing at me.

Who could resist him though? He’s the reason I chose that walk-on song. Every time I’d play it…

image: Otis sitting atop his cat tree, looking down into the living area.

He’d jump up on his cat tree…

image: Otis expectantly gazing at me from his cat tree perch.

…and look at me… expecting a salsa dance. So we did. And still do.

image: Penguin — running forward on an icy tundra — skids to a stop.

Anyhow… When it was time to head to React Miami to see people in person, I felt so excited! People. Finally!

image: Group of penguins on tundra running away

Then I remembered… omg… talking to people. How many of you felt the same? My introvert did a little panic…

image: Penguin — running forward on an icy tundra — skids to a stop.

But excitement won out, y’all!

A story

But you’re here to learn about secret rebrands. So I’ll tell you a story about how we did it.

LaunchDarkly

Before my sabbatical, I was on the UX Next team at LaunchDarkly

Our team’s work enabled other designers and engineers to craft high-quality experiences for their customers.

Maturing the design system

When I first joined there, part of our work included maturing the design system.

Design Systems are ALWAYS the hotness. image: Person in inflatable dinosaur costume flips into a raft floating on a pond and claps with glee.

And I’m a super fan of design systems, so it was exciting!

If you’ve ever worked on a design system, you know there are a lot of things to consider. Which can sometimes feel like…

image: Woman attempting to arrange 10 squirming kittens in a straight line, with varied success

…herding kittens. Always fun, yet lots of moving parts. Similar to herding kittens, maturing a great design system can be tough with so many moving parts. If your squad is small, and not a dedicated system team you have to be very strategic and choose what to tackle.

For example…

Foundations missing

The foundations were missing from our system. We had components, but none of the building blocks, so we began our journey to define them.

Design tokens

Like design tokens.

Naturally, we were just beginning to start re-defining our tokens when…

In an unexpected twist, someone kicked in the door wearing a Big Bird costume!

Hello… brand refresh.

Not really. The creative team entered the scene and announced a brand refresh.

image: LaunchDarkly brand visual assets

So there we were, presented an updated brand North Star!

What is a brand North Star?

You may ask… what is a brand North Star?

There is this article by M81 Creative that gives a great explanation of North star:

“Your brand’s North star is the promise that you are making to your customers.”

At LaunchDarkly, their North Star aligns with their core message:

Software powers the world. LaunchDarkly empowers all teams to deliver and control their software.

image: LaunchDarkly brand visual assets

They lean into this mission with their colors, typography and signature logo.

Creating a strong brand takes time

Now creating a brand North Star takes time to do right.

You have to do things like strategy, competitive analysis, user and customer insights, and creative explorations.

This process take up you know many months, sometimes up to a year.

And once that is done, you will have some key outputs like logos, colors, typography and such.

Creating a strong brand takes time

Once that North Star is established, it’s time for teams to go to work to roll those branding visuals into the marketing site and the product app.

Now it gets tricky.

This is where it gets tricky.

While our team was informed about the brand refresh at the earliest possible moment, we had one wrinkle. And was a big one.

Time to customer rollout: 13 weeks

The time to the rebrand rollout to customers was in 13 weeks.

And it was a secret project,

…for most of the company.

Time to internal rollout: 10 weeks

That meant the time to internal rollout to our teams was 10 weeks

image: Ross from Friends repeatedly yells "pivot!" as Rachel and he maneuver a large chair up an awkward staircase.

So we had to carefully pivot to figure out how to get things done, since…

image: LaunchDarkly marketing site in 2014

Typically, visual refreshes happen more often on the marketing side

image: LaunchDarkly marketing site in 2015

image: LaunchDarkly marketing site in 2018

image: LaunchDarkly marketing site in 2019

image: LaunchDarkly marketing site in 2020

image: LaunchDarkly marketing site in 2021

Product visual refresh: less often

And visual refreshes happen less often in the product app because…

… a rebrand impacts established product roadmaps.

Outdated product visuals break brand promises

And you need to be careful when the visuals between your marketing site and product app get out of sync.

First, your product app will start looking outdated and

Second, outdated app visuals break your brand promise.

image: Mars Babycat sleepily squints amid a cozy comforter

So our team regrouped and thought… how can we get this done?

image: Mars Babycat sleepily squints amid a cozy comforter

And how do we do this without making any questionable decisions that may impact the product, because you don’t want to be making questionable decisions like I don’t know…

image: Shrimp and lime jello mold, plated with apple slices

Making dishes that include jello and shellfish?

It’s just not a good look.

But then… we had an idea.

image: Zootopia's Flash — the best sloth ever — slowly breaks into a grin while Judy Hopps and Nick Wilde patiently wait for information

Why not… use our own product to roll out a rebrand?

Feature flags! This means that…

image: Three baby raccoons nestled inside a tree trunk

We could set up a flagging structure and…

image: Three baby raccoons nestled inside a tree trunk... with a calico cat disguising themself as a sibling

…rebrand in stealth mode.

We could make all of our changes alongside other teams, not disrupt other squads roadmaps.

All while the other squads were not aware that the product brand refresh was actually happening.

How did we use flags?

So what did we use flags for in this case?

Boolean flags, Prerequisite flags, Multivariate flags

Well, we used three types of flags.

Boolean, prerequisite, and multivariate flags.

Well, we used them to control:

Theming, data attributes, fonts, design tokens, HTML templates, and UI updates.

And! Since we had to do it all in 10 weeks…

In secret…

image: the Ruling Ring of Isildur's Bane

We used this one ring…

image: the Ruling Ring of Isildur's Bane

…ahem!

image: the Ruling Ring of Isildur's Bane

One flag as the basis of most of our changes.

Feature Flag: enable-2021-rebrand

The flag that all our other flags were dependent upon was the enable-2021-rebrand flag.

image: Enable 2021 rebrand flag targeting view

And this flag was a prerequisite for other flags.

image: Prerequisite flag toggled off, wrapping a group of four feature flags toggled on

Prerequisite flags are great for testing out additional features in isolation.

image: Prerequisite flag toggled off, wrapping a group of four feature flags toggled on

This means you can have all these sub flags turned on.

But unless the prerequisite flag is on…

image: Enable 2021 rebrand flag targeting view

…the other flags wouldn’t work.

image: Prerequisite flag toggled on, wrapping a group of four feature flags — two toggled on, two toggled off

A prerequisite flag allowed us to test individual features…

image: Prerequisite flag toggled on, wrapping a group of four feature flags — three toggled on, one toggled off

In various combinations without impacting the rest of the app.

image: Enable 2021 rebrand flag targeting view

And because… secret project, we could target our stakeholders to share a preview of our work.

Once this flag was on, we could continue work on things like…

Theming support using…

Data attributes…

Data attributes: data-theme=“osmo" data-theme=“legacy”

…to control whichever theme we wanted to serve up.

New font family: Inter

But we also wanted to use our new font, Inter, in our visual refresh so…

image: Enable Inter flag targeting view

We added the enable-inter flag inter with the rebrand flag as a prerequisite…

Data attributes: data-theme=“osmo" data-theme=“legacy" data-inter=“true”

Now that our font flag was set up, we added data-inter to our set of data attributes so we could test fonts independently within our rebrand theme.

Then, we paired those data attribute values with React Context…

And hooks…

import { createContext, ReactNode, useContext, useEffect, useMemo } from ‘react’;

type Theme = ‘legacy’ | ‘osmo’;

export type ThemeContext = { theme: Theme; };

export const ThemeContext = createContext<ThemeContext>(null as unknown as ThemeContext);

export function ThemeProvider({ isInterEnabled, theme, children, }: { isInterEnabled: boolean; theme: Theme; children: ReactNode; }) {

useEffect(() => { document.documentElement.setAttribute(‘data-theme’, theme); }, [theme]);

useEffect(() => { if (isInterEnabled) { document.documentElement.setAttribute(‘data-inter’, ‘true’); } else { document.documentElement.removeAttribute(‘data-inter’); } }, [isInterEnabled]);

const contextValue = useMemo( () => ({ theme, }), [theme], );

return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>; }

export function useTheme() { return useContext(ThemeContext); }

…to create a theme context

type Theme = ‘legacy’ | ‘osmo’;

export type ThemeContext = { theme: Theme; };

export const ThemeContext = createContext<ThemeContext>null as unknown as ThemeContext);

...and a theme provider that we could use anywhere we need in our app,

children, }: { isInterEnabled: boolean; theme: Theme; children: ReactNode; }) {

const contextValue = useMemo( () => ({ theme, }), [theme], );

return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>; }

We created this useTheme function for wherever we wanted use a theme.

export function useTheme() { return useContext(ThemeContext); }

And used our theme provider in the app entry point, leveraging our rebrand flag value to set the theme

const theme = is2021RebrandEnabled() ? ‘osmo’ : ‘legacy’;

ReactDOM.render( <Provider store={store}> <ThemeProvider theme={theme} isInterEnabled={isInterEnabled()}> <HelmetProvider> <Router history={history}> {content} </Router> </HelmetProvider> </ThemeProvider> </Provider>, container, );

And pass the theme into the Provider

<ThemeProvider theme={theme} isInterEnabled={isInterEnabled()}>

Which uses the useEffect hooks to set data attributes in the root element of our app.

export function ThemeProvider({ isInterEnabled, theme, children, }: { … }) {

useEffect(() => { document.documentElement.setAttribute(‘data-theme’, theme); }, [theme]);

useEffect(() => { if (isInterEnabled) { document.documentElement.setAttribute(‘data-inter’, ‘true’); } else { document.documentElement.removeAttribute(‘data-inter’); } }, [isInterEnabled]);

return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>; }

Data attributes: data-theme=“osmo" data-theme=“legacy" data-inter=“true”

Now that app theming was scaffolded, we could continue work on…

Design tokens. So like I mentioned, before the North Star was revealed, our team was already refactoring our token architecture.

Once this rebrand was announced, we had to adjust our tokens work without losing any of our progress.

And we didn’t have time to automate the process with Style Dictionary.

Design Tokens are an abstraction for everything impacting the visual design of an app/platform. — Sönke Rohde

If you’ve never heard of design tokens, Sönke Rohde Tweeted a great definition: Design Tokens are an abstraction for everything impacting the visual design of an app or platform.

Design Tokens are design decisions propagated through a system. — Nathan Curtis

In other words, design tokens are design decisions propagated through a system.

Base tokens —color-blue-500: hsl(231.5, 100%, 62.5%);

Semantic tokens —focus-color: var(—color-blue-500);

Component tokens —Button-color-text-focus: var(—color-blue-500);

Generally, tokens span three categories:

Base, the raw values. Semantic, aliased to base values and shared across the application, and component tokens, scoped at the component level.

Blended base + semantic tokens

Our legacy token system was a blend of base and semantic…

Blended base + semantic tokens —primary-green-base: hsl(165.1, 72.5%, 45.7%);

Not Scalable

…so it wasn’t scalable, due to the naming conventions used.

It didn’t make sense to have a color name in a semantic token if our primary color changed. Hence the refactor.

Considering the 10-week time frame…The tokens that we were most concerned about were our application-level tokens.

We chose to focus on using the data attribute to map our application-level tokens to base tokens in either the legacy or osmo theme.

Since our theme was controlled by a flag, we could target the data attributes in our CSS…

:root { —primary-color: var(—primary-green-safe); }

:root[data-theme=’osmo’] { —primary-color: var(—color-blue-500); }

…And change values by adjusting the data attribute

We used this 1:1 mapping of our values and tested our rebrand by toggling on our flag.

This was especially helpful because we could introduce a new set of semantic tokens quickly without much fuss.

:root { —primary-color: var(—primary-green-safe); }

:root[data-theme=’osmo’] { —primary-color: var(—color-blue-500); }

Now, we could have a separate set of base tokens…

:root { —color-error-dark: var(—danger-red-dark); —color-error-base: var(—danger-red-base); —color-info-base: var(—info-blue-base); —color-warning-base: var(—alert-orange-base); —color-success-base: var(—primary-green-base);

—focus-color: var(—info-blue-safe); —primary-color: var(—primary-green-safe); —text-color: var(—gray-black); —text-color-danger: var(—danger-red-dark); }

Then we could create a new set of application-level tokens…

:root { —color-error-dark: var(—danger-red-dark); —color-error-base: var(—danger-red-base); —color-info-base: var(—info-blue-base); —color-warning-base: var(—alert-orange-base); —color-success-base: var(—primary-green-base);

—focus-color: var(—info-blue-safe); —primary-color: var(—primary-green-safe); —text-color: var(—gray-black); —text-color-danger: var(—danger-red-dark); }

And change values by adjusting the data attribute

By leveraging those data attributes through feature flagging, we could test out our new token values in the app at runtime, without impacting any of the other teams’ work.

:root[data-theme=’osmo’]

:root[data-theme=’osmo’] { —color-error-dark: var(—color-system-red-600); —color-error-base: var(—color-system-red-400) —color-info-base: var(—color-blue-400); —color-warning-base: var(—color-system-yellow-500); —color-success-base: var(—color-system-green-600);

—focus-color: var(—color-blue-500); —primary-color: var(—color-blue-500); —text-color: var(—color-black-300); —text-color-danger: var(—color-system-red-600); }

Including re-mapping tokens for our font family, test, adjust values quickly

:root[data-inter=’true’]

:root[data-theme=’osmo’]

And set up some component tokens

:root { —Button-color-background-primary: var(—color-success-safe); —Button-color-background-primary-hover: var(—color-success-dark); —Button-color-background-primary-focus: var(—color-success-dark); —Button-color-background-primary-active: var(—color-success-dark);

—Button-color-border-primary: var(—color-success-dark); —Button-color-border-primary-hover: var(—color-success-dark); —Button-color-border-primary-focus: var(—color-success-dark); —Button-color-border-primary-active: var(—color-success-dark);

—Button-color-text-primary: var(—white); }

To theme and streamline our CSS.

Even better, we could use this data attribute to wrap blocks of CSS code, and when it’s time to remove legacy styles, we can quickly remove blocks of code later on.

:root[data-theme=’osmo’] { —Button-color-background-primary: var(—primary-color); —Button-color-background-primary-hover: var(—color-blue-600); —Button-color-background-primary-focus: var(—color-blue-700); —Button-color-background-primary-active: var(—color-blue-700);

—Button-color-border-primary: var(—primary-color); —Button-color-border-primary-hover: var(—color-blue-600); —Button-color-border-primary-focus: var(—color-blue-700); —Button-color-border-primary-active: var(—color-blue-700); —Button-color-text-primary: var(—white); }

Performance optimizations

We also used flags in other ways for performance optimizations

In our HTML templates…

We used the LaunchDarkly Go SDK and Go templates on the backend to bootstrap our flag data into our HTML templates.

This would give us just enough information to render our pages.

And used the enable-2021-rebrand feature flag to set the data-theme attribute as a performance optimization.

This way, we could have the HTML templates check for this flag and render the correct theme while we wait for the JavaScript to load and initialize the HTML, to avoid a flash of incorrectly-styled content

{{% if ._flags.Enable2021Rebrand %}}

<html lang=”en” data-theme=”osmo”> {{% else %}} <html lang=”en” data-theme=”legacy”> {{% end %}}

We also used the enable-inter flag value preload this variable font only if the flag was on.

This way, our app performance wouldn’t take a hit from loading any unnecessary fonts if the flag was off.

{{% if ._flags.Enable2021Rebrand %}}

<html lang=”en” data-theme=”osmo”> {{% else %}} <html lang=”en” data-theme=”legacy”> {{% end %}}

For UI updates…

Left nav

We used feature flags to control whether we would show/hide content in our left nav.

For example, if our rebrand flag was off

import { forwardRef, Ref } from ‘react’; import { useTheme } from ‘theme’;

export function AppNavItem({ className, name, icon, href, itemRef, …other }: AppNavItemProps) {

const { theme } = useTheme();

const navItem = React.createElement( ‘a’, { className: classes, href, …other, ref: itemRef }, [ theme === ‘legacy’ && icon && React.cloneElement(icon, { key: ‘icon’, className: ‘AppNav-itemIcon’, ‘aria-hidden’: true }), <span className=”AppNav-itemText” key=”name”> <span className=”AppNav-itemName”>{name}</span> </span> ], );

return navItem; }

export default forwardRef<Element, AppNavItemProps>((props, ref) => <AppNavItem {…props} itemRef={ref} />);

…the legacy theme would be true...

const { theme } = useTheme();

const navItem = React.createElement( ‘a’, { className: classes, href, …other, ref: itemRef }, [ theme === ‘legacy’ && icon && React.cloneElement(icon, { key: ‘icon’, className: ‘AppNav-itemIcon’, ‘aria-hidden’: true }), <span className=”AppNav-itemText” key=”name”> <span className=”AppNav-itemName”>{name}</span> </span> ], );

return navItem; }

…and we’d show icons in the left nav

const { theme } = useTheme();

const navItem = React.createElement( ‘a’, { className: classes, href, …other, ref: itemRef }, [ theme === ‘legacy’ && icon && React.cloneElement(icon, { key: ‘icon’, className: ‘AppNav-itemIcon’, ‘aria-hidden’: true }), <span className=”AppNav-itemText” key=”name”> <span className=”AppNav-itemName”>{name}</span> </span> ], );

return navItem; }

image: LaunchDarkly left nav in legacy view

So users would have the expected legacy UI experience

image: LaunchDarkly left nav in rebranded view

While we worked on the rebranded nav in secret

Top nav

image: LaunchDarkly legacy view without top nav

There wasn’t a top nav in the legacy UI…

image: LaunchDarkly rebranded view with top nav

…and now there is one.

image: TopBar Spike feature flag targeting view

By adding the topbar-spike flag with the rebrand prerequisite

We could add the top nav into the rebranded UI by turning on the topbar-spike flag.

‘theme’; import Topbar from ‘components/Topbar/Topbar’; import ‘stylesheets/components/App’;

export const App = ({ className, children, isTopbarSpikeEnabled, }: AppProps) => { const classes = classNames(‘App’, className, { ‘App—with-top-bar’: isTopbarSpikeEnabled, }); const { theme } = useTheme();

return ( <div className={classes}> <SkipToContent /> {isTopbarSpikeEnabled && <Topbar />} <AppNavContainer theme={theme} isTopbarSpikeEnabled={isTopbarSpikeEnabled} /> <main className={classNames(‘App-main’, { ‘App-main-with-topbar’: isTopbarSpikeEnabled })} id=”content” > {children} </main> </div> ); };

export default App;

Since this flag was dependent on the enable-2021-rebrand flag...

const classes = classNames(‘App’, className, { ‘App—with-top-bar’: isTopbarSpikeEnabled, });

return ( <div className={classes}> <SkipToContent /> {isTopbarSpikeEnabled && <Topbar />} <AppNavContainer isTopbarSpikeEnabled={isTopbarSpikeEnabled} /> <main id=”content”> {children} </main> </div> ); };

...we could test this in isolation

const classes = classNames(‘App’, className, { ‘App—with-top-bar’: isTopbarSpikeEnabled, });

return ( <div className={classes}> <SkipToContent /> {isTopbarSpikeEnabled && <Topbar />} <AppNavContainer isTopbarSpikeEnabled={isTopbarSpikeEnabled} /> <main id=”content”> {children} </main> </div> ); };

image: LaunchDarkly Switch environment dropdown view

With LaunchDarkly, whenever you create a new project you get two default environments, production, and test, and they are set to specific colors by default.

image: Rebrand Env Colors flag targeting view

We wanted to test out new default environment colors, so created the rebrand-env-colors multivariate flag…

image: Rebrand Env Colors flag variations view

…used blocks on JSON in our variations to test out color combinations on the fly…

And used Go to store the values.

if post.Environments == nil { colors := accounts.EnvironmentColors{} if err := json.Unmarshal(ctx.Flags.EnableRebrandEnvColors(), &colors); err != nil { return err } envs = accounts.MakeDefaultEnvironments(“”, colors)

Favicons

Lastly… Favicons. It’s always a nice touch to have your favicon ready to go when you roll out your rebrand rather than scrambling to update at the last moment.

Especially if you are using them in more than one environment.

image: Favicon Env variations flag targeting view

By adding the favicons-env-variations multivariate flag…

image: Favicon Env variations flag variations view

We could use strings in our variations…

And serve up whichever favicon version we needed, depending on the environment, in our HTML template.

% if ._flags.Enable2021Rebrand %}} <link rel=”icon” type=”image/svg+xml” href=”{{% ./img/{{% ._flags.FaviconEnvVariations %}}.svg”> <link rel=”icon” type=”image/png” href=”{{% ./img/{{% ._flags.FaviconEnvVariations %}}.png”> {{% else %}} <link rel=”icon” type=”image/png” href=”{{% ./img/{{% ._favicon %}}” /> <link rel=”mask-icon” href=”{{% ./img/mask-icon.svg” color=”#055177” /> {{% end %}}

Phew!

That was a lot… So to wrap up…

image: The Ruling Ring of Isildur's Bane

With the power of feature flags…

In a 10-week time frame.

We implemented a substantial amount of changes to a product app.

image: ree baby raccoons nestled inside a tree trunk... with a calico cat disguising themself as a sibling

And rolled out a product rebrand in stealth mode.

Thank you.

React Miami @resource11 Twitter | Instagram | GitHub