Skip to content

babajaga3/react-bricks

Repository files navigation

react-bricks

A tiny (~1 kB gzipped) factory for building composable, Tailwind-friendly React layout component systems. Define your own named layout slots once, then snap them together like bricks anywhere in your codebase.

const MobileLayout = createLayout({
  Main:    { as: 'main',   className: 'flex flex-col min-h-screen' },
  Header:  { as: 'header', className: 'sticky top-0 z-50 h-14 border-b' },
  Content: {               className: 'flex-1 overflow-y-auto px-4 py-6' },
  Footer:  { as: 'footer', className: 'h-16 border-t flex items-center px-4' },
});

function Page() {
  return (
    <MobileLayout.Main>
      <MobileLayout.Header>My App</MobileLayout.Header>
      <MobileLayout.Content>Hello world</MobileLayout.Content>
      <MobileLayout.Footer>© 2025</MobileLayout.Footer>
    </MobileLayout.Main>
  );
}

Features

  • Zero config — works with plain CSS classes, Tailwind, or any utility framework
  • Tailwind-aware — uses tailwind-merge to resolve class conflicts when installed
  • Fully typed — TypeScript generics infer slot names from your config; invalid slot access is a compile error
  • Polymorphic — every slot accepts an as prop to swap the rendered element
  • Composable — extend layouts, merge layouts, override per-instance
  • Tree-shakeable — dual ESM + CJS output, "sideEffects": false
  • React 17+ compatible, framework agnostic (Next.js, Vite, Remix…)

Installation

npm install @babajaga3/react-bricks
# or
pnpm add @babajaga3/react-bricks

Optional peer dependencies (recommended)

For Tailwind class-conflict resolution and conditional class support:

npm install tailwind-merge clsx

The package works without them — it falls back to plain space-joined class concatenation.


API

createLayout(config, name?)

Creates a namespaced object of slot components from a config.

function createLayout<T extends LayoutConfig>(
  config: T,
  name?: string,   // shown in React DevTools as "name.SlotKey"
): Layout<T>

Config shape:

Property Type Default Description
as React.ElementType 'div' The HTML element or component this slot renders as
className string '' Default classes applied to every instance
displayName string inferred Label in React DevTools

Slot component props:

Every generated slot accepts:

Prop Type Description
as React.ElementType Override the rendered element/component for this single instance
className string Extra classes merged on top of defaults (via tailwind-merge if present)
children React.ReactNode Slot content
...rest element props All other props forwarded to the underlying element

extendLayout(base, extension, name?)

Creates a new layout by extending an existing one. Slots in extension override matching slots in base; new keys are added.

const DesktopLayout = extendLayout(
  MobileLayout,
  {
    // Override
    Content: { className: 'flex-1 px-8 max-w-5xl mx-auto' },
    // Add new
    Sidebar: { as: 'aside', className: 'w-64 border-r hidden lg:block' },
  },
  'DesktopLayout',
);

// DesktopLayout.Header  ← from MobileLayout (unchanged)
// DesktopLayout.Content ← overridden
// DesktopLayout.Sidebar ← new

mergeLayouts(a, b)

Merges two already-built layout objects into one. Slots in b win when keys collide.

const CardLayout = createLayout({ Root: {}, Body: {} });
const AppLayout  = mergeLayouts(MobileLayout, CardLayout);

// AppLayout.Main / .Header / .Content / .Footer / .Root / .Body

cn(...classes)

The internal class merger is exported in case you want to use it in your own components.

import { cn } from '@babajaga3/react-bricks';

<div className={cn('px-4', isActive && 'bg-blue-500', props.className)} />

Patterns

One layout file per breakpoint / product area

// layouts/mobile.ts
export const MobileLayout = createLayout({});

// layouts/desktop.ts
export const DesktopLayout = extendLayout(MobileLayout, {});

// layouts/dashboard.ts
export const DashboardLayout = createLayout({});

Per-page className overrides

Default classes live in the layout definition. Instance overrides are merged at render time — Tailwind conflicts are resolved automatically.

// Default: px-4
<MobileLayout.Content className="px-8"></MobileLayout.Content>
// Rendered: px-8  (tailwind-merge resolves the conflict)

Polymorphic slot rendering

// Render Content as <article> for semantic HTML
<MobileLayout.Content as="article" className="prose">
  <h2></h2>
</MobileLayout.Content>

// Render Content as a third-party motion component
import { motion } from 'motion/react';
<MobileLayout.Content as={motion.div} animate={{ opacity: 1 }}></MobileLayout.Content>

TypeScript

Slot names are inferred from your config — accessing a slot that doesn't exist is a compile-time error.

const Layout = createLayout({ Main: {}, Header: {} });

<Layout.Main />    // 
<Layout.Footer />  // ❌ Property 'Footer' does not exist on type 'Layout<…>'

You can also export the layout type for use in other files:

import type { Layout } from '@babajaga3/react-bricks';
import type { myLayoutConfig } from './layouts/mobile';

type MobileLayoutType = Layout<typeof myLayoutConfig>;

Acknowledgement

The majority of the initial codebase has been generated with Claude Sonnet 4.6 - everything from commit 0b67bb3. I had this idea in my mind and it was a fast way to prototype it. Do what you will with that information.


License

MIT

About

A tiny factory for building composable, Tailwind-friendly React layout component systems.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors