Skip to content

Instantly share code, notes, and snippets.

@logickoder
Last active September 30, 2025 14:22
Show Gist options
  • Select an option

  • Save logickoder/2bc242fd7c5bc4f954673c88d2babe70 to your computer and use it in GitHub Desktop.

Select an option

Save logickoder/2bc242fd7c5bc4f954673c88d2babe70 to your computer and use it in GitHub Desktop.
React Flex Wrap

FlexWrap with Separator

A React component that wraps children like CSS flexbox, but intelligently places separators between items only within the same row — never at the start or end of a row.

Features

  • Smart separator placement - Separators appear only between items on the same row
  • 📏 Respects parent constraints - Works with any container width
  • 🔄 Fully responsive - Automatically recalculates layout on resize
  • 🎯 No edge separators - Never renders separators at row boundaries
  • TypeScript support - Fully typed with proper interfaces

How it works

Uses a two-pass rendering approach:

  1. Measurement pass: Renders items invisibly with flex-wrap to let the browser naturally determine layout
  2. Display pass: Groups items by row based on their measured positions, then renders explicit rows with separators

This gives you the natural wrapping behavior of flexbox while maintaining precise control over separator placement.

Usage

<FlexWrap separator={<span className="mx-2"></span>}>
  <Tag>React</Tag>
  <Tag>TypeScript</Tag>
  <Tag>JavaScript</Tag>
  {/* ... more items */}
</FlexWrap>
import React, {
Children,
Fragment,
ReactNode,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
interface FlexWrapProps {
children: ReactNode;
separator: ReactNode;
}
export default function FlexWrap({ children, separator }: FlexWrapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const separatorRef = useRef<HTMLDivElement>(null);
const [rows, setRows] = useState<Array<Array<number>>>([]);
const childArray = useMemo(() => Children.toArray(children), [children]);
useEffect(() => {
function calculateRows() {
if (!containerRef.current || itemRefs.current.length === 0) return;
const containerWidth = containerRef.current.offsetWidth;
const items = itemRefs.current.filter(
(ref) => ref !== null,
) as HTMLDivElement[];
if (items.length === 0) return;
// Get separator width
const separatorWidth = separatorRef.current?.offsetWidth || 0;
// Calculate rows by manually checking widths
const newRows: Array<Array<number>> = [];
let currentRow: Array<number> = [];
let currentRowWidth = 0;
items.forEach((item, index) => {
const itemWidth = item.offsetWidth;
const separatorNeeded = currentRow.length > 0 ? separatorWidth : 0;
const totalWidthNeeded = currentRowWidth + separatorNeeded + itemWidth;
if (currentRow.length === 0 || totalWidthNeeded <= containerWidth) {
// Item fits on current row
currentRow.push(index);
currentRowWidth = totalWidthNeeded;
} else {
// Item doesn't fit, start new row
if (currentRow.length > 0) {
newRows.push([...currentRow]);
}
currentRow = [index];
currentRowWidth = itemWidth;
}
});
// Add the last row if it has items
if (currentRow.length > 0) {
newRows.push(currentRow);
}
setRows(newRows);
}
calculateRows();
const resizeObserver = new ResizeObserver(calculateRows);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => resizeObserver.disconnect();
}, [children, childArray.length]);
return (
<div ref={containerRef} className="w-full">
{/* Hidden measurement elements */}
<div className="invisible absolute -top-full" aria-hidden="true">
{childArray.map((child, index) => (
<div
key={index}
ref={(el) => {
itemRefs.current[index] = el;
}}
className="inline-block"
>
{child}
</div>
))}
<div ref={separatorRef} className="inline-block">
{separator}
</div>
</div>
{/* Actual render - organized into explicit rows */}
<div className="flex w-full flex-col">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="flex items-center overflow-hidden">
{row.map((childIndex, posInRow) => (
<Fragment key={childIndex}>
<div className="flex flex-shrink-0 items-center">
{childArray[childIndex]}
</div>
{posInRow < row.length - 1 && (
<div className="flex flex-shrink-0 items-center">
{separator}
</div>
)}
</Fragment>
))}
</div>
))}
</div>
</div>
);
}
export default function App() {
const [width, setWidth] = useState(600);
const tags = [
'React',
'TypeScript',
'JavaScript',
'CSS',
'HTML',
'Node.js',
'Python',
'Java',
'Go',
'Rust',
'Swift',
'Kotlin',
];
return (
<div className="mx-auto max-w-4xl p-8">
<h1 className="mb-6 text-2xl font-bold">FlexWrap with Separator Demo</h1>
<div className="mb-6">
<label className="mb-2 block font-medium">
Container Width: {width}px
</label>
<input
type="range"
min="300"
max="900"
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
className="w-full"
/>
</div>
<div
className="rounded-lg border-2 border-gray-300 bg-gray-50 p-4"
style={{ width: `${width}px` }}
>
<FlexWrap separator={<span className="mx-2 text-gray-400">•</span>}>
{tags.map((tag, i) => (
<span
key={i}
className="rounded-full bg-blue-500 px-3 py-1 text-sm text-white"
>
{tag}
</span>
))}
</FlexWrap>
</div>
<div className="mt-8 rounded-lg bg-blue-50 p-4">
<h2 className="mb-2 font-semibold">How it works:</h2>
<ul className="space-y-1 text-sm text-gray-700">
<li>
• Two-pass rendering: measure with flex-wrap, then render with
explicit rows
</li>
<li>• Separators only between items in the same row</li>
<li>• No separator at start or end of any row</li>
<li>• Automatically recalculates on resize</li>
</ul>
</div>
</div>
);
}
@logickoder
Copy link
Author

logickoder commented Sep 30, 2025

image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment