Neo Museum
Project Setup
Stack: React 19 + Vite 6 + Tailwind CSS 4 + Motion (Framer Motion) + Lucide React icons + TypeScript
package.json dependencies:
react,react-dom^19.0.1vite^6.2.3@tailwindcss/vite^4.1.14,tailwindcss^4.1.14motion^12.23.24lucide-react^0.546.0@vitejs/plugin-react^5.0.4typescript~5.8.2
Fonts (loaded via Google Fonts in index.css):
- Sans: Inter (weights: 300, 400, 500, 600)
- Mono: JetBrains Mono (weights: 400, 500)
/* index.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
}
@layer utilities {
.text-mega {
font-size: 21vw;
line-height: 0.75;
letter-spacing: -0.04em;
}
}
Global styling: Background #fcfcfc, text #111, selection color bg-black text-white, overflow-x-hidden, font-sans (Inter).
DATA
const chaptersData = [
{ name: "Age of Dinosaurs", image: "https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624247/01_udnber.png" },
{ name: "Fossils of Ancient Life", image: "https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624374/02_pmvxxl.png" },
{ name: "Reptiles of the Mesozoic", image: "https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624236/03_hcp3jc.png" },
{ name: "Marine Fossil Gallery", image: "https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624256/04_get63z.png" },
{ name: "Prehistoric Giants", image: "https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624251/05_kz1tyu.png" }
];
STATE
const [showVideo, setShowVideo] = useState(false);
const [activeChapter, setActiveChapter] = useState(2); // starts at "Reptiles of the Mesozoic"
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
showVideoflips totrueafter a 2800ms delay (setTimeout)activeChapterauto-cycles every 3500ms via setInterval, wrapping(prev + 1) % 5
ANIMATION VARIANTS
const fadeUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
};
const letterBlock = {
initial: { y: 120, opacity: 0 },
animate: {
y: 0,
opacity: 1,
transition: { duration: 1.2, ease: [0.16, 1, 0.3, 1] }
}
};
SECTION 1: HERO (full viewport height)
Container: relative w-full min-h-screen flex flex-col overflow-hidden
1A. HEADER (NHM Logo)
motion.headerwithstaggerChildren: 0.1, delayChildren: 0.1- Padding:
pt-6 px-6 md:px-16,z-20 - The "NHM" logo is a custom inline SVG with
viewBox="0 0 840 100",fill-[#111], full width - The SVG is wrapped in
motion.h1withvariantsthat animate fromscale: 1.03toscale: 1withstaggerChildren: 0.06, delayChildren: 0.1 - Each polygon of each letter uses the
letterBlockvariant (slides up fromy: 120) - Letter N (translate 0,0): Three polygons -- left vertical
0,0 14,0 14,100 0,100, right vertical200,0 214,0 214,100 200,100, diagonal0,0 33,0 214,100 181,100 - Letter H (translate 280,0): Three polygons -- left vertical
0,0 14,0 14,100 0,100, right vertical200,0 214,0 214,100 200,100, crossbar14,43 200,43 200,57 14,57 - Letter M (translate 560,0): Four polygons -- left vertical
0,0 14,0 14,100 0,100, right vertical266,0 280,0 280,100 266,100, left diagonal0,0 26,0 153,100 127,100, right diagonal254,0 280,0 153,100 127,100
1B. SUB-NAV BAR
- Below the SVG logo,
flex justify-between items-start mt-8 - Font:
text-[10px] md:text-[11px] font-mono tracking-[0.2em] uppercase - Uses
fadeUpvariant withduration: 0.8, ease: "easeOut"
Left column (15% width): Three lines -- "Natura" / "History" / "Museum"
Arrow separator (5% width, hidden on mobile): ArrowRight from lucide, size 14, strokeWidth 1, text-gray-400
Center column (flex-1 on mobile, 30% on desktop): "Exploring the story of life on earth through science, discovery and wonder." -- Split differently on desktop (3 lines) vs mobile (4 lines). text-gray-800 leading-relaxed font-mono
Arrow separator (5% width, hidden on mobile): Same as above
Right column (15% width, hidden on mobile): Nav links list -- Visit, Exhibitions, Discover, Learn, About. text-gray-800, hover:text-black hover:underline
Hamburger button (far right, z-60): Two horizontal lines (w-8 h-[1.5px] bg-black), gap-[6px]. Hover: first line shrinks to w-6, second expands to w-10. When open: first rotates 45deg + translateY, second rotates -45deg + translateY (forming an X). Transition: duration-300.
1C. MOBILE MENU OVERLAY
AnimatePresencewrapping amotion.div- Appears below the header, slides in from
y: -20,opacity: 0toy: 0, opacity: 1 bg-[#fcfcfc] border-b border-gray-200 shadow-xl, only visible onmd:hidden- Contains the same nav links as the desktop version,
text-sm font-mono tracking-[0.2em] uppercase,space-y-6
1D. BACKGROUND VIDEO
- Appears after 2800ms delay (controlled by
showVideostate) absolute top-0 left-0 w-full h-full pointer-events-none z-0- Video:
autoPlay loop muted playsInline,w-full h-full object-cover - Video URL:
https://res.cloudinary.com/dsdxaxkiz/video/upload/v1779624998/magnific_use-img-2-as-the-exact-ba_Piu3X0W42C_wnrc8f.mp4
1E. LEFT SIDEBAR CONTENT
motion.divwithstaggerChildren: 0.15, delayChildren: 0.6- Position:
px-10 md:px-16,mt-20 sm:mt-28 md:mt-32,w-[320px],z-10
Section indicator: 01 + horizontal line (w-16 h-[1.5px] bg-black/20), text-xs font-mono
Headline: "TIMELESS WONDERS" -- text-[3.5rem] md:text-[5rem] font-normal tracking-tight leading-[1]. Line break between "TIMELESS" and "WONDERS".
Description: "Step into the natural world and / discover the stories written / millions of years ago." -- text-[13px] md:text-[14px] text-gray-700 w-[240px] leading-[1.6]
CTA Button ("Explore Now"):
- Container:
bg-[#1a1a1a] px-6 py-3.5 border border-[#1a1a1a] rounded-md shadow-sm - Hover: slides up 0.5px, adds
shadow-[3px_3px_0px_rgba(17,17,17,0.5)] - Active: resets translate and shadow
- Has a sliding background panel:
bg-[#fcfcfc]that slides from-translate-x-[101%]totranslate-x-0on hover,duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] - Icon: Custom SVG leaf/plant shape (4 paths forming a stylized leaf), white by default, turns
#111on hover withscale-110 -rotate-12 -translate-y-1transform - Text: "Explore Now",
text-[15px] font-medium, white turning to#111on hover
1F. RIGHT SIDEBAR (hidden on mobile)
motion.divwithstaggerChildren: 0.15, delayChildren: 0.9- Position:
w-[200px] mt-12 md:mt-20,hidden md:flex
Specimen info: "Tyrannosaurus Rex" heading (text-[10px] font-bold font-mono tracking-widest uppercase), subtext "Late Cretaceous period / 68-66 million years ago" (text-[12px] text-gray-600 leading-[1.6])
Stats: "Length" label + "12.3 m" value, "Height" label + "4.0 m" value. Labels: text-[10px] font-mono tracking-widest uppercase text-gray-500. Values: text-[13px] font-medium.
View Details button: Circle (w-10 h-10 rounded-full border border-gray-400) with Plus icon (size 16, strokeWidth 1.5), text "View Details" (text-[10px] font-mono uppercase tracking-widest font-bold). Hover: circle gets border-black bg-[#111], icon turns white.
1G. BOTTOM-LEFT "SCROLL TO EXPLORE"
absolute bottom-10 left-[2.5rem] md:left-[4rem],hidden md:flex- Fade up animation:
delay: 1.2 - Circle (
w-12 h-12 rounded-full border border-gray-300) containing two thin vertical lines (w-[1px] h-[12px] bg-gray-600,gap-[4px]) representing a pause icon - Text: "Scroll to explore" --
text-[10px] font-mono tracking-widest uppercase text-gray-500 font-semibold
SECTION 2: "EXPLORE OUR WORLD"
Container: relative w-full min-h-[75vh] md:min-h-screen bg-[#fcfcfc], flex column centered, pt-24 md:pt-32 pb-0 z-20
2A. SECTION LABEL
[ 02 ] Explore Our World -- text-[10px] md:text-[11px] font-mono tracking-[0.2em], mb-12. "02" in text-gray-500, "Explore Our World" in text-gray-900 font-bold uppercase.
2B. MAIN HEADING
"Unearth the stories of our planet's past through fossils, minerals, and ancient wonders." -- text-[2.2rem] md:text-[3.5rem] lg:text-[4.2rem] leading-[1.1] font-medium tracking-tight text-[#111], max-width 1000px, text-center. Line break on desktop after "past". Animates with whileInView from y: 40, opacity: 0 to y: 0, opacity: 1, once: true, margin -100px.
2C. ACTION PILLS
Five pill buttons in a flex-wrap row, gap-3 md:gap-4, mb-10 md:mb-24. Staggered reveal animation (staggerChildren: 0.1, delayChildren: 0.3). Each pill: rounded-full border border-gray-300 text-[11px] font-medium uppercase tracking-wider bg-white/50 backdrop-blur-sm text-gray-800. Hover: border-black bg-black text-white. Icons from lucide (size 14, strokeWidth 2):
Bone+ "Dinosaurs"Dna+ "Ancient Life"Gem+ "Minerals"Leaf+ "Fossils"BookOpen+ "Learn More"
2D. SPACER
min-h-[220px] md:min-h-[450px] -- provides room for the pterodactyl image from Section 3 to overlap upward.
2E. BOTTOM TEXT
Absolute positioned at bottom, px-8 md:px-16 pb-8 md:pb-12, pointer-events-none. Two text elements at justify-between:
- Left: "WE DON'T JUST TELL STORIES."
- Right: "PALEONTOLOGY (C) 2026"
- Both:
text-[10px] font-mono tracking-widest uppercase text-gray-500 font-medium, hidden on mobile.
SECTION 3: "ANCIENT COLLECTION" (Dark Section)
Container: relative w-full bg-[#0a0a0a] text-white flex flex-col z-30
3A. PTERODACTYL IMAGE (Overlapping)
- Absolute positioned at top, centered horizontally (
left-1/2 -translate-x-1/2) - Width:
w-[160vw] md:w-[1100px] - Image URL:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779625001/ChatGPT_Image_May_23_2026_12_24_44_PM_1_lv1dne.png - Animates with
whileInViewfromy: "-65%", opacity: 0toy: "-78%", opacity: 1,duration: 1.4, ease: "easeOut", viewport margin100px pointer-events-none z-0,mix-blendnot applied here
3B. HEADING AREA
- Padding:
px-8 md:px-16 pt-32 md:pt-48 mb-16,z-10 - Two-column layout on xl (
flex-col xl:flex-row justify-between)
Left -- Main heading: "Curated from millions of years of wonder [3 circle icons] & discovery." -- text-[1.8rem] md:text-[3rem] lg:text-[3.8rem] xl:text-[4rem] leading-[1.15] font-medium tracking-tight text-white. The three circle icons are inline (inline-flex gap-2 md:gap-3 align-middle mx-2 md:mx-4 translate-y-[-4px]), each w-10 h-10 md:w-14 md:h-14 rounded-full border border-gray-600 bg-black text-gray-400. Hover: bg-white text-black border-white. Icons: Bone, Dna, Leaf (size 22).
Right -- Tagline + pills:
- Tagline: "WE DON'T JUST DISPLAY FOSSILS / WE SHARE EARTH'S STORY" --
text-[9px] md:text-[10px] font-mono tracking-widest text-gray-400 uppercase mb-6 leading-relaxed - Three pills: "Educational", "Authentic", "Inspiring" --
px-5 py-2 rounded-full border border-gray-600 text-[9px] font-mono tracking-widest uppercase text-gray-300. Hover:bg-white text-black border-white.
3C. TWO-COLUMN PANEL
Separated by h-[1px] bg-gray-800 line. Flex row on desktop, column on mobile.
Left panel (35% width):
border-r border-gray-800on desktop,border-bon mobilemin-h-[400px] md:min-h-[500px]- Top:
***text (text-gray-500 text-xl tracking-[0.3em]) - Center: Chapter image using
SandTransitionImagecomponent (SVG filter-based sand/dissolve transition). Image:absolute inset-0 w-[80%] h-[80%] m-auto object-contain mix-blend-lighten. UsesAnimatePresence mode="wait". - Bottom: Chapter counter
01 / 05style, with animated number (motion.divslides vertically).text-[10px] font-mono tracking-widest text-[#888] uppercase. Counter numeral color#888, dividertext-[#333].
Right panel (65% width):
- Top bar: "Explore the past. Understand the present." + animated "Chapter 0X" label.
border-b border-gray-800 p-8 text-[10px] font-mono text-gray-400 tracking-widest. - Chapter list: 5 items, each
border-b border-gray-800/80 py-8. Active:text-white, inactive:text-[#444] hover:text-[#999]. Chapter name:text-2xl md:text-[2rem] font-medium tracking-tight. Active item showsArrowUpRighticon (size 22, strokeWidth 1,text-gray-400) that animates in/out. - Clicking a chapter sets
activeChapter.
3D. BOTTOM FOOTER
h-[1px] bg-gray-800divider- Text: "DIGGING INTO OUR PLANET'S PAST" --
px-8 py-8 text-[10px] font-mono tracking-widest text-gray-500 uppercase bg-[#0a0a0a]
SandTransitionImage COMPONENT
A custom component that creates a sand/particle dissolve effect using SVG filters:
function SandTransitionImage({ src, alt, className }) {
// Uses usePresence() from motion/react for AnimatePresence awareness
// Unique filterId per instance via useRef
// requestAnimationFrame loop over 900ms
// Easing: entering = quartic ease-out (1 - Math.pow(1-t, 4)), exiting = cubic (Math.pow(t, 3))
// SVG filter chain:
// 1. feTurbulence: fractalNoise, baseFrequency 1.8, numOctaves 4
// 2. feDisplacementMap: scale up to 150 based on progress
// 3. feOffset: dy up to -80 (enter) or 120 (exit), dx up to -30/+30
// 4. feGaussianBlur: up to 6px
// 5. feColorMatrix: opacity fades (1 - progress * 1.2)
// Image has crossOrigin="anonymous" and referrerPolicy="no-referrer"
}
ALL EXTERNAL ASSET URLs
Video:
https://res.cloudinary.com/dsdxaxkiz/video/upload/v1779624998/magnific_use-img-2-as-the-exact-ba_Piu3X0W42C_wnrc8f.mp4
Images:
- Chapter 1:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624247/01_udnber.png - Chapter 2:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624374/02_pmvxxl.png - Chapter 3:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624236/03_hcp3jc.png - Chapter 4:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624256/04_get63z.png - Chapter 5:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779624251/05_kz1tyu.png - Pterodactyl:
https://res.cloudinary.com/dsdxaxkiz/image/upload/v1779625001/ChatGPT_Image_May_23_2026_12_24_44_PM_1_lv1dne.png
(Note: these are Cloudinary URLs, not CloudFront. The project uses Cloudinary for all hosted media assets.)
KEY DESIGN DETAILS
- Color palette:
#fcfcfc(off-white bg),#111/#1a1a1a(near-black),#0a0a0a(dark section bg). Gray scale via Tailwind:gray-300throughgray-800. - No purple/indigo anywhere. Strictly monochrome black/white/gray.
- Typography hierarchy: Large display headings (3.5-5rem), mono labels (10-11px), body text (13-14px).
- Spacing: 8px base system throughout.
- Transitions: Most hover transitions 300-700ms. Button slide effect uses
cubic-bezier(0.16, 1, 0.3, 1). Letter animations use same cubic bezier. - The page is entirely a single
App.tsxcomponent plus theSandTransitionImagehelper function in the same file.