Skip to main content

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.

FunctionPurposeReturns
cn()Merge class names with deduplicationstring
formatDate()Format dates as short, long, or relativestring
formatNumber()Locale-aware number / currency / percent formattingstring
truncateText()Clip text to a max length with a custom suffixstring
generateId()Generate prefixed unique IDsstring
slugify()Convert text to URL-safe slugsstring
deepMerge()Recursively merge objectsT

Installation

cn requires clsx and tailwind-merge. All other helpers are pure TypeScript with no external dependencies.

src/lib/utils.ts — imports
typescript
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.

Type Signature
typescript
import { type ClassValue } from 'clsx';

function cn(...inputs: ClassValue[]): string
cn() — examples
typescript
import { 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'])
ParameterTypeRequiredDefaultDescription
Any number of class values — strings, objects, arrays, booleans, or undefined.
Returns
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').

Type Signature
typescript
function formatDate(
  date: Date | string,
  format?: 'short' | 'long' | 'relative'
): string
formatDate() — examples
typescript
import { 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())
ParameterTypeRequiredDefaultDescription
The date to format. Accepts a Date instance or any ISO 8601 string.
Output format. 'short' → numeric, 'long' → written month, 'relative' → time ago.
Returns
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.

Type Signature
typescript
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
): string
formatNumber() — examples
typescript
import { 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'
ParameterTypeRequiredDefaultDescription
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.
Returns
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.

Type Signature
typescript
function truncateText(
  text: string,
  maxLength: number,
  suffix?: string
): string
truncateText() — examples
typescript
import { 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>
ParameterTypeRequiredDefaultDescription
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.
Returns
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.

Type Signature
typescript
function generateId(
  prefix?: string,
  length?: number
): string
generateId() — examples
typescript
import { 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>;
ParameterTypeRequiredDefaultDescription
Optional string prepended to the random segment, separated by an underscore.
Length of the random alphanumeric segment (not counting the prefix).
Returns
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.

Type Signature
typescript
function slugify(text: string): string
slugify() — examples
typescript
import { 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)}`,
}));
ParameterTypeRequiredDefaultDescription
The input string to convert into a slug.
Returns
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.

Type Signature
typescript
function deepMerge<T extends object>(
  target: T,
  ...sources: Partial<T>[]
): T
deepMerge() — examples
typescript
import { 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} />;
}
ParameterTypeRequiredDefaultDescription
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.
Returns
TThe mutated target object with all source properties deeply merged in.