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>
);
}- Zero config — works with plain CSS classes, Tailwind, or any utility framework
- Tailwind-aware — uses
tailwind-mergeto 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
asprop 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…)
npm install @babajaga3/react-bricks
# or
pnpm add @babajaga3/react-bricksFor Tailwind class-conflict resolution and conditional class support:
npm install tailwind-merge clsxThe package works without them — it falls back to plain space-joined class concatenation.
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 |
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 ← newMerges 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 / .BodyThe 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)} />// layouts/mobile.ts
export const MobileLayout = createLayout({ … });
// layouts/desktop.ts
export const DesktopLayout = extendLayout(MobileLayout, { … });
// layouts/dashboard.ts
export const DashboardLayout = createLayout({ … });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)// 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>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>;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.
MIT