Official website for Kid Collective, an independent creative agency based in Stockholm, Sweden.
Built with React + Vite + TypeScript, animated with Framer Motion, styled with Tailwind CSS v4.
- Getting Started
- Project Structure
- Tech Stack
- Development
- Adding New Cases
- Animation System
- Deployment
- Environment Variables
- Known Issues
- Node.js v18 or higher
- npm v9 or higher
src/
├── animated-components/ # Scroll and hover animation wrappers
│ ├── AnimatedCaseCard.tsx # Per-card parallax wrapper for cases grid
│ ├── AnimatedFeaturedCaseCard.tsx # Per-card parallax wrapper for featured cases
│ ├── AnimatedFooter.tsx
│ └── StickyGridAnimation.tsx # Scroll-driven two-column parallax grid
├── assets/ # Images, fonts, icons
│ ├── awards/
│ ├── cases/
│ ├── fonts/
│ └── icons/
├── components/ # Reusable UI components
├── data/ # Hardcoded case data (temporary)
│ ├── AddedCasesData.ts
│ └── FeaturedCasesData.ts
├── hooks/ # Custom React hooks
│ └── useIsDesktop.ts # Returns true when viewport is >= 1024px
├── pages/ # Page-level components
├── styles/ # Layout wrappers, global CSS, design tokens
│ ├── CasesGrid.tsx # Infinite scroll wrapper, renders StickyGridAnimation
│ ├── global.css
│ └── variables.css
└── types/
└── Types.ts # Shared TypeScript types
| Category | Library | Version |
|---|---|---|
| Framework | React | 19 |
| Language | TypeScript | 5.9 |
| Build tool | Vite | 8 |
| Routing | React Router DOM | 7 |
| Animation | Framer Motion | 12 |
| Lottie animation | @lottiefiles/dotlottie-react | 0.18 |
| Styling | Tailwind CSS | 4 |
| Component dev | Storybook | 10 |
| Testing | Vitest + Playwright | — |
npm run dev # Start dev server
npm run build # Type-check and build for production
npm run preview # Preview the production build locally
npm run lint # Run ESLint
npm run storybook # Start Storybook on port 6006
npm run build-storybook # Build static Storybook| Branch | Purpose |
|---|---|
main |
Production-ready code |
styling |
Styling and UI work |
origin/styling/animation |
Animation work |
develop |
Integration branch |
This is a Single Page Application — all routing is handled client-side by React Router. The netlify.toml at the project root contains the redirect rule needed for page refreshes and direct URL access:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200This file must be present when deploying to any static host.
Cases are currently stored as static TypeScript arrays. There are two data files:
| File | Used by |
|---|---|
src/data/AddedCasesData.ts |
Projects/Work page — all cases |
src/data/FeaturedCasesData.ts |
Home page — highlighted cases |
type Project = {
id: number; // Must be unique across all cases
title: string; // Case title
client: string; // Client name
category: string[]; // One or more category slugs (see below)
imageUrl: string; // Card image
hoverImageUrl?: string; // Optional — second image for hover reveal effect
text: string; // Short description
overlayColor?: string; // Optional — CSS variable e.g. "var(--brand-red)"
};award-winning pr design
tech-and-innovation growth content-x-some
communication strategy audio
film digital
- Add the case image(s) to
src/assets/(create a subfolder if needed) - Open
src/data/AddedCasesData.ts - Import the image at the top of the file:
import myNewCasePic from "../assets/my-new-case.png";
- Add a new entry to the
addedCasesarray:{ id: 13, // increment from the last id title: "Campaign Title", client: "Client Name", category: ["design", "pr"], imageUrl: myNewCasePic, text: "Short description of the campaign", overlayColor: "var(--brand-green)", }
- If the case should appear on the Home page, add it to
FeaturedCasesData.tsas well
FeaturedCasesData.ts can hold more than 4 entries, but FeaturedCases only renders cards that have a matching entry in layoutFeaturedCases (currently 4 slots). Any project beyond index 3 is silently skipped. To show more featured cases, add a corresponding layout entry in FeaturedCases.tsx.
Categories are defined in src/types/Types.ts:
export const categories: Category[] = [
{ label: "Award winning", slug: "award-winning" },
// add new entry here
];Both label (shown in the filter UI) and slug (used in URLs and data) are required.
Both the Featured Cases section (Home page) and the All Cases grid (Cases page) use a two-column scroll-driven parallax effect. Left and right columns move in opposite vertical directions as the user scrolls, creating a depth effect.
The effect is desktop-only (>= 1024px). On smaller screens, cards render statically at full opacity with no transforms applied.
Each grid has a useScroll target ref. scrollYProgress (0 → 1) drives two useTransform values — one per column — via Framer Motion. Cards in the left column drift in one direction, cards in the right column drift the opposite way.
scrollYProgress 0 → 1
leftColumn: "0px" → "60px" (drifts down)
rightColumn: "0px" → "-60px" (drifts up)
| File | Role |
|---|---|
StickyGridAnimation.tsx |
Creates scrollYProgress, leftColumn, rightColumn; maps each card to its column |
AnimatedCaseCard.tsx |
Applies columnY transform and fade-in per card |
FeaturedCases.tsx |
Same scroll setup for the featured grid |
AnimatedFeaturedCaseCard.tsx |
Applies columnY transform per featured card |
Each layout entry has an explicit column: "left" | "right" field defined in the LayoutCase type. This is the single source of truth — no class name parsing or index-based guessing.
// src/types/Types.ts
export type LayoutCase = {
className?: string;
height?: string;
column: "left" | "right";
};// src/hooks/useIsDesktop.ts
export const useIsDesktop = () => { ... }Uses window.matchMedia("(min-width: 1024px)") and a change listener. Returns a boolean. Both AnimatedCaseCard and AnimatedFeaturedCaseCard receive isDesktop as a prop and skip all style transforms when it is false.
The Cases page loads 8 cards at a time. An IntersectionObserver on a sentinel div at the bottom of the list triggers the next batch when it enters the viewport. The observer resets when the active filter changes.
This is handled entirely in CasesGrid.tsx — StickyGridAnimation only receives the currently visible slice and is unaware of pagination.
CaseCard has two interactive effects:
- Scale — the main image scales to 1.04 on hover via a Framer Motion
animateprop - Clip-path reveal — a second hover image is revealed by an expanding
inset()clip-path driven by mouse position. The reveal shape grows from the cursor outward using a spring-animatedRADIUSmotion value
A shared cardHover MotionValue<number> (0 or 1) is passed from the page level down to each card and also to CursorOnHover, which shows a "View project →" pill that follows the cursor and fades in when any card is hovered.
Deployed on Netlify as a test environment.
| Setting | Value |
|---|---|
| Build command | npm run build |
| Publish directory | dist |
| Node version | 18+ |
The production site will be hosted on a custom .com domain. Steps:
- Purchase domain via Namecheap or Cloudflare Registrar
- In Netlify: Site settings → Domain management → Add custom domain
- Update DNS at the registrar to point to Netlify's nameservers
- SSL certificate is provisioned automatically by Netlify
The build script runs TypeScript type-checking before bundling (tsc -b && vite build). The build will fail if there are any TypeScript errors. Fix all TS errors before deploying.
No environment variables are required for the current static setup.
When a backend or CMS is added in future, create a .env file at the project root:
VITE_API_URL=https://your-api-url.comVite only exposes variables prefixed with VITE_ to the frontend. Never put secrets or private keys in .env files in this project.
Brand colors and typography are defined in src/styles/variables.css and available as CSS custom properties:
--brand-beige: #eee8e2 /* page background */ --brand-red: #fd4e00
--brand-green: #05bd00 --brand-lime: #e9ff69 --brand-blue: #9ec0e8
--brand-purple: #681637 --brand-brown: #91724c;Typography:
font-primary— Familjen Grotesk (loaded from Google Fonts)font-accent— KidSpaghetti (custom font, loaded fromsrc/assets/fonts/)
- Contact form posts to a test URL (
http://httpbin.org/anything) — replace with a real endpoint before going live, suggestions: Nodemailer + Gmail SMTP (requires small backend) - Case detail pages use hardcoded placeholder images and copy — needs to be data-driven per case
These routes exist in the router but the pages are empty placeholders:
/services/our-people/creative-principles/create-like-kid- About page submenu and dropdown navigation
The current hardcoded data approach is temporary. Next step is to connect a headless CMS so cases can be managed without code changes.