Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blairxu13/persona3-website/llms.txt

Use this file to discover all available pages before exploring further.

P3Menu is the central navigation component of the portfolio, rendering a vertical list of five menu items styled after the iconic Persona 3 UI. Each item slides in on mount with a staggered CSS transition, responds to keyboard arrow keys and Enter, and displays a radial glow, a triangular shadow pop, and a bright label highlight whenever it becomes active. The component accepts a single callback prop so the parent can respond to navigation events without coupling to React Router directly.

Props

onNavigate
(page: string) => void
Called when the user selects a menu item — either by pressing Enter on the currently active item or by clicking a row directly. The page string passed to the callback corresponds to the page field of the selected item in the ITEMS array and should match a registered React Router route path.

ITEMS Array

The menu is driven by a static ITEMS array defined at the top of P3Menu.jsx. Each element controls the label text, target page, and all per-item typographic transform values that give the menu its hand-crafted, slanted look. Item shape:
{
  id: string,       // Unique identifier for the item
  label: string,    // Display text rendered in the menu row
  page: string,     // Value passed to onNavigate on selection
  fontSize: number, // Font size in px for this item's label
  offsetX: number,  // marginRight in px — shifts the row horizontally
  offsetY: number,  // marginTop in px — adds vertical breathing room
  skew: number,     // CSS skewX in degrees applied to the label
  skewY: number,    // CSS skewY in degrees applied to the label
}
Default ITEMS array:
const ITEMS = [
  { id: "about",    label: "ABOUT ME",      page: "about",    fontSize: 80, offsetX: 0,  offsetY: 0, skew: -6,  skewY: 10  },
  { id: "resume",   label: "RESUME",        page: "resume",   fontSize: 66, offsetX: 20, offsetY: 8, skew: -11, skewY: -10 },
  { id: "github",   label: "GITHUB LINK",   page: "github",   fontSize: 68, offsetX: 8,  offsetY: 6, skew: 0,   skewY: -4  },
  { id: "socials",  label: "SOCIALS",       page: "socials",  fontSize: 74, offsetX: 16, offsetY: 8, skew: -3,  skewY: 5   },
  { id: "sideproj", label: "SIDE PROJECTS", page: "sideproj", fontSize: 56, offsetX: 10, offsetY: 6, skew: -4,  skewY: 7   },
];
The page value for each item must exactly match a route path registered in your React Router configuration. If the string does not correspond to a valid route, onNavigate will fire but the router will render a fallback or 404 view. See Routing for the full route map.

Keyboard Controls

P3Menu attaches a keydown listener to window inside a useEffect so the user can navigate the menu without a mouse. The listener is cleaned up on unmount and re-registered whenever active changes.
KeyAction
ArrowUpMoves the active index up by one (minimum index: 0)
ArrowDownMoves the active index down by one (maximum: last item)
EnterCalls onNavigate with the page of the active item

CSS Class Reference

The visual identity of the menu is expressed entirely through CSS classes toggled by component state. Below is a description of each key class.
ClassDescription
.p3-rowBase class for each menu item. Starts at opacity: 0 and transform: translateX(36px). Transitions to opacity: 1 and translateX(0) when the .mounted class is added to the container after the 1000 ms delay. Each row receives a staggered transition-delay of i * 80ms.
.p3-row.activeApplied to the currently selected row. Enables the .p3-glow, .p3-shadow-tri, .p3-highlight, and .p3-label-bright sub-elements and switches the label to its red highlight colour.
.p3-label-darkDefault label colour — cyan #3ce2ff. Changes to a deep red #6b0010 when the row is active and brightens to #00d9ff on hover.
.p3-glowA radial gradient element that appears behind the active row, creating a soft bloom effect around the label.
.p3-shadow-triA pink triangle element rendered behind the active label. It plays the p3-shadow-pop keyframe animation each time a new item is activated (see The Shadow Pop Animation).
.p3-hintA keyboard hint element anchored to the bottom-right corner of the menu. It is invisible until the .mounted class is applied, then fades in alongside the menu rows.
.p3-name-tagA watermark element containing the text "jade's / persona", rotated 18 degrees and positioned in the top-left corner of the menu.

Mount Animation

When P3Menu first renders, all .p3-row elements are invisible and offset 36 px to the right via their base CSS. After a 1000 ms delay — giving any preceding page transition time to complete — the component sets mounted to true:
useEffect(() => {
  const t = setTimeout(() => setMounted(true), 1000);
  return () => clearTimeout(t);
}, []);
Setting mounted adds the .mounted class to the menu container, which triggers the CSS transition on every .p3-row simultaneously. Because each row has a different transition-delay (i * 80ms), the items slide in one after another from top to bottom, producing a staggered cascade effect without any JavaScript animation library.

The Shadow Pop Animation

Each time a new item is activated — whether by keyboard or click — the component increments animKey:
const activate = (idx) => {
  setActive(idx);
  setAnimKey(k => k + 1);
};
animKey is passed as the React key prop on the .p3-shadow-tri element. Changing key causes React to unmount and remount the element, which restarts its CSS animation from scratch. This guarantees the triangular pop plays in full every time the active item changes, even if the same item is selected twice in a row. The animation itself is defined by the p3-shadow-pop keyframe:
@keyframes p3-shadow-pop {
  0%   { transform: translateY(-40%) translateX(-12px) scaleX(0) scaleY(1); }
  55%  { transform: translateY(-46%) translateX(-15px) scaleX(1.22) scaleY(1.18); }
  75%  { transform: translateY(-39%) translateX(-11px) scaleX(0.96) scaleY(0.97); }
  100% { transform: translateY(-40%) translateX(-12px) scaleX(1) scaleY(1); }
}
.p3-shadow-tri.pop {
  animation: p3-shadow-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
The animation runs for 0.28 s with cubic-bezier(0.34, 1.56, 0.64, 1) — an overshoot curve that gives the triangle a satisfying elastic snap as it expands.

Adding a Menu Item

To extend the menu with a new entry, append an object to the ITEMS array in P3Menu.jsx. Choose fontSize, offsetX, offsetY, skew, and skewY values that fit visually with the surrounding items.
const ITEMS = [
  // ...existing items...
  {
    id: "contact",
    label: "CONTACT",
    page: "contact",   // must match a registered route
    fontSize: 70,
    offsetX: 12,
    offsetY: 6,
    skew: -5,
    skewY: 8,
  },
];
You will also need to:
1

Register the route

Add a matching <Route path="contact" element={<Contact />} /> in your router configuration. See Routing for details.
2

Create the page component

Build the page component and, if desired, a custom PageTransition variant for it. See PageTransition and Page Transitions.
3

Add a clip shape

P3Menu uses a parallel CLIP_SHAPES array to define the triangular highlight polygon for each row. Add a corresponding clip-path function at the same index as your new item.

Customising Menu Items

Detailed guide to adjusting labels, font sizes, and skew values for each row.

Routing

How React Router v7 route paths map to the page strings used in ITEMS.

Build docs developers (and LLMs) love