ResponsiveGridLayout
Overview
The ResponsiveGridLayout component is a powerful template for creating responsive grid-based dashboards and content layouts. It automatically adapts from a single column on mobile devices to multi-column grids on tablets, laptops, and large monitors. The component includes support for headers, footers, customizable cards, and various styling options.
Preview
- Preview
- Code
Build once. Ship everywhere.
Responsive Grid Layout
The cards below demonstrate how Ignix UI reflows from a single column on phones to multi-column grids on tablets, laptops, and large monitors.
ignix init
Bootstrap a new workspace with linting, testing, and CI wiring in under 3 minutes.
Security Scan
Audit dependencies, secrets, and config drift before every deploy.
One-command deploys
Target preview, staging, or production environments directly from the CLI.
Realtime metrics
Track build duration, bundle size, and regression signals in one place.
Curated starters
Pick from dashboard, marketing, and application templates optimized for Ignix UI.
Guided workflows
Answer a few prompts and let the CLI scaffold pipelines, docs, and governance.
Touch-ready controls
Tap targets, haptics, and gesture helpers make commands friendly on tablets.
Extension marketplace
Compose official and community plugins for auth, payments, data, and more.
<ResponsiveGridLayout
gap="md"
padding="md"
maxWidth="7xl"
header={<HeaderContent />}
footer={<FooterContent />}
items={gridItems}
/>
Installation
- CLI
- manual
ignix add component responsive-grid-layout
import * as React from "react";
import { cn } from "../../../utils/cn";
export interface ResponsiveGridItem {
id?: string | number;
title: string;
description?: string;
badge?: React.ReactNode;
meta?: string;
statValue?: string;
statChange?: string;
statTrend?: "up" | "down" | "neutral";
media?: React.ReactNode;
footer?: React.ReactNode;
actionLabel?: string;
actionHref?: string;
}
export interface ResponsiveGridLayoutProps {
header?: React.ReactNode;
footer?: React.ReactNode;
items?: ResponsiveGridItem[];
renderItem?: (item: ResponsiveGridItem, index: number) => React.ReactNode;
className?: string;
contentClassName?: string;
gridClassName?: string;
cardClassName?: string;
padding?: "sm" | "md" | "lg";
gap?: "sm" | "md" | "lg";
maxWidth?: "3xl" | "4xl" | "5xl" | "6xl" | "7xl" | "full" | "prose";
minCardHeight?: number;
stickyHeader?: boolean;
stickyFooter?: boolean;
enableHover?: boolean;
}
const paddingMap: Record<
NonNullable<ResponsiveGridLayoutProps["padding"]>,
string
> = {
sm: "px-4 py-6 sm:px-6",
md: "px-4 py-8 sm:px-8 lg:px-10",
lg: "px-6 py-10 sm:px-10 lg:px-12",
};
const gapMap: Record<NonNullable<ResponsiveGridLayoutProps["gap"]>, string> = {
sm: "gap-4",
md: "gap-6",
lg: "gap-8",
};
const maxWidthMap: Record<
NonNullable<ResponsiveGridLayoutProps["maxWidth"]>,
string
> = {
"3xl": "max-w-3xl",
"4xl": "max-w-4xl",
"5xl": "max-w-5xl",
"6xl": "max-w-6xl",
"7xl": "max-w-7xl",
full: "max-w-full",
prose: "max-w-prose",
};
const trendColorMap: Record<
NonNullable<ResponsiveGridItem["statTrend"]>,
string
> = {
up: "text-emerald-500",
down: "text-rose-500",
neutral: "text-muted-foreground",
};
const DefaultGridItem = ({
item,
cardClassName,
minCardHeight,
enableHover,
}: {
item: ResponsiveGridItem;
cardClassName?: string;
minCardHeight: number;
enableHover: boolean;
}) => {
const actionElement = item.footer
? item.footer
: item.actionLabel
? item.actionHref
? (
<a
className="inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
href={item.actionHref}
>
{item.actionLabel}
</a>
)
: (
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>
{item.actionLabel}
</button>
)
: null;
return (
<article
className={cn(
"flex h-full flex-col rounded-2xl border border-border/60 bg-background/90 p-5 shadow-sm transition duration-200",
"focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2",
enableHover && "hover:-translate-y-1 hover:shadow-lg",
"touch-manipulation",
cardClassName
)}
style={{ minHeight: minCardHeight }}
tabIndex={0}
aria-label={item.title}
>
<div className="flex flex-col gap-4">
{(item.badge || item.meta) && (
<div className="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{typeof item.badge === "string" ? (
<span className="inline-flex items-center rounded-full bg-primary/10 px-3 py-1 text-primary">
{item.badge}
</span>
) : (
item.badge
)}
{item.meta && (
<span className="text-[0.7rem] font-medium text-muted-foreground">
{item.meta}
</span>
)}
</div>
)}
<div className="space-y-2">
<h3 className="text-lg font-semibold leading-tight">{item.title}</h3>
{item.description && (
<p className="text-sm leading-relaxed text-muted-foreground">
{item.description}
</p>
)}
</div>
{item.media && (
<div className="rounded-xl border border-dashed border-border/70 bg-muted/30 p-4">
{item.media}
</div>
)}
{item.statValue && (
<div className="flex items-end gap-2">
<span className="text-3xl font-bold tracking-tight">
{item.statValue}
</span>
{item.statChange && item.statTrend && (
<span
className={cn(
"text-sm font-semibold",
trendColorMap[item.statTrend]
)}
>
{item.statTrend === "up" && "▲ "}
{item.statTrend === "down" && "▼ "}
{item.statChange}
</span>
)}
</div>
)}
</div>
{actionElement && (
<div className="mt-auto flex items-center justify-between pt-4">
{actionElement}
</div>
)}
</article>
);
};
export const ResponsiveGridLayout: React.FC<ResponsiveGridLayoutProps> = ({
header,
footer,
items = [],
renderItem,
className,
contentClassName,
gridClassName,
cardClassName,
padding = "md",
gap = "md",
maxWidth = "7xl",
minCardHeight = 220,
stickyHeader = false,
stickyFooter = false,
enableHover = true,
}) => {
const resolvedItems = React.useMemo(() => items ?? [], [items]);
return (
<div
className={cn(
className
)}
>
<div
className={cn(
"mx-auto flex w-full flex-col gap-6",
paddingMap[padding],
maxWidthMap[maxWidth],
contentClassName
)}
>
{header && (
<header
className={cn(
"flex flex-col gap-3 rounded-2xl bg-background/80 p-4 shadow-sm",
stickyHeader &&
"sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80"
)}
>
{header}
</header>
)}
<section
className={cn(
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
"auto-rows-[1fr] touch-pan-y",
gapMap[gap],
gridClassName
)}
aria-live="polite"
>
{resolvedItems.map((item, index) => {
const key = item.id ?? index;
if (!renderItem) {
return (
<DefaultGridItem
key={key}
item={item}
cardClassName={cardClassName}
minCardHeight={minCardHeight}
enableHover={enableHover}
/>
);
}
const customNode = renderItem(item, index);
if (React.isValidElement(customNode)) {
return React.cloneElement(customNode, {
key: customNode.key ?? key,
});
}
return (
<React.Fragment key={key}>{customNode}</React.Fragment>
);
})}
</section>
{footer && (
<footer
className={cn(
"rounded-2xl bg-background/80 p-4 text-sm text-muted-foreground",
stickyFooter &&
"sticky bottom-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80"
)}
>
{footer}
</footer>
)}
</div>
</div>
);
};
ResponsiveGridLayout.displayName = "ResponsiveGridLayout";
Usage
Basic Example
import { ResponsiveGridLayout, type ResponsiveGridItem } from './components/responsive-grid-layout';
const gridItems: ResponsiveGridItem[] = [
{
id: "1",
badge: "Getting started",
title: "ignix init",
description: "Bootstrap a new workspace with linting, testing, and CI wiring.",
statValue: "3 min",
statChange: "avg setup",
statTrend: "neutral",
actionLabel: "Run ignix init",
},
{
id: "2",
badge: "Quality",
title: "Security Scan",
description: "Audit dependencies, secrets, and config drift before every deploy.",
statValue: "12 issues",
statChange: "−4 this week",
statTrend: "up",
actionLabel: "View report",
},
];
function Dashboard() {
return (
<ResponsiveGridLayout
header={<div>Dashboard Header</div>}
footer={<div>Dashboard Footer</div>}
items={gridItems}
/>
);
}
Custom Card Renderer
import { ResponsiveGridLayout, type ResponsiveGridItem } from './components/responsive-grid-layout';
import { Card, CardContent, CardHeader, CardTitle } from './components/card';
function CustomDashboard() {
const items: ResponsiveGridItem[] = [
// ... your items
];
return (
<ResponsiveGridLayout
items={items}
renderItem={(item, index) => (
<Card key={item.id ?? index} variant="premium" interactive="lift" className="h-full rounded-3xl">
<CardHeader>
<CardTitle className="text-2xl">{item.title}</CardTitle>
</CardHeader>
<CardContent>
{item.statValue && (
<p className="text-4xl font-bold">{item.statValue}</p>
)}
</CardContent>
</Card>
)}
/>
);
}
Compact Mobile-First Layout
function CompactLayout() {
return (
<ResponsiveGridLayout
items={gridItems.slice(0, 4)}
gap="sm"
padding="sm"
maxWidth="5xl"
className="bg-background"
/>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
header | React.ReactNode | undefined | Content for the header section |
footer | React.ReactNode | undefined | Content for the footer section |
items | ResponsiveGridItem[] | [] | Array of grid items to display |
renderItem | (item: ResponsiveGridItem, index: number) => React.ReactNode | undefined | Custom render function for items |
padding | "sm" | "md" | "lg" | "md" | Padding size for the container |
gap | "sm" | "md" | "lg" | "md" | Gap between grid items |
maxWidth | "3xl" | "4xl" | "5xl" | "6xl" | "7xl" | "full" | "prose" | "7xl" | Maximum width of the container |
minCardHeight | number | 220 | Minimum height for grid cards in pixels |
stickyHeader | boolean | false | Whether header should be sticky |
stickyFooter | boolean | false | Whether footer should be sticky |
enableHover | boolean | true | Whether to enable hover effects on cards |
className | string | undefined | Additional CSS classes for the root container |
contentClassName | string | undefined | Additional CSS classes for the content wrapper |
gridClassName | string | undefined | Additional CSS classes for the grid |
cardClassName | string | undefined | Additional CSS classes for default cards |
ResponsiveGridItem Interface
| Property | Type | Description |
|---|---|---|
id | string | number | Unique identifier for the item |
title | string | Title of the grid item (required) |
description | string | Description text for the item |
badge | React.ReactNode | Badge or label to display |
meta | string | Metadata text (e.g., "Updated 2h ago") |
statValue | string | Statistic value to display |
statChange | string | Change indicator text |
statTrend | "up" | "down" | "neutral" | Trend direction for styling |
media | React.ReactNode | Media content (images, icons, etc.) |
footer | React.ReactNode | Custom footer content |
actionLabel | string | Label for the action button |
actionHref | string | URL for the action link (creates an anchor tag) |
Responsive Breakpoints
The grid automatically adapts at the following breakpoints:
- Mobile (< 640px): 1 column
- Tablet (≥ 640px): 2 columns
- Desktop (≥ 1024px): 3 columns
- Large Desktop (≥ 1280px): 4 columns
You can override these breakpoints by providing custom gridClassName with your own Tailwind grid classes.
Advanced Configuration
Sticky Header and Footer
<ResponsiveGridLayout
header={<HeaderContent />}
footer={<FooterContent />}
stickyHeader={true}
stickyFooter={true}
items={gridItems}
/>
Custom Grid Classes
<ResponsiveGridLayout
items={gridItems}
gridClassName="grid-cols-1 md:grid-cols-3 2xl:grid-cols-5"
/>
Disable Hover Effects
<ResponsiveGridLayout
items={gridItems}
enableHover={false}
/>