@classytic/fluid
v1.5.1
Published
Fluid UI - Custom components built on shadcn/ui and base ui by Classytic
Readme
@classytic/fluid
React 19 component library on top of shadcn/ui + Base UI. Built for Next.js 16 App Router; 17 of 22 entry points are framework-agnostic. ESM-only, server/client split, RSC-friendly, no design-system lock-in (you keep your own shadcn primitives at @/components/ui/*).
Install
npm install @classytic/fluid1. Install core peer deps
npm install react react-dom lucide-react class-variance-authority clsx tailwind-merge2. Install shadcn primitives fluid wraps (40 components, one command)
npx shadcn@latest add accordion alert alert-dialog avatar badge breadcrumb button calendar card checkbox collapsible combobox command context-menu dialog drawer dropdown-menu empty field hover-card input input-group input-otp item kbd navigation-menu pagination popover progress radio-group resizable scroll-area select separator sheet sidebar skeleton switch table tabs textarea tooltipIf you only need a subset, see the peer-dep table and add only the shadcn components your imports need.
3. Import styles
// app/layout.tsx
import "@classytic/fluid/styles.css"
import "@classytic/fluid/document/print.css" // if you use the document subsystem4. Transpile (Next.js only)
// next.config.ts
export default { transpilePackages: ["@classytic/fluid"] }5. (Optional) Wrap with FluidProvider for i18n label overrides:
import { FluidProvider } from "@classytic/fluid/client/core"
export default function RootLayout({ children }) {
return <FluidProvider>{children}</FluidProvider>
}Tailwind v4 required.
styles.cssuses@sourceand@theme inline— Tailwind v3 throws.Next.js is optional. Only
dashboard/client,search, andlayoutsimportnext/navigationornext/link.
Quick example
import { useForm } from "react-hook-form"
import { FormInput, FormTextarea, FormSection } from "@classytic/fluid/forms"
import { ClientSubmitButton } from "@classytic/fluid/client/core"
function ContactForm() {
const { control, handleSubmit } = useForm()
return (
<form onSubmit={handleSubmit(console.log)} className="space-y-4">
<FormSection title="Contact">
<FormInput control={control} name="email" label="Email" required />
<FormTextarea control={control} name="message" label="Message" rows={4} />
</FormSection>
<ClientSubmitButton>Send</ClientSubmitButton>
</form>
)
}Entry points
| Entry | Server-safe | Purpose |
|---|:-:|---|
| @classytic/fluid | ✅ | Layout, display, states, skeletons, utils |
| @classytic/fluid/client/hooks | — | Hooks + storage (no next/navigation) |
| @classytic/fluid/client/core | — | Dialogs, cards, pills, tabs, animations, providers |
| @classytic/fluid/client/table | — | DataTable + toolbar |
| @classytic/fluid/client/theme | — | ModeToggle |
| @classytic/fluid/client/error | — | ErrorBoundary, AsyncBoundary |
| @classytic/fluid/client/calendar | — | EventCalendar |
| @classytic/fluid/client/color-picker | — | Composable HSL color picker |
| @classytic/fluid/client/gallery | — | Image gallery + lightbox |
| @classytic/fluid/client/spreadsheet | — | Editable spreadsheet table |
| @classytic/fluid/forms | — | Form components (react-hook-form integration) |
| @classytic/fluid/floating-label | — | Material-style floating-label inputs |
| @classytic/fluid/dashboard | ✅ | Nav utils, breadcrumbs, types, DashboardContent, PageShell, AppsGrid |
| @classytic/fluid/dashboard/client | — | Sidebar presets, useDashboardBreadcrumbs, HeaderActions |
| @classytic/fluid/dashboard/resource-dashboard | — | ResourceDashboard |
| @classytic/fluid/document | ✅ | Document/print primitives, pagination utils |
| @classytic/fluid/document/client | — | DocumentMultipage, DocumentPaginator |
| @classytic/fluid/numpad | — | <Numpad> + headless useNumpad |
| @classytic/fluid/search | — | Composable search system, URL-synced |
| @classytic/fluid/command | — | Command palette + keyboard shortcuts |
| @classytic/fluid/layouts | — | NavigationBar, ResponsiveSplitLayout |
| @classytic/fluid/seo | ✅ | JSON-LD wrappers + Next 16 Metadata helpers |
All entries resolve
@/components/ui/*from your project's shadcn setup at build time.
Server / Client split — donut pattern
// app/dashboard/layout.tsx — Server Component (no "use client")
import { DashboardContent } from "@classytic/fluid/dashboard"
import { InsetSidebar } from "@classytic/fluid/dashboard/client"
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"
export default function Layout({ children }) {
return (
<SidebarProvider>
<InsetSidebar brand={brand} navigation={nav} />
<SidebarInset className="min-w-0">
<DashboardContent>{children}</DashboardContent>
</SidebarInset>
</SidebarProvider>
)
}{children} passes through the client boundary as a server-rendered hole — page content is prerendered. Same pattern applies to @classytic/fluid/document and @classytic/fluid/document/client.
Peer dependencies by entry point
Core peer deps (react, react-dom, lucide-react, class-variance-authority, clsx, tailwind-merge) are always required. Add the rest only when you import the corresponding entry:
| Entry | Additional peer deps |
|---|---|
| dashboard/client, search, gallery | next |
| client/table | @tanstack/react-table, @tanstack/react-virtual |
| client/theme | next-themes |
| client/error | react-error-boundary |
| client/calendar, forms | date-fns |
| forms | react-hook-form |
| layouts | vaul |
Required Button size extensions
Fluid uses three icon-button sizes that aren't in stock shadcn: icon-xs, icon-sm, icon-lg. The latest shadcn registry button (Base UI variant) ships them, but if you scaffolded from older shadcn or hand-rolled @/components/ui/button.tsx, add them to the size cva variant:
size: {
// ...existing default | sm | lg | icon
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)]",
"icon-lg": "size-9",
}Used by: DrawerWrapper, FormFieldArray, FormPasswordInput, FormNumberInput, FloatingNumberInput, Search.Input, FileUploadInput.
Framework support
| Framework | Works | Doesn't |
|---|---|---|
| Next.js 15/16 | Everything | — |
| Vite / Remix / plain React | All entries except those that import next/link / next/navigation | dashboard/client, search, layouts, gallery |
| React Server Components | @classytic/fluid, dashboard, document, seo | Everything "use client" |
Docs
- Dashboard — sidebar presets, donut pattern, PageShell, RSC HeaderSection recipe, dynamic badges,
define*builders - DataTable / ResourceDashboard — pagination shapes,
resultoverload, styling escape hatches, Suspense recipe - Document subsystem — single-page, multi-page, paginator, POS receipt recipe
- Register-style primitives —
useBarcodeScan,useWakeLock,<Numpad>,<HotkeyHelpDialog>,<AppsGrid> - Components & hooks index — full cheat-sheet of every export
Customization
Three escape hatches — fluid wraps Base UI but never locks you out:
- Pass your own labeled trigger. Trigger slots accept any
ReactNode:<ActionDropdown trigger={<Button aria-label="Row actions"><MoreVertical /></Button>} ... /> - Spread arbitrary props. Wrappers forward unknown props to the underlying primitive.
- Drop to the Base UI primitive when you need full control — every wrapper documents its underlying primitive in JSDoc.
i18n labels (Save / Cancel / Loading... / kit-internal aria) flow from <FluidProvider labels={...}> so non-English a11y trees come out correctly without per-component overrides. Consumer-controlled content (titles, descriptions, empty messages) flows through component props with English defaults — FluidProvider is fully optional.
Dev
npm run build # tsdown — 22 entries
npm run typecheck # tsc --noEmit
npm test # vitestRequirements
React 19+, Next.js 15+, Tailwind v4, shadcn/ui at @/components/ui/*.
License
MIT
