npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zemd/react-slottable

v4.0.0

Published

A lightweight concept to customize subcomponents in React

Readme

React Slottable

A lightweight library for customizing subcomponents in React

npm version

This package provides a lightweight approach that allows your component users to easily customize nested subcomponents. The concept is inspired by Material UI's slots pattern.

Features

  • Simple API with useSlot hook and Slot component
  • Compatible with React Compiler
  • TypeScript support with full type inference
  • Optimized with useMemo and useCallback for performance
  • Minimal bundle size with zero dependencies
  • Development mode warnings for debugging

Installation

npm install @zemd/react-slottable
pnpm add @zemd/react-slottable
yarn add @zemd/react-slottable

Quick Start

The core concept of this library is the slot. A slot is a part of a component that can be overridden or customized by the consumer. Instead of creating numerous props to customize nested components, you can divide your component into slots and let users provide their own implementations.

Creating a Slottable Component

Here is how to create a simple Button component with startDecorator and endDecorator slots:

import { type PropsWithSlots, useSlot } from "@zemd/react-slottable";

// Define your component props with slots
type ButtonProps = PropsWithSlots<
  React.PropsWithChildren<{
    fullWidth?: boolean;
    disabled?: boolean;
    size?: "sm" | "md" | "xl";
    variant?: "solid" | "outlined";
    color?: "primary" | "secondary";
    className?: string;
  }>,
  ["startDecorator", "endDecorator"]
>;

// Optional: define a default component for a slot
const DefaultDecorator: React.FC<{ className?: string }> = ({ className }) => {
  return <div className={className}>Default decorator</div>;
};

export const Button: React.FC<ButtonProps> = (props) => {
  // useSlot returns a render function (React Compiler compatible)
  const renderStartDecorator = useSlot("startDecorator", props, {
    slot: DefaultDecorator, // provide a default component
  });
  const renderEndDecorator = useSlot("endDecorator", props);

  return (
    <button className={props.className}>
      {renderStartDecorator({ className: "decorator-class" })}
      {props.children}
      {renderEndDecorator({})}
    </button>
  );
};

Using the Slot Component (JSX-centric API)

For developers who prefer a more declarative JSX syntax, the Slot component provides an alternative to the useSlot hook:

import { type PropsWithSlots, Slot } from "@zemd/react-slottable";

type AlertProps = PropsWithSlots<{ type: "info" | "error"; title: string; message: string }, ["icon", "title", "message"]>;

export const Alert: React.FC<AlertProps> = (props) => {
  return (
    <div role="alert">
      <Slot name="icon" parentProps={props} default="span">
        {props.type === "info" ? "ℹ️" : "❌"}
      </Slot>
      <Slot name="title" parentProps={props} default="div">
        {props.title}
      </Slot>
      <Slot name="message" parentProps={props} default="div">
        {props.message}
      </Slot>
    </div>
  );
};

Using a Slottable Component

Consumers can now customize the slots by providing their own components:

const MyCustomDecorator: React.FC = () => {
  return <span>Custom decorator</span>;
};

export function App(): React.JSX.Element {
  return (
    <Button
      slots={{
        endDecorator: MyCustomDecorator,
      }}
      slotProps={{
        startDecorator: {
          className: "custom-class",
        },
      }}
      className="my-button"
    >
      Click me
    </Button>
  );
}

API Reference

useSlot

A hook that returns a memoized render function for a given slot.

const renderSlot = useSlot(name, props, options);

Parameters:

  • name - The name of the slot
  • props - The component props containing slots and slotProps
  • options - Optional configuration object
    • slot - Default component to render if no slot is provided
    • ...extraProps - Additional props to pass to the slot (highest priority)

Returns:

A memoized render function (SlotRenderFunction) that accepts props and returns the rendered slot.

Slot

A JSX component alternative to the useSlot hook.

<Slot name="header" parentProps={props} default="div" className="header">
  {children}
</Slot>

Props:

  • name - The name of the slot to render
  • parentProps - The parent component props containing slots and slotProps
  • default - Default component to render if no slot is provided
  • children - Children to render inside the slot
  • ...rest - Additional props passed to the slot component

PropsWithSlots<Props, SlotNames>

A type helper that extends your component props with slots and slotProps using a simple array of slot names.

type MyComponentProps = PropsWithSlots<{ title: string }, ["header", "footer"]>;

This adds the following optional props to your component:

  • slots - An object mapping slot names to custom components
  • slotProps - An object mapping slot names to props passed to each slot

PropsWithTypedSlots<Props, SlotsMap>

A type helper for advanced use cases that provides better TypeScript inference for slot-specific props.

type ButtonSlots = {
  startDecorator: React.FC<{ icon?: string; className?: string }>;
  endDecorator: React.FC<{ label?: string }>;
};

type ButtonProps = PropsWithTypedSlots<{ disabled?: boolean }, ButtonSlots>;

// Now slotProps will have proper type inference:
// slotProps.startDecorator accepts { icon?: string; className?: string }
// slotProps.endDecorator accepts { label?: string }

SlotRenderFunction<T>

The type of the render function returned by useSlot.

import type { SlotRenderFunction } from "@zemd/react-slottable";

type MyRenderFn = SlotRenderFunction<typeof MyComponent>;

How It Works

When a slot is rendered:

  1. If the user provides a custom component via slots, it is used
  2. Otherwise, the default component from options.slot is used
  3. If neither is provided, null is returned (nothing is rendered)

Props are merged in the following order (later values override earlier ones):

  1. Props passed directly to the render function
  2. Props from slotProps
  3. Extra props from the options object

Usage with useSyncExternalStore

The library works seamlessly with external state management. Here's an example using useSyncExternalStore:

import { useSyncExternalStore } from "react";
import { useSlot, type PropsWithSlots } from "@zemd/react-slottable";

// Create an external store
function createCounterStore(initialValue = 0) {
  let state = { count: initialValue };
  const listeners = new Set<() => void>();

  return {
    getSnapshot: () => state,
    subscribe: (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    increment: () => {
      state = { count: state.count + 1 };
      listeners.forEach((l) => l());
    },
  };
}

const store = createCounterStore(0);

// Counter component with customizable slots
type CounterProps = PropsWithSlots<{ min?: number; max?: number }, ["display", "incrementButton"]>;

const Counter: React.FC<CounterProps> = (props) => {
  const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);

  const renderDisplay = useSlot("display", props, { slot: "span" });
  const renderButton = useSlot("incrementButton", props, { slot: "button" });

  return (
    <div>
      {renderDisplay({ children: count })}
      {renderButton({
        onClick: store.increment,
        disabled: count >= (props.max ?? Infinity),
        children: "+",
      })}
    </div>
  );
};

// Multiple counters can share the same store with different slot implementations
function App() {
  return (
    <>
      <Counter />
      <Counter
        slots={{
          display: ({ children }) => <strong>{children}</strong>,
        }}
      />
    </>
  );
}

Performance

The library is optimized for performance:

  • Memoized slot resolution - The resolved slot component is memoized with useMemo
  • Memoized render function - The render function is memoized with useCallback
  • Stable references - Extra props and slot props are memoized to prevent unnecessary re-renders

License

This project is licensed under the Apache License 2.0.

💙 💛 Donate

Support Ukraine