@mks2508/mks-ui
Hooks

useListFormat

Locale-aware list formatting with Intl.ListFormat and animated parts support.

useListFormat

A hook that wraps Intl.ListFormat with enhanced features for animated list displays, including an index property on parts and overflow handling.

Import

import { useListFormat } from '@mks2508/mks-ui/react';

Why This Hook?

Intl.ListFormat is great for formatting lists with locale-aware separators, but it has limitations:

  1. No index - formatToParts() returns { type, value } without position info
  2. No overflow - Can't add "+N more" without breaking grammar
  3. No animation support - Hard to animate individual items

This hook solves all three:

// Native Intl.ListFormat
const parts = new Intl.ListFormat('en').formatToParts(['a', 'b', 'c']);
// [{ type: 'element', value: 'a' }, { type: 'literal', value: ', ' }, ...]
// No way to know which element is at index 0, 1, 2!

// useListFormat
const parts = useListFormat(['a', 'b', 'c'], { type: 'conjunction' });
// [{ type: 'element', value: 'a', index: 0 }, { type: 'literal', value: ', ' }, ...]
// Index included for animation mapping!

Basic Usage

import { useListFormat } from '@mks2508/mks-ui/react';

function FruitList({ fruits }) {
  const parts = useListFormat(fruits, {
    locale: 'en-US',
    type: 'conjunction',
    style: 'long',
  });

  return (
    <span>
      {parts.map((part, i) => (
        part.type === 'element'
          ? <span key={part.index}>{part.value}</span>
          : <span key={i}>{part.value}</span>
      ))}
    </span>
  );
}
// Output: "apple, banana, and orange"

List Types

TypeDescriptionEnglish Example
conjunctionAnd-joined list"a, b, and c"
disjunctionOr-joined list"a, b, or c"
unitUnit list"a, b, c"

List Styles

StyleDescriptionEnglish Example
longFull words"a, b, and c"
shortAbbreviated"a, b, & c"
narrowMinimal"a, b, c"

Overflow Handling

The hasOverflow parameter adds a placeholder to prevent "and" before overflow text:

function TagList({ tags, maxVisible = 3 }) {
  const visible = tags.slice(0, maxVisible);
  const overflow = tags.length - maxVisible;

  const parts = useListFormat(visible, {
    type: 'conjunction',
    hasOverflow: overflow > 0,  // Prevents "and" before overflow
  });

  return (
    <span>
      {parts.map((part, i) => (
        part.type === 'element'
          ? <Tag key={part.index}>{part.value}</Tag>
          : <span key={i}>{part.value}</span>
      ))}
      {overflow > 0 && <span>+{overflow} more</span>}
    </span>
  );
}
// Input: ['a', 'b', 'c', 'd', 'e'], maxVisible=3
// Output: "a, b, c +2 more"  (not "a, b, and c +2 more")

Custom Separator

Override the default separator:

const parts = useListFormat(items, {
  separator: ' | ',
});
// Output: "a | b | c"

With Animation

Combine with SlidingText for animated lists:

import { useListFormat, SlidingText } from '@mks2508/mks-ui/react';

function AnimatedList({ items }) {
  const parts = useListFormat(items, { type: 'conjunction' });

  return (
    <span className="inline-flex items-center gap-1">
      {parts.map((part, i) =>
        part.type === 'element' ? (
          <SlidingText
            key={part.index}
            text={part.value}
            mode="character"
          />
        ) : (
          <span key={i}>{part.value}</span>
        )
      )}
    </span>
  );
}

API Reference

Parameters

useListFormat(
  items: string[],
  options?: IUseListFormatOptions
): IListPart[]

IUseListFormatOptions

OptionTypeDefaultDescription
localestring'en-US'Locale for formatting
typeListFormatType'conjunction'List type
styleListFormatStyle'long'Style length
separatorstringCustom separator override
hasOverflowbooleanfalseAdd placeholder for overflow

Return Type

interface IListPart {
  type: 'element' | 'literal';
  value: string;
  index?: number;  // Only present for 'element' type
}

type ListFormatType = 'conjunction' | 'disjunction' | 'unit';
type ListFormatStyle = 'long' | 'short' | 'narrow';

Browser Support

Intl.ListFormat is supported in all modern browsers:

  • Chrome 72+
  • Firefox 78+
  • Safari 14.1+
  • Edge 79+

For older browsers, the hook falls back gracefully.