This article was written by Andrei Shilkin, a Lead Software Developer at Akvelon, and was published on Medium.
Introduction
Any large enterprise project is the result of many sub-projects merged together. In our case, each project has been developed at different times by different developers and technologies. Initially, they were independent products, but they have been evolved into one unified product/platform. This evolution raises issues and questions about how to fix and manage inconsistency issues. It is particularly relevant for UI/UX. Suppose a design system (DS) was created, but it was unclear how to apply it to all projects.
This article shares an experience of how to start the migration of legacy UI to a new one.
What task was initiated?
The task was to update each sub-product to align with the new design system’s requirements. However, once we merged all sub-products into one system we faced styling issues. UI that had seemed the same until then began looking undesirable because of inconsistencies.
Additional acceptance criteria also included the following:
- Should be one place that is the source of truth for styles
- Shouldn’t be any code duplication among sub-projects (ideally)
- Should be shared so that everyone can contribute and make changes
- Should be possible for things to work in parallel
- Should not be doing the same things more than one time
- Should reduce costs on a future support
Let’s go through the issues we faced and consider what we could do.
What issues did we face? What could we do?
When we dove deeper into new requirements and sub-project implementations, we figured out that:
- The new design system required making significant changes because some components have another structure and UX in comparison with the current implementation
- Each project (5+ projects) uses a different stack of technologies (ASP.NET WebForms, AngularJS 1.*, Angular 2+, ReactJS, different module bundlers (webpack, gulp, browserify) and etc)
- The same components have different implementations depending on the project (although, sometimes we have many implementations in one project)
- The design system does not provide any styles library. In our case, it’s just a specification
- The legacy code and tests have strong dependencies on class name selectors
- Each project already uses and depends on different CSS libraries (and their different versions) – it was not clear if we should add one more or just replace everything
It seems unrealistic to update and unify the UI in all projects at once. What could we do?
We considered two ways to make it:
- Just update UI for each project independently:
➕ We just get things done because each team knows the specifics of the project and implementation details
➕ No extra effort on repairing tests and other related items because there weren’t any significant changes (tests could rely on class names and etc)
➖ Time-wasting because each team should do the same
➖ Even if we migrate to a new design system, we have to do the same the next time
➖ There is still no unification and still a high risk to have the same inconsistencies in UI
- Create shared a styles lib and then reuse this one on projects:
➕ No time-wasting doing the same work again and again
➕ Some kind of unification: each project will use the same package which could be updated easily at once
➕ Using shared lib should clean up hacks and external packages
➖ This step requires additional effort and could hit priorities and deadlines
➖ There is a risk that it will not work anyway
Based on the task and acceptance criteria we decided to go with the second way.
Review and choose an approach
It’s time to review and choose the approach. Nowadays, there are several approaches and strategies on how to integrate and work with styles, including:
- CSS-in-JS
- CSS modules
- CSS libraries / frameworks
- CSS / SASS stylesheets (custom library)
Let’s review all of them quickly and discuss their disadvantages and advantages. This review was done to resolve the issue described above. There are other useful articles that compare these technologies. You can read them to dive deeper into details.
Issues we have regardless of the chosen approach
Regardless of an approach during updating styles, we faced the following issues:
- Styles conflicts and collision issues (CSS inheritance and etc…)
- No ability to change naming for class names due to complex selectors and dependencies in tests
- Great effort needed to change component structures
A little more details and context…
Conflicts and collision issues: we got a broken layout when we added new styles or libraries to the project. Imagine there are styles like:
Once we added new styles (or libraries) our styles were combined with new ones because the library/styles could have styles for button:disabled. So, we could get unexpected results. Moreover, it was unclear if there were other places, styles, or selectors that could be affected by the same issue.
No ability to change naming for class names: some projects and tests rely on class names and complex selectors (yes, this is not a best practice to rely on CSS selectors in tests). In that case, we can either fix tests and selectors to make them work again (that requires additional effort) or just add one more class to a component (that brings complexity and overhead support costs).
Changes in component structure: just keep this in mind. Since DS forces different layouts, we have to make changes in the component structure anyways to align with new requirements.
Disadvantages of each approach
CSS-in-JS and CSS Modules
Deadline, legacy code, and priorities force us to sideline CSS-in-JS and CSS Modules approaches because a variety of projects and technologies cause the focus to shift from migration to new DS to the issue of how to adapt chosen the approach to different technologies and frameworks. We have to fix many build configurations and other related things just to get it working. (Priority is the most important thing, you know).
CSS libraries / frameworks
We already use different CSS libs (different packages and versions). Unfortunately, the application requires a specific UI. CSS libs are designed to apply styles on components that have an agreed structure (CSS libs force a specific component structure that could not satisfy requirements in DS). In our case, many UI/UX parts are custom and could not be done without complex customization or overriding of CSS library styles. Despite this, we looked into CSS libs and did POC. As a result, we faced the generic issues described above. Moreover, we understood that adding one more library just brings complexity and causes a mess.
CSS/SASS stylesheets
This is the last approach we looked at. It’s much more universal than the rest of the styling strategies. Since it’s about styling only and it does not depend on what technologies and how we render UI, we decided to focus on this approach. Will we create just a typical CSS library like Bootstrap? That’s not entirely true…
Custom styles library. Is it what we need?
Most CSS libraries provide class names and docs on what class names (and how) should be applied to what element. We would like to make a custom library that provides just a set of styles. The idea is that you as a consumer can inject/mix styles into any class name or selector in your stylesheets.
Let’s take a look at an example:
In the case of CSS library/framework, we could have an issue that styles for button element or for button-danger the class name could be mixed with library styles unexpectedly. To make sure it works correctly, we should either inspect and decide what styles we actually need or change class names. It makes things more complex in most cases.
What could we do?
Mixins allow us to define styles that can be reused throughout our stylesheets. The styles library should provide mixins instead of class names. So, in order to update styles we should input the mixin to the proper class name that we already have in our stylesheet:
Now, we know on what elements and selectors we used new styles. It’s easier and quicker to add, polish, override styles and resolve any inconsistency issues.
To sum up, the styles library is a merge result of two concepts: mixins (SCSS) and utility classes approach.
What could make things simpler?
To simplify things and make them maintainable on the first iteration, we accepted the following best practices:
- Minimize or do not use complex selectors in order to make styles independent and reusable
- Minimize or do not use selectors on elements (examples: selectors on elements button , input and other)
- Split styles into small and independent mixins to make them easier to apply <=> split the library into modules (of course)
Example
An example is Switch / Toggle a component that has 3 variations of the same control. Each variation has a different HTML structure and uses different selectors but looks the same.
The purpose of an example is to demonstrate how the described approach and styles library could be used for updating styles and unification of the (Switch / Toggle) component.
Placeholder: The example uses Switch and ReactJS just for demonstration purposes.
There are two sets of components:
- components-v1 folder contains components before applying the styles library (demonstrates how many different selectors we have and how easily to bring inconsistencies by unexpected changes in stylesheets)
- components-v2 folder contains components that use mixins from the styles library (demonstrates how styles could be replaced with mixins in order to make them look similar and unify)
Here is the sandbox:
What we have initially
There are 3 variations of the sameSwitch component:
Each component has a different HTML structure and uses different selectors but looks the same. You could discover how easy it is to break components if you change something in the stylesheet for some variation.
Here is the link to the components-v1 folder with components: [CodeSandbox: /components-v1]
What mixins do we have to style?
Conceptually, theSwitch component has two sub-components: container and toggle:
So, it makes sense to group styles for each sub-component into independent mixins. We could split styles (visual, position, and other kinds of styles) into separate mixins also. Here is the link to mixins: [CodeSandbox: Mixins]
What should we do to update the styling? What results?
At this moment, we know:
- The structure of Switch component
- Initial styles and selectors for Switch component (components-v1 folder)
- Mixins that we have and the mixins that could be used to style each sub-component, in other words, part of Switch component (styles-lib folder)
It’s time we could replace our plain styles with mixins in the following way:
- Open the stylesheet and choose the selector that we are going to update
2. Since we identified what mixins we need: switchContainer and hiddenTextContent we are ready to reuse them
So, we updated and replaced old styles without any changes to the component structure level.
You could find components that use mixin instead of plain styles here: [CodeSandbox /components-v2].
Conclusion
We understand that a design system is changing and updating constantly. We hope this approach can help you to migrate from your legacy UI to a new one. Since it is a source of up-to-date design specs for everyone: developers and designers are speaking in one “language”.
Thank you for your interest in this article!
This article was written by Andrei Shilkin, a Lead Software Developer at Akvelon.