Helper functions
Zero-dependency utilities for the tasks every app needs — class merging, date formatting, number formatting, slugification, ID generation, deep merge. All fully typed.
Overview
All helpers live in src/lib/utils.ts and can be imported individually.
| Function | Purpose | Returns |
|---|---|---|
cn() | Merge class names with deduplication | string |
formatDate() | Format dates as short, long, or relative | string |
formatNumber() | Locale-aware number / currency / percent formatting | string |
truncateText() | Clip text to a max length with a custom suffix | string |
generateId() | Generate prefixed unique IDs | string |
slugify() | Convert text to URL-safe slugs | string |
deepMerge() | Recursively merge objects | T |
Installation
cn requires clsx and tailwind-merge. All other helpers are pure TypeScript with no external dependencies.
import { clsx, type ClassValue } from 'clsx';
// Re-export everything from a single entry point
export { cn, formatDate, formatNumber, truncateText, generateId, slugify, deepMerge };cn()
Merges Tailwind CSS class names using clsx for conditional logic and tailwind-merge to resolve conflicts. Duplicate or conflicting utilities (e.g. p-2 and p-4 on the same element) are resolved in favour of the last value.
import { type ClassValue } from 'clsx';
function cn(...inputs: ClassValue[]): stringimport { cn } from '@/lib/utils';
// Basic merge
cn('px-4 py-2', 'bg-blue-500')
// → 'px-4 py-2 bg-blue-500'
// Conflict resolution — tailwind-merge picks the last value
cn('p-2', 'p-4')
// → 'p-4'
// Conditional classes via clsx syntax
cn(
'rounded-lg font-medium',
isActive && 'bg-brand-500 text-white',
isDisabled && 'opacity-50 cursor-not-allowed',
variant === 'outline' && 'border border-brand-500 text-brand-500'
)
// Object syntax
cn({
'bg-brand-500': isPrimary,
'bg-surface-elevated': !isPrimary,
})
// Array syntax
cn(['base-class', isValid ? 'text-green-500' : 'text-danger-500'])| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Any number of class values — strings, objects, arrays, booleans, or undefined. |
stringA single deduplicated and conflict-resolved class string.formatDate()
Formats a Date object or ISO date string into a human-readable string. Supports three built-in formats: short (numeric date), long (written month), and relative (e.g. '3 days ago').
function formatDate(
date: Date | string,
format?: 'short' | 'long' | 'relative'
): stringimport { formatDate } from '@/lib/utils';
const date = new Date('2024-06-15T10:30:00Z');
// short — locale numeric date
formatDate(date, 'short')
// → '6/15/2024'
// long — full written date
formatDate(date, 'long')
// → 'June 15, 2024'
// relative — human-friendly distance from now
formatDate(date, 'relative')
// → '3 months ago' (depends on current time)
// string input is accepted too
formatDate('2024-01-01', 'long')
// → 'January 1, 2024'
// default (no format arg) falls back to 'short'
formatDate(new Date())| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| The date to format. Accepts a Date instance or any ISO 8601 string. | ||||
| Output format. 'short' → numeric, 'long' → written month, 'relative' → time ago. |
stringFormatted date string in the requested style.formatNumber()
Formats numbers using the Intl.NumberFormat API. Supports decimal, currency, and percent styles with locale customization. Handles large numbers, compact notation, and international formatting out of the box.
interface FormatNumberOptions {
style?: 'decimal' | 'currency' | 'percent';
currency?: string; // ISO 4217 code, e.g. 'USD'
locale?: string; // BCP 47 tag, e.g. 'id-ID'
compact?: boolean; // Abbreviate large numbers (1K, 1M…)
decimals?: number; // Fixed decimal places
}
function formatNumber(
num: number,
options?: FormatNumberOptions
): stringimport { formatNumber } from '@/lib/utils';
// Plain decimal
formatNumber(1234567.89)
// → '1,234,567.89'
// Currency
formatNumber(99.99, { style: 'currency', currency: 'USD' })
// → '$99.99'
// Rupiah
formatNumber(1500000, { style: 'currency', currency: 'IDR', locale: 'id-ID' })
// → 'Rp 1.500.000'
// Percent
formatNumber(0.845, { style: 'percent' })
// → '84.5%'
// Compact large numbers
formatNumber(2400000, { compact: true })
// → '2.4M'
// Fixed decimals
formatNumber(3.14159, { decimals: 2 })
// → '3.14'| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| The numeric value to format. | ||||
| Intl.NumberFormat style. Use currency together with the currency option. | ||||
| ISO 4217 currency code, used when style is currency. | ||||
| BCP 47 locale tag that controls digit grouping and decimal separators. | ||||
| When true, abbreviates large numbers (1K, 2.4M, 1B). | ||||
| Fix the number of decimal places in the output. |
stringLocale-formatted number string.truncateText()
Clips a string to a maximum character length, appending a configurable suffix when truncation occurs. Safe for use in table cells, card previews, and tooltips — does not cut words mid-character.
function truncateText(
text: string,
maxLength: number,
suffix?: string
): stringimport { truncateText } from '@/lib/utils';
const bio = 'The quick brown fox jumped over the lazy dog near the river bank.';
// Default suffix is '…'
truncateText(bio, 30)
// → 'The quick brown fox jumped ove…'
// Custom suffix
truncateText(bio, 30, ' [more]')
// → 'The quick brown fox jumped ove [more]'
// No truncation if text is short enough
truncateText('Hello', 20)
// → 'Hello'
// Use in JSX
<p className="text-sm text-(--text-muted)">
{truncateText(post.body, 120)}
</p>| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| The source string to potentially truncate. | ||||
| Maximum number of characters before truncation. The suffix is appended after this limit. | ||||
| String appended when the text is truncated. Defaults to the ellipsis character. |
stringOriginal string if within maxLength, or truncated string with suffix appended.generateId()
Generates a random alphanumeric identifier, optionally prefixed with a category string. Useful for client-side list keys, temporary object IDs, and form field ids where a full UUID is overkill.
function generateId(
prefix?: string,
length?: number
): stringimport { generateId } from '@/lib/utils';
// Default — random 8-char alphanumeric
generateId()
// → 'k7x2mq9f'
// With prefix
generateId('user')
// → 'user_k7x2mq9f'
// Custom length
generateId('item', 12)
// → 'item_k7x2mq9fah3b'
// Keying dynamic list items
const items = data.map((d) => ({ ...d, _id: generateId('row') }));
// Generate a form input id
const inputId = generateId('input');
return <label htmlFor={inputId}>Name <input id={inputId} /></label>;| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| Optional string prepended to the random segment, separated by an underscore. | ||||
| Length of the random alphanumeric segment (not counting the prefix). |
stringA random ID string, optionally prefixed (e.g. 'user_k7x2mq9f').slugify()
Converts arbitrary text into a URL-safe slug — lowercased, spaces replaced with hyphens, special characters stripped. Handles Unicode input by normalizing to ASCII where possible, making it suitable for generating URL path segments, file names, or anchor IDs.
function slugify(text: string): stringimport { slugify } from '@/lib/utils';
slugify('Hello, World!')
// → 'hello-world'
slugify('Next.js 15 Release Notes')
// → 'nextjs-15-release-notes'
slugify(' Extra Spaces ')
// → 'extra-spaces'
slugify('Über die Straße')
// → 'uber-die-strasse'
// Use for dynamic route segments
const slug = slugify(article.title);
router.push(`/blog/${slug}`);
// Anchor link generation
const headings = content.map((h) => ({
label: h,
anchor: `#${slugify(h)}`,
}));| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| The input string to convert into a slug. |
stringLowercase, hyphenated, URL-safe string with special characters removed.deepMerge()
Recursively merges one or more source objects into a target, deeply combining nested objects rather than overwriting them at the top level. Arrays in source objects replace (not concatenate) arrays in the target. Returns the merged result typed as T.
function deepMerge<T extends object>(
target: T,
...sources: Partial<T>[]
): Timport { deepMerge } from '@/lib/utils';
const defaults = {
theme: { color: 'blue', size: 'md' },
pagination: { page: 1, perPage: 20 },
features: { search: true, export: false },
};
const userConfig = {
theme: { color: 'green' }, // deep merge — size stays 'md'
features: { export: true }, // deep merge — search stays true
};
const config = deepMerge(defaults, userConfig);
// → {
// theme: { color: 'green', size: 'md' },
// pagination: { page: 1, perPage: 20 },
// features: { search: true, export: true },
// }
// Multiple sources — later sources win on conflicts
const merged = deepMerge(base, overrideA, overrideB);
// Merge component props with defaults
function Button({ style, ...rest }: ButtonProps) {
const resolvedStyle = deepMerge(defaultButtonStyle, style ?? {});
return <button style={resolvedStyle} {...rest} />;
}| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| The base object. It is mutated in place and returned. | ||||
| One or more source objects whose properties are merged into target. Later sources take precedence. |
TThe mutated target object with all source properties deeply merged in.