chris chen

Sequence of many of the UI components used within Heartbeat

heartbeat

what is heartbeat?

Creators are often overwhelmed with starting and maintaining engaged communities for their cohort-based courses, hobby groups, or other online spaces.

Heartbeat gives creators the tools to easily run personalized, monetized communities with conversations, events, and content.

As the founding designer, I handled everything design. Heartbeat co-founder, Mayhul Arora, handles the engineering. The resulting component library is a result of our interwoven efforts.

This was also a personal labor of love bringing Heartbeat closer to a systematic way of handling design.

results

Our component library enabled us to ship features and requests with speed and accuracy. These features ranged from hosting full courses to handling payments, all the way to creating and hosting events - everything budding community admins needed to get a community running, engaged, and monetized.

A grid showcasing a list of all the features we shipped at Heartbeat. The full list: re-designed desktop experience, payments, mobile app, courses, groups, events, search, community customization, video calls, weekly digests, embeds, analytics, kickstart (onboarding), custom domains, upsells, invitations, chats, polls.

These ultimately contributed to our company's growth by growing our community count by 40x and facilitating community payments.

constraints

Two foundational constraints shaped the look and feel of our design system: personalization and friendliness.

Heartbeat aims to be a fully user-customized platform, with custom domains, branding, and colors. Primitives are often color agnostic and lack an opinionated Heartbeat branding, instead opting to emphasize each community's branding.

Sequence of many of the UI components used within Heartbeat

Both community admins and their members prefer friendliness and ease of use over information density - as a result, our components are generally larger and rounder.

Challenges

A problem we faced early on was users getting confused with all the different tools and their own slightly different design patterns. Each section of our all-in-one product had different interaction and navigation patterns. This problem was only exacerbated by more disparate patterns on mobile.

A sequence of the different navigation sidebars across product areas and device sizes

The component library standardized our different product sections. these included primitives themselves as well as patterns when creating entities like events, docs, or threads. The introduction of new embed components also helped users flow from one section of the product to another seamlessly.

Showing the Heartbeat UI of the embed card across desktop and mobile

Documentation

From pretty early on, I was interested in a written design history of an evolving system. I was learning a lot - mostly through failures, and it felt really important to record how I was changing how I thought about our design system.

Below is an adapted internal doc written in early 2022, talking about strategies for how granular we get with components and their associated variants in Figma.

avoiding figma complexity

In the process of building our component library, I've found overly complex components and variants take more time to maintain and use. In time, these components stop being used and fall out of maintenance. This doc lists some general rules to avoid this situation to create tidy, efficient, and easy-to-maintain components.

Before we even talk about the components themselves, it's important to note how components get used in figma (as opposed to how they might be used and configured in code.) I find figma is most efficient in a “top-down” style. Assuming you already have pre-made components, this means you generally configure from parent layers downwards until you get to the lowest layer objects. This is for several reasons:

  1. it's generally easier to select and manage parent layers than child layers
  2. lack of visibility into nested subcomponents and all their states/variants
  3. designers are more inclined to think this way

Consider a standard workflow (given that you're using pre-made components): you insert a larger component, swap variants on the component, change variants on subcomponents to properly convey state, then modify text. I personally conceptualize this as three types of components:

primitives

Component primitives are the smallest atoms of our library and are essential for scaling updates and attaching interactions. examples of primitives, like buttons, input fields, or tags.

Screen capture shows a pointer icon selects interacts and selects filters without any search terms being entered in through the keyboard, demonstrating the use of the search feature with only a mouse

while we generally want to avoid excessive variants on any component, it's better to do it on increasingly lower levels. this is because efficiently built higher-level components (collections of primitive components) use correct variant instances of primitives, capturing the majority of possible component states in a batched format.

as primitives sit at the lowest levels, variant/property messes are mostly obscured in the actual use of the component. see our dropdown component: all of our variant complexity is baked within the dropdown item level, not the dropdown itself. this also makes prototype interactions much simpler to implement.

Top level components

Top level components are collections of larger subcomponents, like the thread component or the lesson component.

Screen capture shows a user using search using keystrokes, with their keystrokes appearing with each key press

I generally try to be pretty sparse with variants here on this high of a level. it's generally pretty handy to have a couple of toggles, but try to avoid handling all component states on this high of a level.

Subcomponents

subcomponents are really useful because they allow for a simplistic variant structure at our top level component, while all possible states a component can be captured by subcomponents that are exactly one level under.

A component that is overly complicated with intermediary components between the top level component and the atomic units below them

however, when we nest even further subcomponents into those, we may be causing ourselves more harm. see above for an example. the resulting nesting structure looks like lesson → assignment → submission, which leads to over-complication.

it becomes confusing to figure out where the variant or state controls for a subcomponent are stored if there are multiple levels of control to a single one. this also makes updating subcomponents more complicated in the case of specific state changes.

takeaway

set up your variants to be fully encompassing at the primitives level, then take care of component-specific states at the highest or second-highest levels. if you focus on these two first, you shouldn't need to create further subcomponents. the resulting components created should be cleaner, more understandable, more usable, and easier to maintain.