Skip to content
← Components

Shared Layout Transition

Shared Layout Transition brings controlled expression to your interface.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/shared-layout-transition.json

Use it

Scroll horizontally for long lines
"use client";
import { SharedLayoutTransition } from "@/components/jez-ui/ui/shared-layout-transition";

export default function Example() {
  return (
    <>
      <SharedLayoutTransition
        items={[
          {
            id: "one",
            title: "Explore the idea",
            description:
              "Open a little space for the unexpected. This panel expands while its neighbours move naturally.",
          },
          {
            id: "two",
            title: "Find the next step",
            description: "A small, clear action is often all you need.",
          },
        ]}
      />
    </>
  );
}

Props & types

Scroll horizontally for long lines
React.ComponentProps<"div"> & {
  items: { id: string; title: string; description: string }[];
  className?: string;
}

Source

View 2 source files

registry/ui/shared-layout-transition.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
import { motion, LayoutGroup, useReducedMotion } from "motion/react";
export function SharedLayoutTransition({
  items,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  items: { id: string; title: string; description: string }[];
  className?: string;
}) {
  const [active, setActive] = React.useState<string | null>(null);
  const id = React.useId();
  const reduce = useReducedMotion();
  return (
    <LayoutGroup id={id}>
      <div {...props} className={cn("grid gap-3", className)}>
        {items.map((i) => (
          <motion.button
            key={i.id}
            layout
            transition={{ duration: reduce ? 0 : 0.3 }}
            onClick={() => setActive(active === i.id ? null : i.id)}
            aria-expanded={active === i.id}
            className="rounded-xl border border-border p-5 text-left"
          >
            <motion.span layout="position" className="block font-medium">
              {i.title}
            </motion.span>
            {active === i.id && (
              <motion.span
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="mt-3 block text-sm text-muted-foreground"
              >
                {i.description}
              </motion.span>
            )}
          </motion.button>
        ))}
      </div>
    </LayoutGroup>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

motion@13.2.0 · clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Respect reduced-motion preferences. Decorative effects must not carry essential information. WebGL scenes include a static fallback and only render while visible.

Read the accessibility and performance guide →