DEV Community

FEConf
FEConf

Posted on

Cross-Platform Design System: A 1.5-Year Journey (Part 1)

Cross-Platform Design System: A 1.5-Year Journey

This article summarizes the presentation "Cross-Platform Design System: A 1.5-Year Journey" from FEConf2023. The presentation material is divided into two parts. Part 1 explores design systems and design tokens, and examines component composition to solve communication issues between designers and developers. Part 2 builds upon Part 1 to implement components and design APIs. All images in this article are from the presentation with the same title, and no separate source attribution is provided. The presentation materials can be downloaded from the FEConf2023 website.

▶FEConf 2024 Session Speaker Applications Open

▶FEConf 2024 Lightning Talk Speaker Applications Open

"Cross-Platform Design System: A 1.5-Year Journey" presented at FEConf2023 by Ha Taeyoung, Frontend Engineer at Danggeun

Hello, I'm Ha Taeyoung, working on design systems at Danggeun. In this article, I'll share various approaches I've tried while building design systems over the past year and a half, the pitfalls I've encountered, and the lessons learned from these experiences.

1. Design System

Goals of a Design System

When building a design system, you'll notice that the term "design system" has a very broad range of interpretations, which can lead to miscommunication among team members or confusion about the team's direction. In this section, we'll explore how to clearly set the direction of a design system, accurately understand its design goals, and prepare for potential pitfalls when setting these goals.

The image below shows well-known open-source design systems: Chakra and Adobe's Spectrum. Even for implementing the same range slider functionality, there are significant differences. Chakra requires you to explicitly specify all sub-components, while Spectrum only needs a single line for the range slider.

Image description

Why do these differences exist? I believe it's because the two design systems are built on different core values. Chakra was created with the goal of being "a library that developers can flexibly use through open source," while Spectrum was made for use in Adobe's product suite. Chakra values flexibility more, while Spectrum prioritizes consistency, leading to these differences.

For this reason, I view these two design systems from different perspectives, calling Chakra-like systems "general-purpose design systems" and Spectrum-like systems "product languages." Product languages include not only Spectrum but also many others like Material, Carbon, and Fluent. But why do many companies create separate design system organizations and build product languages instead of using general-purpose design systems?

The reason is that UI needs to reflect company-specific decisions like branding and app characteristics. Product languages also encompass "managing a collection of decisions," going beyond the general functionality of design systems. To emphasize further, the essence of a product language is a collection of design decisions, and the UI library is just a means to implement them.

Image description

Companies can use external libraries when the library is either unrelated to decision-making or can fully reflect their decisions. However, UI libraries often cannot reflect these decisions, which is why many companies create design system organizations and build product languages.

Pitfall: Setting Design System Goals

I encountered several pitfalls while building the design system. First, I failed to set clear goals, which led to poor judgment in choosing reference materials. Danggeun's app needed a product language as its design system, with mobile as the priority and implementations needed for web, iOS, and Android. In the early stages of team formation, I referenced design systems designed for responsive web, which was appropriate for that context but led to incorrect approaches for our product.

Another pitfall was the desire to cover multiple environments and products with a single design system. As the service grew, we needed to support not only mobile apps but also websites, admin panels, and more. Since branding is generally similar across all products, I wanted to cover everything with one well-made general-purpose design system. However, this attempt either proved impossible or created situations requiring more cost than building a product language.

Image description

Through this experience, I learned that rather than creating a perfect general-purpose design system, it's more important to utilize recurring methodologies that exist regardless of what product language you create. The most important of these is "a system that effectively synchronizes design decisions across all product design-related areas." I jokingly call this the "design system-system." In the next section, we'll explore "design tokens" as a means to effectively manage and synchronize these design decisions.

2. Design Tokens

In this section, we'll explore:

  1. Understanding the meaning of design tokens
  2. Examining design token usage cases
  3. Preparing for potential pitfalls in design token implementation

Building a design system is ultimately a series of design decisions. Statements like "The button component's height will be 40px" or "The brand color will be #ff6f0f" are all design decisions.

Image description

And as with most products, decisions change over time. As the product grows, we learn what constitutes better UI. So what's the fastest way to reflect these decision changes across all products?

It's the approach of encoding decisions in a machine-readable format and having products read and reflect them. "Design tokens," the title of this section, refers to encoding design decisions in a way that both humans and machines can read.

Let's explore design tokens in more detail.

Encoding Values

First, design tokens can encode values. What meaning does the design token in the image below have?

Image description

You could say "This color's name is $carrot-500." This isn't wrong, but I think it contains a more important meaning. The precise meaning I think is "We will use this color in our product, and its name is $carrot-500."

These two expressions might seem similar or like wordplay, but there's an important difference. The meaning of "we will use this color in our product" implies that we won't use colors that haven't been declared. Through this, the design system team can maintain design decisions as a flexible set and effectively manage design decisions. However, this might reduce design flexibility and freedom.

While there are infinite concepts in the world, we can communicate effectively with a finite set of words. I believe that similarly, while there are infinite design values, we can communicate with a finite set of design tokens.

Image description

Encoding Intent

To use design tokens more flexibly, we can add layers. As shown below, we can add layers that encode intent to the finite set of design tokens that encode values. This design token expression encodes the intent: "We will use $carrot-500 in areas intended for primary brand."

Image description

By adding these layers, we can express the decision that while it's currently represented by the same color, it might be represented by a different color in the future. Also, by pursuing a way of thinking that designs based on intent rather than values, we improve decision-making and communication in the product design process.

Image description

Context

Finally, design tokens can assign different values based on context. For example, we can add branches for light mode and dark mode to the same $carrot-500 value.

Image description

Summarizing the three aspects above, design tokens are structured through context and layers as shown below. By utilizing context and layers, design tokens gain sufficient expressive power for use in product design.

Image description

At Danggeun, we manage design tokens by creating a simple DSL (Domain-Specific Language) based on this definition. This expression semantically matches Figma Variables added this year, so if you're a team newly building design tokens, it might be worth actively considering this approach. At Danggeun, we put this DSL inside Figma component frames and use these components as the source of truth. Through this approach, we've automated documentation synchronization in Figma and implemented features like dark mode previews.

Image description

Also, when deploying components, we add webhooks to trigger actions, through which we use Figma as the source of truth and generate platform-specific code to synchronize design decisions across platforms.

Image description

Design Tokens to Code

Let's look more closely at how to apply this in frontend development. In the stylesheet below, we implement context branching through data attributes and design tokens and layers through CSS Variables.

Image description

And in CSS-in-JS, we create a package that aliases CSS Variables for easier use.

Image description

This alias package can be used in Emotion as shown in the first image below, and in vanilla-extract as shown in the second image. Given Danggeun's diverse technology stack, we can't apply only specific CSS-in-JS technologies, so we've implemented various approaches suitable for each technology.

Image description

Pitfall: Premature Abstraction of Semantic Tokens

The difficulties I faced while creating design tokens were more operational than technical. One particularly memorable misjudgment was prematurely defining semantic tokens. If we have a FAB component like the one below, can we say that this component's background color has the meaning of floating?

Image description

Based on my experience, such premature semantic token definitions often create more design confusion rather than being useful.

So what's the method for defining useful semantic tokens? It's to examine what concerns are shared by considering various use cases. When various input fields like Input, Text area, and Number Input exist and they all need to share the same background color, defining their background color as Field-bg can be a useful semantic token.

Image description

Semantic tokens can be powerful tools when well-defined. However, poorly defined semantic tokens can cause greater confusion. Therefore, it's dangerous to hastily assign intent to semantic tokens. In other words, since abstraction comes through observation, it's safer to examine common concerns shared by multiple cases and extract intent through this process.

We've now explored design tokens. While design tokens are a powerful means for operating a design system organization, what users expect from a design system is components. In the next section, we'll explore components in design systems.

3. Components

In this section, we'll explore:

  1. Why Atomic Design is confusing
  2. Accurately understanding component composition
  3. Solving communication problems between designers and developers

Atomic Design

Atomic Design was a frequently mentioned keyword until a few years ago, but it's rarely heard now. Are you successfully applying Atomic Design to your products without significant modifications? At least I find it difficult to apply Atomic Design as is.

In the radio group component shown below, which part is the atomic unit?

Image description

Actually, there's no single correct answer. The answer changes depending on the criteria. In terms of form units, the yellow box is the atom, but in terms of functionality, a single radio button has no meaning, so the blue box's functional unit is the atom. And in terms of accessibility, since we have an obligation to provide form element levels, the red box's accessibility unit is the atom.

Let's briefly explore this approach of separating functionality and form.

Components: Form

The checkbox below has the form of an empty box when not selected, and a box with a check mark when selected. This rendering of styles corresponding to current states is the formal aspect of components.

Image description

Components: Functionality

As shown in the image below, a checkbox can toggle selection through clicks. In this case, how the checkbox looks is not important. The order of checkboxes might change, colors might differ, or it might take a completely different form like a toggle switch instead of a checkbox.

So if we remove rendering and leave only state, the functional aspect of components becomes clearer. Therefore, the functional aspect of components is defined by how the component's state transitions in response to user input, excluding rendering.

Image description

Why Atomic Design is Confusing

So why is Atomic Design confusing? In conclusion, atomic functionality and atomic form can exist. However, atomic components cannot be determined as a single unit. Therefore, trying to classify only components and define them as minimum units is likely to fail. We need to think separately about style sheets that provide atomic form and headless components that provide atomic functionality to eliminate confusion about what the minimum unit is and allow the team to move forward. Now, let's move beyond Atomic Design and dissect the composition elements of components for both functionality and form.

Dissecting Components: Functionality

First, component functionality consists of structure, state, interaction, and context.

Image description

First, structure defines what parts the component consists of and what role each part plays in user interaction. For example, the component above consists of a checkbox with the role of control and an Item Label text with the role of label, wrapped in a Root.

Second, state defines the component's states that can change through user input. Representative examples include Pressed (active), Hover, Focused, Selected(Checked), etc.

Third, interaction is the unit that causes state changes. For example, in a checkbox, clicking changes Checked, and tabbing changes Focused.

Finally, context is an option injected in code that affects behavior. For example, injecting a Disabled option deactivates the checkbox, and no matter how much you click, Checked won't change. Of course, state can also affect behavior, but there's an important difference: state changes through user input, while context changes only through code.

Dissecting Components: Form

Next, let's explore component form. Component form consists of structure, visual options, state options, and design decisions.

First, let's look at structure. The structure in form has much overlap with structure in functionality but doesn't necessarily match. Here, structure defines what parts the component consists of and what layout each part has. For example, an icon doesn't exist functionally but exists formally. In Figma, it's good to structure component frames as close as possible to this.

Image description
Second, visual options define how form changes according to set options. Size, Variant, etc., are representative examples.

Image description

Third, state options define how form changes based on states and context derived from the functionality explained earlier. Hovered, Focused, Checked, etc., are representative examples.

Image description

Finally, design decisions are a complete organization of what design values are assigned to which properties for each structure for all combinations of these state options and visual options. And design tokens can be used as a means to more effectively express these design decisions.

Image description

We've now explored what composition elements exist for both functionality and form. To summarize, while structure in functionality and structure in form have significant overlap, they can differ, and while states and context in functionality correspond almost exactly to state options in form, there's still a problem here.

Image description

Problems with Functionality and Form Structure

From a designer's perspective, state options have only 5 possible cases as shown on the left. However, from a developer's perspective, the combination of states and context can have 2^4, or 16 possible cases. I once argued that we should draw all these cases in Figma as well. Of course, this wasn't welcomed, and it was indeed unnecessary work.

Image description
When states increase more severely, combinatorial state explosion occurs as shown below, creating an unmanageable number of cases.

Image description

However, we know from experience that there are priorities between states. For example, when Disabled, we don't need to consider hover. The state combinations that actually affect rendering are limited, and these limited states will align with the number of cases from the designer's visual perspective. Therefore, by organizing the combination of states and context into a one-dimensional enum based on specific conditions, we can compress these states and effectively grasp and communicate the relevant context and state options.

Image description

Separation of Concerns

After completing state compression, we can identify common and unique concerns between functionality and form. Structure, state, and context exist as common concerns on both sides, while interaction exists only in functionality, and visual options and design decisions exist only in form. What benefits can we gain from this separation of concerns?

Image description

Communication Circular Reference

Separation of concerns is the key to solving communication circular references. Let's consider a situation like this:

Developer: I can't implement the component yet because there's no design.

Designer: What variants are needed to design the component?

Designer: I've designed it, but the developer says it can't be made this way.

Developer: But it's already published in Figma and designers are using it.

This kind of situation is called a communication circular reference problem. These are problems that can actually occur in organizations developing at the component level or with design systems. I think these problems are issues with the "design first, then implement components" approach. When designing, it's difficult to know in advance what states will exist in development, creating a dependency on development. Meanwhile, when developing, there's a dependency on design because developers think they need the design to write code.

Solving Communication Circular References

So how can we solve these mutually dependent communication circular references? In development, we often use interfaces to reverse the direction of dependencies. This can be applied to our way of working. Based on the separation of concerns mentioned earlier, we know what the common interface between design and development is. Based on this, I'll outline what I believe is an effective workflow for a design system team.

First, designers and developers together sketch the component they want to create and define what structure and state options it will have. The image below shows the common interface that will be used in this component.

Image description

Developers consider user accessibility within the given structure, retain only the functionally meaningful aspects of that structure, and understand state transitions in response to user interaction. With this, they either implement headless components directly or bring in existing libraries.

Image description

Designers design components according to the structure based on state options.

Image description

Finally, developers write styles based on the designed component and combine them with the previously written headless component to implement both functionality and form. And they complete the component, ensuring it's synchronized with the Figma designs.

Image description

To summarize, rather than working in the way of "design first, then implement components" where design from Figma leads to development, we can solve communication circular references by first defining common interfaces like structure and state, and then having this content lead to both Figma and development.

Through this process, we can learn something. Ultimately, it's more aligned with our actual workflow to view Figma as an environment akin to development, treating it as an implementation target, much like our development platforms.

Image description

We've now explored how to define components and how design and development can work together. In the next article, we'll explore how to apply this content to actual component implementation.

Top comments (0)