Cards View Icon
Four squares in a 2×2 grid that reorder via Motion layout animation when active, signaling a card view mode.
Installation
File Structure
cards-view-icon.tsx
Usage
import { useState } from "react";
import { CardsViewIcon } from "@/components/unlumen-ui/cards-view-icon";
export default function Example() {
const [isActive, setIsActive] = useState(false);
return (
<button onClick={() => setIsActive((prev) => !prev)}>
<CardsViewIcon isActive={isActive} />
</button>
);
}API Reference
CardsViewIcon
isActiveboolean—When true, the four squares reorder from [TL,TR,BL,BR] to [TR,BR,TL,BL] via Motion layout animation.
className?string—Extra classes applied to the root <span> element.
Notes
- Uses Motion
layoutprop to animate each square to its new grid position. - The active permutation places the right column first, then the left — giving a subtle diagonal feel.
- Each square has a staggered delay of
0.067 s × index, longer than the compact icon to emphasize each card tile. - Set
aria-labelon the parent button to describe the action.
Credits
Built by leo.
Keep in mind
Most components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.
cards-view-icon.tsx
"use client";
import { motion } from "motion/react";
const CARD_DELAY_STEP = 0.067;
// Active permutation: TR BR / TL BL (indices in a 2×2 grid 0-3)
const ACTIVE_ORDER = [1, 3, 0, 2];
const DEFAULT_ORDER = [0, 1, 2, 3];
export interface CardsViewIconProps {
/** Whether the icon is in its active (animated) state. */
isActive: boolean;
/** Extra classes applied to the root element. */
className?: string;
}
export function CardsViewIcon({ isActive, className }: CardsViewIconProps) {
const order = isActive ? ACTIVE_ORDER : DEFAULT_ORDER;
return (
<motion.span
className={`inline-grid h-3 w-3 place-content-center grid-cols-2 gap-[2.5px] ${className ?? ""}`}
aria-hidden="true"
>
{order.map((id, i) => (
<motion.span
key={id}
layout
className="size-[4px] rounded-[1px] bg-current"
animate={isActive ? { opacity: [0.65, 1, 1] } : { opacity: 1 }}
transition={{
duration: 0.42,
delay: i === 0 ? 0 : i * CARD_DELAY_STEP,
ease: "easeOut",
layout: {
duration: 0.45,
delay: i === 0 ? 0 : i * CARD_DELAY_STEP,
ease: "easeInOut",
},
}}
/>
))}
</motion.span>
);
}