Build an Animated Wellness Website
Use Claude Code and GSAP to build a bilingual hot spring wellness site.
Introduction
30 Second Summary
Premium websites with smooth scroll animations and multiple languages used to take weeks of careful development. With AI-assisted coding tools, you can now deliver client-ready work from raw materials in a fraction of the time.
In this project, you will use Claude Code to build a premium animated bilingual website for a hot spring wellness center. The site features complex scroll-driven animations and a language switcher that toggles all content between English and Traditional Chinese.
What You'll Build
A single-page wellness website with a deep green luxury aesthetic, GSAP ScrollTrigger animations (parallax, pinning, staggered reveals), and a JSON-based internationalization system that switches between English and Traditional Chinese.
By the end of this project, you'll have:
- A Vite-powered project with parallax hero, pinned service sections, and staggered reveal animations powered by GSAP ScrollTrigger.
- A JSON-based internationalization system with a visible language toggle that switches all content between English and Traditional Chinese instantly.
- A responsive, performant site with a luxury green aesthetic that demonstrates professional-grade AI-assisted development with Claude Code.
- Secret Mission: Add a smooth scroll-to navigation bar with active state indicators that animate on scroll.
Are there any prerequisites?
You need Node.js 20.19+ installed, Claude Code installed with an active Claude Pro or Max subscription, and basic comfort with HTML, CSS, and JavaScript.
Set Up the Project
To build a premium animated website, you need a fast development environment and the right animation tools.
In this step, you will scaffold a Vite project for instant hot-reloading, install GSAP for scroll-driven animations, and set up a CLAUDE.md file so Claude Code understands your project from the start.
In this step, get ready to:
- Scaffold a Vite vanilla project and install GSAP.
- Create the project folder structure for styles, scripts, and translations.
- Initialize a CLAUDE.md context file for reproducible Claude Code sessions.
Scaffold the Vite project and install GSAP
Vite is a modern build tool that gives you instant browser updates as you write code. You will use its vanilla JavaScript template to keep the project lightweight.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your Vite project by running this command:
npm create vite@latest
What will this command ask me?
The Vite wizard will prompt you with several questions. Here is what to enter for each:
- Project name: type hot-spring-wellness and press Enter.
- Select a framework: choose Vanilla.
- Select a variant: choose JavaScript.
- Install and start now: choose No. You will install GSAP first and start manually.
- Move into the new project folder and install GSAP by running these commands:
cd hot-spring-wellness
npm install
npm install gsap
What do these commands do?
- cd hot-spring-wellness moves your terminal into the project folder Vite just created.
- npm install downloads all base dependencies Vite needs to run.
- npm install gsap adds the GSAP animation library (version 3.15) to your project as a dependency.
- Verify the dev server works by running:
npm run dev
You should see a local URL printed in the terminal (typically http://localhost:5173). Open it in your browser to confirm the Vite starter page loads.
Don't see a URL in the terminal?
- Make sure you ran npm install before npm run dev. Without dependencies installed, the dev server cannot start.
- Check that you are inside the hot-spring-wellness folder. Run pwd to confirm your current directory ends with hot-spring-wellness.
Ask for help: Why isn't my Vite dev server starting?
- Stop the dev server by pressing Ctrl+C in the terminal. You will start it again later.
Create the project file structure
Your project needs dedicated folders for styles, JavaScript modules, translation files, and client materials.
macOS
- Create the folder structure by running this command:
mkdir -p src/lang src/styles src/js public docs
Windows
- Create the folder structure by running this command in PowerShell:
mkdir src/lang, src/styles, src/js, public, docs
What are these folders for?
- src/styles/ holds your CSS files for the green luxury theme.
- src/js/ holds JavaScript modules for animations and the language switcher.
- src/lang/ holds JSON translation files for English and Traditional Chinese.
- public/ holds static assets that Vite serves without processing.
- docs/ holds client PDF files that Claude Code will read for content extraction.
- Confirm the folders exist by running:
ls
You should see docs, node_modules, public, and src listed in the output.
- Place your two client PDF files (the wellness center brochure and location guide) into the docs/ folder. Drag them into the folder using Finder (macOS) or File Explorer (Windows).
No PDF files yet?
If you do not have client PDFs, you can create placeholder text files in docs/ with sample content about hot spring locations, services, and philosophy. Claude Code will work with whatever content you provide.
Initialize CLAUDE.md
A CLAUDE.md file gives Claude Code persistent instructions about your project. It reads this file at the start of every session, so Claude always knows your tech stack, design direction, and file conventions.
- Start Claude Code in your project by running:
claude
What does this command do?
Running claude inside a project folder launches Claude Code in interactive mode. Claude Code will ask for permission to access files and tools on its first run.
- Once Claude Code is running, type the following command inside the Claude Code session:
/init
Claude Code analyzes your project and generates a starter CLAUDE.md file. Since the project is fresh, the generated file will be minimal.
- Open CLAUDE.md in your editor and replace its contents with the following project context:
# Hot Spring Wellness Website
## Project Overview
A premium bilingual website for a Wellness & Rehabilitation Center featuring hot spring locations.
## Tech Stack
- Vanilla JavaScript with Vite
- GSAP 3.15 with ScrollTrigger for scroll-driven animations
- JSON-based i18n system for English and Traditional Chinese (zh-TW)
## Design Direction
- Deep green luxury color palette with gold accents and cream backgrounds
- Premium typography and generous spacing
- Parallax hero, pinned scroll sections, staggered reveal animations
## Architecture
- Translation files in src/lang/ (en.json, zh-TW.json)
- HTML elements use data-i18n attributes for translation keys
- Styles in src/styles/
- JavaScript modules in src/js/
- Client content PDFs in docs/
## Workflow
- Build section by section, prompting Claude Code for each part
- Use npm run dev to preview changes instantly
- Keep animations performant: only animate transform and opacity
Why write this file manually?
The /init command gives you a starting point, but it cannot know your design direction or content strategy. Replacing the generated file with specific project context means every future prompt to Claude Code produces results aligned with your vision.
- Save CLAUDE.md.
- Confirm Claude Code loaded the file by typing /memory inside the Claude Code session. You should see CLAUDE.md listed among the loaded files.
Claude Code not finding CLAUDE.md?
- Make sure CLAUDE.md is at the project root (inside hot-spring-wellness/), not inside a subfolder.
- If you launched Claude Code before creating the file, exit with /exit and run claude again. Claude Code loads CLAUDE.md at session start.
Ask for help: Why isn't Claude Code reading my CLAUDE.md file?
Your project foundation is in place. You have a fast dev server, the GSAP animation library ready, and a Claude Code session that understands exactly what you are building. Next up, you will feed your client PDFs to Claude Code and build out the full HTML structure.
Extract Content and Build the HTML Structure
Your project skeleton is in place and the dev server is running. Now it is time to turn raw client materials into a structured website.
In this step, you will use Claude Code to read the client PDF files, extract meaningful content (locations, services, philosophies), and generate a complete HTML page with a premium CSS foundation.
In this step, get ready to:
- Feed client PDFs to Claude Code and extract structured content.
- Build a full-page HTML structure with semantic sections and translation-ready attributes.
- Create a CSS file with the deep green luxury color palette and responsive layouts.
Feed PDFs to Claude Code
Claude Code can read files directly from your project directory. The client PDFs contain all the raw material you need: location names, service descriptions, wellness philosophies, and testimonials.
- Switch back to your Claude Code session in the terminal.
- Prompt Claude Code to read and extract the content from the PDF files by typing this:
Read the PDF files in @docs/ and extract all structured content. Organize the
extracted content into these categories:
- Location names and descriptions
- Services and treatments
- Brand philosophy and about text
- Testimonials
- Contact information
Present the organized content so I can review it before we build the HTML.
What does this prompt do?
The @docs/ syntax tells Claude Code to include the files in that directory as context. Claude Code reads the PDF content and organizes it into categories that map directly to the website sections you will build next.
Reviewing the extracted content before generating code lets you catch any misinterpretations early. This is faster than fixing errors in the finished HTML.
- Review Claude Code's output and confirm the content looks accurate.
- If any content is missing or incorrectly categorized, ask Claude Code to re-read specific sections.
PDFs not parsing correctly?
If the PDFs contain scanned images rather than selectable text, Claude Code may struggle to extract content. In that case, copy the text content from the PDFs manually and paste it into a new file called docs/content.txt so Claude Code can read it directly.
Build the full-page HTML structure
With the content extracted, you can now prompt Claude Code to generate the complete page structure. The HTML needs semantic sections for each part of the site, plus data-i18n attributes on every translatable text element so the language switcher can find and swap them later.
- Prompt Claude Code to generate the HTML file by typing this:
Create index.html in the project root with the following requirements:
1. Semantic HTML5 sections: hero, about, locations, services, testimonials, contact
2. Add data-i18n attributes to ALL text elements that need translation
(headings, paragraphs, buttons, labels)
3. Use the content extracted from the PDFs to populate each section
4. Include a <link> to /src/styles/main.css in the <head>
5. Include a <script type="module" src="/src/js/main.js"> before </body>
6. Add a language toggle button in the header (EN / 繁中)
7. Set lang="en" on the <html> element
Use the Vite project structure - index.html goes at the project root,
not inside src/.
Why data-i18n attributes?
The data-i18n attribute is a custom HTML data attribute you place on any element whose text content needs translation. Your JavaScript language switcher will later query all elements with this attribute and replace their text from a JSON translation file.
This approach keeps translation logic in a single HTML file instead of duplicating the entire page per language. It also means animations stay intact when the language switches because no page reload is needed.
- Review the generated index.html and confirm it has all six sections: hero, about, locations, services, testimonials, and contact.
- Check that translatable text elements have data-i18n attributes with descriptive keys (e.g., data-i18n="hero.title", data-i18n="about.description").
- Confirm the <link> tag points to /src/styles/main.css and the <script> tag points to /src/js/main.js.
Why does index.html go at the project root?
In a Vite project, index.html is the entry point to your application. Vite treats it as source code and part of the module graph. It resolves <script> and <link> references from the project root, so paths like /src/styles/main.css work automatically during development.
- Save index.html if Claude Code has not already saved it.
- Refresh your browser at http://localhost:5173. You should see the raw HTML content rendered without any styling.
Seeing a blank page?
Make sure index.html is saved at the project root (the same level as package.json), not inside the src/ folder. Vite only serves the root index.html as the entry point.
Also check that the old scaffolded index.html from the Vite template was replaced, not that you have a duplicate file.
help me fix my blank page at localhost:5173
Establish the green luxury CSS foundation
The visual identity of this site is built on a deep green luxury palette. CSS custom properties let you define the theme colors once and reuse them everywhere. This makes the design consistent and easy to tweak later.
- Prompt Claude Code to create the CSS file by typing this:
Create src/styles/main.css with a premium green luxury theme:
1. CSS custom properties (variables) on :root for:
- Deep forest greens (primary: around #1a3c2a, secondary: #2d5a3f)
- Gold accents (#c4a962 or similar)
- Cream/off-white backgrounds (#faf8f2 or similar)
- Text colors (dark for body, light for dark backgrounds)
2. Premium typography:
- Use a serif font for headings (Georgia or similar system font)
- Sans-serif for body text (system font stack)
- Generous line-height and letter-spacing
3. Responsive grid layouts:
- Locations section: 3-column grid on desktop, 2 on tablet, 1 on mobile
- Services section: flexible grid layout
- Breakpoints at 768px and 375px
4. Base styles:
- Full-width sections with max-width content containers
- Smooth box-sizing and margin resets
- Hero section should be full viewport height
What are CSS custom properties?
CSS custom properties (also called CSS variables) are values you define once with a -- prefix and reuse throughout your stylesheet. For example, --color-primary: #1a3c2a defines a dark green that you reference anywhere with var(--color-primary).
This pattern keeps your color values in one place. When a client says "make the green a bit lighter," you change one line instead of searching through hundreds of declarations.
- Review the generated src/styles/main.css and confirm it has a :root block with color variables for greens, golds, and creams.
- Check that responsive grid rules exist with breakpoints for tablet and mobile layouts.
- Save src/styles/main.css if Claude Code has not already saved it.
- Refresh your browser at http://localhost:5173. You should see the page styled with the deep green color scheme, serif headings, and a full-height hero section.
Styles not showing up?
Confirm that index.html has a <link rel="stylesheet" href="/src/styles/main.css"> in the <head>. The path must start with /src/ so Vite can resolve it from the project root.
Also check that the file is saved at src/styles/main.css (not styles/main.css without the src/ prefix).
help me debug why my CSS is not loading in Vite
Your page now has real client content, semantic HTML structure, and a polished green luxury foundation. Next up, you will bring it to life with GSAP ScrollTrigger animations that make the page feel premium and interactive.
Implement GSAP ScrollTrigger Animations
Your HTML structure and CSS theme are in place. The site looks polished, but it feels static.
Scroll-driven animations are what transform a flat page into a premium, immersive experience. In this step, you will prompt Claude Code to build a full animation suite using GSAP ScrollTrigger. The hero section will parallax, content will pin in place while scrolling, and elements will reveal themselves as visitors scroll down.
In this step, get ready to:
- Create a parallax hero section with pinning and text reveal animations.
- Add staggered fade-in and pinned scrub animations for content sections.
- Add responsive adjustments and accessibility support with gsap.matchMedia().
Hero parallax and entrance animation
A parallax effect makes background layers move at different speeds than foreground content as the user scrolls. Combined with a brief pin (the hero stays fixed while the user scrolls through it), this creates a cinematic entrance.
- Switch to your Claude Code terminal session.
- Prompt Claude Code to create the hero animations by typing the following:
Create src/js/animations.js. Import gsap and ScrollTrigger from "gsap" and "gsap/ScrollTrigger". Register the ScrollTrigger plugin. Then create a hero parallax animation: use ScrollTrigger to move the hero background layer at a slower rate than the foreground content as the user scrolls. Add a GSAP timeline that reveals the hero headline text with a slide-up and fade-in effect. Pin the hero section briefly so it stays in place while the parallax plays out. Use start: "top top" and end: "+=500" for the pin duration.
What is ScrollTrigger pinning?
When you set pin: true on a ScrollTrigger, the trigger element stays fixed in the viewport while the rest of the page scrolls underneath it. GSAP automatically adds spacing below to prevent content from jumping when the pin releases.
The scrub: 1 setting links the animation progress directly to scroll position, creating that smooth parallax feel. A value of 1 means it takes 1 second for the animation to "catch up" to the scrollbar.
- After Claude Code generates the file, verify it includes these key patterns:
- An import gsap from "gsap" statement at the top.
- An import ScrollTrigger from "gsap/ScrollTrigger" statement.
- A gsap.registerPlugin(ScrollTrigger) call before any animations.
- A ScrollTrigger configuration with pin: true on the hero section.
- Save the file and refresh your browser at localhost:5173.
- Scroll slowly past the hero. You should see the hero section stay pinned in place briefly while the background moves at a different speed than the headline text.
Hero not pinning or parallax not visible?
Make sure the hero section in your HTML has enough height for the pin to take effect. If the section is too short, the pin releases before you notice it.
Check that animations.js is imported in your main.js or index.html. If Claude Code did not add the import, ask it to link the animation file to your entry point.
Why isn't my hero parallax animation working in GSAP ScrollTrigger?
Section reveal animations
With the hero animated, the rest of the page needs matching energy. Staggered reveals make content appear progressively as the user scrolls down, and a pinned services section lets you scrub through treatments like a carousel controlled by scrolling.
- Prompt Claude Code to add the section animations by typing:
In src/js/animations.js, add three more animation groups:
1. Staggered fade-in for location cards - as each card scrolls into view, it should fade in and slide up with a stagger delay between cards. Use opacity: 0 and y: 50 as the starting state.
2. A pinned services section - pin the services container and scrub through the individual treatment items as the user scrolls. Use a timeline with scrub: 1 and pin: true.
3. Text slide-up reveals for all section headings - each heading should slide up from below and fade in when it enters the viewport. Use start: "top 80%" so they trigger before reaching center screen.
What does stagger do in GSAP?
When you animate a group of elements (like location cards), stagger adds a time offset between each element's animation start. Instead of all cards appearing at once, they cascade in one after another. This creates a polished, intentional reveal that guides the viewer's eye.
The pinned services section uses scrub: 1 to tie a timeline's progress directly to scroll position. As the user scrolls through the pinned area, different service items animate in and out sequentially.
- After Claude Code adds the animations, verify the file now includes a gsap.to or gsap.from call targeting location cards with a stagger property.
- Confirm there is a second pin: true ScrollTrigger on the services section.
- Check that section headings have their own ScrollTrigger with start: "top 80%" or similar.
- Save and refresh your browser.
- Scroll past the hero into the locations section. You should see cards fade in and slide up one after another as they enter the viewport.
- Continue scrolling to the services section. It should pin in place while individual service items animate through as you scroll.
Cards all appear at once instead of staggering?
Check that the selector targets individual card elements, not the parent container. If gsap.from targets a single wrapper div, there is nothing to stagger between.
If the services section jumps instead of pinning smoothly, make sure animations target elements inside the pinned container, not the pinned element itself. GSAP's docs warn that animating a pinned element directly throws off its internal measurements.
Help me fix staggered card animations that trigger all at once in GSAP ScrollTrigger.
Polish and performance
Professional animations respect two things: the user's device size and their accessibility preferences. Some visitors experience motion sickness from scroll-driven animations. GSAP's gsap.matchMedia() lets you adapt animations based on viewport width and the prefers-reduced-motion media query.
- Prompt Claude Code to add responsive and accessibility adjustments:
In src/js/animations.js, wrap the animations in gsap.matchMedia(). Add conditions for isDesktop: "(min-width: 800px)", isMobile: "(max-width: 799px)", and reduceMotion: "(prefers-reduced-motion: reduce)". On mobile, disable the pinned sections (pin: false) since they don't work well on small screens. When reduceMotion is true, set animation duration to 0 so elements jump to their final state instantly without motion. Make sure all animations only use transform and opacity properties for 60fps performance - no width, height, top, or left animations.
What is gsap.matchMedia()?
This utility runs setup code only when specific media queries match. When a query stops matching (like resizing from desktop to mobile), GSAP automatically reverts all animations created inside that block. No manual cleanup needed.
The reduceMotion: "(prefers-reduced-motion: reduce)" condition detects users who have enabled reduced motion in their operating system. Setting duration to 0 for these users means elements still reach their final visual state, but without any motion that could cause discomfort.
- After Claude Code updates the file, verify the code contains a gsap.matchMedia() call with the conditions object.
- Check that the callback destructures context.conditions to read isDesktop, isMobile, and reduceMotion.
- Confirm no animation uses properties like width, height, top, or left. Only transform (via x, y, scale, rotation) and opacity are performant at 60fps.
- Save and test in the browser at full desktop width. All animations should work as before.
- Open DevTools and toggle the device toolbar to simulate a mobile viewport (below 800px wide). The pinned sections should no longer pin. Content should scroll normally.
- Test the reduced motion path by opening DevTools, pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), typing reduced motion, and selecting the emulation option. Refresh the page. Elements should appear in their final positions without any animation motion.
Animations still play with reduced motion enabled?
Make sure the reduceMotion condition is inside the gsap.matchMedia() conditions object, not in a separate if statement. The conditions object must include the key reduceMotion: "(prefers-reduced-motion: reduce)".
If mobile still shows pinned sections, check that the pin configuration is conditional: pin: isDesktop instead of pin: true.
Help me debug gsap.matchMedia conditions for reduced motion and mobile breakpoints.
Why only transform and opacity?
Browsers can animate transform and opacity on the GPU compositor thread without triggering layout recalculations. Animating width, height, top, or left forces the browser to recalculate layout on every frame, which drops performance below 60fps and causes visible jank during scroll.
Your site now has a full animation suite that responds to scroll, adapts to screen size, and respects accessibility preferences. Next up, you will build the language switching system so all that beautiful content can serve both English and Traditional Chinese visitors.
Build the i18n Language Switching System
Your site has stunning animations that bring the wellness center to life. Now it needs to speak two languages.
A bilingual site requires a system that swaps all visible text instantly without reloading the page. You'll build a JSON-based internationalization system that stores English and Traditional Chinese translations in separate files and swaps them with a single button click.
In this step, get ready to:
- Create English and Traditional Chinese translation JSON files from the extracted content.
- Build a language switcher engine with localStorage persistence.
- Integrate and test both languages with existing GSAP animations.
Create translation JSON files
Every translatable element in your HTML already has a data-i18n attribute with a unique key. The i18n system works by looking up those keys in a JSON file and replacing the element's text content with the matching translation.
What is i18n?
i18n is shorthand for "internationalization" (the 18 letters between "i" and "n"). It refers to the practice of designing software so it can be adapted to different languages without changing the code.
In this project, you'll use a data-attribute pattern: mark HTML elements with data-i18n keys, store translations in JSON files, and use JavaScript to swap the text dynamically.
- Prompt Claude Code to generate translation JSON files from the extracted PDF content by typing this prompt:
Read the content in @index.html and look at all elements with data-i18n attributes. Create two translation JSON files:
1. src/lang/en.json - English translations for every data-i18n key
2. src/lang/zh-TW.json - Traditional Chinese translations for the same keys
The JSON structure should be a flat object where each key matches a data-i18n attribute value, and each value is the translated string. Include all sections: hero tagline, about text, location descriptions, service names, testimonials, and CTA buttons.
For zh-TW.json, translate the content naturally into Traditional Chinese (not simplified). Use appropriate traditional characters throughout.
What does this prompt do?
This prompt tells Claude Code to scan your HTML for every data-i18n attribute, then generate two JSON files with matching keys. The English file captures all the current content, and the Traditional Chinese file provides natural translations.
Using a flat key structure (no nesting) keeps the lookup logic simple. Each key maps directly to a single translatable string.
- Allow Claude Code to create both files when it asks for permission to write to src/lang/en.json and src/lang/zh-TW.json.
- Verify the files were created by running:
ls src/lang/
You should see en.json and zh-TW.json listed in the output.
Don't see the lang folder?
Claude Code may not have created the src/lang/ directory before writing the files. Re-prompt with: "Create the src/lang/ directory first, then write the JSON files inside it."
help me troubleshoot why my src/lang directory wasn't created by Claude Code
- Open src/lang/en.json and confirm it has keys matching your HTML's data-i18n attributes.
- Open src/lang/zh-TW.json and confirm the same keys exist with Traditional Chinese values.
Check for missing keys
Search your index.html for data-i18n and count the unique values. Then check that your JSON files have the same number of keys. Every data-i18n value in the HTML needs a corresponding entry in both JSON files.
Build the language switcher engine
The translation files are ready. Now you need a JavaScript module that fetches the correct JSON file, walks through every data-i18n element in the DOM, and replaces its text content with the matching translation.
- Prompt Claude Code to create the language switcher engine by typing this prompt:
Create src/js/i18n.js with these features:
1. A loadLanguage(lang) function that:
- Fetches the JSON file from src/lang/${lang}.json
- Finds all elements with [data-i18n] attributes
- Updates each element's textContent with the matching translation value
- Saves the language choice to localStorage with key "lang"
- Updates the document's html lang attribute ("en" or "zh-TW")
2. An EN / 繁中 toggle button rendered in the site header that:
- Shows "EN" and "繁中" as clickable options
- Highlights the currently active language
- Calls loadLanguage() when clicked
- Has a class name "lang-toggle" for styling
3. On page load:
- Check localStorage for a saved language preference
- Default to "en" if no preference is saved
- Call loadLanguage() with the saved or default value
Also add the toggle button HTML to the header in index.html, and add styles for .lang-toggle in src/styles/main.css. The toggle should match the luxury green theme.
What does this prompt accomplish?
This single prompt handles three things at once. It creates the src/js/i18n.js module, adds the toggle button HTML to your header, and adds matching CSS styles. Claude Code can coordinate changes across multiple files in one prompt.
The localStorage persistence means a visitor's language choice survives page refreshes. When they return, the site loads in their preferred language automatically.
- Allow Claude Code to create src/js/i18n.js and modify index.html and src/styles/main.css when prompted for permission.
- Switch to your browser and refresh the page at localhost:5173.
- Look for the EN / 繁中 toggle button in the header area.
You should see a language toggle button styled to match the green luxury theme in your site header.
Toggle button not showing up?
Check that the i18n.js file is imported in your index.html or main.js. If Claude Code created the module but didn't import it, prompt: "Import src/js/i18n.js in the main entry point so it runs on page load."
Also verify the toggle button HTML was actually added to the header section of index.html. Open the file and search for lang-toggle.
help me debug why my language toggle button is not visible on the page
Why data-i18n instead of separate HTML pages?
With only two languages, a dynamic JSON-swap approach keeps the project to a single HTML file. This is critical for preserving animation state. Separate HTML files per language would duplicate all GSAP setup and break animation continuity on navigation.
The data-i18n pattern also means no page reload is needed. Text swaps instantly while scroll position and animation timelines stay intact.
Integrate and test both languages
The system is wired up. Now you need to confirm that text swaps correctly, animations still fire properly after switching languages, and the language preference persists across reloads.
- Click the 繁中 option in the toggle button.
- Scroll through the entire page and verify that all text has switched to Traditional Chinese.
- Click EN to switch back and confirm all text returns to English.
All section headings, descriptions, button text, and testimonials should swap between languages cleanly.
- While on the Traditional Chinese version, scroll through the page and check that GSAP animations still trigger correctly.
Why check animations after switching?
Traditional Chinese text is often shorter than English, which can change element heights. If your animations use ScrollTrigger with fixed pixel values for start and end positions, a layout shift could misalign triggers.
If you notice animations firing too early or too late after switching languages, prompt Claude Code to add a ScrollTrigger.refresh() call inside the loadLanguage() function after updating text.
- If animations are misaligned after language switching, prompt Claude Code with this fix:
In src/js/i18n.js, add a ScrollTrigger.refresh() call at the end of the loadLanguage() function, after all text has been updated. This recalculates scroll positions after the layout changes from language switching.
- Test localStorage persistence by switching to Traditional Chinese, then refreshing the page.
- After the refresh, the page should load in Traditional Chinese automatically.
- Open your browser's DevTools console and run:
localStorage.getItem('lang')
You should see "zh-TW" returned if you last selected Traditional Chinese, or "en" if you switched back to English.
Language not persisting on reload?
Check that loadLanguage() calls localStorage.setItem("lang", lang) before or after fetching translations. Also confirm the page-load initialization reads from localStorage and calls loadLanguage() with the stored value.
help me debug why my language preference isn't persisting after page reload
Your wellness site now speaks two languages fluently. Next up, you'll make everything responsive and add final visual polish so the site looks stunning on every screen size.
Responsive Design and Final Polish
Your site now animates beautifully and speaks two languages. There is one problem though. Try shrinking your browser window to a phone-sized width.
Pinned scroll sections that feel premium on desktop become unusable on small screens. The language switcher might be hidden or cramped. In this step, you will prompt Claude Code to add responsive breakpoints, gracefully degrade animations on mobile, and add the final polish that makes this site feel client-ready.
In this step, get ready to:
- Add responsive CSS breakpoints for tablet (768px) and mobile (375px).
- Disable pinned scroll animations on small screens via gsap.matchMedia().
- Add hover states, smooth transitions, and a scroll progress indicator.
Add responsive breakpoints and disable pinning on mobile
Desktop pinned sections that scrub through content on scroll break on phones. The viewport is too short for pinning to feel natural. A premium mobile experience replaces pinning with simple fade-in animations instead.
- In your Claude Code session, prompt Claude Code to add responsive breakpoints and mobile-friendly animation degradation by typing this prompt:
Add responsive CSS breakpoints to src/styles/main.css for tablet (max-width: 768px) and mobile (max-width: 375px). Adjust grid layouts to single columns on mobile. Make the language switcher button larger and easier to tap on small screens. Then in src/js/animations.js, use gsap.matchMedia() to disable all ScrollTrigger pinning on screens smaller than 768px. Replace pinned sections with simple fade-in animations on mobile instead.
What does this prompt do?
This prompt asks Claude Code to make two changes at once. First, it adds CSS media queries that collapse multi-column grids into single columns and enlarge touch targets on small screens. Second, it wraps the pinned GSAP animations inside gsap.matchMedia() so they only run on desktop-sized viewports.
On mobile, simpler fade-in animations replace the complex pinned scrolls. This keeps the site feeling smooth without fighting the smaller viewport.
- Review the changes Claude Code proposes. Accept them when you are satisfied the breakpoints target 768px and 375px.
- Open your browser at localhost:5173 and open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Click the device toolbar icon (or press Cmd+Shift+M / Ctrl+Shift+M) to toggle responsive mode.
- Set the viewport to 768px wide. Scroll down and confirm that previously pinned sections no longer stick. They should fade in smoothly instead.
- Set the viewport to 375px wide. Confirm that grid layouts display in a single column and the language toggle button is easily tappable.
- Click the EN / 繁中 toggle on the mobile viewport. Verify text swaps language correctly without breaking the layout.
Pinning still active on mobile?
If sections still pin on small screens, the gsap.matchMedia() wrapper may not be targeting the correct breakpoint. Ask Claude Code to double check that all ScrollTrigger instances with pin: true are inside the desktop-only media query branch.
Also try a hard refresh (Cmd+Shift+R / Ctrl+Shift+R) to clear any cached JavaScript.
Help me debug why ScrollTrigger pinning is still active on mobile viewports
Add hover states, transitions, and a scroll progress indicator
A premium site responds to interaction. Buttons should shift on hover, sections should transition smoothly, and a scroll progress bar tells visitors how far down the page they are.
- Prompt Claude Code to add visual polish and the scroll progress indicator by typing this prompt:
Add these finishing touches to the site:
1. In src/styles/main.css, add subtle hover states on all interactive elements (buttons, links, cards) using transforms and opacity transitions. Add smooth CSS transitions (0.3s ease) as defaults on interactive elements. Add a loading state style for when language JSON is being fetched.
2. Refine the typography scaling: make headings scale down proportionally on smaller screens using clamp() for font sizes. Ensure spacing between sections feels balanced at every breakpoint.
3. Add a scroll progress indicator: create a thin bar fixed at the very top of the page (full width, 3px tall, using the gold accent color). In index.html, add a div with id="scroll-progress" at the top of the body. In src/js/animations.js, animate its scaleX from 0 to 1 based on overall page scroll progress using GSAP ScrollTrigger.
What is a scroll progress indicator?
A scroll progress indicator is a thin horizontal bar pinned to the top of the viewport. It starts at zero width and grows to full width as the visitor scrolls to the bottom of the page. It gives visitors a sense of how much content remains.
Using GSAP ScrollTrigger to drive the animation means the bar tracks scroll position perfectly. No extra scroll event listeners or manual math required.
- Review Claude Code's proposed changes. Confirm that index.html now has a scroll-progress element near the top of the body.
- Accept the changes when you are happy with the implementation.
- Switch back to your browser and refresh the page.
- Scroll slowly from top to bottom. You should see a thin gold bar at the very top of the viewport that grows from left to right as you scroll down.
- Hover over buttons and cards. You should see subtle scale or opacity transitions on hover.
- Toggle the language switcher. While the JSON loads, you should see a brief loading state before text updates.
Don't see the progress bar?
Check that index.html contains a <div id="scroll-progress"></div> element. If it is missing, ask Claude Code to add it as the first child of <body>.
Also verify that src/styles/main.css gives #scroll-progress a fixed position at the top, a height of about 3px, and transform-origin: left so it scales from the left edge.
Help me figure out why my scroll progress indicator is not showing up
- Test the full site one more time at desktop, tablet (768px), and mobile (375px) widths. At every size, confirm that animations play smoothly, text is readable, and the language toggle works.
Use Lighthouse for a final check
Open DevTools and run a Lighthouse performance audit. GSAP animations using only transform and opacity should score well. If Lighthouse flags layout shifts, ask Claude Code to fix them.
Secret mission
Animated Scroll-To Navigation Bar
Your site looks stunning, but navigating it means scrolling blindly. Premium sites guide users with a fixed navigation bar that animates in and out as they scroll. Can you add one that smoothly scrolls to each section, highlights the active section, and switches language with the rest of the site?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. The only resources to manage are local files and processes on your machine.
Resources you used:
- Vite dev server (running process in your terminal).
- Project folder on your Desktop (HTML, CSS, JS, JSON, and node_modules).
- Client PDF files in the docs/ folder.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep iterating on the animations and design.
- Your Vite dev server continues running in the terminal.
- Your project files stay on your Desktop.
- Node.js and Claude Code remain installed on your machine (these were pre-existing prerequisites).
Pause - I'll come back to this later
Shut down the running dev server to free up your terminal and system resources. Your project files stay on disk for later.
- Switch to the terminal where your Vite dev server is running.
- Press Ctrl+C to stop the server.
- Your project folder remains on the Desktop. When you are ready to resume, open a terminal in that folder and run npm run dev to start up again.
Delete - I don't want to use this again
Remove all project files from your machine. This cannot be undone.
- Switch to the terminal where your Vite dev server is running and press Ctrl+C to stop it.
- Delete your project folder by running the commands below.
On macOS/Linux:
cd ~/Desktop
rm -rf your-project-folder
On Windows:
cd ~\Desktop
rmdir /s /q your-project-folder
Replace with your actual folder name
Replace your-project-folder with the name you chose when running npm create vite@latest in Step 1.
- Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir ~\Desktop (Windows). Your project folder should no longer appear.
That's a Wrap!
That's a Wrap!
Nice work! You just built a premium, fully animated bilingual website for a hot spring wellness center using Claude Code to accelerate the entire process from raw client PDFs to a polished, deployed site.
You've learned how to:
- Implemented complex GSAP ScrollTrigger animations including a parallax hero, pinned scroll sections, staggered card reveals, and text entrance animations.
- Built a JSON-based internationalization system with English and Traditional Chinese language switching, persisted across page reloads using localStorage.
- Used Claude Code to extract structured content from client PDFs and transform it into a responsive, accessible, production-ready website with a luxury green aesthetic.
- Secret Mission: Added an animated scroll-to navigation bar with active state indicators and show/hide on scroll behavior.
Ready to quiz yourself?