A simple problem: make sure that the different elements in the app are the same height, as if they were in a table.
Let's start with a sample react app that renders 3 cards with different items (styles are omitted, but at the end they are all flex boxes):
const ItemCard = ({ title, items, footerItems, }: { title: string; items: string[]; footerItems: string[]; }) => { return ( <div className="card"> <h2>{title}</h2> <div className="separator" /> <div className="items"> {items.map((item) => ( <p>{item}</p> ))} </div> <div className="separator" /> <div className="footer"> {footerItems.map((footerItem) => ( <p>{footerItem}</p> ))} </div> </div> ); }; export const App = () => { return ( <div> <ItemCard title="Card one" items={['One', 'Two']} footerItems={['One']} /> <ItemCard title="Card two" items={['One', 'Two', 'Three', 'Four']} footerItems={['One', 'Two', 'Three']} /> <ItemCard title="Card three" items={['One']} footerItems={['One']} /> </div> ); };
When you run this app, you will get this result:

The desired result would be something like this:

In order to synchronize the height I came with the following idea: a custom hook that stores the references to all different elements that have to be matched in a {[key: string]: value: array of elements} object, and when there is a change in dependencies, the height of elements gets recalcualted in useLayoutEffect:
import { MutableRefObject, useLayoutEffect } from 'react'; type Target = MutableRefObject<HTMLElement | null>; // Store all elements per key, so it is easy to retrieve them const store: Record<string, Target[]> = {}; // Triggered when useLayoutEffect is executed on any of the components that use useSyncRefHeight hook const handleResize = (key: string) => { // get all elements with the same key const elements = store[key]; if (elements) { let max = 0; // find the element with highest clientHeight value elements.forEach((element) => { if (element.current && element.current.clientHeight > max) { max = element.current.clientHeight; } }); // update height of all 'joined' elements elements.forEach((element) => { if (element.current) { element.current.style.minHeight = `${max}px`; } }); } }; // Add element to the store when component is mounted and return cleanup function const add = (key: string, element: Target) => { // create store if missing if (!store[key]) { store[key] = []; } store[key].push(element); // cleanup function return () => { const index = store[key].indexOf(element); if (index > -1) { store[key].splice(index, 1); } }; }; // Receives multiple elements ([key, element] pairs). This way one hook can be used to handle multiple elements export type UseSyncRefHeightProps = Array<[string, Target]>; export const useSyncRefHeight = (refs: UseSyncRefHeightProps, deps?: any[]) => { useLayoutEffect(() => { // store cleanup functions for each entry const cleanups: (() => void)[] = []; refs.forEach(([key, element]) => { // add element ref to store cleanups.push(add(key, element)); }); return () => { // cleanup when component is destroyed cleanups.forEach((cleanup) => cleanup()); }; }, []); useLayoutEffect(() => { // when any of the dependencies changes, update all elements heights refs.forEach(([key]) => { handleResize(key); }); }, deps); };
By using this hook we can change a bit ItemCard element:
const ItemCard = ({ title, items, footerItems, }: { title: string; items: string[]; footerItems: string[]; }) => { // create ref to the parent container, to only target its children instead of running query on the entire document const itemsRef = useRef(null); const footerRef = useRef(null); // align elements with class items // deps is an empty array, so it will only be aligned when the component is mounted. // You can add your dependencies, or remove it to make sure the hook runs at every render useSyncRefHeight( [ ['items', itemsRef], ['footer', footerRef], ], // trigger hook when items of footerItems changes, since it may change height [items, footerItems], ); return ( <div className="card"> <h2>{title}</h2> <div className="separator" /> <div className="items" ref={itemsRef}> {items.map((item) => ( <p>{item}</p> ))} </div> <div className="separator" /> <div className="footer" ref={footerRef}> {footerItems.map((footerItem) => ( <p>{footerItem}</p> ))} </div> </div> ); };
Now, items and footer elements height will be matched across all cards.