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
{
  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,
}: {
  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 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 →