Grid to Flex
Convert a grid layout to a flex layout.
Grid to List
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
John Doe
john@doe.com
Overview
The App component allows users to toggle between a Grid View and a List View for displaying items. The component persists the selected view in localStorage and includes smooth animations for layout changes using framer-motion.
Features
- Grid/List Toggle: Users can switch between two display modes.
- Persisted State: The selected view is saved to localStorageand loaded on subsequent visits.
- Smooth Animations: Transitions between views are animated.
- Reusable Subcomponents: Components for items, avatars, and options are provided.
Dependencies
- framer-motion: For animation.
- lucide-react: For icons (- LayoutGrid,- List, and- Trash2).
- cnUtility: For conditional class names.
Installation
Install the required dependencies:
npm install framer-motion lucide-reactUsage
import App from "@/components/App";
 
function MyApp() {
  return <App />;
}Run the following command
It will create a new file Hamburger.tsx inside the components/menu/Hamburger.tsx directory.
mkdir -p components/layouts && touch components/layouts/grid-to-flex.tsxPaste the code
Open the newly created file and paste the following code:
"use client";
 
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { LayoutGrid, List, LucideIcon, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
 
type ViewType = "grid" | "list";
 
const listItems: {
  name: ViewType;
  icon: LucideIcon;
}[] = [
  {
    name: "grid",
    icon: LayoutGrid,
  },
  {
    name: "list",
    icon: List,
  },
];
 
export default function App() {
  const [view, setView] = useState<ViewType>("grid");
 
  // Load view from localStorage on mount
  useEffect(() => {
    const savedView = localStorage.getItem("view");
    if (savedView) {
      setView(savedView as ViewType);
    }
  }, []);
 
  // Memoize the view change handler
  const handleViewChange = useCallback((view: ViewType) => {
    localStorage.setItem("view", view);
    setView(view);
  }, []);
 
  return (
    <div className="size-full overflow-y-auto py-20 rounded-lg">
      <div className="flex flex-col gap-10 max-w-5xl w-full mx-auto p-10">
        <Header view={view} onViewChange={handleViewChange} />
        {view === "grid" ? <GridView /> : <ListView />}
      </div>
    </div>
  );
}
 
type HeaderProps = {
  view: ViewType;
  onViewChange: (view: ViewType) => void;
};
 
const Header = ({ view, onViewChange }: HeaderProps) => {
  return (
    <div className="flex items-center w-full justify-between h-16 border-b border-border pb-10">
      <h1 className="text-4xl font-bold">Grid to List</h1>
      <motion.div
        layout
        className="rounded-lg bg-muted p-1 gap-4 flex items-center relative z-0"
      >
        {listItems.map((item) => (
          <button
            key={item.name}
            onClick={() => onViewChange(item.name)}
            className="size-10 flex items-center justify-center rounded z-10"
          >
            <item.icon aria-hidden="true" className="size-4" />
          </button>
        ))}
        <motion.div
          layoutId="grid-line"
          className="size-10 bg-background absolute rounded-md"
          animate={{
            x: view === "grid" ? 0 : 56, // this value is had coded cause I don't really have the time to calculate  and it's not really centered // but it get the work done lol
            transition: {
              duration: 0.2,
            },
          }}
        />
      </motion.div>
    </div>
  );
};
 
const GridView = () => {
  return (
    <motion.div>
      <div className="grid grid-cols-3 gap-4 ">
        {Array.from({ length: 10 }).map((_, index) => (
          <Item key={index} index={index} className="flex flex-col gap-4">
            <div className="flex items-center justify-between">
              <Avatar index={index} />
              <Options index={index} />
            </div>
            <Other index={index} />
          </Item>
        ))}
      </div>
    </motion.div>
  );
};
 
const ListView = () => {
  return (
    <motion.div className="flex flex-col gap-4">
      {Array.from({ length: 10 }).map((_, index) => (
        <Item
          key={index}
          index={index}
          className="flex items-center justify-between gap-4"
        >
          <Avatar index={index} />
          <Other index={index} />
          <Options index={index} />
        </Item>
      ))}
    </motion.div>
  );
};
 
const Item = ({
  index,
  children,
  className,
}: {
  index: number;
  children: React.ReactNode;
  className?: string;
}) => {
  return (
    <motion.div
      className={cn("bg-muted w-full rounded-lg p-4", className)}
      layoutId={`item-${index}`}
    >
      {children}
    </motion.div>
  );
};
 
const Avatar = ({ index }: { index: number }) => {
  return (
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-4">
        <motion.div
          layoutId={`avatar-${index}`}
          className="size-10 rounded-full bg-background"
        />
        <div className="flex flex-col">
          <motion.h3 layoutId={`name-${index}`} className="text-lg font-bold">
            John Doe
          </motion.h3>
          <motion.p
            layoutId={`email-${index}`}
            className="text-sm text-gray-500"
          >
            john@doe.com
          </motion.p>
        </div>
      </div>
    </div>
  );
};
 
const Options = ({ index }: { index: number }) => {
  return (
    <div className="flex items-center gap-4">
      <motion.button
        layoutId={`delete-${index}`}
        className="size-8 rounded-full border-2 border-background flex items-center justify-center"
      >
        <Trash2 className="size-4 text-gray-500" />
      </motion.button>
    </div>
  );
};
 
const Other = ({ index }: { index: number }) => {
  return (
    <motion.div
      layoutId={`other-component-${index}`}
      className="flex flex-col gap-2 w-full "
    >
      <motion.div
        layoutId={`other-${index}`}
        className="w-full h-2 rounded-full bg-background flex items-center justify-center"
      />
      <motion.div
        layoutId={`other-another-${index}`}
        className="w-1/2 max-w-56 h-2 rounded-full bg-background flex items-center justify-center"
      />
    </motion.div>
  );
};Code Breakdown
1. App Component
The main component initializes the view state (grid or list) and handles its persistence in localStorage.
Key Features:
- useEffect: Loads the saved view from- localStorageon mount.
- handleViewChange: Updates the view state and saves it to- localStorage.
2. Header Component
Displays the title and view toggle buttons.
Props:
- view: Current view mode (- gridor- list).
- onViewChange: Callback to update the view.
Features:
- Toggle Buttons: Uses motion.divfor animated transitions of the active button highlight.
3. GridView and ListView Components
Render the items in their respective layouts.
- GridView: Displays items in a 3-column grid.
- ListView: Displays items in a single column list.
4. Item Component
A container for individual items with animations.
Props:
- index: Unique index for layout animations.
- children: Content of the item.
- className: Additional CSS classes.
5. Avatar Component
Displays an avatar with placeholder user information.
Props:
- index: Unique index for layout animations.
6. Options Component
Provides action buttons for each item, including a delete button.
Props:
- index: Unique index for layout animations.
7. Other Component
Renders additional UI elements for each item.
Props:
- index: Unique index for layout animations.
Styling
The component uses Tailwind CSS for styling. Key classes include:
- size-full: Full width/height container.
- bg-muted: Background for muted sections.
- rounded-lg: Rounded corners.
- shadow-md,- hover:shadow-lg: Box shadow for elevation.
Animations
Layout Animations
framer-motion is used for:
- Layout Changes: Smooth transitions when toggling between views.
- Item Animations: Animations for avatars, options, and other item elements.
Highlight Animation
The active toggle button is highlighted using an animated motion.div.
Customization
- Extendable Items: Modify Item,Avatar,Options, andOthercomponents for custom content.
- Custom Animations: Tweak animation settings by adjusting motionprops.
Limitations
- Hardcoded Animation Values: The highlight animation uses hardcoded xvalues for simplicity. These can be calculated dynamically for a more flexible layout.
Credits
Built by Bossadi Zenith