A custom design system: how @memori.ai/ui transformed memori-react

Share
@memori.ai/ui
@memori.ai/ui

Every time a user opens an agent, they enter a conversation. Not a technical panel, not an anonymous widget: an experience that needs to feel consistent, reliable, and polished—even when it is powered by different layouts, custom themes, 3D avatars, history drawers, artifact panels, and dozens of integration settings.

For years, this is how we built memori-react: component by component, layout by layout, requirement by requirement. It worked. But as the product grew—embedded chat, full page, totem, website assistant, hidden chat—the fragmentation became increasingly apparent. A button here, a modal there, a drawer with different animations elsewhere. The same actions looked different. The same brand did not always behave consistently.

It was not just a matter of aesthetics. It was a matter of trust. When an interface is inconsistent, users perceive the product as less robust—and in a conversational context, where AI needs to feel present and reliable, every visual inconsistency matters.

At some point, the question was no longer “how do we fix this component?” but “how do we build a UI language that can support the entire AIsuru ecosystem?” The answer was @memori.ai/ui: a custom React library designed for the AIsuru platform, with shared design tokens, accessible components, multilingual support, and a catalog documented in Storybook. Not a generic off-the-shelf kit, but a design system built around the patterns we use every day: chat, login forms, drawers, tooltips, alerts, and tables.

In this post, we describe that journey. First, the why—what was not working and what we wanted to achieve. Then, the library—its architecture, stack, and components. Finally, the integration into memori-react: how we connected tokens, providers, portals, and layered CSS without losing the flexibility required by every customer integration. Most importantly, we explain what changed in practice: a more consistent interface, a more predictable experience, and a widget that finally behaves like a single product—even when it takes on many different forms.


Why @memori.ai/ui

When we started evaluating the options, the question was not “do we need a UI library?” It was already clear that we did. The real questions were: which one, and for whom?

Memori is not an app with a single screen and a single flow. It is a widget that lives inside different customers’ websites, with configurable colors, themes, and layouts. It is a conversational platform where chat, avatars, history, uploads, artifacts, OTP login, feedback, and settings coexist. It is the same product that needs to work on desktop, on mobile, in a physical totem, and as an assistant that opens from the corner of a web page.

General-purpose UI libraries cover many use cases—but rarely ours. A design system built for admin dashboards does not know what to do with a chat bubble that exposes contextual actions. Every time we forced a generic component into our context, we ended up writing compensating CSS, custom wrappers, and layout-specific behavior. The initial savings turned into technical debt.

There was also a second, less visible but equally concrete goal: moving away from the libraries we had inherited. The AIsuru dashboard used Ant Design—a solid system designed for admin panels, not conversations. In memori-react, we used Headless UI: accessible primitives, but without a visual identity.

There was also a second, less visible but equally concrete goal: using a single library instead of the different ones inherited by each product. In memori-react, we used Headless UI: accessible primitives, but without a visual identity. Every surface styled them in its own way, with predictable results—the same actions looked different, and compensating CSS was everywhere. The AIsuru dashboard still uses Ant Design: a solid system designed for admin panels, not conversations. That replacement has not started yet, but it is part of the same plan: when the dashboard moves to @memori.ai/ui, it will not need to invent another language.

We therefore wanted a shared visual language across our aisuru.com dashboard and memori-react. If each contains buttons, modals, and forms independently, end users perceive them as two different products—even when the same technology powers both.

The decision to build @memori.ai/ui came from this need. It was not a branding exercise, but an operational investment:

  • Control over the patterns that matter—drawers, tooltips, alerts, forms, dropdowns, and tables: components we use dozens of times, not once a year.
  • Native theming—an OKLCH color system, semantic CSS tokens, and light/dark support without forking the code for every integration.
  • Accessibility and i18n from day one—automated tests, Storybook as living documentation, and built-in translations for the components that need them.
  • Independent releases—a library published on npm, versioned, and consumable by memori-react and other projects without tying UI evolution to the release cycle of a single repository.

In practice, we separated two responsibilities that had previously been mixed together: @memori.ai/ui answers the question “how should a button, modal, or input field behave in the Memori world?”; memori-react answers “how do we build a complete conversational experience on top of those foundations?”

It was not the shortest path. Building and maintaining a design system requires discipline: naming conventions, visual reviews, token updates, and compatibility across versions. But the alternative—continuing to patch isolated components across hundreds of files—was already more expensive; the cost was simply spread over time and less visible in planning.

The outcome we wanted was clear from the beginning: a widget that feels as though it was designed as a whole, even though it contains six layouts, dozens of configuration options, and years of accumulated features. @memori.ai/ui is the foundation on which that consistency rests. In the next section, we look at how it is built—its stack, components, and design-token system—before exploring the memori-react integration in detail.


Inside the library

@memori.ai/ui is not a collection of “hand-styled” components scattered across a folder. It is a complete design system: accessible primitives, design tokens, a publishable build, Storybook documentation, and a TypeScript API designed for developers integrating the widget—not only for those building it.

Architecture and technology stack

The library is built with React and TypeScript, with React 17/18 as a peer dependency and support for i18next / react-i18next wherever components need to speak the user’s language.

For interactivity and accessibility, we rely on @base-ui/react: headless primitives that handle focus traps, ARIA, positioning, and states without imposing a prebuilt look. We built Memori’s visual identity on top of this foundation—not the other way around.

Other building blocks in the stack include:

Technology

Role

TanStack Table

Table component for complex tabular data (filters, pagination, bulk actions)

lucide-react

Consistent iconography across the library

OKLCH + color-mix()

Modern color system with automatic variants for hover, focus, and borders

CSS cascade layers

Clear separation between reset, tokens, components, and overrides

Vitest + axe

Unit tests and WCAG accessibility checks in CI

ESM + CJS

Dual build for different consumers; CSS emitted as a single file

CSS is not a secondary detail: it is the theming contract. Integrators import @memori.ai/ui/styles.css once and get the reset, variables, component classes, and responsive behavior. The default font (Lexend Deca) is not bundled—the host loads it or overrides --memori-font-family.

To explore variants, states, and behavior, the official Storybook is the reference: it contains interactive examples, prop controls, and a11y checks that are only briefly mentioned in the README.

The components that make up the system

The public API exports everything from @memori.ai/ui. There is no need to navigate internal paths or import source files. Components are grouped by responsibility:

Actions and navigation
Button (with primary, secondary, outline, ghost, danger, and toolbar variants), compound Dropdown (Trigger, Menu, Item, Separator, Group).

Overlays and containers
ModalDrawerPopoverTooltipConfirmDialog—all with focus and positioning management delegated to the underlying primitives.

Forms and inputs
FormField (Root, Label, Description, Error, Control), FieldGroupInputCheckboxSliderComboboxSelectBoxAutocomplete.

Feedback and status
Spin for loading, plus an alert system with AlertProviderAlertViewportuseAlertManager, and createAlertOptions for contextual toasts.

Content and structure
Card, compound TabsExpandable for truncated text, Section for module headers, Collapsible, and Table with TanStack support.

Infrastructure
MemoriUIProvider for themes and portal containers, useTheme for light/dark mode, and MemoriI18nProvider and addMemoriTableToI18n for table translations.

It is not an endless catalog—it is a curated set of what Memori and AIsuru genuinely need. A new component enters the library only if it makes sense in multiple contexts, not to solve an isolated case.

Design tokens and the theming system

The visual core of the library is its CSS custom properties. Instead of hardcoding colors and spacing in components, everything flows through semantic tokens that the host can override:

:root {
  --memori-primary-color: oklch(0.55 0.22 290);
  --memori-secondary-color: oklch(0.7 0.15 200);
  --memori-font-family: 'Lexend Deca', sans-serif;
}

Hover and active states, focus rings, shadows, and borders are derived automatically from these values. Dark mode is enabled by setting data-theme="dark" on the root—no JavaScript fork and no duplicate component set.

The main token families are:

Family

Examples

Use

Brand

--memori-primary-color, --memori-secondary-color

Visual identity for each integration

Surfaces

--memori-main-background, --memori-inset-background

Chat, drawer, and card backgrounds

Typography

--memori-font-family, --memori-text-size-base

Consistent text across every layout

Spacing

--memori-spacing-xs  --memori-spacing-5xl

Vertical and horizontal rhythm

Shape

--memori-radius-field, --memori-radius-box

Consistent border radii

Motion

--memori-motion-duration-fast, --memori-motion-ease

Unobtrusive animations

Feedback

--memori-success, --memori-error, --memori-warning

Alerts, validation, and states

Components expose classes with a memori- prefix (for example, memori-buttonmemori-drawer, and memori-tooltip__popup), allowing the host to refine details without breaking internal encapsulation.

This model—tokens → components → targeted overrides—is what makes the library flexible without turning it into a minefield of !important rules and CSS hacks.


How we integrated it into memori-react

Having a good UI library is only half the job. The other half is integrating it into a mature codebase such as memori-react—with hundreds of components, six layouts, customer-specific configurations, web components, Storybook, and snapshot tests—without breaking anything or losing the customization each integration requires.

1. Dependency and global stylesheet

The first step was declarative: adding @memori.ai/ui as an npm dependency and importing the compiled CSS into memori-react’s global stylesheet.

The design system enters the cascade once, at the very beginning. Domain styles—chat bubbles, totem layouts, and the artifact drawer—are layered on top and remain memori-react’s responsibility. The library provides the foundations; the widget builds the conversational experience.

During the build, postcss-import inlines the import before publication, so package consumers receive a single, consistent styles.css.

2. Application-level providers

Some library components require React context. In index.tsx, we added AlertProvider for the global toast system. In i18n.tsaddMemoriTableToI18n(i18n) merges the library’s strings with those already available in the widget—five languages, one i18next instance.

We did not duplicate the translation infrastructure: we extended it.

3. MemoriUIProvider: themes and portals inside the widget

The most delicate part of the integration is the embedded widget. Tooltips, dropdowns, modals, and drawers use portals to escape the local DOM. But in Memori, the widget lives inside a fixed-height container, has its own theme, and is often placed inside a shadow DOM or iframe.

In fact, our aisuru.com dashboard imports the chat widget directly, and the challenge was precisely to keep portals from escaping the chat DOM.

MemoriUIProvider solves this: it receives a reference to the widget root (container={widgetRootEl}) and the theme (theme={widgetTheme}), ensuring that everything portaled out of the normal DOM flow respects both.

Alongside it, AlertViewport lives inside the themed provider, so toasts inherit the correct tokens in dark mode.

This architectural compromise emerged from real constraints: in the hidden_chat and website_assistant layouts, the widget collapses to minimal dimensions in the document flow, and tooltips without this handling ended up stacking letters vertically. It is not a library bug—the usage context requires explicit configuration.

4. Layered CSS: default theme and integration overrides

In styles.css, we introduced cascade layers:

@layer theme, integration;

The theme layer contains the widget defaults (font, primary colors, and the shared conversation axis). The integration layer—populated dynamically from integrationConfig—overrides tokens such as --memori-primary-color for the customer’s brand without touching component code.

This mechanism allows a single widget to feel like it belongs to the host website while remaining structurally identical.

5. Component-by-component migration

The integration was not a big-bang migration. The initial commit (feat: integrate @memori.ai/ui components) laid the foundations; then dozens of style: and refactor: commits migrated one component at a time:

  • Header and ChatInputs—buttons, credit-usage dropdowns, and tooltips
  • ChatBubble—contextual actions, expandable content, and modals
  • ChatHistory—history drawer, filter selects, and confirmation alerts
  • LoginDrawer—OTP forms, cards, and validation
  • ArtifactDrawer—tabs, action dropdowns, and copy/export
  • StartPanel—combobox, modal, and expandable hints
  • FeedbackButtonsShareButtonSettingsDrawerKnownFacts—and many others

Today, more than sixty files import from @memori.ai/ui. Custom CSS for modals, drawers, and buttons has been reduced; what remains handles layouts and behavior specific to the conversational domain rather than repeating UI patterns that have already been solved.


Before and after: what changed

Visual consistency: one language

Previously, opening the widget in different layouts meant encountering micro-differences everywhere: different border radii between inputs and bubbles, buttons with misaligned padding, drawers with inconsistent animations and shadows, and headers whose structure changed between chat and full-page views.

After the integration, buttons, modals, drawers, tooltips, and dropdowns share the same tokens: the same radius, typographic hierarchy, hover/focus/active states, and spacing logic. Users do not “see” @memori.ai/ui—they see a product that gets the details right.

Layout and responsiveness: the conversation axis

One of the less obvious but most impactful improvements is the introduction of a shared conversation axis: a maximum width (--memori-conversation-max-width), consistent inline padding, and alignment across the message list, empty state, and input area.

Previously, each layout managed these constraints independently—and the discrepancies were particularly visible on mobile. Now, chat, full page, and zoomed full body share the same containment logic, with adaptations only where required by the layout (totem and website assistant).

Global backgrounds configured through integrationConfig (globalBackground) now also integrate with the library’s surface system without compromising contrast or readability in dark mode.

Light/dark theme: finally aligned

The theme is no longer a matter of scattered CSS and fragile overrides. MemoriUIProvider and data-theme on the widget root remain synchronized: tooltips, alerts, drawers, and modals rendered through portals inherit the same tokens as the container.

In practice, switching from light to dark no longer means manually fixing components that escape the DOM flow—it means relying on a single source of truth.

What changes for users (UX)

Visual consistency is the prerequisite; the experience is the outcome. These are the areas where the change is genuinely noticeable:

Before

After

Feedback after an action (like/dislike, upload, error) used different patterns

Consistent toasts via useAlertManager, positioned below the header and readable in both themes

Login forms had inconsistent validation and error messages

Form + Field + Input with standard error states and alerts for confirmations and network issues

Contextual menus were implemented differently in each component

Compound Dropdown with the same keyboard navigation and positioning

Inconsistent loading states (custom spinners, “loading…” text)

Shared Spin across all layouts, with a consistent overlay

Tooltips behaved unpredictably in some layouts

Base UI Tooltip, with explicit fixes for embedded layouts (hidden_chat, website_assistant)

Chat history was difficult to use on mobile

Redesigned Drawer, filters with SelectBox, and deletion confirmations with alerts

Long response text had no consistent handling

Expandable truncates and expands content without breaking the bubble layout

None of these rows is “just UI.” They are friction removed from flows that users go through every day: asking a question, reading a response, giving feedback, resuming a conversation, signing in, and uploading a document.


Conclusion

Building @memori.ai/ui and integrating it into memori-react was not an overnight revolution. It was a gradual migration—commit by commit, component by component—guided by a goal that is simple to state and difficult to achieve: making Memori feel like a single product, everywhere and in every configuration.

We came away with two lessons.

From a technical perspective, a shared design system pays off when it is native to your patterns, not retrofitted after the fact. CSS tokens, providers for portals and themes, and thin domain wrappers are not implementation details; they are the foundations that make it possible to evolve without starting over with every feature.

From a product perspective, users do not distinguish between a “library component” and a “widget component.” They notice whether the experience is smooth, predictable, and polished. @memori.ai/ui gave us a shared language; memori-react continues to tell the conversational story. Together, for the first time, they tell the same story.

Useful resources