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 localStorage and 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).
  • cn Utility: For conditional class names.

Installation

Install the required dependencies:

npm install framer-motion lucide-react

Usage

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.tsx

Paste 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 localStorage on 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 (grid or list).
  • onViewChange: Callback to update the view.

Features:

  • Toggle Buttons: Uses motion.div for 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, and Other components for custom content.
  • Custom Animations: Tweak animation settings by adjusting motion props.

Limitations

  • Hardcoded Animation Values: The highlight animation uses hardcoded x values for simplicity. These can be calculated dynamically for a more flexible layout.

Credits

Built by Bossadi Zenith