This is the second entry in a two-part series covering various strategic and functional aspects of design systems.
The challenges discussed in this entry are specifically relevant to individuals that have roles in engineering leadership, user interface architecture, and design systems engineering.
You can find the first entry on scope and governance here.
Beyond Scope And Governance
In the previous entry I discussed some strategic pitfalls that teams can embrace in the hopes of creating the perfect design system. Succinctly, adoption can become abysmal and team velocity can grind to a halt. But, what about teams that embrace a strategy with a liberal governance model and humble scope? Is success all but guaranteed?
Unfortunately not.
Beyond the abstract world of strategy lies the far more practical world of implementation, which presents its own unique blend of dragons and mirages.
Let’s dive in.
The Dreaded Platforms
I have routinely found that there is an undeniable misalignment between teams that are building cross-platform user experiences and the users that are consuming them. In my view, this misalignment is best described as follows:
Designers and developers want…
- To deliver a unique experience as part of the competitive landscape on a given platform.
- To have this experience be largely consistent across the platforms they support.
Meanwhile, users want…
- A consistent and familiar experience between competing apps on a given platform.
- The best possible experience on each platform they use, even if the platforms are inconsistent.
This is because users largely understand (and even embrace) that the platforms are meaningfully different and likely always will be. Furthermore, users are seriously uninterested in incurring the cognitive load of learning a litany of custom controls, interactions, and patterns for each app they install and/or website they visit.
The Myth That Won't Die
Despite this fact, there has been no shortage of attempts over the years to bridge the platforms with a single UI implementation. I’ve been around long enough to have experienced the evolution of this phenomenon from Java, to Flash, to several lower profile players in between, and finally arriving at our most recent industry additions with React Native / React Native Web / Expo / Electron / Flutter / Ionic / anything else I am forgetting.
For design system teams this approach can be extremely compelling. Imagine delivering a universal experience while resolving various platform inconsistencies!Every man who has ventured here over the centuries has looked up to the light and imagined climbing to freedom. So easy... So simple... And like shipwrecked men turning to sea water from uncontrollable thirst...
...many have died trying.
There are about a dozen good reasons why (over the long term) this is a very, very bad strategy for engineering organizations to embrace. But, even if these holy grail chasing libraries were able to achieve their historically elusive potential for development teams, this strategy still deliberately:
- Violates platform best practices.
- Undermines user expectations.
- Increases cognitive load.
All of which will deliver an objectively worse experience to the very people paying their salary.
One Ring To Rule Them All
For many designers and developers that are already on board the #usetheplatform train, the above might seem painfully self-evident.
To those readers, I say: we aren’t out of the woods yet.
Design system teams in medium to large organizations often find themselves in the position of having several development teams deploying applications to a single platform. In fact, deploying multiple applications to a single platform is often the impetus for developing a robust design system in the first place.
For the sake of specificity, assume for a moment…
- The platform in question is the Web.
- Every application uses the
<button>
element.
Historically, web applications kept business and render logic on the server and sent static HTML and CSS to the browser with a minimal JavaScript footprint for interactivity. In such architectures, there was no incentive to abstract and implement something as primitive as a <button>
in JavaScript only to be sluggishly sent over the wire, parsed, and executed at runtime in the browser.
Thus, the design systems of days past were often limited to reliably documented specifications and, perhaps, some accompanying CSS.
With the dawn of the Single Page Application (SPA), and subsequently component-first JavaScript libraries like React, this incentive structure was entirely reversed.
If You Give A Mouse JavaScript Developer A Cookie Library
The power of JavaScript is seductive, and with the SPA architectural paradigm shift regression a rush of new developers embraced the UI portion of the stack. Conjoined with their arrival was a deluge of pedantic tendencies, embraced en masse, that have not translated effectively to how UI development delivers optimal experiences in practice (here’s looking at you, functional programming).
For design systems, the impact of this shift has been catastrophic.
As it turns out, if your development team is routing in JavaScript…and fetching in JavaScript…and rendering in JavaScript…it’s quite obvious why they would also be inclined to wrap their entire design system in JavaScript as well.
What this means at scale is that JavaScript-first design system teams will inevitably take the <button>
element and...
- Abandon the vendor agnostic and zero runtime behavior they get from the browser for free.
- Wrap it in an overly complex, painfully sluggish, statically typed, transpiled, build chain dependent, vendor locked render function.
Even worse, these teams often rewrite the very layout primitives of the Web (see: Flexbox) in proprietary JavaScript components that create no additional behavior other than what is already provided by the browser. In other words, wrapping a Flexbox container in JavaScript purely for the sake of JavaScript.
Make no mistake about it, the utility of this approach for user experiences is entirely nonexistent. As such, those of us who prioritize our users are often told that positive externalities will necessarily emerge from the ideal developer experience.
Remember those development teams working on different applications targeting the same platform? As it turns out, when you implement a design system that wraps every primitive down to the layout modules in proprietary JavaScript...
- They are forced to use the same vendor library as the design system.
- They are forced to maintain a compatible version of that library.
- They are forced to maintain compatible patterns (such as classes versus hooks) within that library.
- They are forced to wrestle with integrating it into their build chain and deployment.
- They are forced to degrade their own performance by sending their users JavaScript they don't even need.
How is that documentation and CSS looking right about now?
Here is the good news: industry consensus that the SPA architecture is, by and large, a regression for the Web has finally emerged, and its departure will almost certainly include all of the headaches and nightmares described above.
So, where does this leave us in regards to choosing an implementation strategy for a design system?
Principle #1: Design Native And Build Native
Ignore the false prophets of WORA. Learn the patterns, controls, and best practices for Web, iOS, and Android platforms respectively.
Abide by them at all costs.
Bridge the platforms with your information architecture, user journeys, and design tokens.
Principle #2: Documentation Is Paramount
Documentation is flexible, scalable, platform agnostic, empowers non-technical contributors, and allows for development teams to exercise necessary discretion on the specifics of implementation.
If you can trust your development teams with the hard stuff (async programming, business logic, etc.) then surely you can trust them with the easy stuff as well (styling a <button>
element according to some basic documentation).
Narrowing down to the Web platform, specifically…
Principle #3: Don't Undervalue CSS
If there is a strong justification to share design system assets via code on the Web, the optimal place to start is with some basic CSS. Every visual aspect of your design system will necessarily be delivered to the user via CSS, and wrapping this behavior in runtime JavaScript adds nothing and changes nothing.
It’s still…CSS.
Certainly, there are annoyances to be incurred and challenges to be overcome, such as naming conventions, cascading, inheritance, minifying, versioning, deployment, removing dead/unused code, and so forth.
But the web remains the most open, resilient, and backwards compatible platform in the history of software. Any CSS written will long outlive a JavaScript counterpart, and likely the design system itself.
Principle #4: Use JavaScript As A Last Resort
As my colleague, nemesis, and frenemy Dave O. affectionately pointed out in his own blog:
As Scott O'Brien always says, JavaScript is bad.
I am certain I have said this many, many times! Putting aside the trolling and hyperbole for a moment, the harsh reality we developers have been reluctant to accept is that the Web is not conducive to client-side only user experiences written entirely in JavaScript.
Thus, do not permanently embed JavaScript in your design system except for absolutely necessary, mission-critical, robustly interactive, non-standard controls. An example of this might be a paginated, sortable, searchable, async data grid that is used in dozens of places across your web applications. Even then, strongly consider avoiding any proprietary libraries and opt for a more standardized approach using vanilla JavaScript or Web Components.