Skip to main content
ASoc
Tutorial

Next.js UI Library Roundups List Five. This Site Uses Zero.

Zero of shadcn, Radix, MUI, Chakra or daisyUI in this package.json. 92 components run on Tailwind v4 tokens and one enforced import rule instead.

The ASoc Team8 min read

This storefront's package.json lists 8 production dependencies. One of them touches the UI at all — lucide-react, for icons. No shadcn/ui, no Radix, no MUI, no Chakra, no daisyUI, no NextUI: the roundups that dominate this search all recommend one of those five, and this codebase's 92 components use none of them.

The short answer

"Best Next.js UI library" roundups compare shadcn/ui, Radix, MUI, Chakra and daisyUI. This site ships zero of them — 92 components (7 atoms, 41 molecules, 33 organisms, 11 templates) built directly on Tailwind v4's @theme tokens, with a single import rule enforcing the hierarchy a library would otherwise impose for you.

What the roundups recommend

Search "next js ui library" and the top results — a DaisyUI landing page, a ThemeSelection roundup, a WrapPixel "25+ Top UI frameworks" list — converge on the same five names: shadcn/ui (Radix primitives, code you copy rather than a package you install), Chakra UI, daisyUI (Tailwind class names, no client runtime), NextUI, and Radix UI unstyled underneath most of the above. Every one solves the same problem: a library maintains the components, ships accessible defaults, and hands you a theming API to override.

What this codebase ships instead

Nothing from that list. A grep of package.json for any of the five names, or their usual peers (@radix-ui/*, @chakra-ui/*, class-variance-authority, clsx), returns zero matches. The only UI-adjacent dependency is lucide-react for icons. Everything else — buttons, cards, accordions, modals, carousels — is a hand-written component against Tailwind utility classes, organized into an explicit four-layer hierarchy:

LayerFolderCountRole
Atomssrc/components/atoms7Smallest primitives — Button, Container, SectionHeading
Moleculessrc/components/molecules41One reusable unit — FaqItem, TemplateCard, BuyButton
Organismssrc/components/organisms33A full page section — Header, Hero, Footer
Templatessrc/components/templates11Page-level composition — HomeTemplate, PricingTemplate

92 files, zero of them importing a component-library package.

The rule that replaces the library's guardrails

A UI library's real value isn't just the components — it's that you can't accidentally import a Dialog into a Card in a way that breaks the design system, because the package boundary prevents it. This codebase gets the same guarantee from one enforced rule instead of a package boundary:

A level may only import from the levels below it. Never import sideways within a level, and never import an organism into a molecule.

Button.tsx (an atom) can only import from React/Next.js itself:

// src/components/atoms/Button.tsx (trimmed)
const variants: Record<ButtonVariant, string> = {
  primary: "bg-primary text-white hover:bg-primary-600",
  outline:
    "border border-stroke-tertiary bg-white text-text-color hover:bg-gray-50",
  dark: "bg-gray-900 text-white hover:bg-gray-800",
};

export default function Button({ variant = "primary", href = "#", className = "", children, ...rest }) {
  const classes = `${base} ${variants[variant]} ${className}`.trim();
  return isRoutable(href, external)
    ? <Link href={href} className={classes} {...rest}>{children}</Link>
    : <a href={href} className={classes} {...rest}>{children}</a>;
}

Three variants, one className escape hatch for call-site overrides — no cva, no cn() helper, no theming provider. A molecule can import this atom; an atom can never import a molecule back. Nothing enforces that at the TypeScript level — it's a convention, checked by review, not a compiler — which is the actual cost side of skipping a library: you get the freedom of hand-written components without a package's compile-time guardrail, and you keep the rule alive by discipline instead of a linter.

The same variant pattern, repeated by hand across every atom

A library like shadcn/ui gives you cva() (class-variance-authority) to declare variants once, generically, for any component. This codebase has no such helper — every atom that needs variants declares its own plain Record, and every atom that doesn't still follows the same "sensible default, override via className" shape:

// src/components/atoms/SectionHeading.tsx
export default function SectionHeading({
  as: Tag = "h2",
  className = "text-3xl font-bold !leading-[1.2] text-title-color md:text-[40px]",
  children,
}: { as?: "h1" | "h2"; className?: string; children: ReactNode }) {
  return <Tag className={className}>{children}</Tag>;
}

No variants object here at all — SectionHeading only ever has one look, so its "API" is a full-string default plus a className prop that replaces it outright. Button needed an actual variants record because it has three distinct looks; SectionHeading and SectionLabel don't, so they skip the abstraction rather than building a generic variant system nothing in the codebase would use. A library tends to ship one mechanism (cva, or a variant prop convention) for every component regardless of how many looks it actually has; this codebase's atoms each carry only as much variant machinery as their own call sites require.

Where zero-library has a real, documented cost

The Button atom above has no dark-mode variants baked in — every call site that needs a dark background does its own override:

<Button variant="primary" className="dark:bg-gray-800 dark:text-white/80">

A library with a theming system (Chakra's color mode, shadcn's CSS variables) solves this once, centrally. This codebase's documented convention is the opposite: "the shared Button atom has no dark variants — use the established call-site className workarounds." That's a real, acknowledged tradeoff, not a hidden one — 92 hand-written components mean 92 places a future dark-mode change might need a matching edit, instead of one theming object a library would let you update in place.

The comparison

shadcn/ui, Radix, MUI, Chakra, daisyUIThis codebase
InstallA package (or copied generator output)Nothing — plain .tsx files against Tailwind utilities
ThemingA provider or CSS-variable system, updated onceTailwind v4 @theme tokens in globals.css, plus per-call-site className overrides
Accessibility defaultsBuilt into the library's primitivesHand-implemented per component (e.g. Button's aria-disabled + tabIndex={0} span for a disabled CTA)
Layer boundariesEnforced by the package's exportsEnforced by a documented convention, not the compiler
Bundle costA runtime dependency, sometimes a JS component even for static markupZero extra dependency; a Server Component by default unless it needs "use client"
Dark modeCentralized in the library's theme systemPer-component dark: classes, no central override for shared atoms

When a library is the right call instead

This isn't an argument that libraries are wrong — it's what one storefront's actual tradeoff looks like. A UI library earns its cost fast on anything with genuinely hard interaction contracts: a combobox with keyboard nav and screen-reader announcements, a date-range picker, a data table with sortable/resizable columns. This catalog has none of those — its most complex client interaction is an accordion (FaqItem) and a carousel (TemplateGallery) — so the accessibility work a library would otherwise buy has a small, one-time surface here instead of an ongoing maintenance one.

Troubleshooting

SymptomCauseFix
A new component looks inconsistent with the rest of the siteNo library enforcing shared tokensReuse existing atoms (Button, SectionHeading) instead of writing new markup from scratch
Dark mode looks wrong on a new componentThe shared atom has no dark variant by designAdd the dark: override at the call site, matching the pattern in nearby components
An organism got imported into a moleculeNo compiler boundary enforcing the layer ruleReview-time discipline is the only guard here — flag it in review, there's no lint rule that catches it
A "simple" custom modal has accessibility bugs a library would have preventedHand-rolled focus trapping/ARIA without a library's tested defaultsFor genuinely complex interaction patterns, that's the signal a library (even just Radix's primitives) is worth the dependency
Bundle size grew after adding a new interactive componentA new client component pulled in a heavy dependencyCheck whether it needs "use client" at all — most sections here render as Server Components

Frequently asked questions

Is it bad practice to build a Next.js app without a UI library? No — it's a tradeoff. You give up centralized theming and pre-built accessibility in exchange for zero dependency weight and full control. This site's 92 components make that tradeoff explicitly and document where it costs the most (dark-mode overrides).

What replaces shadcn/ui's theming here? Tailwind v4's @theme tokens in globals.css — a --color-primary scale and semantic tokens (text-color, title-color) that every atom and molecule reads from, the same role a library's theme object plays.

Doesn't skipping a library mean rebuilding accessibility from scratch? For each component, yes — see Button's hand-built aria-disabled + tabIndex={0} pattern for a disabled CTA. That cost is proportional to how many genuinely complex interactive patterns you have; this catalog's are few.

How does this codebase stop components from being imported out of order? A documented rule ("a level may only import from levels below it"), enforced by code review — not the compiler. A library's package boundary would enforce the equivalent automatically.

Templates in this post

ASoc Ignite, ASoc Iris and ASoc Keystone are Next.js + Tailwind landing page templates, built on the same library-free component system described above.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial8 min read

The Latest Next.js Version Is 16.3.4. This Repo Runs 16.2.9.

npm carries sixteen dist-tags for `next` and `latest` is only one of them. Which version to be on, measured from a repo that pins the framework and ships it to other people.

Read more
Tutorial12 min read

Automated Product Screenshots with Playwright, 111 Templates Deep

The capture is four lines; deciding what is in frame is the work. Nav-driven page discovery, the is-this-the-product check, and the proxy bug that blanks every Chromium request.

Read more
Tutorial10 min read

A Podcast Landing Page Is a Registry, a Feed, and One CSP Line

Episodes belong in a typed registry, the feed is the actual product, and the player embed renders blank unless your Content-Security-Policy names its host.

Read more