@underverse-ui/underverse
v1.0.113
Published
Underverse UI – reusable React/Next.js UI components
Maintainers
Readme
Underverse UI
Docs: https://underverse.infiniq.com.vn/vi/docs/underverse
Author: Tran Van Bach
A comprehensive UI component library for React/Next.js applications, extracted from the main project. Built with Tailwind CSS, clsx, and tailwind-merge.
✨ Features
- 🎨 60+ UI Components - Buttons, Modals, DatePicker, DataTable, and more
- 🌐 Multi-language Support - Built-in translations for English, Vietnamese, Korean, Japanese
- ⚡ Tree-shakeable - Import only what you need
- 🔌 Flexible i18n - Works with
next-intlor standalone React - 🎯 TypeScript First - Full type definitions included
- 🌙 Dark Mode Ready - Supports light/dark themes via CSS variables
Supported Locales
| Locale | Language | Flag |
| ------ | ---------- | ---- |
| en | English | 🇺🇸 |
| vi | Tiếng Việt | 🇻🇳 |
| ko | 한국어 | 🇰🇷 |
| ja | 日本語 | 🇯🇵 |
Requirements
- Node >= 18
- Peer dependencies:
react,react-dom - Optional:
next,next-intl(for Next.js projects)
Agent-Readable Metadata
For coding agents and automation tools:
AGENTS.md: concise usage and integration rules.api-reference.json: generated export index fromsrc/index.ts.llms.txt: compact LLM-friendly quickstart.agent-recipes.json: structured setup/use recipes.
Regenerate API metadata:
npm run generate:apiInstallation
# Install the package
npm i @underverse-ui/underverse
# For Next.js projects (with next-intl)
npm i react react-dom next next-intl
# For standalone React projects (Vite, CRA, etc.)
npm i react react-domTailwind CSS Configuration
Components use color variables like primary, secondary, destructive, etc. Make sure your Tailwind theme/tokens include these variables.
⚡ Performance Optimization
Optimize Package Imports (Next.js)
For best performance, add optimizePackageImports to your Next.js config:
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ["lucide-react", "@underverse-ui/underverse"],
},
};This provides:
- ✅ 15-70% faster dev boot
- ✅ 28% faster builds
- ✅ 40% faster cold starts
- ✅ Automatic tree-shaking for barrel imports
Dynamic Imports for Heavy Components
For pages that conditionally show DataTable or DatePicker:
import dynamic from "next/dynamic";
const DataTable = dynamic(() => import("@underverse-ui/underverse").then((m) => m.DataTable), { ssr: false, loading: () => <Skeleton /> });Web Interface Guidelines Compliant
All components follow Vercel Web Interface Guidelines:
- ✅
focus-visiblering (not:focus) - ✅ Label
htmlForattribute - ✅ ARIA attributes for accessibility
- ✅
overscroll-behavior: containfor modals - ✅ Proper ellipsis (
…) typography - ✅ Locale-aware date formatting with
Intl.DateTimeFormat
Package Exports
Hiện tại package publish một public entry point duy nhất:
import {
Button,
DataTable,
Form,
FormField,
UEditor,
} from "@underverse-ui/underverse";Không có subpath export như @underverse-ui/underverse/form ở version hiện tại.
Lưu ý:
- Nhiều component trong package là client component và nên dùng trong môi trường React client.
- Form primitives yêu cầu
react-hook-formvà@hookform/resolvers. UEditorvà các component dựa trên Tiptap yêu cầu peer dependencies tương ứng.
�🚀 Quick Start
Overlay Scrollbars (Optional, Recommended)
Underverse now uses opt-in, component-level OverlayScrollbars. There is no global DOM scanning, no default global mount, and no app-wide MutationObserver.
import "overlayscrollbars/overlayscrollbars.css";
import { OverlayScrollbarProvider, ScrollArea, DataTable } from "@underverse-ui/underverse";
function App() {
return (
<OverlayScrollbarProvider theme="os-theme-underverse" autoHide="leave">
<ScrollArea className="h-56 rounded-xl border border-border" useOverlayScrollbar>
{/* long content */}
</ScrollArea>
<DataTable
columns={columns}
data={rows}
useOverlayScrollbar
/>
</OverlayScrollbarProvider>
);
}Provider behavior:
- Provider is configuration only (theme/options context).
- Scrollbars initialize only on components explicitly enabled via
useOverlayScrollbar. - Hard skip targets:
html,body,[data-radix-portal],[role="dialog"],[aria-modal="true"],[data-sonner-toaster]. - Per-node opt-out remains available via
data-os-ignore.
Provider props:
enabled?: booleantheme?: stringvisibility?: "visible" | "hidden" | "auto"autoHide?: "never" | "scroll" | "leave" | "move"autoHideDelay?: numberdragScroll?: booleanclickScroll?: booleanexclude?: stringdefault:html, body, [data-os-ignore], [data-radix-portal], [role='dialog'], [aria-modal='true'], [data-sonner-toaster]selector?: string(deprecated, ignored; kept for backward compatibility)
Component-level enable flags:
ScrollArea:useOverlayScrollbar?: boolean(defaultfalse)classNamestyles the outer wrappercontentClassNamestyles the scroll viewport- set border/radius explicitly; the primitive no longer hardcodes rounded corners
Table:useOverlayScrollbar?: boolean(defaultfalse)DataTable:useOverlayScrollbar?: boolean(defaultfalse)Combobox:useOverlayScrollbar?: boolean(defaultfalse)MultiCombobox:useOverlayScrollbar?: boolean(defaultfalse)CategoryTreeSelect:useOverlayScrollbar?: boolean(defaultfalse)Textarea:useOverlayScrollbar?: boolean(defaultfalse)OverlayScrollArea: dedicated wrapper for heavy scroll zones (enableddefaulttrue)
When to use:
- Long virtualized/table/list panels
- Fixed-height navigation panels and log viewers
When not to use:
- Normal form fields
- Short modal/dialog content
- Full page root scrolling
Standalone React (Vite, CRA, etc.)
import { TranslationProvider, Button, DatePicker, ToastProvider, useToast } from "@underverse-ui/underverse";
function App() {
return (
<TranslationProvider locale="vi">
<ToastProvider>
<MyComponent />
</ToastProvider>
</TranslationProvider>
);
}
function MyComponent() {
const { addToast } = useToast();
return (
<div>
<DatePicker onChange={(date) => console.log(date)} />
<Button onClick={() => addToast({ type: "success", message: "Hello!" })}>Click me</Button>
</div>
);
}Next.js (with next-intl)
import { Button, ToastProvider, useToast } from "@underverse-ui/underverse";
function App() {
const { addToast } = useToast();
return (
<ToastProvider>
<Button onClick={() => addToast({ type: "success", message: "Hello" })}>Click me</Button>
</ToastProvider>
);
}Exported Components
Core Components
- Buttons:
Button - Display:
Badge,Card,Avatar,Skeleton,Progress - Form Inputs:
Input,Textarea,Checkbox,Switch,Label
Feedback & Overlays
Modal,ToastProvider,useToast,Tooltip,Popover,Sheet(includesDrawer,SlideOver,BottomSheet,SidebarSheet),Alert,GlobalLoading(includesPageLoading,InlineLoading,ButtonLoading)
Form Controls & Pickers
RadioGroup,Slider,DatePicker,Combobox,MultiCombobox,CategoryTreeSelect
Navigation & Structure
Breadcrumb,Tabs(includesSimpleTabs,PillTabs,VerticalTabs),DropdownMenu,Pagination,Section,ScrollArea,OverlayScrollArea
Data Display
Table,DataTable
Media Components
SmartImage,ImageUpload,Carousel,UEditor
Utilities
ClientOnly,Loading,NotificationModal,AccessDenied,OverlayControls- Headless controls:
ThemeToggle,LanguageSwitcher - Utility functions:
cn,DateUtils, style constants
Important Notes
- Library is i18n‑agnostic: components have sensible English defaults and accept text via props.
- If your app uses
next-intl, you can merge our ready‑made messages to localize built‑in texts. NotificationBellis not exported (depends on project-specific API/socket implementations).FloatingContactsremains app-only and is not exported from the package.
📦 Date Utilities
The package includes standalone date utilities with locale support (no Next.js required):
import { DateUtils } from "@underverse-ui/underverse";
// Format dates with locale
DateUtils.formatDate(new Date(), "ko"); // "2026년 1월 5일"
DateUtils.formatDate(new Date(), "ja"); // "2026年1月5日"
DateUtils.formatDate(new Date(), "vi"); // "05/01/2026"
DateUtils.formatDate(new Date(), "en"); // "January 5, 2026"
// Relative time formatting
DateUtils.formatTimeAgo(new Date(Date.now() - 3600000), "ko"); // "1시간 전"
DateUtils.formatTimeAgo(new Date(Date.now() - 3600000), "ja"); // "1時間前"
// Smart date formatting (Today, Yesterday, or full date)
DateUtils.formatDateSmart(new Date(), "ja"); // "今日 14:30"
// Utility checks
DateUtils.isToday(new Date()); // true
DateUtils.isYesterday(new Date(Date.now() - 86400000)); // true
// Get day of week
DateUtils.getDayOfWeek(new Date(), "ko"); // "일요일"
DateUtils.getDayOfWeek(new Date(), "ja"); // "日曜日"
// Form input formatting
DateUtils.formatDateForInput(new Date()); // "2026-01-05"
DateUtils.formatDateTimeForInput(new Date()); // "2026-01-05T14:30"Available Date Functions
| Function | Description |
| ------------------------------- | ----------------------------------- |
| formatDate(date, locale) | Full date format |
| formatDateShort(date, locale) | Short date format |
| formatTime(date, locale) | Time only (HH:mm) |
| formatDateTime(date, locale) | Date + time |
| formatTimeAgo(date, locale) | Relative time (e.g., "2 hours ago") |
| formatDateSmart(date, locale) | Today/Yesterday/Full date |
| isToday(date) | Check if date is today |
| isYesterday(date) | Check if date is yesterday |
| getDayOfWeek(date, locale) | Get localized day name |
| formatDateForInput(date) | YYYY-MM-DD format |
| formatDateTimeForInput(date) | YYYY-MM-DDTHH:mm format |
🎨 Animation Utilities
The package includes ShadCN-compatible animation utilities:
import { useShadCNAnimations, injectAnimationStyles, getAnimationStyles } from "@underverse-ui/underverse";
// React hook - automatically injects styles on mount
function MyComponent() {
useShadCNAnimations();
return <div className="animate-accordion-down">Content</div>;
}
// Manual injection (for non-React usage)
injectAnimationStyles();
// Get CSS string for custom injection
const cssString = getAnimationStyles();Available Animations
| Class | Description |
| ------------------------------ | ---------------------------- |
| animate-accordion-down | Accordion expand animation |
| animate-accordion-up | Accordion collapse animation |
| animate-caret-blink | Blinking caret cursor |
| animate-fade-in | Fade in effect |
| animate-fade-out | Fade out effect |
| animate-slide-in-from-top | Slide in from top |
| animate-slide-in-from-bottom | Slide in from bottom |
| animate-slide-in-from-left | Slide in from left |
| animate-slide-in-from-right | Slide in from right |
| animate-zoom-in | Zoom in effect |
| animate-zoom-out | Zoom out effect |
next-intl Integration (Next.js App Router)
- Configure plugin and time zone (to avoid
ENVIRONMENT_FALLBACK):
// next.config.ts
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin({
locales: ["vi", "en"],
defaultLocale: "vi",
timeZone: "Asia/Ho_Chi_Minh", // important for SSR
});
export default withNextIntl({
// your other Next config
});- Merge underverse messages with your app messages:
// app/layout.tsx (simplified)
import { NextIntlClientProvider, getMessages } from "next-intl/server";
import { underverseMessages } from "@underverse-ui/underverse";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const appMessages = await getMessages();
const locale = "vi"; // derive from params/headers
const uv = underverseMessages[locale] || underverseMessages.en;
const messages = { ...uv, ...appMessages }; // app overrides uv if overlaps
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}- Use components normally. Any built‑in texts (DatePicker/Pagination/DataTable/Alert/ImageUpload…) will use merged messages. You can still override labels via props if desired.
🌐 TranslationProvider API
For standalone React apps (without next-intl):
import { TranslationProvider } from "@underverse-ui/underverse";
function App() {
return (
<TranslationProvider
locale="ko" // "en" | "vi" | "ko" | "ja"
translations={{
// Optional: override default translations
Common: {
close: "닫기 (custom)",
},
}}
>
{children}
</TranslationProvider>
);
}TranslationProvider Props
| Prop | Type | Default | Description |
| -------------- | ------------------------------ | ----------- | ---------------------------- |
| locale | "en" \| "vi" \| "ko" \| "ja" | "en" | Active locale |
| translations | Translations | undefined | Custom translation overrides |
| children | ReactNode | - | Child components |
Message Keys Summary
Common: close, closeAlert, notifications, newNotification, readStatus, openLink, theme, lightTheme, darkTheme, systemTheme, density, compact, normal, comfortable, columnsValidationInput: required, typeMismatch, pattern, tooShort, tooLong, rangeUnderflow, rangeOverflow, stepMismatch, badInput, invalidLoading: loadingPage, pleaseWaitDatePicker: placeholder, today, clearPagination: navigationLabel, showingResults ({startItem},{endItem},{totalItems}), firstPage, previousPage, previous, nextPage, next, lastPage, pageNumber ({page}), itemsPerPage, search, noOptionsOCR.imageUpload: dragDropText, browseFiles, supportedFormats
📋 Exported Components
Core Components
- Buttons:
Button - Display:
Badge,Card,Avatar,Skeleton,Progress - Form Inputs:
Input,PasswordInput,NumberInput,SearchInput,Textarea,Checkbox,Switch,Label,TagInput
Feedback & Overlays
Modal,ToastProvider,useToast,Tooltip,PopoverSheet(includesDrawer,SlideOver,BottomSheet,SidebarSheet)Alert,GlobalLoading(includesPageLoading,InlineLoading,ButtonLoading)
Form Controls & Pickers
RadioGroup,Slider,DatePicker,DateRangePicker,TimePicker,CalendarCombobox,MultiCombobox,CategoryTreeSelect,ColorPicker
Navigation & Structure
Breadcrumb,Tabs(includesSimpleTabs,PillTabs,VerticalTabs)DropdownMenu,Pagination,SimplePagination,CompactPaginationSection,ScrollArea
Data Display
Table,DataTable,List,Grid,Timeline
Media Components
SmartImage,ImageUpload,Carousel,FallingIcons,Watermark,UEditor
Utilities
ClientOnly,Loading,NotificationModal,AccessDenied,OverlayControlsThemeToggle,LanguageSwitcher(headless)cn,DateUtils,useShadCNAnimations
License
MIT
Author
Tran Van Bach
Headless Components Usage
These variants avoid app-specific contexts and routing so you can wire them to your own state.
ThemeToggle (headless)
import { ThemeToggle } from "@underverse-ui/underverse";
import type { ThemeToggleProps, ThemeMode } from "@underverse-ui/underverse";
import { useState } from "react";
export default function ExampleThemeToggle() {
const [theme, setTheme] = useState<ThemeMode>("system");
return (
<ThemeToggle
theme={theme}
onChange={setTheme}
// optional labels
labels={{ heading: "Theme", light: "Light", dark: "Dark", system: "System" }}
/>
);
}If you use next-themes or a custom context, pass your current theme and the setter to onChange.
LanguageSwitcher (headless)
import { LanguageSwitcher } from "@underverse-ui/underverse";
import type { LanguageOption } from "@underverse-ui/underverse";
import { useRouter, usePathname } from "next/navigation";
const locales: LanguageOption[] = [
{ code: "vi", name: "Tiếng Việt", flag: "🇻🇳" },
{ code: "en", name: "English", flag: "🇺🇸" },
{ code: "ko", name: "한국어", flag: "🇰🇷" },
{ code: "ja", name: "日本語", flag: "🇯🇵" },
];
export default function ExampleLanguageSwitcher({ currentLocale }: { currentLocale: string }) {
const router = useRouter();
const pathname = usePathname();
const onSwitch = (code: string) => {
// Replace first segment as locale, e.g. /vi/... -> /en/...
const segs = pathname.split("/");
segs[1] = code;
router.push(segs.join("/"));
};
return <LanguageSwitcher locales={locales} currentLocale={currentLocale} onSwitch={onSwitch} labels={{ heading: "Language" }} />;
}📁 Full Export Reference
// Core Components
import {
Button,
Badge,
Card,
Avatar,
Skeleton,
Progress,
Input,
PasswordInput,
NumberInput,
SearchInput,
Textarea,
Checkbox,
Switch,
Label,
TagInput,
} from "@underverse-ui/underverse";
// Overlays
import {
Modal,
ToastProvider,
useToast,
Tooltip,
Popover,
Sheet,
Drawer,
SlideOver,
BottomSheet,
SidebarSheet,
Alert,
GlobalLoading,
PageLoading,
InlineLoading,
ButtonLoading,
} from "@underverse-ui/underverse";
// Pickers
import {
DatePicker,
DateRangePicker,
TimePicker,
Calendar,
Combobox,
MultiCombobox,
CategoryTreeSelect,
ColorPicker,
RadioGroup,
Slider,
} from "@underverse-ui/underverse";
// Navigation
import {
Breadcrumb,
Tabs,
SimpleTabs,
PillTabs,
VerticalTabs,
DropdownMenu,
Pagination,
SimplePagination,
CompactPagination,
Section,
ScrollArea,
} from "@underverse-ui/underverse";
// Data Display
import { Table, DataTable, List, Grid, Timeline, Watermark } from "@underverse-ui/underverse";
// Media
import { SmartImage, ImageUpload, Carousel, FallingIcons, UEditor } from "@underverse-ui/underverse";
// Utilities
import {
cn,
DateUtils,
useShadCNAnimations,
injectAnimationStyles,
ClientOnly,
Loading,
NotificationModal,
AccessDenied,
ThemeToggle,
LanguageSwitcher,
} from "@underverse-ui/underverse";
// i18n
import {
TranslationProvider,
useUnderverseTranslations,
useUnderverseLocale,
underverseMessages,
getUnderverseMessages,
} from "@underverse-ui/underverse";
// Types
import type {
ButtonProps,
InputProps,
DatePickerProps,
ComboboxProps,
PaginationProps,
DataTableColumn,
Locale,
Translations,
} from "@underverse-ui/underverse";🧪 Testing
Test with React (Vite)
# Create new Vite project
npm create vite@latest my-test-app -- --template react-ts
cd my-test-app
# Install underverse
npm i @underverse-ui/underverse
# Add Tailwind CSS
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -pTest with Next.js
# Create new Next.js project
npx create-next-app@latest my-test-app --typescript --tailwind
cd my-test-app
# Install underverse
npm i @underverse-ui/underverse next-intl