@hanamura/react-containers v1.4.5
⚛️ React Containers
Flexible and responsive container components for React applications.
Installation
npm install @hanamura/react-containers
# or
yarn add @hanamura/react-containersFramework 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.
// 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
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)
// 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)
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
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)
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:
- Order Matters: Queries should be defined from most general to most specific.
- Last Match Wins: When multiple media queries match, the last one in your array will be used.
- Priority Example:
// 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 isdiv)paddingInline: Controls horizontal paddingpaddingBlock: 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:
<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 itemsdivider: 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 appearstart: 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:
<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):
<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:
// 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 alignmentjustify: Main-axis alignment
Reel
A container that creates a horizontally scrollable area for creating carousels, galleries, and other swipeable content.
Options:
gap: Space between itemshideScrollbar: 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:
.reel-item {
scroll-snap-align: center; /* or start, end */
flex-shrink: 0; /* Prevent items from shrinking */
}Example with horizontal scrolling:
<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:
<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:
<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:
<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
contentoption 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:
<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:
<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):
<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