How to build drag and drop UI editor from scratch 🐻‍❄️

In this post, I want to share my experience building drag-and-drop UI design systems from scratch. Stay tuned and read one guys 😎

Lately, I took on a challenge to build a complete UI editor based on Shopify's polaris design system - similar to Open Chakra App from the Chakra UI ecosystem. I love Chakra UI and I have used it in many of my projects, but I wanted to try something new and build a similar editor based on Shopify's polaris design system since this is where I am working right now - making commerce better for everyone. I also learned a ton from the open Chakra app as it's fully open-source. I wanted to give back to the community and share my experience building a similar editor from scratch.

DnD App Components screenshot

Typically, when you want to build a UI editor, you need to have a design system in place. This is a must-have for any UI editor. You can't build a UI editor without a design system. You can build a UI editor without a design system, but it will be a mess and you will end up with a lot of technical debt. So, the first thing you need to do is to build a design system. I did cover this topic in my previous post, please feel free to read 😉. Below are several key elements that make up a UI editor as per my discovery:

  • Design system
  • State management
  • Drag and Drop functionality
  • Component inspector
  • Code generator
  • Playground

Design system

Shopify design system is called Polaris. It's a set of foundations, components, tokens, and icons that help us build a consistent user experience for one of Shopify's core products, the admin. It's a living system that evolves. It's also open-source and you can find it on GitHub

State management

State management is the heart of the application. UI is just a map of our state. I chose to use Zustand for managing the application state. I have used Redux in the past and I don't want to use it again. In my humble opinion, it's too verbose and it's hard to maintain at scale. Zustand is a great alternative to Redux and it's very easy to use. It's also very lightweight and it's built on top of React hooks.

Since our application needs to store the rendered components. I stored this information in the store as a representation of the tree structure. Below is the store state:

type StoreState = {
  renderedComponents: RenderedComponent[]
  // other state
}

type RenderedComponent = {
  id: string
  children: RenderedComponent[]
  componentName: ComponentName
  props: any
}

I chose to use a nested data structure to represent the tree structure. Someone might prefer a flat data structure, but I think a nested data structure is more intuitive and easier to work with. It's also how I learned to work with tree structure back in the day, hence I'm a bit biased 🙈 - https://github.com/trekhleb/javascript-algorithms.

Besides, I also let the users to be able to share their work with others. It is achieved by URL sharing. I encoded the store state into a string and put it into the URL. When the user opens the URL, I decode the string and update the store state accordingly 🔗

Drag and Drop functionality

I chose to use DnD Kit for drag and drop functionality. It's a great library and it's very easy to use. It's also very lightweight and it's built on top of React hooks. It's also very flexible and it allows us to build a custom drag-and-drop experience with ease. There is one gotcha here though. Since the Polaris design system does not allow components to accept ref as props. I could not easily attach dnd functionality into the wrapper components. I had to use a workaround to make it work. I needed to create an extra wrapper around each component and attach dnd functionality to this wrapper. This is not ideal, but it works for now. Chakra UI does not have this issue since it allows components to accept ref as props. There is no right or wrong here, it's just a design decision. By not allowing components to accept ref as props, it makes the design system more consistent and it's easier to maintain at scale. I think this is a good trade-off.

Yet, there is another problem I faced when dealing with compound components as they are meant to be used as a group. I had to copy over all the styles from the rendered component to the wrapper component as if it were an actual Polaris component. This is surely not ideal yet serves the purpose just great 😊

Last but not least, the most time-consuming part for me is handling drag-and-drop cases. In total, I discovered 9 cases that I need to handle as below:

  1. Case drag from the menu to the canvas, not on top of any component
  2. Case drag from the menu to the canvas, on top of the root components that cannot have children
  3. Case drag from the menu to the parent component that can have children
  4. Case drag from the canvas to the component
  5. Case drag from the canvas to the canvas
  6. Case drag from the component to the canvas
  7. Case drag inside the same parent component
  8. Case drag from one parent to another parent
  9. Case drag root component to the canvas

The DnD logic is around 300 LOCs, please feel free to have a look at the code for more details 😉

Component inspector

Component inspector is the place where we can inspect and edit the properties of the selected component. Each component in the design system has its own set of properties. We can easily build the corresponding tailor components for each component in the design system. Since each property can only be in JavaScript primitive types, arrays, or objects. I defined a set of default props of those supported components and let the tailor component edit those props at runtime. It is as simple as editing the state in our store. Since the active component will have its ID, we can find the corresponding component in our store and update its props accordingly.

Code generator

The real fun is here. As I never built a UI editor before, I had to do a lot of research and experiments to find the best way to generate code from the rendered components. First and foremost, I take on the input and rendered component from the store defined above. Then I need to traverse the tree and recursively generate the code for all branches. Finally, I use Prettier to format the code before showing the result on the screen.

To optimize the generated code, I decided to remove some unnecessary props that are also defined in the component's default props. For example, if the component has a prop called isVertical and its default value is false. If the user does not change the value of this prop, I will not include this prop in the generated code. This is a good way to optimize the generated code and make it more readable. Another thing I did was special handling of icon props. Since icons are exposed from another package and represented as a string in the inspector, I need to import the icon components from the "@shopify``/polaris-icons"` package and replace it instead of a string in the generated code 😁

Please feel free to have a look at the code for more details on how I did it 😉

Playground

This functionality is surprisingly easy to implement. I use Stackblitz SDK to create a new project and inject the generated code into the project. It's that simple with just 50 LOCs 😎

Final result 🎉

DnD From Scratch UI screenshot

There you have it guys, how you can build drag and drop UI editor from scratch for your organization in roughly 10K LOCs. The code is fully open-source under MIT license, please feel free to reference it however you like 😊. Peace out and until next time 💞

❤️❤️❤️ Be well, Be happy ❤️❤️❤️