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

slot-jsx

v1.0.2

Published

Custom JSX pragma for slottable components with asChild pattern.

Readme

🎰 slot-jsx

A custom JSX pragma that enables declarative slottable components for powering asChild or render function prop patterns.

Features

  • 🪆 Nested Slottables: Supports deeply nested slottable components
  • 🔥 No React.cloneElement: Transforms tree at creation time, outside render phase
  • React Server Components: Fully compatible with RSC and SSR
  • Async Components: Can slot onto async server components
  • 🧹 Streamlined React tree: No more SlotClone components in devtools
  • 🧩 Composable: Can be composed with other JSX pragmas
  • 🛡️ Type-Safe: Full TypeScript support

Installation

pnpm add slot-jsx

Update your tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "slot-jsx/react"
  }
}

Important: Always import from slot-jsx/react, not from the package root.

Quick Start

1. Create a Slottable Component

import { Slot } from 'slot-jsx/react';

interface ButtonProps extends React.ComponentProps<'button'> {
  asChild?: boolean;
}

export function Button({ asChild, children, ...props }: ButtonProps) {
  const Comp = asChild ? Slot : 'button';
  return <Comp {...props}>{children}</Comp>;
}

Note: The prop name asChild is a convention from Radix UI, but you can name it whatever you want.

2. Use It

<Button asChild>
  <a href="/home">Go Home</a>
</Button>

// Result: <a href="/home">Go Home</a>

How It Works

When Slot is rendered, the component's root element is replaced by its child element, while preserving the component's internal structure.

Simple Case (No Slottable needed)

<Button asChild onClick={handleClick}>
  <a href="/foo">Click me</a>
</Button>

Button internally renders:

<Slot {...props}>{children}</Slot>

The pragma transforms this to:

<a href="/foo" onClick={handleClick}>
  Click me
</a>

Complex Case (With Slottable)

When you have siblings to the children (like icons or wrappers), use Slottable to specify where the child's content goes:

<IconButton asChild onClick={handleClick}>
  <a href="/foo">Click me</a>
</IconButton>

IconButton internally renders:

<Slot {...props}>
  <Icon />
  <Slottable>{props.children}</Slottable>
</Slot>

The pragma transforms this to:

<a href="/foo" onClick={handleClick}>
  <Icon />
  Click me
</a>

Nested slottables?:

<Slot {...props}>
  <Icon />
  {/* nested in a span */}
  <span>
    <Slottable>{children}</Slottable>
  </span>
</Slot>

The pragma transforms this to:

<a href="/foo" onClick={handleClick}>
  <Icon />
  <span>Click me</span>
</a>

With Render Prop

If you want to define a render prop API like Ariakit or Base UI, use the as prop on Slottable.

Example:

export function Button({ render, ...props }) {
  const Comp = render ? Slot : 'button';
  return (
    <Comp {...props}>
      <Slottable as={render}>{props.children}</Slottable>
    </Comp>
  );
}

Usage:

<Button render={<a href="/foo" />}>Click me</Button>

or:

<Button render={(props) => <a {...props} href="/foo" />}>Click me</Button>

This pattern gives consumers full control over the rendered element while still preserving the slot mechanics.

When using the function pattern, Slot will not merge props for you, to give you control over prop forwarding and composition.

Note: render functions cannot be passed to a client comp from an RSC, so bear that in mind if you decide to use this API.

Ejecting JSX Pragma

You can eject the pragma to configure it, or compose it with other custom pragmas for styling or other transformations.

To eject, create your own custom JSX runtime files in your project:

app/jsx-runtime/jsx-runtime.ts:

import { jsx as baseJsx, jsxs as baseJsxs, Fragment } from 'react/jsx-runtime';
import { withSlot, withSlotJsxs, Options } from 'slot-jsx/react';
import { withCss, withCssJsxs } from '@some-lib/css-pragma';

// optionally define your own custom merge props behaviour
export const mergeProps: Options['mergeProps'] = (outerProps, hostProps) => {
  return { ...outerProps, ...hostProps };
};

export const jsx = withCss(withSlot(baseJsx, { mergeProps }));
export const jsxs = withCssJsxs(withSlotJsxs(baseJsxs, { mergeProps }));
export { Fragment };

app/jsx-runtime/jsx-dev-runtime.ts:

import { jsxDEV as baseJsxDEV, Fragment } from 'react/jsx-dev-runtime';
import { withSlotDev } from 'slot-jsx/react';
import { withCssDev } from '@some-lib/css-pragma';
import { mergeProps } from './jsx-runtime';

export const jsxDEV = withCssDev(withSlotDev(baseJsxDEV, { mergeProps }));
export { Fragment };

Update your tsconfig.json:

{
  "compilerOptions": {
    "jsxImportSource": "./src/jsx-runtime"
  }
}

Important: Use "jsxImportSource": "@/jsx-runtime" in NextJS projects.

Prop Merging

When slotting occurs, props are merged intelligently:

  • className: Concatenates both classes
  • style: Merges style objects (host wins on conflicts)
  • ref: Safely composes both refs (React 17+)
  • Event handlers: Calls both handlers in sequence
  • Other props: Host props take precedence

Rules & Edge Cases

  1. Slottable is optional: If you don't have wrappers or siblings to the children, you don't need Slottable
  2. Single Slottable: Only one Slottable per Slot (if you use it)
  3. Direct child: Slottable must be a direct child of Slot; use its render function to build nested markup
  4. Single Host Element: The child you're slotting onto must be exactly one React element
  5. Prop Merging: Host props override component props (except for className, style, ref, and event handlers)

Examples

Check out the demo app for working examples.

Inspiration

This implementation is inspired by Radix UI's Slot utility.