Skip to content

Instantly share code, notes, and snippets.

@kirkegaard
Created October 2, 2025 20:12
Show Gist options
  • Save kirkegaard/42be866b8aa5e571d1958c9c0bbc7ed6 to your computer and use it in GitHub Desktop.
Save kirkegaard/42be866b8aa5e571d1958c9c0bbc7ed6 to your computer and use it in GitHub Desktop.
/* Container styles */
.container {
display: flex;
flex-wrap: var(--wrap, wrap);
flex-direction: var(--direction, row);
width: 100%;
margin: calc(var(--spacing, 0px) / -2);
box-sizing: border-box;
}
/* Item styles */
.item {
padding: calc(var(--spacing, 0px) / 2);
box-sizing: border-box;
flex-basis: var(--size, auto);
flex-grow: 0;
flex-shrink: 1;
max-width: 100%;
}
/* When size is auto */
.item[style*="--size: auto"] {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
}
/* When size is 100% (full width) */
.item[style*="--size: 100%"] {
flex-basis: 100%;
flex-grow: 0;
max-width: 100%;
}
/* Responsive breakpoints */
@media (min-width: 600px) {
.container {
margin: calc(var(--spacing-sm, var(--spacing, 0px)) / -2);
}
.item {
padding: calc(var(--spacing-sm, var(--spacing, 0px)) / 2);
flex-basis: var(--size-sm, var(--size, auto));
}
.item[style*="--size-sm: auto"] {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
}
}
@media (min-width: 900px) {
.container {
margin: calc(
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px))) / -2
);
}
.item {
padding: calc(
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px))) / 2
);
flex-basis: var(--size-md, var(--size-sm, var(--size, auto)));
}
.item[style*="--size-md: auto"] {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
}
}
@media (min-width: 1200px) {
.container {
margin: calc(
var(
--spacing-lg,
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px)))
) /
-2
);
}
.item {
padding: calc(
var(
--spacing-lg,
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px)))
) /
2
);
flex-basis: var(
--size-lg,
var(--size-md, var(--size-sm, var(--size, auto)))
);
}
.item[style*="--size-lg: auto"] {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
}
}
@media (min-width: 1536px) {
.container {
margin: calc(
var(
--spacing-xl,
var(
--spacing-lg,
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px)))
)
) /
-2
);
}
.item {
padding: calc(
var(
--spacing-xl,
var(
--spacing-lg,
var(--spacing-md, var(--spacing-sm, var(--spacing, 0px)))
)
) /
2
);
flex-basis: var(
--size-xl,
var(--size-lg, var(--size-md, var(--size-sm, var(--size, auto))))
);
}
.item[style*="--size-xl: auto"] {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
}
}
import { forwardRef } from "react";
import clsx from "clsx";
import styles from "./Grid.module.css";
type BreakpointValues<T> = {
xs?: T;
sm?: T;
md?: T;
lg?: T;
xl?: T;
};
type ResponsiveValue<T> = T | BreakpointValues<T>;
interface GridProps {
children?: React.ReactNode;
className?: string;
container?: boolean;
size?: ResponsiveValue<number | boolean | "auto">;
spacing?: ResponsiveValue<number>;
columns?: ResponsiveValue<number>;
direction?: ResponsiveValue<
"row" | "row-reverse" | "column" | "column-reverse"
>;
wrap?: "nowrap" | "wrap" | "wrap-reverse";
columnSpacing?: ResponsiveValue<number>;
rowSpacing?: ResponsiveValue<number>;
offset?: ResponsiveValue<number>;
}
const breakpointKeys = ["xs", "sm", "md", "lg", "xl"] as const;
function isResponsiveValue<T>(
value: T | BreakpointValues<T>
): value is BreakpointValues<T> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function generateResponsiveStyles<T>(
value: ResponsiveValue<T>,
property: string
): Record<string, T> {
if (!isResponsiveValue(value)) {
return { [property]: value };
}
const styles: Record<string, T> = {};
breakpointKeys.forEach((breakpoint) => {
if (value[breakpoint] !== undefined) {
styles[`${property}-${breakpoint}`] = value[breakpoint];
}
});
return styles;
}
function calculateFlexBasis(
size: number | boolean | "auto",
columns: number
): string {
if (size === "auto") return "auto";
if (size === true) return "100%";
if (typeof size === "number" && typeof columns === "number") {
return `${(size / columns) * 100}%`;
}
return "100%";
}
export const Grid = forwardRef<HTMLDivElement, GridProps>(
(
{
children,
className,
container = false,
size,
spacing = 0,
columns = 12,
direction = "row",
wrap = "wrap",
columnSpacing,
rowSpacing,
...props
},
ref
) => {
const isContainer = container;
const isItem = !isContainer && size !== undefined;
const baseClass = isContainer ? styles.container : styles.item;
const computedStyles: Record<string, string | number> = {};
if (isContainer) {
// Handle container spacing
const finalSpacing = columnSpacing ?? rowSpacing ?? spacing;
if (isResponsiveValue(finalSpacing)) {
const spacingStyles = generateResponsiveStyles(
finalSpacing,
"--spacing"
);
Object.keys(spacingStyles).forEach((key) => {
computedStyles[key] = `${(spacingStyles[key] as number) * 8}px`;
});
} else {
computedStyles["--spacing"] = `${(finalSpacing as number) * 8}px`;
}
// Handle direction
if (isResponsiveValue(direction)) {
const directionStyles = generateResponsiveStyles(
direction,
"--direction"
);
Object.assign(computedStyles, directionStyles);
} else {
computedStyles["--direction"] = direction;
}
// Handle wrap
computedStyles["--wrap"] = wrap;
}
if (isItem && size !== undefined) {
// Special handling when size is the same object reference as columns
// This means the item should take full width at each breakpoint
if (size === columns) {
computedStyles["--size"] = "100%";
breakpointKeys.forEach((breakpoint) => {
computedStyles[`--size-${breakpoint}`] = "100%";
});
} else if (isResponsiveValue(size)) {
breakpointKeys.forEach((breakpoint) => {
if (size[breakpoint] !== undefined) {
const breakpointColumns = isResponsiveValue(columns)
? columns[breakpoint] || 12
: (columns as number);
const flexBasis = calculateFlexBasis(
size[breakpoint],
breakpointColumns
);
computedStyles[`--size-${breakpoint}`] = flexBasis;
}
});
} else {
const currentColumns = isResponsiveValue(columns)
? 12
: (columns as number);
const flexBasis = calculateFlexBasis(size, currentColumns);
computedStyles["--size"] = flexBasis;
}
}
return (
<div
ref={ref}
className={clsx(baseClass, className)}
style={computedStyles}
{...props}
>
{children}
</div>
);
}
);
Grid.displayName = "Grid";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment