ReuseUI - A Modular UI Component Library and Design System

ReuseUI is a modular, reusable component library built on React + TypeScript + Tailwind CSS with a strong emphasis on accessibility, customization, and performance. It aims to provide a scalable and maintainable set of UI components to streamline frontend development while maintaining a high level of design consistency.

ReuseUI Design System

I wanted to build a React + Tailwind component library to learn how design systems are created behind the scenes. As a front-end developer, I find building design systems both crucial and very interesting. I also took inspiration from libraries such as MUI and Chakra UI, and since most developers use Tailwind, my motivation was to create something React-based, highly customizable, and scalable. This is a case study of the steps, design decisions, and architecture I came up with, plus how I overcame various challenges in the process.


Table of Contents

  1. Motivation & Intro
  2. Project Goals
  3. High-Level Design (HLD)
  4. Low-Level Design (LLD)
  5. Technical Architecture
  6. Code Examples
  7. Challenges I Faced & How I Solved Them
  8. Conclusion

Motivation & Intro

I really wanted to see what it takes to build a design system from scratch—particularly a React library that pairs seamlessly with Tailwind CSS. I’d always admired libraries such as MUI and Chakra UI for their theming capabilities and well-structured components. So I decided: “I’m going to learn by doing.” My main objective was to:

  1. Experiment with a compound component pattern to keep things flexible and context-based.
  2. Leverage Tailwind’s utility classes but still provide a theming system that users can override.
  3. Ensure accessibility best practices (ARIA roles, keyboard nav, etc.).
  4. Provide coverage with tests that focus on user interactions (e.g. toggling with Space, focusing out with Tab).

Project Goals

  • Reusable Building Blocks: Minimal friction in using <Button>, <Modal>, <Tabs>, etc.
  • Accessible: Must follow WAI-ARIA guidelines wherever possible.
  • Compound: For instance, <Modal.Header> and <Modal.Body> read from the same context, letting me keep state in <Modal> only.
  • Theme Overriding: Supply a defaultTheme, but allow partial user overrides to brand it differently.
  • Clear Architecture: Each component in its own folder with tests, story files, theming references, etc.

High-Level Design (HLD)

3.1 Atoms, Molecules, Compound Pattern

I used an atomic design approach:

  1. Atoms: The smallest components (e.g. <Spinner>, <Badge>, <Checkbox>).
  2. Molecules: More advanced combos (like <Tabs> with sub-components, or <Dropdown>).
  3. Organisms: e.g. <Navbar>, <Sidebar> that combine multiple molecules/atoms.
  4. Compound pattern: For example, <Tabs.Group> holds context with activeTab; <Tabs.Item> is a child that consumes that state.

3.2 Diagrams

HLD Diagram

+----------------------------------+ | <ReuseUI> (top-level provider) | | merges defaultTheme + userTheme | +-------------------+--------------+ | +---------v---------------+ | useTheme(), ThemeContext| +---------+---------------+ | +-----------------v------------------+ | Atoms | Molecules | Organisms | | (<Badge>,| <Dropdown>,| <Navbar>, | | <Button>,| <Tabs>,... | <Sidebar>) | | etc... | etc... | etc... | +------------------------------------+

Key: <ReuseUI> merges user overrides, sets context. Each sub-component folder is used for “atom,” “molecule,” or “organism,” with a compound approach.


Low-Level Design (LLD)

4.1 Detailed Component Diagrams

Modal Example:

+--------------------------------------------+ | <Modal show={...} onClose={...}> | | <Modal.Header> ... </Modal.Header> | | <Modal.Body> ... </Modal.Body> | | <Modal.Footer> ... </Modal.Footer> | | </Modal> | +--------------------------------------------+ All sub-components read a shared "modal" context (e.g., isOpen, closeModal).

Tabs Example:

+----------------------------------------------------+ | <Tabs.Group style="underline" onActiveTabChange> | | <Tabs.Item title="Profile">Profile content</Tabs.Item> | <Tabs.Item title="Settings">Settings content</Tabs.Item> | </Tabs.Group> | +----------------------------------------------------+ <Tabs.Group> holds the activeTab in state, children just reference it.

4.2 Compound Components & State

I discovered that letting each sub-component manage part of the state was messy. Instead, I moved the entire logic (like whether a modal is open) into the parent (<Modal>). Children become “dumb,” referencing a shared context or calling a single callback. This pattern is repeated with <Tabs>, <Accordion>, etc.


Technical Architecture

5.1 Folder Structure

src/lib/ components/ Accordion/ Accordion.tsx AccordionPanel.tsx AccordionTitle.tsx AccordionContent.tsx ... Modal/ Modal.tsx ModalHeader.tsx ModalBody.tsx ModalFooter.tsx ... ... helpers/ mergeDeep.ts floating.ts ... theme/ default.ts ... index.ts
  • Each component is isolated in its own subfolder.
  • Tests & stories live alongside each component (e.g., Accordion.test.tsx, Accordion.stories.tsx).

5.2 Theming with mergeDeep

In theme/default.ts:

export default { button: { base: "group flex h-min items-center justify-center ...", color: { info: "...", gray: "...", // etc }, }, // ... };

User can supply a partial override:

const customTheme = { button: { color: { success: "bg-green-700 text-white hover:bg-green-800", }, }, };

Then internally, I do:

import { mergeDeep } from "../../helpers/mergeDeep"; const theme = mergeDeep(useTheme().theme.button, customTheme);

This merges deeply, so user overrides only what they want.

5.3 Accessibility (ARIA)

  • Modal: Uses role="dialog", focuses first element on open, [Escape] to close.
  • Tabs: role="tablist", each <Tabs.Item> => role="tab", arrow keys for navigation.
  • Accordion: toggles aria-expanded, uses role="button" for titles, etc.

5.4 Floating UI (Overlays)

For things like dropdowns and tooltips, I used Floating from @floating-ui/react:

const { x, y, reference, floating, strategy } = useFloating({ middleware: [offset(8), flip(), shift({ padding: 8 })], });

Which ensures menus never overflow and can shift automatically.


Code Examples

6.1 Compound Components (Modal/Tabs)

Modal:

<Modal show={modalOpen} onClose={() => setModalOpen(false)}> <Modal.Header>Some Title</Modal.Header> <Modal.Body>Content inside the body</Modal.Body> <Modal.Footer> <Button onClick={() => setModalOpen(false)}>Cancel</Button> <Button color="info">Save</Button> </Modal.Footer> </Modal>

6.2 Theme Overriding

import { ReuseUI } from "reuseui"; const customTheme = { button: { color: { info: "bg-blue-900 text-white hover:bg-blue-700", }, }, }; export default function App() { return ( <ReuseUI theme={{ theme: customTheme }}> <Button color="info">Custom Info Button</Button> </ReuseUI> ); }

6.3 Testing Strategies

Using @testing-library/react:

it('should toggle when pressing Space on the ToggleSwitch', async () => { const user = userEvent.setup(); const mockFn = vi.fn(); render(<ToggleSwitch label="Enable" checked={false} onChange={mockFn} />); await user.tab(); // focus await user.keyboard('[Space]'); expect(mockFn).toHaveBeenCalledWith(true); });

Challenges I Faced & How I Solved Them

7.1 Centralized State in Parent

Problem: If each sub-component had its own local state, I’d do a lot of prop drilling or have complicated logic. For example, in <Accordion>, each panel might want to update its open/close state individually.

Solution: I keep a single context in the parent (<Accordion>). For instance:

Code Example for Centralizing State

// Accordion.tsx (simplified) const AccordionComponent: FC<AccordionProps> = ({ alwaysOpen = false, children, ...props }) => { // Single state storing open panel index const [isOpen, setOpen] = useState(0); const panels = useMemo( () => Children.map(children, (child, i) => cloneElement(child, { // pass isOpen data & setter isOpen: isOpen === i, setOpen: () => setOpen(i), }), ), [children, isOpen], ); return <div {...props}>{panels}</div>; };

Then each <Accordion.Panel> references isOpen to see if it’s expanded and calls setOpen() when it toggles. No more scattered local states.


7.2 Keyboard & Accessibility Issues

Problem: I needed consistent ARIA roles, keyboard interactions, and correct spacing for focus states. For example, toggling a checkbox with [Space] or focusing out with [Tab] had to be consistent across all components.

Solution:

  • Used aria-label, role, and tabIndex to define the correct semantics.
  • Wrote tests that specifically check focus transitions.
  • Centralized the approach with custom hooks (e.g. useKeyDown).

Code Example for Keyboard A11y

// ToggleSwitch.tsx (excerpt) const handleKeyPress = (event: KeyboardEvent<HTMLButtonElement>): void => { // Preventing default so that pressing Enter doesn't accidentally submit forms event.preventDefault(); if (event.key === " ") { onChange(!checked); // toggles on space } }; // Also, focusing with Tab is auto since it's a button, but we ensure aria-checked return ( <button role="switch" aria-checked={checked} // ... onKeyPress={handleKeyPress} > <div className={classNames(theme.toggle.base /* ... */)} /> <span>{label}</span> </button> );

7.3 Theming Collision & Partial Overrides

Problem: Overriding nested theme structures in a library that spans multiple components often leads to collisions if we do shallow merges (like Object.assign() only one level deep). Another big issue was preserving spacing definitions (like my-2, px-4) when a user overwrote color but not spacing.

Solution:

  • Built a custom mergeDeep() function that recursively merges nested objects.
  • The user’s partial override merges with my defaultTheme, preserving spacing classes:

Code Example for Theming Collisions

// mergeDeep.ts function isObject(item: unknown): item is Record<string, unknown> { return ( item !== null && typeof item === "object" && item.constructor === Object ); } export function mergeDeep<T extends object, S extends object>( target: T, source: S, ): T & S { const output = { ...target } as T & S; if (isObject(source)) { Object.keys(source).forEach((key) => { if (isObject(source[key]) && key in target && isObject(target[key])) { // recursive merge output[key] = mergeDeep(target[key] as object, source[key] as object); } else { output[key] = isObject(source[key]) ? mergeDeep(target[key] as object, source[key] as object) : source[key]; } }); } return output; }

That snippet ensures spacing classes remain in the theme unless the user explicitly overrides them.


7.4 Spacing Issues & Fixing Them

Problem: In some components, spacing got inconsistent or overshadowed by user overrides. For instance, an <Accordion.Title> might lose its py-5 px-5 if the user theme had base = 'p-3'.

Solution:

  • I used consistent naming for classes (theme.item.base, theme.title.base, etc.).
  • In some cases, I combined user override with default spacing. For example, in the AccordionTitle.tsx, I explicitly merge classes:

Code Example for Spacing Overlaps

// AccordionTitle.tsx export const AccordionTitle: FC<AccordionTitleProps> = ({ className, theme: customTheme = {}, ...props }) => { const theme = mergeDeep(useTheme().theme.accordion.title, customTheme); return ( <button type="button" // Note how I'm combining default spacing with user classes className={classNames( theme.base, // "flex w-full items-center justify-between..." theme.flush[flush ? "on" : "off"], theme.open[isOpen ? "on" : "off"], className, // user can add p-3, but existing "py-5 px-5" remains )} {...props} > {children} {/* arrow icon here... */} </button> ); };

This ensures the default theme’s py-5 px-5 remains unless the user specifically overwrote them in theme.base. That way, spacing is still consistent.


Conclusion

Ultimately, building ReuseUI taught me the power and flexibility behind compound components, which minimize overhead by centralizing state in the parent while children remain “dumb.” The approach using Tailwind plus a custom mergeDeep theming solution offers a developer-friendly experience with partial overrides. Testing for [Space] and [Arrow] key handling confirmed accessibility.

Conclusion Diagram

+------------------------------+ | ReuseUI Library | | (React + Tailwind + Themes) | +---------+--------------------+ | +---------------------v---------------------+ | Compound Components (Modal, Tabs, etc) | | Centralized State + Accessible UI | +---------------------+---------------------+ | +---------------------v---------------------+ | Theming Merge | Well-tested code | | (mergeDeep) | (Keyboard, ARIA) | +------------------------------------------+

By combining well-tested, accessible React components with an extensible theming system, I ended up with a design system that can be easily integrated, customized, and scaled for various React/Tailwind applications.