# Editorial fullscreen menu

> A “Menu” button opens a circular curtain that flips the colours, and big serif links rise one after another.

- Category: [Navigation](https://library.aimran.es/components/navigation)
- Built with: React + Motion
- Live editor: https://library.aimran.es/blocks/editorial-fullscreen-menu
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## Where to use it

This fullscreen menu in React suits sites with few pages and a lot of personality: an architecture studio, an interior designer’s portfolio, a small magazine. Four or five links set huge in serif read like a table of contents, and hovering one dims the rest, so the choice feels calm and deliberate.

It works best with short link names; long ones wrap and lose the effect. Start by setting your brand name and accent, then play with the cascade delay. Escape closes it and focus goes back to the button, so keyboard users are covered.

## Code

### React (Motion)

```tsx
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { ArrowUpRight } from "lucide-react";

const EDITORIAL_LINKS = [
  { label: "Projects", title: "Places worth remembering." },
  { label: "Studio", title: "Twelve people, one long table." },
  { label: "Daily", title: "Notes from the workshop." },
  { label: "Contact", title: "Let’s talk about your space." },
];

const EDITORIAL_EASE = [0.22, 1, 0.36, 1] as const;

interface EditorialMenuProps {
  accent?: string;
  stagger?: number;
  brand?: string;
}

export default function EditorialMenu({
  accent = "#ff4d00",
  stagger = 0.06,
  brand = "Estudio Norte",
}: EditorialMenuProps) {
  const [open, setOpen] = useState(false);
  const [active, setActive] = useState(0);
  const [hover, setHover] = useState<number | null>(null);
  const reduce = useReducedMotion();
  const toggleRef = useRef<HTMLButtonElement>(null);
  const firstLinkRef = useRef<HTMLButtonElement>(null);
  const current = EDITORIAL_LINKS[active]!;
  const origin = "at calc(100% - 58px) 40px";

  useEffect(() => {
    if (!open) return;
    firstLinkRef.current?.focus({ preventScroll: true });
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setOpen(false);
        toggleRef.current?.focus();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  const go = (i: number) => {
    setActive(i);
    setOpen(false);
    setHover(null);
    toggleRef.current?.focus();
  };

  const onTop = open ? "text-[#f4f1ea] dark:text-[#141312]" : "";

  return (
    <div className="@container relative flex h-full min-h-[360px] w-full flex-col overflow-hidden bg-[#f4f1ea] text-[#141312] dark:bg-[#141312] dark:text-[#f4f1ea]">
      <header className="relative z-20 flex items-center justify-between px-6 pt-5">
        <span
          className={
            "font-serif text-[19px] italic tracking-tight transition-colors duration-500 " + onTop
          }
        >
          {brand}
        </span>
        <motion.button
          ref={toggleRef}
          type="button"
          aria-expanded={open}
          aria-label={open ? "Close menu" : "Open menu"}
          onClick={() => setOpen((o) => !o)}
          whileTap={{ scale: 0.94 }}
          className={
            "flex h-10 items-center gap-2.5 rounded-full border pl-4 pr-3.5 text-[13px] font-medium outline-none transition-colors duration-500 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-current " +
            (open
              ? "border-white/20 text-[#f4f1ea] focus-visible:ring-offset-[#141312] dark:border-black/15 dark:text-[#141312] dark:focus-visible:ring-offset-[#f4f1ea]"
              : "border-black/15 hover:border-black/40 focus-visible:ring-offset-[#f4f1ea] dark:border-white/20 dark:hover:border-white/50 dark:focus-visible:ring-offset-[#141312]")
          }
        >
          <span className="relative block h-4 w-[3.25rem] overflow-hidden text-left">
            <motion.span
              className="absolute inset-x-0 top-0 flex flex-col leading-4"
              animate={{ y: open ? -16 : 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 32 }}
            >
              <span>Menu</span>
              <span>Close</span>
            </motion.span>
          </span>
          <span className="relative grid h-3 w-4 place-items-center" aria-hidden>
            <motion.span
              className="absolute h-[1.5px] w-4 rounded-full bg-current"
              animate={open ? { y: 0, rotate: 45 } : { y: -3, rotate: 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 28 }}
            />
            <motion.span
              className="absolute h-[1.5px] w-4 rounded-full bg-current"
              animate={open ? { y: 0, rotate: -45 } : { y: 3, rotate: 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 28 }}
            />
          </span>
        </motion.button>
      </header>

      <main className="relative flex flex-1 flex-col justify-end px-6 pb-7">
        <p className="mb-4 flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.22em] text-[#8a847a]">
          <span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: accent }} />
          {"0" + (active + 1)} · {current.label}
        </p>
        <AnimatePresence mode="wait" initial={false}>
          <motion.h2
            key={active}
            initial={{ opacity: 0, y: 20, filter: "blur(6px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            exit={{ opacity: 0, y: -10, filter: "blur(6px)" }}
            transition={{ duration: reduce ? 0 : 0.5, ease: EDITORIAL_EASE }}
            className="max-w-[12ch] font-serif text-[clamp(34px,11cqw,64px)] leading-[0.98] tracking-[-0.02em]"
          >
            {current.title}
          </motion.h2>
        </AnimatePresence>
      </main>

      <AnimatePresence>
        {open && (
          <motion.nav
            key="menu"
            aria-label="Main"
            initial={{ clipPath: "circle(0% " + origin + ")" }}
            animate={{ clipPath: "circle(150% " + origin + ")" }}
            exit={{ clipPath: "circle(0% " + origin + ")" }}
            transition={reduce ? { duration: 0 } : { duration: 0.75, ease: [0.76, 0, 0.24, 1] }}
            className="absolute inset-0 z-10 flex flex-col justify-end bg-[#141312] px-6 pb-6 pt-24 text-[#f4f1ea] dark:bg-[#f4f1ea] dark:text-[#141312]"
          >
            <ul onPointerLeave={() => setHover(null)} className="mb-10">
              {EDITORIAL_LINKS.map((link, i) => {
                const dim = hover !== null && hover !== i;
                return (
                  <li key={link.label} className="overflow-hidden">
                    <motion.button
                      ref={i === 0 ? firstLinkRef : undefined}
                      type="button"
                      onClick={() => go(i)}
                      onPointerEnter={() => setHover(i)}
                      onFocus={() => setHover(i)}
                      aria-current={active === i ? "page" : undefined}
                      initial={{ y: "105%" }}
                      animate={{ y: 0 }}
                      transition={{
                        duration: reduce ? 0 : 0.7,
                        ease: EDITORIAL_EASE,
                        delay: reduce ? 0 : 0.28 + i * stagger,
                      }}
                      className="group flex w-full items-center gap-4 py-1.5 text-left outline-none transition-opacity duration-300"
                      style={{ opacity: dim ? 0.3 : 1 }}
                    >
                      <span className="w-5 font-mono text-[11px] opacity-50">{"0" + (i + 1)}</span>
                      <span
                        className="font-serif text-[clamp(36px,11cqw,60px)] leading-[1.05] tracking-[-0.02em] transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-2 group-focus-visible:translate-x-2"
                        style={{
                          color: active === i ? accent : undefined,
                          fontStyle: active === i ? "italic" : undefined,
                        }}
                      >
                        {link.label}
                      </span>
                      <ArrowUpRight
                        className="ml-auto h-6 w-6 -translate-x-2 opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-70 group-focus-visible:translate-x-0 group-focus-visible:opacity-70"
                        strokeWidth={1.5}
                      />
                    </motion.button>
                  </li>
                );
              })}
            </ul>
            <motion.div
              initial={{ opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{
                duration: 0.4,
                delay: reduce ? 0 : 0.4 + EDITORIAL_LINKS.length * stagger,
              }}
              className="flex items-end justify-between border-t border-current/15 pt-4 text-[12px] opacity-60"
            >
              <span>hola@estudionorte.es</span>
              <span>Valencia · Lisboa</span>
            </motion.div>
          </motion.nav>
        )}
      </AnimatePresence>
    </div>
  );
}
```

## Prompt for AI assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Editorial fullscreen menu” block (Navigation) from AIMRAN Library to my project.

## What it is
A “Menu” button opens a circular curtain that flips the colours, and big serif links rise one after another.

## Chosen settings
- Accent: #ff4d00
- Stagger: 0.06s (from 0.03 to 0.15s)
- Brand: "Estudio Norte"

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with React + TypeScript, with framer-motion for the animation and lucide-react for the icons. If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { ArrowUpRight } from "lucide-react";

const EDITORIAL_LINKS = [
  { label: "Projects", title: "Places worth remembering." },
  { label: "Studio", title: "Twelve people, one long table." },
  { label: "Daily", title: "Notes from the workshop." },
  { label: "Contact", title: "Let’s talk about your space." },
];

const EDITORIAL_EASE = [0.22, 1, 0.36, 1] as const;

interface EditorialMenuProps {
  accent?: string;
  stagger?: number;
  brand?: string;
}

export default function EditorialMenu({
  accent = "#ff4d00",
  stagger = 0.06,
  brand = "Estudio Norte",
}: EditorialMenuProps) {
  const [open, setOpen] = useState(false);
  const [active, setActive] = useState(0);
  const [hover, setHover] = useState<number | null>(null);
  const reduce = useReducedMotion();
  const toggleRef = useRef<HTMLButtonElement>(null);
  const firstLinkRef = useRef<HTMLButtonElement>(null);
  const current = EDITORIAL_LINKS[active]!;
  const origin = "at calc(100% - 58px) 40px";

  useEffect(() => {
    if (!open) return;
    firstLinkRef.current?.focus({ preventScroll: true });
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setOpen(false);
        toggleRef.current?.focus();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  const go = (i: number) => {
    setActive(i);
    setOpen(false);
    setHover(null);
    toggleRef.current?.focus();
  };

  const onTop = open ? "text-[#f4f1ea] dark:text-[#141312]" : "";

  return (
    <div className="@container relative flex h-full min-h-[360px] w-full flex-col overflow-hidden bg-[#f4f1ea] text-[#141312] dark:bg-[#141312] dark:text-[#f4f1ea]">
      <header className="relative z-20 flex items-center justify-between px-6 pt-5">
        <span
          className={
            "font-serif text-[19px] italic tracking-tight transition-colors duration-500 " + onTop
          }
        >
          {brand}
        </span>
        <motion.button
          ref={toggleRef}
          type="button"
          aria-expanded={open}
          aria-label={open ? "Close menu" : "Open menu"}
          onClick={() => setOpen((o) => !o)}
          whileTap={{ scale: 0.94 }}
          className={
            "flex h-10 items-center gap-2.5 rounded-full border pl-4 pr-3.5 text-[13px] font-medium outline-none transition-colors duration-500 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-current " +
            (open
              ? "border-white/20 text-[#f4f1ea] focus-visible:ring-offset-[#141312] dark:border-black/15 dark:text-[#141312] dark:focus-visible:ring-offset-[#f4f1ea]"
              : "border-black/15 hover:border-black/40 focus-visible:ring-offset-[#f4f1ea] dark:border-white/20 dark:hover:border-white/50 dark:focus-visible:ring-offset-[#141312]")
          }
        >
          <span className="relative block h-4 w-[3.25rem] overflow-hidden text-left">
            <motion.span
              className="absolute inset-x-0 top-0 flex flex-col leading-4"
              animate={{ y: open ? -16 : 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 32 }}
            >
              <span>Menu</span>
              <span>Close</span>
            </motion.span>
          </span>
          <span className="relative grid h-3 w-4 place-items-center" aria-hidden>
            <motion.span
              className="absolute h-[1.5px] w-4 rounded-full bg-current"
              animate={open ? { y: 0, rotate: 45 } : { y: -3, rotate: 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 28 }}
            />
            <motion.span
              className="absolute h-[1.5px] w-4 rounded-full bg-current"
              animate={open ? { y: 0, rotate: -45 } : { y: 3, rotate: 0 }}
              transition={{ type: "spring", stiffness: 420, damping: 28 }}
            />
          </span>
        </motion.button>
      </header>

      <main className="relative flex flex-1 flex-col justify-end px-6 pb-7">
        <p className="mb-4 flex items-center gap-2 text-[11px] font-medium uppercase tracking-[0.22em] text-[#8a847a]">
          <span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: accent }} />
          {"0" + (active + 1)} · {current.label}
        </p>
        <AnimatePresence mode="wait" initial={false}>
          <motion.h2
            key={active}
            initial={{ opacity: 0, y: 20, filter: "blur(6px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            exit={{ opacity: 0, y: -10, filter: "blur(6px)" }}
            transition={{ duration: reduce ? 0 : 0.5, ease: EDITORIAL_EASE }}
            className="max-w-[12ch] font-serif text-[clamp(34px,11cqw,64px)] leading-[0.98] tracking-[-0.02em]"
          >
            {current.title}
          </motion.h2>
        </AnimatePresence>
      </main>

      <AnimatePresence>
        {open && (
          <motion.nav
            key="menu"
            aria-label="Main"
            initial={{ clipPath: "circle(0% " + origin + ")" }}
            animate={{ clipPath: "circle(150% " + origin + ")" }}
            exit={{ clipPath: "circle(0% " + origin + ")" }}
            transition={reduce ? { duration: 0 } : { duration: 0.75, ease: [0.76, 0, 0.24, 1] }}
            className="absolute inset-0 z-10 flex flex-col justify-end bg-[#141312] px-6 pb-6 pt-24 text-[#f4f1ea] dark:bg-[#f4f1ea] dark:text-[#141312]"
          >
            <ul onPointerLeave={() => setHover(null)} className="mb-10">
              {EDITORIAL_LINKS.map((link, i) => {
                const dim = hover !== null && hover !== i;
                return (
                  <li key={link.label} className="overflow-hidden">
                    <motion.button
                      ref={i === 0 ? firstLinkRef : undefined}
                      type="button"
                      onClick={() => go(i)}
                      onPointerEnter={() => setHover(i)}
                      onFocus={() => setHover(i)}
                      aria-current={active === i ? "page" : undefined}
                      initial={{ y: "105%" }}
                      animate={{ y: 0 }}
                      transition={{
                        duration: reduce ? 0 : 0.7,
                        ease: EDITORIAL_EASE,
                        delay: reduce ? 0 : 0.28 + i * stagger,
                      }}
                      className="group flex w-full items-center gap-4 py-1.5 text-left outline-none transition-opacity duration-300"
                      style={{ opacity: dim ? 0.3 : 1 }}
                    >
                      <span className="w-5 font-mono text-[11px] opacity-50">{"0" + (i + 1)}</span>
                      <span
                        className="font-serif text-[clamp(36px,11cqw,60px)] leading-[1.05] tracking-[-0.02em] transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-2 group-focus-visible:translate-x-2"
                        style={{
                          color: active === i ? accent : undefined,
                          fontStyle: active === i ? "italic" : undefined,
                        }}
                      >
                        {link.label}
                      </span>
                      <ArrowUpRight
                        className="ml-auto h-6 w-6 -translate-x-2 opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-70 group-focus-visible:translate-x-0 group-focus-visible:opacity-70"
                        strokeWidth={1.5}
                      />
                    </motion.button>
                  </li>
                );
              })}
            </ul>
            <motion.div
              initial={{ opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{
                duration: 0.4,
                delay: reduce ? 0 : 0.4 + EDITORIAL_LINKS.length * stagger,
              }}
              className="flex items-end justify-between border-t border-current/15 pt-4 text-[12px] opacity-60"
            >
              <span>hola@estudionorte.es</span>
              <span>Valencia · Lisboa</span>
            </motion.div>
          </motion.nav>
        )}
      </AnimatePresence>
    </div>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
