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

A presentation at reactJSday 2021 in November 2021 in by Kathleen McMahon

Slide 1

Slide 1

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 rebrand in secret using feature flags

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

Slide 2

Slide 2

https://noti.st/resource11

For those who like to follow along as I talk, my presentation is posted on Notist, that’s https://noti.st/resource11.

Slide 3

Slide 3

@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…

Slide 4

Slide 4

Kathleen McMahon | Engineer • Designer • Speaker

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

Slide 5

Slide 5

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

And… I race bikes. Very badly.

Slide 6

Slide 6

[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.

Slide 7

Slide 7

UX Next

So while I’m an engineer, designer, and speaker, I’m a design engineer on the UX Next team at LaunchDarkly

Our team enables other designers and engineers to craft high quality experiences for our customers.

Slide 8

Slide 8

Maturing our design system

Part of our work includes maturing the design system.

Slide 9

Slide 9

If you’ve ever worked on a design system, you know there are a lot of things to consider.

Slide 10

Slide 10

[image] woman attempting to arrange 10 squirming kittens in a straight line, with varied success

Similar to herding kittens, maturing a great design system can be tough with so many moving parts.

If your squad is small, you have to be very strategic. Especially when your product roadmap changes. For example…

Slide 11

Slide 11

Hello… brand refresh.

What happens if you find out the creative team has refreshed the brand?

Slide 12

Slide 12

[image] Collection of LaunchDarkly brand assets

And your team is presented an updated brand North Star?

Slide 13

Slide 13

That’s definitely a Surprise Pikachu moment…

Slide 14

Slide 14

What is a brand North Star?

You may ask… what is a brand North Star?

Slide 15

Slide 15

“Your brand's North star is the promise that you are making to your customers.” — M81 Creative

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.”

https://www.m81creative.com/blog/what-is-your-brands-north-star

Slide 16

Slide 16

LaunchDarkly’s North Star is brought through in their core message:

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

Slide 17

Slide 17

[image] Collection of LaunchDarkly brand assets

We lean into this mission with our colors, typography and signature logo.

Slide 18

Slide 18

Even the position of our logo mark, to the right of the logo type embodies our core beliefs:. In a world where every company is a software company, LaunchDarkly leads the way and saves the day.

Slide 19

Slide 19

Creating a strong brand takes time

Creating a brand North Star takes time to do right.

You have to do things like strategy competitive analysis, users and customers insights, creative explorations.

This process takes up to many months, sometimes up to a year.

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

Slide 20

Slide 20

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.

Slide 21

Slide 21

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 it’s a big one.

Slide 22

Slide 22

The time to the rebrand rollout to our customers is in 13 weeks

Slide 23

Slide 23

…and it’s a secret project,

Slide 24

Slide 24

…for most of the company.

Slide 25

Slide 25

That means the time to internal rollout to our our teams is 10 weeks

Slide 26

Slide 26

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

So we had to pivot to figure out how to get things done, because of the inherent differences between a marketing site update and a product app.

Slide 27

Slide 27

[image] LaunchDarkly marketing site in 2014

If you look at our marketing site over the years, visual refreshes happen more often

Slide 28

Slide 28

[image] LaunchDarkly marketing site in 2015

If you look at our marketing site over the years, visual refreshes happen more often

Slide 29

Slide 29

[image] LaunchDarkly marketing site in 2018

If you look at our marketing site over the years, visual refreshes happen more often

Slide 30

Slide 30

[image] LaunchDarkly marketing site in 2019

If you look at our marketing site over the years, visual refreshes happen more often

Slide 31

Slide 31

[image] LaunchDarkly marketing site in 2020

If you look at our marketing site over the years, visual refreshes happen more often

Slide 32

Slide 32

[image] LaunchDarkly marketing site in 2021

If you look at our marketing site over the years, visual refreshes happen more often

Slide 33

Slide 33

Product visual refresh: less often

On the other hand, visual refreshes happen less often in the product app because…

Slide 34

Slide 34

…a rebrand impact established product roadmaps.

Slide 35

Slide 35

Outdated product visuals break brand promises

And you need to be careful when the visuals between your marketing site and product app gets out of sync. First, your product APP will start looking outdated and Second, outdated app visuals breaks your brand promise.

Slide 36

Slide 36

[image] Mars Babycat sleepily squints amid a cozy comforter

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

Slide 37

Slide 37

[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…

Slide 38

Slide 38

[image] Shrimp and lime jello mold, plated with apple slices

Making dishes that include jello and shellfish.

It’s just just not a good look.

Thankfully, we work at a company that has a really cool product.

Slide 39

Slide 39

Feature flags. This means that…

Slide 40

Slide 40

[image] Three baby raccoons nestled inside a tree trunk

We could set up a flagging structure and…

Slide 41

Slide 41

[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 product squads are not aware that the brand refresh was happing in the product.

Slide 42

Slide 42

How did we use flags?

So what did we use flags for in this case?

Slide 43

Slide 43

Well, we used them to control theming, data attributes, fonts, design tokens, HTML templates, and UI updates.

Slide 44

Slide 44

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

Slide 45

Slide 45

In secret…

Slide 46

Slide 46

[image] The Ruling Ring of Isildur's Bane

We used this one ring…

Slide 47

Slide 47

[image] The Ruling Ring of Isildur's Bane

..ahem!

Slide 48

Slide 48

One flag to rule them all

One flag as the basis of most of our changes, and used it as a prerequisite for any other flags we needed for testing out additional features in isolation.

Slide 49

Slide 49

Feature Flag: enable-2021-rebrand

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

Slide 50

Slide 50

[image] Enable 2021 rebrand flag targeting view

This flag was a prerequisite for other flags.

Slide 51

Slide 51

[image] Enable 2021 rebrand flag targeting view

So if this flag wasn’t turned on, the other flags wouldn’t work…

Slide 52

Slide 52

[image] Enable 2021 rebrand flag targeting view

And because… secret project, we targeted specific users to be able to preview what we were working on.

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

Slide 53

Slide 53

Theming support using…

Slide 54

Slide 54

Data attributes.

Slide 55

Slide 55

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

……to control whichever theme we wanted to serve up.

Slide 56

Slide 56

New font family: Inter

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

Slide 57

Slide 57

[image] Enable Inter flag targeting view

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

Slide 58

Slide 58

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

And added it to our set of data attributes for theming.

Slide 59

Slide 59

We paired those data attribute values with React Context…

Slide 60

Slide 60

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

Slide 61

Slide 61

…to create a theme context

type Theme = ‘legacy’ | ‘osmo’;

export type ThemeContext = { theme: Theme; };

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

Slide 62

Slide 62

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

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

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

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

Slide 63

Slide 63

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

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

Slide 64

Slide 64

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, );

Slide 65

Slide 65

And pass the theme into the provider

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

Slide 66

Slide 66

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

Slide 67

Slide 67

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

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

Slide 68

Slide 68

Design tokens. 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.

Slide 69

Slide 69

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

Slide 70

Slide 70

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

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

Slide 71

Slide 71

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.

Slide 72

Slide 72

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%);

Slide 73

Slide 73

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.

Slide 74

Slide 74

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

Slide 75

Slide 75

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

Slide 76

Slide 76

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

}

Slide 77

Slide 77

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

}

Slide 78

Slide 78

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

Slide 79

Slide 79

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

:root[data-inter=’true’]

:root[data-theme=’osmo’]

Slide 80

Slide 80

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

Slide 81

Slide 81

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(—color-blue-500); —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(—color-blue-500); —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); }

Slide 82

Slide 82

Performance optimizations

We also used flags in other ways for performance optimizations

Slide 83

Slide 83

In our HTML templates…

Slide 84

Slide 84

we used the Go SDK and Go templates on the back end to bootstrap our flag data into our HTML templates to give us just enough information to render our pages.

Slide 85

Slide 85

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

Slide 86

Slide 86

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

Slide 87

Slide 87

For UI updates…

Slide 88

Slide 88

Left nav

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

Slide 89

Slide 89

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

Slide 90

Slide 90

…the legacy theme would be true, 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; }

Slide 91

Slide 91

[image] LaunchDarkly left nav in legacy view

So our users would have the expected UI experience

Slide 92

Slide 92

[image] LaunchDarkly left nav in rebranded view

While we worked on the rebranded nav in secret

Slide 93

Slide 93

Top nav

Slide 94

Slide 94

[image] LaunchDarkly legacy view without top nav

We didn’t have a top nav before…

Slide 95

Slide 95

[image] LaunchDarkly rebranced view with top nav

…and now we do.

Slide 96

Slide 96

[image] Topbar Spike feature flag targeting view

By adding the topbar-spike flag

Slide 97

Slide 97

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

import classNames from ‘classnames’; import { useTheme } from ‘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;

Slide 98

Slide 98

Since this flag was dependent on the enable-2021-rebrand flag, 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> ); };

Slide 99

Slide 99

[image] LaunchDarkly Switch environment dropdown view

Environment Switcher

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

Slide 100

Slide 100

[image] Rebrand Env Colors flag targeting view

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

Slide 101

Slide 101

[image] Rebrand Env Colors flag variations view

…and use blocks on JSON in our variations to test out color combinations on the fly.

Slide 102

Slide 102

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)

Slide 103

Slide 103

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

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

Slide 104

Slide 104

[image] Favicon Env variations flag targeting view

By adding the favicons-env-variations multivariate flag

Slide 105

Slide 105

[image] Favicon Env variations flag variations view

We could use strings in our variations…

Slide 106

Slide 106

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

Slide 107

Slide 107

Phew!

That was a lot… So to wrap up

Slide 108

Slide 108

[image] The Ruling Ring of Isildur's Bane

With the power of feature flags…

Slide 109

Slide 109

In a 10-week time frame…

Slide 110

Slide 110

We could implement a substantial amount of changes to our product…

Slide 111

Slide 111

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

And rollout a product rebrand in stealth mode.

Slide 112

Slide 112

Thank you.

Slide 113

Slide 113

https://noti.st/resource11