npm package: @hanamura/react-containers
Description: Flexible container components for React
Author: Taro Hanamura
Latest version: 1.4.5

# ⚛️ React Containers

Flexible and responsive container components for React applications.

## Installation

```bash
npm install @hanamura/react-containers
# or
yarn add @hanamura/react-containers
```

## Framework Compatibility

### Next.js App Router

All components in this library include the `'use client'` directive, making them fully compatible with Next.js App Router without any additional configuration.

```tsx
// page.tsx or layout.tsx in Next.js App Router
import { Stack, Tile } from '@hanamura/react-containers'

export default function Page() {
  return (
    <Stack options={{ gap: 16 }}>
      <h1>My Page</h1>
      <Tile options={{ gap: 16 }}>
        {/* Content */}
      </Tile>
    </Stack>
  )
}
```

## Basic Usage

```tsx
import { Tile, Stack, Cluster, Reel, Switcher } from '@hanamura/react-containers'

// Define your breakpoints
type MyBreakpoints = 'mobile' | 'tablet' | 'desktop'

// Define your media queries
const queries: Array<[MyBreakpoints, { query: string }]> = [
  ['mobile', { query: '(max-width: 599px)' }],
  ['tablet', { query: '(min-width: 600px) and (max-width: 1199px)' }],
  ['desktop', { query: '(min-width: 1200px)' }],
]

// Usage example
function App() {
  return (
    <div>
      {/* Combine default settings with context-specific overrides */}
      <Tile<MyBreakpoints>
        // Default settings applied to all contexts
        options={{
          columns: 1,
          gap: 16,
        }}
        // Media queries
        queries={queries}
        // Context-specific overrides
        adaptiveOptions={{
          tablet: { columns: 2 },
          desktop: { columns: 4, gap: 24 },
        }}
      >
        {/* Content */}
      </Tile>
    </div>
  )
}
```

## Practical Usage in Projects

When using the same media queries throughout your project, it's recommended to create shared breakpoint definitions and container wrappers. This approach reduces repetition and improves consistency across your application.

### 1. Create App Configuration (app-config.ts)

```tsx
// Define your application breakpoints
export type AppBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'

// Define breakpoint values in pixels
export const breakpointValues = {
  xs: 0, // Extra small devices
  sm: 600, // Small devices like phones
  md: 960, // Medium devices like tablets
  lg: 1280, // Large devices like laptops
  xl: 1920, // Extra large devices like desktops
}

// Generate media queries
// IMPORTANT: Order matters! Queries should be arranged from most general to most specific.
// The last matching query in this array will be used for adaptive options.
export const queries: Array<[AppBreakpoint, { query: string }]> = [
  // Most general query first (matches all viewport widths)
  ['xs', { query: '(min-width: 0px)' }],
  // More specific queries follow
  ['sm', { query: `(min-width: ${breakpointValues.sm}px)` }],
  ['md', { query: `(min-width: ${breakpointValues.md}px)` }],
  ['lg', { query: `(min-width: ${breakpointValues.lg}px)` }],
  ['xl', { query: `(min-width: ${breakpointValues.xl}px)` }], // Most specific, highest priority
]

// Define standardized spacing values using CSS variables
export type AppSpacing =
  | 'var(--spacing-xs)'
  | 'var(--spacing-sm)'
  | 'var(--spacing-md)'
  | 'var(--spacing-lg)'
  | 'var(--spacing-xl)'

// Convenience object for accessing spacing values
export const spacing: Record<string, AppSpacing> = {
  xs: 'var(--spacing-xs)',
  sm: 'var(--spacing-sm)',
  md: 'var(--spacing-md)',
  lg: 'var(--spacing-lg)',
  xl: 'var(--spacing-xl)',
}

// Helper function for creating adaptive options (optional)
export function createAdaptiveOptions<T>(
  options: Partial<Record<AppBreakpoint, Partial<T>>>
) {
  return options
}
```

### 2. Define Container Defaults (optional)

```tsx
import { createAdaptiveOptions, AppBreakpoint, spacing } from './app-config'

// Container default settings
export const containerDefaults = {
  // Tile default settings
  tile: {
    // Default values applied to all contexts
    defaults: {
      columns: 1,
      gap: spacing.md,
    },
    // Context-specific overrides
    adaptive: createAdaptiveOptions<{
      columns: number
      gap: AppSpacing
    }>({
      sm: { columns: 2 },
      md: { columns: 3 },
      lg: { columns: 4, gap: spacing.lg },
      xl: { columns: 6 },
    }),
  },

  // Other container settings...
}
```

### 3. Create App-Specific Container Components (AppContainers.tsx)

This approach offers significant advantages:

- Pre-configured with your app's breakpoints and queries
- Standardized spacing across components
- No need to repeat type parameters or queries in multiple places
- Makes it easier to enforce UI consistency

```tsx
import {
  Cluster,
  ClusterProps,
  Reel,
  ReelProps,
  Stack,
  StackProps,
  Switcher,
  SwitcherProps,
  Tile,
  TileProps,
} from '@hanamura/react-containers'

import { AppBreakpoint, AppSpacing, queries } from './app-config'

// Create pre-configured components with your app's setup
export function AppTile(props: TileProps<AppBreakpoint, AppSpacing>) {
  return <Tile<AppBreakpoint, AppSpacing> queries={queries} {...props} />
}

export function AppStack(props: StackProps<AppBreakpoint, AppSpacing>) {
  return <Stack<AppBreakpoint, AppSpacing> queries={queries} {...props} />
}

export function AppCluster(props: ClusterProps<AppBreakpoint, AppSpacing>) {
  return <Cluster<AppBreakpoint, AppSpacing> queries={queries} {...props} />
}

export function AppReel(props: ReelProps<AppBreakpoint, AppSpacing>) {
  return <Reel<AppBreakpoint, AppSpacing> queries={queries} {...props} />
}

export function AppSwitcher(props: SwitcherProps<AppBreakpoint>) {
  return <Switcher<AppBreakpoint> queries={queries} {...props} />
}
```

### 4. Use in Your Application (App.tsx)

```tsx
import React from 'react'
import { AppTile, AppStack } from './components/AppContainers'
import { spacing } from './app-config'

const App = () => {
  const images = [
    { src: '/photo1.jpg', alt: 'Photo 1' },
    { src: '/photo2.jpg', alt: 'Photo 2' },
    // ...
  ]

  return (
    <AppStack
      options={{
        gap: spacing.md,
      }}
      adaptiveOptions={{
        md: { gap: spacing.lg },
      }}
    >
      <h1>Photo Gallery</h1>

      {/* Single column layout without specifying columns */}
      <AppTile
        options={{
          gap: spacing.md,
        }}
      >
        <img src="/hero-image.jpg" alt="Hero Image" style={{ width: '100%', height: 'auto' }} />
      </AppTile>

      {/* Multi-column grid with responsive behavior */}
      <AppTile
        options={{
          columns: 2,
          gap: spacing.md,
        }}
        adaptiveOptions={{
          sm: { columns: 1 },
          md: { columns: 2 },
          lg: { columns: 3, gap: spacing.lg },
        }}
      >
        {images.map((image, index) => (
          <img
            key={index}
            src={image.src}
            alt={image.alt}
            style={{ width: '100%', height: 'auto' }}
          />
        ))}
      </AppTile>
    </AppStack>
  )
}
```

## Media Query Priority

When working with adaptive containers, it's important to understand how media queries are prioritized:

1. **Order Matters**: Queries should be defined from most general to most specific.
2. **Last Match Wins**: When multiple media queries match, the last one in your array will be used.
3. **Priority Example**:
   ```typescript
   // In this example, if both '(min-width: 0px)' and '(min-width: 768px)' match:
   // - The 'desktop' context will be active
   // - The 'desktop' adaptive options will be applied
   const queries = [
     ['mobile', { query: '(min-width: 0px)' }], // More general
     ['desktop', { query: '(min-width: 768px)' }], // More specific (higher priority)
   ]
   ```

This behavior allows you to create intuitive responsive designs where more specific breakpoints override more general ones.

## Available Containers

All containers share common options that can be set both in `options` and `adaptiveOptions`:

### Common Options

- `as`: Lets you change the rendered HTML element (default is `div`)

- `paddingInline`: Controls horizontal padding
- `paddingBlock`: Controls vertical padding

Components also support a rich set of accessibility attributes:

- Standard HTML attributes: `id`, `role`, `tabIndex`
- ARIA attributes: `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-hidden`, `aria-expanded`, `aria-controls`, `aria-disabled`
- Testing attributes: `data-testid`

Additionally, any other custom attributes (like `data-*` attributes) will be forwarded to the rendered element.

Both properties accept the following formats:

- Single value: `paddingInline: value`
- Single value in array: `paddingBlock: [value]`
- Two values in array: `paddingInline: [value1, value2]` (for left/right or top/bottom)

Example with adaptive padding:

```tsx
<Tile
  options={{
    columns: 3,
    // Common padding applied to all contexts
    paddingInline: 16,
    paddingBlock: [8, 24], // [top, bottom]
  }}
  adaptiveOptions={{
    mobile: {
      columns: 1,
      // Override padding for specific contexts
      paddingInline: 8,
      paddingBlock: 8,
    },
    desktop: {
      columns: 4,
      paddingBlock: [16, 32],
    },
  }}
>
  {/* Content */}
</Tile>
```

### Tile

A container that arranges items in a uniform grid layout.

Options:

- `columns`: Number of columns in the grid (defaults to 1 if not specified or if value is less than 2)
- `gap`: Space between grid items (can be a single value or `[rowGap, columnGap]`)

### Stack

A container that stacks items vertically. It's the simplest layout component, focused on doing one thing well.

Options:

- `gap`: Space between stacked items
- `divider`: Either a React element to use as a divider, or a function that returns a divider element (receives `{ index, position }` props)
- `dividerPositions`: Object controlling where dividers appear
  - `start`: Whether to show a divider at the start (default: false)
  - `between`: Whether to show dividers between items (default: true)
  - `end`: Whether to show a divider at the end (default: false)

Example with function dividers:

```tsx
<Stack
  options={{
    gap: 16,
    divider: ({ index, position }) => (
      <hr style={{ margin: 0, borderTop: '1px solid #ccc' }} />
    ),
    dividerPositions: {
      start: false,
      between: true,
      end: false,
    },
  }}
  adaptiveOptions={{
    desktop: {
      gap: 24,
      dividerPositions: {
        between: false, // Disable dividers on desktop
      },
    },
  }}
>
  <div>Item 1</div>
  <div>Item 2</div>
  <div>Item 3</div>
</Stack>
```

Example with React element dividers (Next.js server component compatible):

```tsx
<Stack
  options={{
    gap: 16,
    divider: <hr className="my-divider" />,
    dividerPositions: {
      start: false,
      between: true,
      end: false,
    },
  }}
>
  <div>Item 1</div>
  <div>Item 2</div>
  <div>Item 3</div>
</Stack>
```

Example with custom HTML element:

```tsx
// When changing the HTML element using the 'as' prop
<Stack<string, string>
  as="ul"
  options={{ gap: 16 }}
  style={{ listStyle: 'none', padding: 0, margin: 0 }}
>
  <li>List item 1</li>
  <li>List item 2</li>
  <li>List item 3</li>
</Stack>
```

For more complex layouts, use Stack in combination with Tile or Cluster components.

### Cluster

A container that arranges items in a wrapping flex layout.

Options:

- `gap`: Space between flex items (can be a single value or `[rowGap, columnGap]`)
- `align`: Cross-axis alignment
- `justify`: Main-axis alignment

### Reel

A container that creates a horizontally scrollable area for creating carousels, galleries, and other swipeable content.

Options:

- `gap`: Space between items
- `hideScrollbar`: Whether to hide the scrollbar (default: false)
- `snap`: Scroll snapping behavior ('none', 'mandatory', or 'proximity')
- `columns`: Number of columns or column width:
  - When number: Container width is divided into that many equal columns
  - When string: Each column gets the exact width specified (e.g., '150px')

**Note**: When using the `snap` option, you need to add `scroll-snap-align` CSS property to child elements yourself:

```css
.reel-item {
  scroll-snap-align: center; /* or start, end */
  flex-shrink: 0; /* Prevent items from shrinking */
}
```

Example with horizontal scrolling:

```tsx
<Reel
  options={{
    gap: 16,
    hideScrollbar: true,
    snap: 'mandatory',
  }}
>
  <div className="reel-item">Item 1</div>
  <div className="reel-item">Item 2</div>
  <div className="reel-item">Item 3</div>
</Reel>
```

Example with custom styling:

```tsx
<Reel
  options={{
    gap: 8,
    paddingInline: 16,
    snap: 'proximity',
  }}
  style={{ maxWidth: '100%' }}
>
  <div className="reel-item">Item 1</div>
  <div className="reel-item">Item 2</div>
  <div className="reel-item">Item 3</div>
</Reel>
```

Example with fixed width columns:

```tsx
<Reel
  options={{
    gap: 16,
    columns: '150px', // All items will be exactly 150px wide
    snap: 'mandatory',
  }}
>
  <div className="reel-item">Item 1</div>
  <div className="reel-item">Item 2</div>
  <div className="reel-item">Item 3</div>
</Reel>
```

Example with equal division columns:

```tsx
<Reel
  options={{
    gap: 16,
    columns: 3, // Container divided into 3 equal columns (accounting for gap)
    snap: 'proximity',
  }}
>
  <div className="reel-item">Item 1</div>
  <div className="reel-item">Item 2</div>
  <div className="reel-item">Item 3</div>
</Reel>
```

### Switcher

A component that renders different content based on the active breakpoint. This is useful for completely changing the UI at different screen sizes or for inline content variations.

Options:

- `content`: The content to display at the current breakpoint

The Switcher component differs from other container components in these ways:
- It doesn't take children props; instead, it uses the `content` option to determine what to render
- It renders content directly without adding a container element
- It can be used inline within text or other components

Example with responsive content switching:

```tsx
<Switcher
  options={{
    content: <div>Default content for all breakpoints</div>
  }}
  adaptiveOptions={{
    mobile: {
      content: <div>Mobile-specific content</div>
    },
    tablet: {
      content: <div>Tablet-specific content</div>
    },
    desktop: {
      content: <div>Desktop-specific content</div>
    }
  }}
/>
```

Example with complex content:

```tsx
<Switcher
  options={{
    content: <p>Basic layout for small screens</p>
  }}
  adaptiveOptions={{
    desktop: {
      content: (
        <Cluster
          options={{ gap: 24, justify: 'space-between' }}
        >
          <div>Left sidebar</div>
          <div>Main content</div>
          <div>Right sidebar</div>
        </Cluster>
      )
    }
  }}
/>
```

Example with inline usage (unique to Switcher):

```tsx
<p>
  This paragraph contains 
  <Switcher
    options={{ content: <span>default text</span> }}
    adaptiveOptions={{
      tablet: { content: <span className="highlight">highlighted text on tablets</span> },
      desktop: { content: <span className="highlight-large">emphasized text on desktop</span> },
    }}
  />
  that changes based on screen size.
</p>
```

## Browser Compatibility

React Containers is compatible with all modern browsers that support CSS Grid and CSS Custom Properties (CSS Variables):

- Chrome 57+
- Firefox 52+
- Safari 10.1+
- Edge 16+

The library is tested against the following browser targets (defined in `.browserslistrc`):

- Last 2 versions of major browsers
- Browsers with > 0.5% market share
- No dead browsers
- No IE 11 support

## License

MIT