Glrk UI

npm package

Install @glraj/ui as a dependency, set up styles, and theme the components app-wide with configureUI.

Prefer a dependency over copied source? Every wrapper is published as @glraj/ui. You get fixes by bumping the version, and you can still restyle the base components app-wide with configureUI.

Installation

npm i @glraj/ui @base-ui/react lucide-react

Requires React 19 and Tailwind CSS v4. Some components need extra peer dependencies — install only the ones you use (each component page lists them in its npm tab):

ComponentPeer dependencies
data-table@tanstack/react-table (v9), @tanstack/react-virtual
autocomplete-virtualised, combobox-virtualised@tanstack/react-virtual
date-picker, field-wrapper*@daypicker/react, date-fns
carouselembla-carousel-react
field-wrapper-rhfreact-hook-form
field-wrapper-tf@tanstack/react-form

Styles

Add to your global CSS:

globals.css
@import "tailwindcss";
@import "tw-animate-css";

/* shadcn theme tokens — skip if your app already defines them */
@import "@glraj/ui/theme.css";

/* lets Tailwind see the package's class names */
@import "@glraj/ui/styles.css";

Without @glraj/ui/styles.css, Tailwind never scans the package and the components render unstyled.

styles.css makes Tailwind generate the classes of every component in the package. Classes shared with your app (flex, px-2, …) are generated once, so the extra CSS is only what's unique to components you don't use — typically a few kB gzipped.

Usage

Each component has its own entry point, so you only bundle what you import:

import { Button } from "@glraj/ui/button"
import { SelectWrapper } from "@glraj/ui/select"
import { DataTable, useDataTable } from "@glraj/ui/data-table"
import { cn, parseAllowedPrimitive } from "@glraj/ui/utils"

In the examples across these docs, replace @/components/ui/ with @glraj/ui/ and @/lib/utils with @glraj/ui/utils.

Option types are exported — the package adds nothing to your global scope:

import type { itemsT, menuItemsT, allowedPrimitiveT } from "@glraj/ui/types"

Use cn from @glraj/ui/utils in your app too (e.g. export { cn } from "@glraj/ui/utils" in your own lib/utils.ts). Your app and the package then merge classes the same way, and you ship one class-merging library instead of two.


App-wide styles (configureUI)

configureUI changes how the base components look everywhere — including inside composed components (dialog footers, pagination, data-table menus, date picker, …) — without forking the package.

KeyAcceptsAffects
buttona cva() function, or (props, base) => stringButton and everything that renders or styles like it
badgesame as buttonBadge
togglesame as buttonToggle, ToggleGroupItem
inputa class string, or (base) => stringInput (also inside InputGroup)
textareaa class string, or (base) => stringTextarea

Every key is optional — without configureUI the components use their default look. Calling it again merges keys (a later value for the same key replaces the earlier one).

Merge order: library classes → your configureUI classes → per-instance className. Conflicting classes are resolved by tailwind-merge, so your config replaces library defaults and a className on one element still wins.

Where to call it

Create one file and import it before any component renders.

src/lib/ui-config.ts
import { configureUI } from "@glraj/ui/config"
import { cn } from "@glraj/ui/utils"

configureUI({
  // …see the use cases below
})

It is module-level state (not React context), so import it once in every JavaScript runtime that renders components:

Server and client code run as separate module graphs — import it in both:

app/layout.tsx
import "./globals.css"
import "@/lib/ui-config" // server components (Badge, Input, Textarea, buttonVariants())
app/providers.tsx
"use client"

import "@/lib/ui-config" // client components (SSR + browser)

export function Providers({ children }: { children: React.ReactNode }) {
  return children
}

If a runtime misses the import, nothing breaks — components rendered there just use the default look. Missing the server import in Next.js shows up as default-styled Badge / Input in server components.

Use case: tweak one variant

Extend the library's classes — the original function arrives as base:

configureUI({
  button: (props, base) =>
    cn(base(props), props?.variant === "outline" && "shadow-xs"),
})

Use case: change sizes and spacing

const buttonSizes: Record<string, string> = {
  default: "h-10 px-4",
  sm: "h-8 px-3 text-sm",
  lg: "h-12 px-6 text-base",
}

configureUI({
  button: (props, base) => cn(base(props), buttonSizes[props?.size ?? "default"]),
})

Use case: replace a variant completely

Extending can't remove a class that doesn't conflict with yours — e.g. the default variant's [a]:hover:bg-primary/80 keeps winning over a custom hover:bg-* on link buttons. Ask base for no variant classes and supply your own:

const ownVariants: Record<string, string> = {
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
}

configureUI({
  button: (props, base) => {
    const variant = props?.variant ?? "default"
    const own = ownVariants[variant]
    return cn(base(own ? { ...props, variant: null } : props), own)
  },
})

Use case: add a new variant

configureUI({
  button: (props, base) =>
    props?.variant === "brand"
      ? cn(base({ ...props, variant: null }), "bg-pink-600 text-white hover:bg-pink-700")
      : base(props),
})

variant="brand" works at runtime. To type it, re-export a narrowed alias once and import Button from there:

src/components/ui/button.tsx
import { Button as UIButton } from "@glraj/ui/button"

type Props = Omit<React.ComponentProps<typeof UIButton>, "variant"> & {
  variant?: React.ComponentProps<typeof UIButton>["variant"] | "brand"
}

export const Button = UIButton as (props: Props) => React.JSX.Element

Use case: your own design system

Pass a full cva() to replace the library's classes entirely:

import { cva } from "class-variance-authority"

configureUI({
  button: cva(
    "inline-flex items-center justify-center gap-2 rounded-full font-semibold transition-colors disabled:opacity-50",
    {
      variants: {
        variant: {
          default: "bg-primary text-primary-foreground hover:bg-primary/90",
          outline: "border border-input bg-background hover:bg-muted",
          secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
          ghost: "hover:bg-muted",
          destructive: "bg-destructive text-white hover:bg-destructive/90",
          link: "text-primary underline-offset-4 hover:underline",
        },
        size: {
          default: "h-10 px-5",
          xs: "h-7 px-2 text-xs",
          sm: "h-8 px-3 text-sm",
          lg: "h-12 px-7",
          icon: "size-10",
          "icon-xs": "size-7",
          "icon-sm": "size-8",
          "icon-lg": "size-12",
        },
      },
      defaultVariants: { variant: "default", size: "default" },
    },
  ),
})

Keep every variant and size name the library uses — composed components pass them (e.g. dialog footers use outline, the calendar uses ghost).

Use case: form fields

input and textarea receive the library's class string as base:

const fieldCls = "h-10 px-3 text-base md:text-sm rounded-lg"

configureUI({
  input: base => cn(base, fieldCls),     // extend
  textarea: base => cn(base, fieldCls),
  // input: "h-10 w-full rounded-lg border …", // or replace with a full string
})

Use case: badges and toggles

configureUI({
  badge: (props, base) => cn(base(props), "rounded-md px-2.5"),
  toggle: (props, base) => cn(base(props), "data-pressed:bg-primary data-pressed:text-primary-foreground"),
})

Use case: one theme across several apps

Put the config in a shared package (or a workspace package in a monorepo) and import it from each app's entry points:

packages/theme/index.ts
import { configureUI } from "@glraj/ui/config"

configureUI({ /* … */ })
apps/web/app/layout.tsx
import "@acme/theme"

Rules of thumb

  • Don't call the exported buttonVariants / badgeVariants inside an override — they already route through configureUI. Use the base argument.
  • Per-instance className is always merged last, so one-off tweaks keep working without !.
  • Colours belong in the theme CSS variables (--primary, --radius, …); use configureUI for shape, spacing, sizes and states.

Text and i18n

UIProvider from @glraj/ui/ui-provider overrides every built-in string — see Text and i18n.

NavigationMenuWrapper, NavList and NavLinkItem render a plain <a> by default. Pass your router's link for client-side navigation:

import Link from "next/link" // or react-router, TanStack Router, …

<NavigationMenuWrapper linkAs={Link} items={items} />