MacBook Showcase Landing Page - React, Vite, TypeScript, Tailwind CSS, GSAP, Three.js, Zustand Frontend Project
A single-page, client-side marketing-style experience inspired by Apple product pages. It combines scroll-driven storytelling (GSAP), interactive 3D (Three.js via React Three Fiber), and a small global state slice (Zustand) so learners can see how modern landing pages mix layout, motion, and WebGL without a traditional backend or REST API.
- Live Demo: https://macbook-ui.vercel.app/
- What you will learn
- Keywords at a glance
- Tech stack & why each piece exists
- Project structure
- Features & how they work
- API, backend & data flow
- Environment variables (
.env) - How to run & scripts
- Build, preview & deploy
- Reusing parts in other projects
- Linting & type-checking
- Further reading
- Conclusion
- How a Vite + React + TypeScript SPA is organized for clarity and scale.
- How GSAP + ScrollTrigger tie scroll position to timelines (pinning, scrubbing, parallax-style motion).
- How React Three Fiber mounts a Three.js scene inside React and loads GLTF models with drei helpers.
- How Zustand shares a few values (color, scale, screen video texture) between UI controls and 3D materials.
- How Tailwind CSS v4 (via
@tailwindcss/vite) keeps styling co-located with components using@layerrules insrc/index.css.
| Keyword / topic | Short meaning in this repo |
|---|---|
| SPA | One HTML shell; all routes are client-side (no Next.js router). |
| ScrollTrigger | GSAP plugin: run animations based on scroll position. |
| R3F | React renderer for Three.js (<Canvas>, <mesh>, hooks). |
| drei | Helpers: useGLTF, useVideoTexture, Environment, etc. |
| GLTF / GLB | 3D model format; .glb is binary, used for MacBook meshes. |
| Zustand | Minimal global store; no Redux boilerplate. |
| Vite | Fast dev server & optimized production bundles. |
Tailwind @theme |
Design tokens (fonts, colors) declared in CSS for v4. |
| Technology | Role here |
|---|---|
| Vite 7 | Dev server, HMR, production build, asset pipeline. |
| React 19 | UI composition, hooks, concurrent-friendly patterns. |
| TypeScript | Types for props, refs, GLTF results, and store shape. |
| Tailwind CSS 4 | Utility classes + @layer components for section layout (see index.css). |
| GSAP 3 | Timelines, easing, ScrollTrigger for scroll-synced motion. |
| @gsap/react | useGSAP hook with proper cleanup in React. |
| three + R3F + drei | 3D scene, models, lights, video textures on the laptop screen. |
| react-responsive | useMediaQuery for mobile vs desktop tuning (e.g. model scale). |
| clsx | Conditional class names in JSX (e.g. active swatches). |
| Zustand | color, scale, texture + setters for product viewer & features. |
Note: This is not a Next.js appβthere is no app/ router, no getServerSideProps, and no built-in API routes. Everything ships as static HTML/JS/CSS plus files under public/.
macbook-ui/
βββ public/ # Static assets (served as-is at /)
β βββ fonts/ # Custom OTF faces
β βββ models/ # MacBook GLB / transformed GLB
β βββ videos/ # Hero, game, feature loops
β βββ robots.txt
β βββ β¦png / .svg # Icons, performance art, etc.
βββ src/
β βββ main.tsx # React root + StrictMode
β βββ App.tsx # Registers ScrollTrigger; composes sections
β βββ index.css # Tailwind import + @layer component styles
β βββ vite-env.d.ts # Vite client types (e.g. CSS modules)
β βββ constants/index.ts # Nav links, features copy, image lists, etc.
β βββ store/index.ts # Zustand Macbook store
β βββ types/macbookGltf.ts# Shared typing helper for GLTF + meshes
β βββ components/
β βββ NavBar.tsx
β βββ Hero.tsx
β βββ ProductViewer.tsx
β βββ Showcase.tsx
β βββ Performance.tsx
β βββ Features.tsx
β βββ Highlights.tsx
β βββ Footer.tsx
β βββ models/ # Macbook.tsx, Macbook-14.tsx, Macbook-16.tsx
β βββ three/ # StudioLights.tsx, ModelSwitcher.tsx
βββ docs/ # Internal design / guardrail notes (optional read)
βββ index.html # Entry HTML + SEO + preload hints
βββ vite.config.js # Plugins, manualChunks for heavy vendors
βββ vercel.json # Security headers + long cache for /assets/*
βββ tsconfig*.json # TypeScript project references
βββ eslint.config.js # Flat ESLint + TypeScript rules
βββ LICENSE # MIT
βββ README.md # This file
- Static header: logo, faux nav links, search/cart icons.
- Styles live under
@layer components { header { β¦ } }insrc/index.css.
- Title image + hero MP4 (
playbackRatebumped inuseEffect). - Plain Buy button (no server actionβpurely presentational).
<Canvas>from@react-three/fiberwithStudioLights+ModelSwitcher.- Zustand drives swatches (14β³ / 16β³) and body color;
PresentationControls+ GSAP fade/slide between two GLTF groups.
- Full-width video + masked logo; ScrollTrigger
pin+scrubtimeline on desktop to sync mask scale and text opacity.
- Collage of performance images with a scrubbed GSAP timeline repositioning images on desktop; paragraph fades in on scroll.
- Pinned feature canvas: MacBook model rotates on scroll; feature videos preloaded in
useEffect; texture swaps coordinated with.box1β¦.box5opacity tweens.
- Masonry-style cards; GSAP reveals columns on scroll (different trigger on mobile vs desktop).
- Static links and copyright lineβno fetch.
GSAP registration (once at app level):
import gsap from "gsap";
import { ScrollTrigger } from "gsap/all";
gsap.registerPlugin(ScrollTrigger);Zustand store (shared 3D + UI state):
import { create } from "zustand";
const useMacbookStore = create<MacbookState>()((set) => ({
color: "#2e2c2e",
setColor: (color) => set({ color }),
scale: 0.08,
setScale: (scale) => set({ scale }),
texture: "/videos/feature-1.mp4",
setTexture: (texture) => set({ texture }),
reset: () =>
set({ color: "#2e2c2e", scale: 0.08, texture: "/videos/feature-1.mp4" }),
}));
export default useMacbookStore;| Question | Answer |
|---|---|
| REST / GraphQL API? | None. All content is static JSON/arrays in src/constants/index.ts or hardcoded JSX. |
| Server-side rendering? | No. First paint is the built SPA; crawlers see index.html meta + shell. |
| Where do videos/models live? | Under public/βURLs like /videos/hero.mp4 resolve at runtime from the deployed origin. |
| Authentication? | None. |
If you later add a real backend, you would typically introduce fetch calls, optional React Query / TanStack Query, and environment-based base URLs (see Environment variables).
You do not need any .env file to run or build this projectβthere are no secret keys, database URLs, or third-party API tokens in the codebase today.
Optional pattern for future work (Vite convention: only variables prefixed with VITE_ are exposed to client code):
-
Create
.env.local(gitignored) in the project root:# .env.local (example β not required for this repo) VITE_PUBLIC_APP_NAME=MacBook Pro Landing -
Read in code (after you actually need it):
const appName = import.meta.env.VITE_PUBLIC_APP_NAME ?? "MacBook Pro Landing Page";
-
Never commit real secrets. Use Vercel/hosting Environment Variables UI for production values.
Prerequisites: Node.js 20+ (LTS recommended) and npm.
# Install dependencies
npm install
# Start dev server (default http://localhost:5173)
npm run dev| Script | What it does |
|---|---|
npm run dev |
Vite dev server with hot reload. |
npm run build |
TypeScript-aware production build to dist/. |
npm run preview |
Serves the production build locally for testing. |
npm run lint |
ESLint over src/ (and config files). |
npm run type-check |
tsc -b β full project type check, no emit. |
npm run build
npm run previewVercel (current demo host): connect the Git repo or deploy the dist/ output as a static site. vercel.json adds security headers and long-lived caching for hashed assets under /assets/. public/robots.txt guides crawlers.
| Piece | How to reuse |
|---|---|
| Section components | Copy a component + matching @layer block from index.css, or move styles into modules. |
| Zustand store | Extract store/index.ts into a package or lib/store in a monorepo; keep types alongside. |
| R3F canvas | Wrap <Canvas> in Suspense; preload GLBs with useGLTF.preload in the same module as use. |
| GSAP ScrollTrigger | Always registerPlugin once; kill/refresh triggers on route change if you add a router later. |
| Constants | Replace src/constants/index.ts with CMS/API-driven data when you outgrow static arrays. |
npm run lint
npm run type-checkESLint uses the flat config (eslint.config.js) with TypeScript ESLint, React Hooks, and React Refresh rules suited for Vite.
MacBook UI is a focused learning sandbox: one scroll narrative, two major βwowβ layers (motion + 3D), and a tiny amount of shared state. It intentionally avoids backend complexity so you can study layout β animation β WebGL in isolation, then bolt on APIs or a meta-framework when your product needs them.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.wistant.dev.
Enjoy building and learning! π
Thank you! π
