category: In-AppOct 02, 2024

The Full-Stack Components Customization Pyramid

Explore the Novu component's customization pyramid, styling options, and composable architecture, offering the perfect blend of user experience and developer-friendly design.

A while back, I came across a tweet by Colin from Clerk.com stating that “Components are the new API.” I couldn’t agree more. For years, we’ve been dedicated to building embeddable components for our customers. The objective is straightforward: How can we create a fully functional component that feels so integrated and seamless that it appears to be built in-house by the host application?

Previously, a common pattern for embedding components using controllable iframes led to inconsistent behavior and, in many cases, was felt disconnected. The rise of the meta frameworks and the serverless motion provided an easier paradigm to embed native to the framework components that are one with the host application.

You’ve probably encountered numerous examples already: Stripe, Clerk, Liveblocks, Algolia, and others.

Component anatomy

All of the successful components I’ve interacted with showcased the following characteristics:

  • First-class User Experience (UX): It felt like a dedicated team crafted the component just for my application
  • Amazing Developer Experience (DX): It was intuitive and easy to get started within minutes
  • Customizability: It could seamlessly blend into my host application, but still provided a strong opinion
  • Lightweight bundle size: It was efficient and minimal in size
  • Common framework: The component was available in the framework I used in my app

Did I miss anything you think is important? Share your observations in the comments section.

In this article, we will focus on component customization and styling. I’ll share some lessons we learned while building the new Novu <Inbox /> component and how we approached meeting our customers’ needs.

The customization pyramid

Customization and styling exist on a spectrum between more control and less control. Whether you are just starting out and want to get up and running quickly, or you are a Fortune 500 company with a strict design system, a team of designers, and complex requirements, you should always be able to achieve your goals in the least amount of time and still have an amazing developer experience.

Let’s explore the customization pyramid concept using Novu’s new <Inbox /> component as an example. By the way, our source code is open-source and publicly available in the Novu GitHub Repository.

Finally, we’ve also made our Figma design asset file publicly available, too.

Appearance Prop

You can think of the appearance prop as a white-labeled internal to the component UI system, that allows styling and modifying different aspects of the component.

A component should be opinionated and render a beautifully crafted UI out of the box. For Novu’s <Inbox /> component, it’s as simple as rendering <Inbox /> in your application. Instantly, you get a fully functional UI with a bell icon that opens a popover implementation.

The first thing we might want to do is to make sure that our brand colors and design guidelines are matched within the component. This is where the appearance prop comes into play

<Inbox appereance={{
    variables: {
        primaryColor: 'pink',
        borderRadius: '4px'
    },
    elements: {
        notificationSubject: {
            marginBottom: '5px'
        },
        notificationPrimaryAction__button {
            lineHeight: '18px'
        }
    }
}} />

Variables

These are the variables that apply to all major elements in the UI. Adjusting just a few of these should allow the component to immediately adopt the look and feel of the host application. Variables for typography, colors, and spacing should be included here.

Elements

This section provides more in-depth control over visible elements within the component. At Novu, we namespace components as follows:

  • notificationSubject
  • notificationBody
  • notificationPrimaryAction
  • notificationPrimaryAction__button
    • For sub-elements, we use an underscore notation to namespace repeated components such as buttons and labels.

Customers should be able to choose their preferred styling framework, whether it’s Tailwind, CSS Modules, or plain CSS, and it should work seamlessly out of the box.

🌟 Tip: We’ve added a 🔔 icon to the class name list to help identify the element names. Everything before this emoji can be targeted and styled easily using the elements field.

Custom Rendering aka “renderProp”

In some use cases, styling with CSS alone isn’t sufficient. For example, in our world, customers might want to render their own custom notification item components, enrich them with live data, or reuse existing UI elements as part of the notification flow.

Here’s how this can look:

<Inbox renderNotification={(notification) => <CustomersCustomItem />} />

This approach provides significantly more control over the look, feel, and behavior of the notifications while still abstracting away the complexities of layouts, real-time updates, fetching, and notification settings.

Composition

While your components should be opinionated, you should not limit your customers to achieving their end goal of implementation. In some cases, you cannot predict the UX pattern they want to use that fits the specifics of their business.

There is a huge gap that sits between a “headless” mode, where you have to build everything from scratch, and the one-liner component that abstracts away pretty much all the internals. Compostable sub-components should bridge this gap, by breaking the one-liner into multiple components that can be re-organized to match different UX patterns.

To facilitate this, your components should be composable. Break down the component structure into independent elements, each focusing on a single goal. In our example:

  • <Bell />
  • <Notifications />
  • <Preferences />
  • <Inbox />

Each element can be rendered as a standalone component and combined to create various layouts, still abstracting away complexity.

React Hooks (AKA Headless SDK)

When customers have strict design and UX requirements, using React Hooks are invaluable. However, resorting to a headless approach like this sometimes indicates a failure to provide adequate styling and composition options, or potentially a framework-specific SDK. To better understand the reasoning, we engage with customers who choose this route to understand if their use case can benefit others.

The React Hooks headless SDK allows customers to build any UI they want, managing the look and feel, state, interactions, and layout themselves.

Here’s an example:

import { Novu } from "@novu/js";

const novu = new Novu({
    subscriberId: "SUBSCRIBER_ID",
    applicationIdentifier: "APPLICATION_IDENTIFIER",
});

const notifications = await novu.notifications.list({
    limit: 30,
});

novu.on("notifications.notification_received", (data) => {
    console.log("new notification =>", data);
});

The headless SDK provides no UI but offers a straightforward set of wrappers around your component’s most common API and server actions.

Conclusion

Thinking from the perspective of your users and setting up the right architecture in the beginning will go a long way. Invest the time debating DX, show this to your users, and try to understand the main friction points in their adoption. Lastly, think of your customization strategy in layers to allow your customers to start fast and not block them if they have more specific styling and UX requirements.

I’m curious to see where the future of “Components are the new API” will take us as a team, and I’m looking forward to learning from our amazing community.

Finally, I’d be remiss if I didn’t tell you to take a look at our result. Here, you’ll find a fully functional <Inbox /> playground we styled to mimic popular apps (Notion and Reddit), and then you can also experiment by customizing your own.