npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@andreyfedkovich/elegant-grid

v1.1.1

Published

A composition-first Data Grid for React. Batteries included (Sorting, Pagination, Empty States) but flexible rows. No complex configuration objects — just write standard React components

Readme

@andreyfedkovich/elegant-grid

A premium React data grid with compound components, built on Tailwind CSS.

npm version License: MIT

🎮 Live Demo

Features

  • 🧩 Compound Component API - Intuitive ElegantGrid.Row, ElegantGrid.Cell, ElegantGrid.ActionCell pattern
  • 🏗️ Composition-Based Headers - Define columns with JSX using ElegantGrid.Headers and ElegantGrid.Header
  • 📐 CSS Grid Layout - Modern div-based layout (not tables)
  • ☑️ Multi-row Selection - Checkboxes with select-all support
  • 🔀 Sortable Columns - Click headers to sort (asc → desc → none)
  • ↔️ Column Resizing - Drag column borders to resize
  • 📄 Full Pagination - Page size selector, jump-to-page, refresh button
  • 💀 Skeleton Loading - Built-in loading states with height stability
  • 📭 Empty State - Customizable empty state with icon and action
  • 🎨 Custom Cells - Full control over cell rendering
  • ⚙️ GridConfig - Fine-tune layout, scrolling, and identification
  • 🌍 i18n Support - Customizable labels for internationalization
  • 📘 TypeScript-first - Complete type definitions
  • 🎯 Spread Props - Pass native HTML attributes to Row, Cell, ActionCell

Installation

npm install @andreyfedkovich/elegant-grid

Quick Start

import { ElegantGrid } from '@andreyfedkovich/elegant-grid';
import '@andreyfedkovich/elegant-grid/styles.css'; // Required for styling

const headers = [
  { key: 'name', label: 'Name', sortable: true },
  { key: 'email', label: 'Email' },
];

const data = [
  { id: '1', name: 'John Doe', email: '[email protected]' },
  { id: '2', name: 'Jane Smith', email: '[email protected]' },
];

function MyGrid() {
  return (
    <ElegantGrid headers={headers} totalCount={data.length}>
      {data.map((item) => (
        <ElegantGrid.Row key={item.id} data={item} selectable>
          <ElegantGrid.Cell>{item.name}</ElegantGrid.Cell>
          <ElegantGrid.Cell>{item.email}</ElegantGrid.Cell>
        </ElegantGrid.Row>
      ))}
    </ElegantGrid>
  );
}

Styles

Import the required CSS file once in your app's entry point:

import '@andreyfedkovich/elegant-grid/styles.css';

The styles include scoped CSS variables that won't conflict with your existing design system.

Dark Mode

ElegantGrid automatically adapts to dark mode when:

  • A parent element has the dark class (e.g., <html class="dark">)
  • Or add the dark class directly to the grid's parent container
// Works automatically with next-themes or similar
<div className="dark">
  <ElegantGrid ... />
</div>

Complete Example

Here's a full-featured example showing all capabilities:

import { useState } from 'react';
import { ElegantGrid, type Header, type SortOrder } from '@andreyfedkovich/elegant-grid';
import '@andreyfedkovich/elegant-grid/styles.css';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';

// Sample data
const sampleTransactions = [
  { id: '1', fund: 'Growth Fund Alpha', type: 'Subscription', amount: 150000, status: 'completed', date: '2024-01-15' },
  { id: '2', fund: 'Tech Ventures III', type: 'Redemption', amount: 75000, status: 'pending', date: '2024-01-14' },
  { id: '3', fund: 'Real Estate Plus', type: 'Distribution', amount: 25000, status: 'completed', date: '2024-01-13' },
  { id: '4', fund: 'Global Macro Fund', type: 'Subscription', amount: 500000, status: 'failed', date: '2024-01-12' },
  { id: '5', fund: 'Private Credit II', type: 'Capital Call', amount: 200000, status: 'completed', date: '2024-01-11' },
];

// Header configuration
const headers: Header[] = [
  { key: 'fund', label: 'Fund Name', sortable: true, minWidth: 200, width: 250 },
  { key: 'type', label: 'Type', sortable: true, minWidth: 100, width: 120 },
  { key: 'amount', label: 'Amount', sortable: true, minWidth: 120, width: 150, align: 'right' },
  { key: 'status', label: 'Status', sortable: true, minWidth: 100, width: 120, align: 'center' },
  { key: 'date', label: 'Date', sortable: true, minWidth: 100, width: 120 },
  { key: 'actions', label: 'Actions', minWidth: 100, width: 100, align: 'center', resizable: false },
];

// Custom cell components
const FundCell = ({ name }: { name: string }) => (
  <div className="flex items-center gap-3">
    <Avatar className="h-8 w-8">
      <AvatarFallback className="bg-primary/10 text-primary text-xs font-medium">
        {name.split(' ').map(w => w[0]).join('').slice(0, 2)}
      </AvatarFallback>
    </Avatar>
    <span className="font-medium">{name}</span>
  </div>
);

const StatusBadge = ({ status }: { status: string }) => {
  const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
    completed: 'default',
    pending: 'secondary',
    failed: 'destructive',
  };
  return (
    <Badge variant={variants[status] || 'outline'} className="capitalize">
      {status}
    </Badge>
  );
};

// Main component
export default function TransactionsPage() {
  const [sortOrder, setSortOrder] = useState<SortOrder | null>(null);
  const [selectedRows, setSelectedRows] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);

  const handleQueryChange = (query: { offset: number; limit: number }) => {
    console.log('Query changed:', query);
    // Fetch data based on offset and limit
  };

  const handleRefresh = () => {
    setLoading(true);
    setTimeout(() => setLoading(false), 1000);
  };

  const handleEdit = (id: string) => {
    console.log('Edit:', id);
  };

  const handleDelete = (id: string) => {
    console.log('Delete:', id);
  };

  const formatAmount = (amount: number) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 0,
    }).format(amount);
  };

  const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
      year: 'numeric',
    });
  };

  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-4">Transactions</h1>
      
      {selectedRows.length > 0 && (
        <div className="mb-4 p-3 bg-primary/5 rounded-lg">
          Selected {selectedRows.length} transaction(s) - Total: {
            formatAmount(selectedRows.reduce((sum, row) => sum + row.amount, 0))
          }
        </div>
      )}

      <ElegantGrid
        headers={headers}
        totalCount={sampleTransactions.length}
        loading={loading}
        onSort={setSortOrder}
        onSelectionChange={setSelectedRows}
        config={{
          maxHeight: '400px',
          minHeight: '400px',
          rowIdKey: 'id',
          skeletonRows: 5,
        }}
        pagerOptions={{
          onQueryChange: handleQueryChange,
          onRefresh: handleRefresh,
          defaultPageSize: 25,
          pageSizeOptions: [10, 25, 50, 100],
        }}
        emptyState={{
          title: 'No transactions found',
          description: 'There are no transactions to display.',
        }}
      >
        {sampleTransactions.map((transaction) => (
          <ElegantGrid.Row
            key={transaction.id}
            data={transaction}
            selectable
            onClick={() => console.log('Row clicked:', transaction.id)}
            data-testid={`row-${transaction.id}`}
          >
            <ElegantGrid.Cell>
              <FundCell name={transaction.fund} />
            </ElegantGrid.Cell>
            <ElegantGrid.Cell>{transaction.type}</ElegantGrid.Cell>
            <ElegantGrid.Cell align="right">
              <span className="font-mono">{formatAmount(transaction.amount)}</span>
            </ElegantGrid.Cell>
            <ElegantGrid.Cell align="center">
              <StatusBadge status={transaction.status} />
            </ElegantGrid.Cell>
            <ElegantGrid.Cell>{formatDate(transaction.date)}</ElegantGrid.Cell>
            <ElegantGrid.ActionCell
              onEdit={() => handleEdit(transaction.id)}
              onDelete={() => handleDelete(transaction.id)}
            />
          </ElegantGrid.Row>
        ))}
      </ElegantGrid>
    </div>
  );
}

Composition-Based Headers

Instead of passing a headers prop array, you can define columns using JSX composition:

Basic Usage

<ElegantGrid totalCount={data.length}>
  <ElegantGrid.Headers>
    <ElegantGrid.Header dataKey="name" label="Name" sortable minWidth={150} />
    <ElegantGrid.Header dataKey="email" label="Email" minWidth={200} />
    <ElegantGrid.Header dataKey="status" label="Status" align="center" />
  </ElegantGrid.Headers>

  {data.map((item) => (
    <ElegantGrid.Row key={item.id} data={item}>
      <ElegantGrid.Cell>{item.name}</ElegantGrid.Cell>
      <ElegantGrid.Cell>{item.email}</ElegantGrid.Cell>
      <ElegantGrid.Cell align="center">{item.status}</ElegantGrid.Cell>
    </ElegantGrid.Row>
  ))}
</ElegantGrid>

Custom Header Content

Use children for custom JSX in header cells:

<ElegantGrid.Header dataKey="status" label="Status" minWidth={100}>
  <span className="flex items-center gap-1.5">
    <Circle className="h-2.5 w-2.5 fill-current" />
    Status
  </span>
</ElegantGrid.Header>

Fill Column

Use fill to make a column expand and occupy remaining space:

<ElegantGrid.Header dataKey="description" label="Description" fill />

ElegantGrid.Header Props

| Property | Type | Default | Description | |----------|------|---------|-------------| | dataKey | string | required | Column identifier (maps to Header.key) | | label | string | required | Display text | | sortable | boolean | false | Enable column sorting | | resizable | boolean | true | Enable column resizing | | minWidth | number | 100 | Minimum column width in pixels | | width | number | 150 | Initial column width in pixels | | align | 'left' \| 'center' \| 'right' | 'left' | Text alignment | | fill | boolean | false | Expand to fill remaining space | | children | ReactNode | - | Custom header content (takes precedence over label) |

Note: If both headers prop and <ElegantGrid.Headers> are provided, the prop takes precedence.

API Reference

Header Interface

| Property | Type | Default | Description | |----------|------|---------|-------------| | key | string | required | Unique column identifier | | label | string | required | Column header text | | sortable | boolean | false | Enable column sorting | | minWidth | number | 100 | Minimum column width in pixels | | width | number | 150 | Initial column width in pixels | | align | 'left' \| 'center' \| 'right' | 'left' | Text alignment | | resizable | boolean | true | Enable column resizing | | fill | boolean | false | Expand column to fill remaining space | | customContent | ReactNode | - | Custom content for the header cell |

ElegantGridProps

| Property | Type | Description | |----------|------|-------------| | headers | Header[] | Column definitions | | totalCount | number | Total number of items (for pagination) | | loading | boolean | Show skeleton loading state | | onSort | (order: SortOrder \| null) => void | Called when sort changes | | onSelectionChange | (selectedData: any[]) => void | Called when selection changes | | pagerOptions | PagerOptions | Pagination configuration | | emptyState | EmptyStateConfig | Empty state configuration | | config | GridConfig | Grid layout and styling configuration | | children | ReactNode | Grid rows | | className | string | Additional CSS classes |

SortOrder

interface SortOrder {
  key: string;
  direction: 'asc' | 'desc';
}

GridConfig

Configure grid layout, scrolling behavior, and row identification.

| Property | Type | Default | Description | |----------|------|---------|-------------| | checkboxColumnWidth | number | 48 | Width of the checkbox column in pixels | | defaultColumnWidth | number | 150 | Default column width when not specified | | minColumnWidth | number | 80 | Minimum column width | | cellPadding | string | "p-3" | Cell padding className | | stickyHeaderZIndex | number | 10 | Z-index for sticky header | | maxHeight | string | - | Maximum height before vertical scroll (e.g., "400px", "50vh") | | height | string | - | Fixed height of grid body (takes precedence over maxHeight) | | minHeight | string | - | Minimum height to maintain during loading | | styledScrollbar | boolean | true | Enable custom scrollbar styling | | rowIdKey | string | "id" | Property key for row identification | | skeletonRows | number | - | Number of skeleton rows during loading (defaults to defaultPageSize or 5) |

GridConfig Example

<ElegantGrid
  headers={headers}
  totalCount={data.length}
  config={{
    maxHeight: '500px',
    minHeight: '500px',
    rowIdKey: 'userId',        // Use 'userId' instead of 'id' for row identification
    skeletonRows: 10,          // Show 10 skeleton rows during loading
    checkboxColumnWidth: 56,   // Wider checkbox column
    cellPadding: 'p-4',        // Larger cell padding
  }}
>
  {/* rows */}
</ElegantGrid>

PagerOptions

| Property | Type | Description | |----------|------|-------------| | onQueryChange | (query: { offset: number; limit: number }) => void | Called when page changes | | onRefresh | () => void | Called when refresh button clicked | | defaultPageSize | number | Initial page size (default: 25) | | pageSizeOptions | number[] | Available page sizes (default: [10, 25, 50, 100]) | | labels | PagerLabels | Custom labels for internationalization |

PagerLabels (i18n)

Customize pagination text for different languages.

interface PagerLabels {
  /** Custom function to format "Showing X-Y of Z" text */
  showingText?: (start: number, end: number, total: number) => string;
  /** Label for rows per page selector (default: "Rows:") */
  rowsLabel?: string;
  /** Label for jump to page input (default: "Go to:") */
  goToLabel?: string;
}

i18n Example (German)

<ElegantGrid
  headers={headers}
  totalCount={data.length}
  pagerOptions={{
    onQueryChange: handleQueryChange,
    defaultPageSize: 25,
    labels: {
      showingText: (start, end, total) => `Zeige ${start}-${end} von ${total}`,
      rowsLabel: 'Zeilen:',
      goToLabel: 'Gehe zu:',
    },
  }}
>
  {/* rows */}
</ElegantGrid>

i18n Example (Japanese)

pagerOptions={{
  onQueryChange: handleQueryChange,
  labels: {
    showingText: (start, end, total) => `${total}件中 ${start}-${end}件を表示`,
    rowsLabel: '表示件数:',
    goToLabel: 'ページ:',
  },
}}

EmptyStateConfig

| Property | Type | Description | |----------|------|-------------| | title | string | Empty state title | | description | string | Optional description text | | icon | ReactNode | Custom icon component | | action | ReactNode | Action button or link |

ElegantGrid.Row Props

Extends React.HTMLAttributes<HTMLDivElement> - supports all native div attributes including onClick, style, data-*, etc.

| Property | Type | Description | |----------|------|-------------| | children | ReactNode | Row cells | | data | any | Row data (passed to selection callback) | | selectable | boolean | Show selection checkbox | | className | string | Additional CSS classes | | onClick | (e: MouseEvent) => void | Native click handler | | style | CSSProperties | Inline styles | | data-* | string | Data attributes |

Spread Props Example

<ElegantGrid.Row
  data={item}
  selectable
  onClick={() => handleRowClick(item.id)}
  onDoubleClick={() => handleRowDoubleClick(item.id)}
  data-testid={`row-${item.id}`}
  style={{ cursor: 'pointer' }}
  aria-label={`Row for ${item.name}`}
>
  <ElegantGrid.Cell>{item.name}</ElegantGrid.Cell>
  <ElegantGrid.Cell>{item.email}</ElegantGrid.Cell>
</ElegantGrid.Row>

ElegantGrid.Cell Props

Extends React.HTMLAttributes<HTMLDivElement> - supports all native div attributes.

| Property | Type | Description | |----------|------|-------------| | children | ReactNode | Cell content | | align | 'left' \| 'center' \| 'right' | Text alignment | | className | string | Additional CSS classes |

ElegantGrid.ActionCell Props

Extends React.HTMLAttributes<HTMLDivElement> - supports all native div attributes.

| Property | Type | Description | |----------|------|-------------| | onEdit | () => void | Edit button click handler | | onDelete | () => void | Delete button click handler | | customActions | CustomAction[] | Additional action buttons | | className | string | Additional CSS classes |

CustomAction

interface CustomAction {
  icon: ReactNode;
  label: string;
  onClick: () => void;
  variant?: 'default' | 'destructive';
}

Custom Actions Example

import { Eye, Download } from 'lucide-react';

<ElegantGrid.ActionCell
  onEdit={() => handleEdit(item.id)}
  onDelete={() => handleDelete(item.id)}
  customActions={[
    {
      icon: <Eye className="h-4 w-4" />,
      label: 'View Details',
      onClick: () => handleView(item.id),
    },
    {
      icon: <Download className="h-4 w-4" />,
      label: 'Download',
      onClick: () => handleDownload(item.id),
    },
  ]}
/>

Empty State Example

import { FileX } from 'lucide-react';
import { Button } from '@/components/ui/button';

<ElegantGrid
  headers={headers}
  totalCount={0}
  emptyState={{
    title: 'No transactions found',
    description: 'Get started by creating your first transaction.',
    icon: <FileX className="h-12 w-12 text-muted-foreground" />,
    action: (
      <Button onClick={() => setShowCreateModal(true)}>
        Create Transaction
      </Button>
    ),
  }}
>
  {/* rows */}
</ElegantGrid>

Peer Dependencies

{
  "react": ">=18.0.0",
  "react-dom": ">=18.0.0",
  "lucide-react": ">=0.400.0"
}

Note: All UI components (buttons, checkboxes, inputs, selects, etc.) are bundled with this package. You don't need Tailwind CSS or shadcn/ui installed in your project.

TypeScript Exports

// Components & Types
import {
  ElegantGrid,
  // Components for composition API
  ElegantGridHeaders,
  ElegantGridHeaderComponent,
  // Types
  type Header,
  type SortOrder,
  type PagerOptions,
  type PagerLabels,
  type EmptyStateConfig,
  type GridConfig,
  type ResolvedGridConfig,
  type ElegantGridProps,
  type ElegantGridRowProps,
  type ElegantGridCellProps,
  type ElegantGridActionCellProps,
  type ElegantGridHeaderProps,
  type GridContextValue,
  DEFAULT_GRID_CONFIG,
} from '@andreyfedkovich/elegant-grid';

// Styles (import once in your app entry point)
import '@andreyfedkovich/elegant-grid/styles.css';

License

MIT © Andrey Fedkovich