A custom design system: how @memori.ai/ui transformed memori-react
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 |
|
lucide-react | Consistent iconography across the library |
OKLCH + | 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 navigationButton (with primary, secondary, outline, ghost, danger, and toolbar variants), compound Dropdown (Trigger, Menu, Item, Separator, Group).
Overlays and containersModal, Drawer, Popover, Tooltip, ConfirmDialog—all with focus and positioning management delegated to the underlying primitives.
Forms and inputsForm, Field (Root, Label, Description, Error, Control), FieldGroup, Input, Checkbox, Slider, Combobox, SelectBox, Autocomplete.
Feedback and statusSpin for loading, plus an alert system with AlertProvider, AlertViewport, useAlertManager, and createAlertOptions for contextual toasts.
Content and structureCard, compound Tabs, Expandable for truncated text, Section for module headers, Collapsible, and Table with TanStack support.
InfrastructureMemoriUIProvider 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 |
| Visual identity for each integration |
Surfaces |
| Chat, drawer, and card backgrounds |
Typography |
| Consistent text across every layout |
Spacing |
| Vertical and horizontal rhythm |
Shape |
| Consistent border radii |
Motion |
| Unobtrusive animations |
Feedback |
| Alerts, validation, and states |
Components expose classes with a memori- prefix (for example, memori-button, memori-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.ts, addMemoriTableToI18n(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
- FeedbackButtons, ShareButton, SettingsDrawer, KnownFacts—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 |
Login forms had inconsistent validation and error messages |
|
Contextual menus were implemented differently in each component | Compound |
Inconsistent loading states (custom spinners, “loading…” text) | Shared |
Tooltips behaved unpredictably in some layouts | Base UI |
Chat history was difficult to use on mobile | Redesigned |
Long response text had no consistent handling |
|
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
- UI library: github.com/memori-ai/ui
- Component Storybook: memori-ai.github.io/ui
- React widget: github.com/memori-ai/memori-react
- Widget Storybook: memori-ai.github.io/memori-react