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

@zukhruf/shadcn

v0.7.0

Published

Every shadcn/ui component for Base UI with the nova preset, as one package: import the components from one entry and the styles from one stylesheet.

Readme

@zukhruf/shadcn

Each shadcn/ui component, as one package. The components use Base UI as their headless library and nova as their preset. An app imports the components from one entry and the styles from one stylesheet. The app sets its theme with CSS variables.

The words in these documents have one meaning each. See the glossary in CONTEXT.md.

The problem

shadcn/ui is not a package. Its CLI copies the source of each component into the repo of the app. Thus each app keeps its own copy, and the copies drift apart. A fix in one copy does not go into the other copies. For example, two apps each wrote the same fix for a tall dialog, and other apps still do not have it. Also, shadcn add --overwrite writes over a copy, and it removes the changes that an app made by hand.

This package keeps one copy. A fix goes into the package one time, and each app gets it with the next version.

Install

npm i @zukhruf/shadcn

The peer dependencies are react 19, react-dom 19 and tailwindcss 4. The other dependencies, for example @base-ui/react and lucide-react, install with the package.

Use it

Import the stylesheet in the Tailwind CSS file of the app, after Tailwind:

@import 'tailwindcss';
@import '@zukhruf/shadcn/styles.css';

Import the components from the entry:

import { Button, Dialog, DialogContent } from '@zukhruf/shadcn';

Obey two rules:

  • Use a plain @import. Do not write @import '@zukhruf/shadcn/styles.css' layer(components);. A layer() puts all of the stylesheet into a layer, also its @utility and @source rules. Tailwind CSS refuses these rules in a layer. The build stops with the error "@utility cannot be nested."
  • Do not add an @source for the package. The stylesheet registers its own dist folder. Tailwind CSS then finds the classes of the components. See ADR 0003.

Theme

The theme is the values of a set of CSS variables, for example --primary, --background and --radius. To change the theme, set the variables after the import:

@import 'tailwindcss';
@import '@zukhruf/shadcn/styles.css';

:root {
  --primary: oklch(0.55 0.2 260);
  --radius: 0.5rem;
}

.dark {
  --primary: oklch(0.7 0.15 260);
}
  • The package sets the default theme in @layer base. A rule outside of a layer wins over it. A rule in @layer base that comes after the import also wins over it. Thus a host theme in a layer, for example the ChatGPT tokens of OpenAI, can set the variables. A rule in @layer theme loses, because Tailwind CSS declares theme before base. See ADR 0005.
  • The .dark class switches to the dark theme. Put it on an element, usually <html>. The element and all elements in it then use the .dark variables.

The preset is fixed. The class strings of each component contain the preset, for example the rounded corners and the spacing of the dialog. Thus an app cannot change the preset. An app changes only the theme. See ADR 0004.

The open state of the sidebar

SidebarProvider keeps its open state in a cookie. SIDEBAR_COOKIE_NAME is the name of that cookie. Use it to read the open state of the sidebar on the server, and give the value to defaultOpen:

import { SIDEBAR_COOKIE_NAME, SidebarProvider } from '@zukhruf/shadcn';

const defaultOpen = cookies.get(SIDEBAR_COOKIE_NAME)?.value !== 'false';

<SidebarProvider defaultOpen={defaultOpen}>{children}</SidebarProvider>;

Composition

Base UI uses the render prop where Radix UI uses asChild. The render prop sets the element that the component renders.

Radix UI:

<DialogTrigger asChild>
  <Button variant="outline">Open</Button>
</DialogTrigger>

Base UI:

<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>

What the stylesheet adds to stock shadcn/ui

The stylesheet has three patches:

  • A pointer cursor on enabled buttons. In Tailwind CSS 4, a button has the default cursor of the browser, not the pointer. The upgrade guide of Tailwind CSS gives a rule that sets the pointer again. The patch is that rule, in @layer base.
  • A cap on the height of a dialog. A dialog is never taller than the window minus 2rem, and it scrolls when its content is taller. Thus the buttons at the bottom of a tall dialog stay on the screen. The rule is max-height: calc(100dvh - 2rem); overflow-y: auto on [data-slot="dialog-content"]. The patch is in @layer components.
  • The width of the mobile sidebar. On a small screen, the sidebar is in a Sheet. The class data-[side=left]:w-3/4 of the Sheet wins over the class w-(--sidebar-width) of the sidebar. Thus the sidebar was 3/4 of the window: 292.5px, not 288px, in a window of 390px. The patch sets width: var(--sidebar-width) on [data-slot="sidebar"][data-mobile="true"][data-side].

The cursor and the cap are in layers before utilities. Thus a utility class of the app, for example cursor-wait or max-h-none, still changes them.

The width patch is in @layer utilities, because only a rule in that layer wins over a utility class. Its selector has three attributes, so it wins over the two parts of data-[side=left]:w-3/4. The sidebar does not give its className to the mobile Sheet, so a utility class of the app cannot get to that element. To change the mobile width, write a rule outside of a layer.

Links that look like buttons

A control that goes to a different page must stay a link. Do not write <Button render={<Link />} nativeButton={false}>. This makes an <a> with role="button". A screen reader then calls the link a button, and the Space key activates it.

Put the classes of the button on the link:

import { cn } from 'cn';

import { buttonVariants } from '@zukhruf/shadcn';

<Link
  href="/settings"
  className={cn(buttonVariants({ variant: 'outline', size: 'sm' }))}
>
  Settings
</Link>;

Use cn around buttonVariants. Button merges the classes that conflict through cn, which uses tailwind-merge. Without cn, the classes that conflict stay on the link, and the link looks different from a button. In one app, the link had a radius of 10px, not 8px, and it was 2px wider than the button.

Use the components well

  • Use a component of the package first. Before you write a new component, look for one in this package. Use it when it does the job.

  • Keep your own components in the app. A component that you make from the components of this package belongs to the app. Do not add it to this package. The sync writes over the generated source, and the package holds only the components of shadcn/ui.

  • Remove the outline of a Card with ring-0. Card draws its outline with ring-1, not with a border. Thus border-0 does not remove the outline.

  • Use the standard names of the theme variables. Set --primary, --background and the other variables of the theme. Do not add a brand prefix, for example --acme-primary. The components read only the standard names.

  • Put a DropdownMenuLabel in a DropdownMenuGroup. DropdownMenuLabel uses Menu.GroupLabel of Base UI, and that component reads the context of a group. A label outside a group throws Base UI error #31, and the full menu stops. Radix UI accepted a label outside a group, thus an app that moves from Radix UI must find these labels. text2sql found one when it moved.

    <DropdownMenuContent>
      <DropdownMenuGroup>
        <DropdownMenuLabel>Account</DropdownMenuLabel>
        <DropdownMenuItem>Profile</DropdownMenuItem>
      </DropdownMenuGroup>
    </DropdownMenuContent>
  • Show an error at the place where it occurs. Show a form error or a field error next to the field or the action that failed, for example with FieldError. Show the error of a background task or of a mutation as a toast. Use toast and Toaster of this package, which use the toast of Base UI.

Your own components

An app that writes its own components often needs cn, the function that joins class names. Import it from the cn package:

import { cn } from 'cn';

This package does not export cn. Each symbol has one home (zukhruf ADR 0001, rule 6).

One entry

The package has one entry, @zukhruf/shadcn. It exports all components, all hooks and the hand-written modules, for example SIDEBAR_COOKIE_NAME. There are no subpaths such as @zukhruf/shadcn/button.

  • A development server and plain Node.js load all components and their dependencies.
  • A production bundle has only the components that the app uses. The sideEffects field of package.json lets the bundler remove the others.
  • The bundler resolves each import of the entry before it removes code. Thus the app needs the peers of all dependencies, also when it does not use the component. The package lists react-is, a peer of recharts, as its own dependency. Then an app builds also when npm does not install peers, for example with legacy-peer-deps=true.
  • The stylesheet gives the classes of all components, also of the components that the app does not use. An app that uses only Button and Dialog got 212 kB of CSS (31 kB with gzip) on 2026-10-15.

See ADR 0002.

Documentation

Decisions: the architecture decision records tell why the design is as it is.

  1. The shadcn CLI writes the source, and the commit pins it
  2. One entry, and private imports between the components
  3. The stylesheet registers its own source and keeps its patches in layers
  4. The components use Base UI and the nova preset
  5. The default theme is in @layer base

Development

This package is part of the zukhruf workspace. Run the commands from the workspace root.

npm install
npx nx run shadcn:sync        # writes the generated source again with the shadcn CLI
npx nx run shadcn:test        # builds, then runs the tests
npx nx run shadcn:typecheck   # formats, lints, then type checks
npx nx run shadcn:build       # compiles src/ to dist/
src/
  components/   generated: one file for each component
  hooks/        generated: the hooks that the components use
  lib/          generated: the helpers that the components use
  index.ts      generated: the entry
  hand-written/ written by hand: the exports that the generated source does not give
theme.css       generated: the CSS variables and the dark variant; imports shadcn/tailwind.css
styles.css      written by hand: imports theme.css, registers dist, holds the patches
components.json the configuration of the shadcn CLI

The generated source

The shadcn CLI writes src/components, src/hooks, src/lib, src/index.ts and theme.css. Do not edit these files. The next sync writes over them, and the edit is lost. See ADR 0001.

  • The lint rules and the TypeScript flags that the stock code fails are off. These exceptions apply only to the generated source.
  • To change what a component does, add a patch to styles.css, in a layer.

Sync

nx run shadcn:sync writes the generated source again. It uses [email protected] and the live registry of shadcn/ui. The registry has no versions, so a sync on a different day can write different source. Read the diff before a commit.

The sync does these steps, in this order:

  1. It resets theme.css to one line: @import "tw-animate-css";.
  2. It runs shadcn add index, then shadcn add --all.
  3. It puts the :root rule and the .dark rule of theme.css into @layer base. The CLI writes them outside of a layer.
  4. It writes src/index.ts: one export * line for each component, each hook and each hand-written module.
  5. It formats the generated source with Prettier.

Known facts about the sync:

  • theme.css keeps the line @import "shadcn/tailwind.css";. That file holds the variants and the animations of the components, for example data-open. The sync does not copy it. The shadcn package is a dependency of this package, so npm installs the file with it, and a new version of shadcn gets to each app through npm.
  • src does not import date-fns, but date-fns stays a dependency. The calendar item of the registry declares it.
  • One sync in four failed one time, and the cause is not known. A second run passed.