react-create-slot
v0.2.0
Published
A lightweight, type-safe Slot/Fill pattern for React using portals
Downloads
27
Maintainers
Readme
react-create-slot
A lightweight, type-safe Slot/Fill pattern for React. Primitives for rendering content into a named slot from anywhere in the component tree, using portals.
Motivation
A page title that belongs in the header. Action buttons that should appear in a toolbar. Filters that live in a sidebar. In React, rendering content into a different part of the layout usually means lifting state, threading render props, or writing boilerplate context + portal wiring by hand — every time.
react-create-slot is a declarative helper that takes care of all that plumbing for you. One function call — and you get a ready-to-use Provider, Slot, and Fill triplet. No manual context creation, no ref juggling, no portal setup. Just declare where content should appear and what to put there:
const { HeaderSlot, HeaderSlotFill, HeaderSlotProvider } =
createSlot('HeaderSlot');That's it. The Slot marks the target, the Fill projects content into it from anywhere in the tree — and the helper handles the React context, DOM refs, and createPortal under the hood.
Existing slot/fill libraries are either abandoned, tied to a specific framework, or solve a different problem entirely (prop merging, not content projection). This library is ~80 lines, zero dependencies, fully typed with TypeScript template literal types, and extracted from a production application.
Install
npm install react-create-slotpnpm add react-create-slotyarn add react-create-slotRequires react and react-dom >= 16.8 as peer dependencies.
Quick Start
import { createSlot } from 'react-create-slot';
// 1. Create a slot
const { HeaderSlot, HeaderSlotFill, HeaderSlotProvider } =
createSlot('HeaderSlot');
// 2. Wrap your app with the provider
function App() {
return (
<HeaderSlotProvider>
<Header />
<MainContent />
</HeaderSlotProvider>
);
}
// 3. Place the slot where content should appear
function Header() {
return (
<header>
<nav>Logo</nav>
<HeaderSlot as="div" className="header-actions" />
</header>
);
}
// 4. Fill it from anywhere in the tree
function MainContent() {
return (
<main>
<HeaderSlotFill>
<input type="search" placeholder="Search..." />
</HeaderSlotFill>
<p>Page content here</p>
</main>
);
}The <input> is defined inside MainContent but rendered inside Header.
Full Example
A layout with multiple slots — each page injects its own header and sidebar content:
// slots.ts
import { createSlot } from 'react-create-slot';
export const { HeaderSlot, HeaderSlotFill, HeaderSlotProvider } =
createSlot('HeaderSlot');
export const { SidebarSlot, SidebarSlotFill, SidebarSlotProvider } =
createSlot('SidebarSlot');// Layout.tsx
function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<header>
<HeaderSlot />
</header>
<aside>
<SidebarSlot />
</aside>
<main>{children}</main>
</div>
);
}// App.tsx
function App() {
return (
<HeaderSlotProvider>
<SidebarSlotProvider>
<Layout>
<Routes />
</Layout>
</SidebarSlotProvider>
</HeaderSlotProvider>
);
}// pages/Dashboard.tsx
function Dashboard() {
return (
<>
<HeaderSlotFill>
<h1>Dashboard</h1>
<UserMenu />
</HeaderSlotFill>
<SidebarSlotFill>
<DashboardNav />
</SidebarSlotFill>
<DashboardContent />
</>
);
}API
createSlot(name)
Creates a Slot/Fill pair. The name must end with "Slot".
Returns an object with three components:
| Component | Description |
| ---------------- | ---------------------------------------------------------------- |
| {name}Provider | Context provider. Wrap the part of the tree that needs access. |
| {name} | The slot. Renders a portal target element. |
| {name}Fill | The fill. Its children are portaled into the corresponding slot. |
const { HeaderSlot, HeaderSlotFill, HeaderSlotProvider } =
createSlot('HeaderSlot');
// ^ Slot ^ Fill ^ ProviderSlot Props
| Prop | Type | Default | Description |
| ----------- | ---------------------------------- | ------- | ---------------------------------------------- |
| as | ContainerElement | 'div' | HTML tag to render (void elements excluded). |
| className | string | — | CSS class for the slot element. |
| style | React.CSSProperties | — | Inline styles for the slot element. |
| id | string | — | ID for the slot element. |
| ...rest | HTML attributes for the chosen tag | — | Any valid HTML attribute for the as element. |
<HeaderSlot /> {/* renders <div> */}
<HeaderSlot as="section" /> {/* renders <section> */}
<HeaderSlot as="nav" className="toolbar" aria-label="actions" />Fill Props
| Prop | Type | Default | Description |
| ---------- | ----------------- | ------- | ------------------------------------------------------ |
| children | React.ReactNode | — | Content to portal into the slot. |
| fallback | React.ReactNode | null | Rendered when the slot is not mounted (or during SSR). |
<HeaderSlotFill>
<SearchBar />
</HeaderSlotFill>
<HeaderSlotFill fallback={<Skeleton />}>
<SearchBar />
</HeaderSlotFill>
{/* Fallback can contain another SlotFill for graceful degradation */}
<PrimarySlotFill
fallback={
<SecondarySlotFill>
<SearchBar />
</SecondarySlotFill>
}
>
<SearchBar />
</PrimarySlotFill>How It Works
createSlotcreates a React context that holds a reference to a DOM element.- The Slot component renders an HTML element (default
<div>, configurable viaas) and registers it as the portal target viaref. Any additional props are forwarded to the element. - The Fill component reads that DOM element from context and uses
createPortalto render its children into it. - If the Slot hasn't mounted yet, Fill renders the
fallback(or nothing). This also applies during SSR — DOM refs are never set on the server, so Fill renders the fallback automatically.
Because it uses portals, React event bubbling and context work as expected — the filled content stays in the React tree of the Fill, not the Slot.
TypeScript
createSlot uses template literal types to generate correctly named components:
const result = createSlot('HeaderSlot');
result.HeaderSlot; // (props: SlotProps<E>) => ReactElement | null
result.HeaderSlotFill; // React.FC<FillProps>
result.HeaderSlotProvider; // React.FC<PropsWithChildren>
result.WrongName; // TS ErrorThe name parameter is constrained to strings ending in "Slot", catching typos at compile time.
The Slot component is fully polymorphic — the as prop determines which HTML attributes are available:
<HeaderSlot as="nav" aria-label="main" /> // ✅ aria-label is valid for <nav>
<HeaderSlot as="div" type="text" /> // ❌ TS error: type is not valid for <div>Void elements (img, input, br, etc.) are excluded at the type level — they cannot contain children, so they cannot be portal targets:
<HeaderSlot as="img" /> // ❌ TS error: void elements are not allowed
<HeaderSlot as="section" /> // ✅Comparison with Alternatives
| Library | Approach | TypeScript | Maintained | Size | | -------------------------------- | -------------------------------- | ----------------------------- | ---------------- | ----- | | react-create-slot | Portal-based | Full (template literal types) | Active | ~1 KB | | @radix-ui/react-slot | Prop merging (different pattern) | Yes | Yes | ~2 KB | | react-slot-fill | Portal-based | No | Abandoned (2018) | ~8 KB | | @blackbox-vision/react-slot-fill | Context-based | Partial | Low activity | ~4 KB |
Note: @radix-ui/react-slot solves a different problem — it merges parent props onto a child element (the
asChildpattern). It does not provide portal-based content projection. Both libraries can coexist in the same project.
