Build a Netflix-Like Frontend with React
Build a dark-themed Netflix-style movie browsing UI with React.
Introduction
30 Second Summary
Think about the Netflix homepage with its rows of movie posters and a big hero banner at the top. That kind of polished, interactive interface is exactly what frontend developers build in the real world.
In this project, you will build a Netflix-inspired movie browsing interface from scratch using React and CSS. You will create reusable components, style interactive hover effects, and make the whole layout responsive.
What You'll Build
Open the finished app and you will see a dark Netflix-style page with a fixed navbar, a cinematic hero banner, and four scrollable rows of movie posters that scale up and reveal title overlays when you hover over them.
By the end of this project, you'll have:
- A dark-themed navbar and a cinematic hero banner that showcase a featured movie with its title, description, and Play/More Info buttons.
- Four horizontal-scrolling movie rows organized by category, each filled with poster cards you can browse by dragging or swiping.
- Smooth hover effects that scale up movie cards and fade in a title overlay with a play icon, plus a responsive layout that adapts cleanly from desktop to mobile.
- Secret Mission: Add a "My List" feature where users can save movies by clicking a button on each card and see their saved picks in a dedicated row at the top.
Are there any prerequisites?
This project assumes you are comfortable with React fundamentals like components, props, state, and JSX. You should already have Node.js and VS Code installed on your machine.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Project
To build a Netflix-style interface, you need a project to work in. React is the library you'll use to build reusable UI components, and Vite is the build tool that bundles everything and gives you instant feedback as you code.
In this step, you'll scaffold a fresh React project with Vite, strip out the default demo content, and set up a clean dark-themed canvas to build on.
In this step, get ready to:
- Scaffold a new React project with Vite.
- Remove default boilerplate files.
- Set up global dark-theme styles.
Scaffold the project and install dependencies
- 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
- Create a new React project by running this command:
npm create vite@latest netflix-clone -- --template react
What does this command do?
npm create vite@latest uses Vite's project scaffolder to generate a ready-to-go project structure.
netflix-clone is the folder name, and --template react tells Vite to set up a React project with JavaScript.
If npm asks whether you want to install the create-vite package, enter y and press Enter to proceed.
- Move into the new project folder and install its dependencies by running these commands:
cd netflix-clone
npm install
- Confirm the project files were created by running:
ls
You should see files like package.json, vite.config.js, and a src/ folder listed in the output.
Command not found?
If npm is not recognized, Node.js may not be installed or its path is not configured. Vite requires Node.js version 20.19+ or 22.12+.
Check your version by running node --version in a new terminal window. If nothing appears, reinstall Node.js from nodejs.org.
Help me troubleshoot my Node.js installation
Clean out the boilerplate and set up the dark theme
The default Vite template comes with demo content, logos, and a sample counter app. You need to clear all of that out before building the Netflix UI.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the netflix-clone folder, then click Open.
You should see the project files listed in the Explorer sidebar on the left. Now delete the boilerplate files you don't need.
- In the Explorer sidebar, expand the src/ folder.
- Right-click on App.css and select Delete. Confirm the deletion if prompted.
- Right-click on the assets folder and select Delete. Confirm the deletion if prompted.
Your src/ folder should now contain only App.jsx, index.css, and main.jsx.
Now replace the default code in the remaining files. The current App.jsx imports the files you just deleted, so it needs a fresh start.
- Click on App.jsx in the Explorer sidebar to open it.
- Select all the existing code with Cmd+A (macOS) or Ctrl+A (Windows) and replace it with the following:
function App() {
return (
<div className="app">
<h1>Netflix Clone</h1>
</div>
);
}
export default App;
What does this code do?
This is a minimal React component that renders a heading inside a wrapper div. The className="app" gives you a hook to style the entire app container later.
You'll replace this placeholder heading with real Netflix components in the next steps.
- Save App.jsx with Cmd+S (macOS) or Ctrl+S (Windows).
- Now click on index.css in the Explorer sidebar to open it.
- Select all the existing code with Cmd+A (macOS) or Ctrl+A (Windows) and replace it with the following:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #141414;
color: #ffffff;
overflow-x: hidden;
}
a {
text-decoration: none;
color: inherit;
}
What do these styles do?
- The * selector resets all default browser spacing and switches to border-box sizing so elements behave predictably.
- body sets the dark background (#141414) and white text that give the Netflix feel.
- overflow-x: hidden prevents any horizontal scrollbar from appearing on the page.
- The a selector removes link underlines and inherits the white text color.
- Save index.css.
✔️ Awesome, I've got everything!
Great. Double check you've saved both App.jsx and index.css.
ⓧ I'd like to double check the full code
Here is the full src/App.jsx:
function App() {
return (
<div className="app">
<h1>Netflix Clone</h1>
</div>
);
}
export default App;
Here is the full src/index.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #141414;
color: #ffffff;
overflow-x: hidden;
}
a {
text-decoration: none;
color: inherit;
}
Verify the blank canvas
Everything is in place. Before you start the dev server, think about what you expect to see in the browser based on the code you just wrote.
- Open a terminal inside VS Code by clicking Terminal in the top menu bar, then selecting New Terminal.
- Start the dev server by running this command:
npm run dev
- Open your browser and go to http://localhost:5173.
You should see a dark page with white "Netflix Clone" heading text. No Vite logos, no counter button, no default content.
Why is the background dark?
The #141414 background color you set in index.css is the same near-black that Netflix uses. It makes bright poster images and white text pop.
Seeing an error or the default Vite page?
- If you see the default Vite demo with logos and a counter, you may not have saved your changes to App.jsx. Open the file and check for a white dot on the tab (unsaved changes).
- If the terminal shows an import error mentioning App.css or assets, you deleted the files but didn't replace the App.jsx code. The old code still tries to import them.
- If the page has a white background, check that index.css was saved with the dark-theme styles.
Help me troubleshoot my Vite dev server
Your blank canvas is ready. Next up, you'll build the Navbar and Hero Banner to give the page its signature Netflix look.
Build the Navbar and Hero Banner
You have a blank dark canvas running at localhost:5173. Every streaming app starts with two things you notice right away: a navigation bar across the top and a large hero banner showcasing a featured title.
In this step, you'll build both of those as reusable React components. The Navbar handles navigation, and the Banner displays a featured movie with a gradient overlay and call-to-action buttons.
In this step, get ready to:
- Create a Navbar component with logo text, navigation links, and a profile icon.
- Build a Hero Banner with a background image, gradient overlay, and call-to-action buttons.
- Wire both components into your App to see them live in the browser.
Create the Navbar component
The Navbar is a fixed bar pinned to the top of the screen. It holds the logo, page links, and a profile icon placeholder.
- Create a components folder inside src/ by running this command in your terminal:
mkdir src/components
- Create a new file called Navbar.jsx inside src/components/ and add the following code:
import './Navbar.css';
// Navbar renders a fixed navigation bar at the top of the page
function Navbar() {
return (
<nav className="navbar">
{/* Logo text on the left */}
<div className="navbar-logo">NETFLIX</div>
{/* Navigation links in the center */}
<ul className="navbar-links">
<li>Home</li>
<li>Movies</li>
<li>Series</li>
<li>My List</li>
</ul>
{/* Profile icon placeholder on the right */}
<div className="navbar-profile">U</div>
</nav>
);
}
export default Navbar;
What does this code do?
- import './Navbar.css' loads a CSS file that will style this component. You'll create that file in a moment.
- The <nav> element holds three sections: a logo div, a <ul> list of navigation links, and a profile icon placeholder.
- The component is exported so other files (like App.jsx) can import and use it.
✔️ Awesome, I've got everything!
Great. Make sure you've saved Navbar.jsx before moving on.
ⓧ I'd like to double check the full code
import './Navbar.css';
function Navbar() {
return (
<nav className="navbar">
<div className="navbar-logo">NETFLIX</div>
<ul className="navbar-links">
<li>Home</li>
<li>Movies</li>
<li>Series</li>
<li>My List</li>
</ul>
<div className="navbar-profile">U</div>
</nav>
);
}
export default Navbar;
The component exists, but the app doesn't know about it yet. You need to import it into App.jsx.
- Replace the entire contents of src/App.jsx with this code:
import Navbar from './components/Navbar.jsx';
// App renders the top-level layout
function App() {
return (
<div className="app">
<Navbar />
</div>
);
}
export default App;
- Save App.jsx.
- Check your browser. You should see the text NETFLIX, the four navigation links (Home, Movies, Series, My List), and a small U character. They appear unstyled as plain white text on a dark background.
Don't see the Navbar text?
- Make sure the import path in App.jsx is './components/Navbar.jsx' with a capital N. File names are case-sensitive.
- Confirm that Navbar.jsx is inside src/components/, not directly in src/.
- Check the terminal for error messages. A red overlay in the browser usually means a syntax or import error.
Why can't my browser find the Navbar component?
The Navbar text is on the page, but it needs styling to look like a real navigation bar. Time to create the CSS.
- Create a new file called Navbar.css inside src/components/.
- Add the navbar layout, logo, and links container styles by pasting this code:
/* Fixed navbar pinned to the top of the viewport */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 40px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, transparent 100%);
transition: background-color 0.3s ease;
}
/* Netflix-red bold logo text */
.navbar-logo {
font-size: 1.8rem;
font-weight: 700;
color: #e50914;
letter-spacing: 2px;
cursor: pointer;
}
/* Horizontal list of navigation links */
.navbar-links {
display: flex;
list-style: none;
gap: 25px;
}
What do these styles do?
- position: fixed locks the navbar to the top of the screen so it stays visible when the user scrolls.
- The background gradient fades from a dark overlay at the top to fully transparent at the bottom, creating a subtle fade effect over the page content.
- justify-content: space-between pushes the logo to the left, links to the center, and the profile icon to the right.
- Save Navbar.css and check your browser.
- You should see the NETFLIX logo in red on the left, with the navigation links spaced out horizontally beside it.
- Add the link item styles and profile icon below your existing code in Navbar.css:
/* Individual link items */
.navbar-links li {
font-size: 0.95rem;
color: #e5e5e5;
cursor: pointer;
transition: color 0.2s ease;
}
/* Brighten links on hover */
.navbar-links li:hover {
color: #ffffff;
}
/* Small red profile icon square */
.navbar-profile {
width: 32px;
height: 32px;
border-radius: 4px;
background-color: #e50914;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
}
What do these styles do?
- The transition: color 0.2s ease on each link makes the text smoothly brighten from light gray to white when hovered.
- The .navbar-profile creates a small red square with a centered letter, acting as a placeholder for a user avatar.
- Save Navbar.css and check your browser.
- The navbar now shows the NETFLIX logo in red on the left, soft gray links in the center, and a small red profile square with a U on the right. Hover over any link to see it brighten to white.
Navbar still looks unstyled?
- Make sure Navbar.css is in the same folder as Navbar.jsx (src/components/). The import path './Navbar.css' expects them side by side.
- Check for typos in your class names. The JSX uses className="navbar" and the CSS targets .navbar. These must match exactly.
Why aren't my Navbar CSS styles showing up?
The navbar looks great on a wide screen. Now add responsive styles so it adapts to smaller screens too.
- Add this media query below your existing code in Navbar.css:
/* Collapse links on small screens */
@media (max-width: 768px) {
.navbar {
padding: 12px 20px;
}
.navbar-links {
display: none;
}
.navbar-logo {
font-size: 1.4rem;
}
}
What does this media query do?
When the screen is narrower than 768px, the navigation links disappear entirely with display: none. Only the logo and profile icon remain. This prevents the links from overflowing on mobile.
- Save Navbar.css.
- Shrink your browser window to less than half its width. The navigation links should disappear, leaving just the NETFLIX logo and the profile icon.
- Expand the window back to full width. The links reappear.
✔️ Awesome, I've got everything!
Navbar is done. Make sure Navbar.css is saved with all three sections before moving on.
ⓧ I'd like to double check the full code
Navbar.css should have three sections. Scroll through your file and confirm each is present, in order:
- Section 1: .navbar, .navbar-logo, and .navbar-links selectors (layout, logo, and link container).
- Section 2: .navbar-links li, .navbar-links li:hover, and .navbar-profile selectors (link items and profile icon).
- Section 3: @media (max-width: 768px) block (responsive styles).
Create the Hero Banner component
The hero banner is the large cinematic section below the navbar. It fills most of the viewport with a background image and overlays the movie title, a description, and action buttons on top.
- Create a new file called Banner.css inside src/components/.
- Add the banner layout, gradient overlay, and content container styles by pasting this code:
/* Full-width banner section with background image */
.banner {
position: relative;
width: 100%;
height: 80vh;
background-size: cover;
background-position: center top;
display: flex;
align-items: flex-end;
padding: 0 40px 80px;
}
/* Gradient that fades the image into the page background */
.banner-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(20, 20, 20, 0.4) 50%,
#141414 100%
);
}
/* Constrains the text content area */
.banner-content {
position: relative;
z-index: 1;
max-width: 550px;
}
What do these styles do?
- .banner takes up 80% of the viewport height and uses align-items: flex-end to push content to the bottom of the image.
- .banner-overlay creates a gradient that fades from transparent at the top to the page's background color (#141414) at the bottom. This makes the text readable without covering the whole image.
- .banner-content uses z-index: 1 to sit above the overlay so the text is visible.
- Now create Banner.jsx inside src/components/ and add this code:
import './Banner.css';
// Banner displays a featured movie with a background image and overlay
function Banner({ movie }) {
return (
<header
className="banner"
style={{ backgroundImage: `url(${movie.backdropUrl})` }}
>
{/* Gradient overlay to darken the bottom of the image */}
<div className="banner-overlay" />
<div className="banner-content">
<h1 className="banner-title">{movie.title}</h1>
<p className="banner-description">{movie.description}</p>
{/* Call-to-action buttons */}
<div className="banner-buttons">
<button className="banner-btn banner-btn-play">
▶ Play
</button>
<button className="banner-btn banner-btn-info">
ⓘ More Info
</button>
</div>
</div>
</header>
);
}
export default Banner;
What does this component do?
- Banner receives a movie object as a prop. This is how React passes data from a parent component to a child.
- The background image is set with an inline style attribute because the URL comes from the movie prop and changes dynamically.
- ▶ and ⓘ are HTML entities that render as a play triangle and an info circle icon.
✔️ Awesome, I've got everything!
Make sure you've saved Banner.jsx.
ⓧ I'd like to double check the full code
import './Banner.css';
function Banner({ movie }) {
return (
<header
className="banner"
style={{ backgroundImage: `url(${movie.backdropUrl})` }}
>
<div className="banner-overlay" />
<div className="banner-content">
<h1 className="banner-title">{movie.title}</h1>
<p className="banner-description">{movie.description}</p>
<div className="banner-buttons">
<button className="banner-btn banner-btn-play">
▶ Play
</button>
<button className="banner-btn banner-btn-info">
ⓘ More Info
</button>
</div>
</div>
</header>
);
}
export default Banner;
Both component files exist. Now wire the Banner into your app so it shows up in the browser.
- Replace the entire contents of src/App.jsx with this updated code:
import Navbar from './components/Navbar.jsx';
import Banner from './components/Banner.jsx';
// Hardcoded featured movie data for the hero banner
const featuredMovie = {
title: 'Midnight Horizon',
description:
'When a deep-space signal reveals an ancient alien structure on the edge of the solar system, a crew of astronauts must race against time to uncover its secrets before a rival faction reaches it first.',
backdropUrl: 'https://picsum.photos/seed/netflix-banner/1600/900',
};
function App() {
return (
<div className="app">
<Navbar />
<Banner movie={featuredMovie} />
</div>
);
}
export default App;
What's happening here?
- featuredMovie is a plain JavaScript object with the movie's title, description, and a background image URL.
- The image URL uses picsum.photos with a seed value. The seed (netflix-banner) produces the same random image every time, so your banner looks consistent.
- <Banner movie={featuredMovie} /> passes the object as a prop. Inside Banner, it arrives as movie.title, movie.description, and movie.backdropUrl.
- Save App.jsx and check your browser.
- You should see the navbar at the top, and below it a large banner area with a background photo. The movie title Midnight Horizon and its description appear near the bottom of the banner, with Play and More Info buttons below the text.
Don't see the banner?
- If the banner area is blank with no image, check that your backdropUrl in App.jsx matches 'https://picsum.photos/seed/netflix-banner/1600/900' exactly.
- If you see a compile error, confirm that Banner.css and Banner.jsx are both saved inside src/components/.
- If the text appears but the background is plain dark, the image may still be loading. Give it a few seconds and refresh.
Why isn't my Banner component rendering?
The banner layout works, but the typography and buttons need refinement. Time to add the remaining styles.
- Add the title, description, and button base styles below your existing code in Banner.css:
.banner-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: 16px;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7);
}
.banner-description {
font-size: 1.1rem;
line-height: 1.5;
color: #e5e5e5;
margin-bottom: 24px;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.7);
}
.banner-buttons {
display: flex;
gap: 12px;
}
/* Shared base styles for both action buttons */
.banner-btn {
padding: 10px 28px;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: opacity 0.2s ease;
}
What do these styles do?
- text-shadow on both the title and description adds a dark blur behind the text, making it readable over any background image.
- .banner-btn sets shared styles for both buttons. The specific colors come in the next chunk.
- Save Banner.css and check your browser.
- The movie title should now appear large and bold. The description text is a lighter gray beneath it. Both buttons are visible but currently unstyled gray rectangles.
- Add the button color variants and responsive styles below your existing code in Banner.css:
.banner-btn:hover {
opacity: 0.8;
}
/* White Play button */
.banner-btn-play {
background-color: #ffffff;
color: #000000;
}
/* Translucent gray More Info button */
.banner-btn-info {
background-color: rgba(109, 109, 110, 0.7);
color: #ffffff;
}
What do these styles do?
The Play button gets a white background with black text, and the More Info button gets a translucent gray. Both fade slightly on hover via the opacity transition.
- Save and check your browser. The Play button should now be white and the More Info button translucent gray.
- Finally, add responsive styles for the banner below your existing code in Banner.css:
/* Responsive adjustments for smaller screens */
@media (max-width: 768px) {
.banner {
height: 60vh;
padding: 0 20px 60px;
}
.banner-title {
font-size: 1.8rem;
}
.banner-description {
font-size: 0.9rem;
}
.banner-btn {
padding: 8px 18px;
font-size: 0.85rem;
}
}
What does this media query do?
On screens below 768px, the banner shrinks to 60% viewport height, the title and description get smaller font sizes, and the buttons get tighter padding. This keeps everything readable on phones and tablets.
- Save Banner.css.
✔️ Awesome, I've got everything!
Banner styles are complete. Make sure Banner.css is saved with all four sections.
ⓧ I'd like to double check the full code
Banner.css should have four sections. Scroll through your file and confirm each is present, in order:
- Section 1: .banner, .banner-overlay, and .banner-content (layout, gradient, content container).
- Section 2: .banner-title, .banner-description, .banner-buttons, and .banner-btn (typography and button base).
- Section 3: .banner-btn:hover, .banner-btn-play, and .banner-btn-info (hover effect and button color variants).
- Section 4: @media (max-width: 768px) block (responsive styles).
✔️ Awesome, my App.jsx is up to date!
You're all set. Make sure App.jsx is saved.
ⓧ I'd like to double check App.jsx
import Navbar from './components/Navbar.jsx';
import Banner from './components/Banner.jsx';
const featuredMovie = {
title: 'Midnight Horizon',
description:
'When a deep-space signal reveals an ancient alien structure on the edge of the solar system, a crew of astronauts must race against time to uncover its secrets before a rival faction reaches it first.',
backdropUrl: 'https://picsum.photos/seed/netflix-banner/1600/900',
};
function App() {
return (
<div className="app">
<Navbar />
<Banner movie={featuredMovie} />
</div>
);
}
export default App;
Before you look at the result, what do you expect to see in the browser right now? Think about what each component contributes to the page.
Check your browser at http://localhost:5173. You should see a dark navbar pinned to the top with NETFLIX in red, four navigation links, and a small red profile square. Below it, a large hero banner fills most of the screen with a background photo, a gradient fade at the bottom, the title Midnight Horizon, a description, and white Play and gray More Info buttons.
Your app has a proper visual anchor now. The Navbar and Banner give the page structure and a cinematic feel. Next up, you'll fill the rest of the page with horizontal-scrolling rows of movie posters.
Create Movie Rows with Poster Cards
Your navbar and hero banner look great. But scroll below the banner and the page is empty.
The movie rows are what make a Netflix-style interface feel like a real browsing experience. In this step, you'll create the hardcoded movie data and build a reusable row component that displays poster cards in a horizontal-scrolling layout.
In this step, get ready to:
- Create a movie data file with four categories of films.
- Build a MovieRow component with horizontal scrolling.
- Wire the rows into App to populate the page below the banner.
Create the movie data file
Every movie row needs data to display. You'll create a dedicated data file that exports an array of category objects, each containing a title and a list of movies with placeholder poster images.
- In VS Code, create a new folder called data inside your src/ folder.
- Inside src/data/, create a new file called movies.js.
- Add the first two movie categories by pasting the following code into movies.js:
// Each category has a title and an array of movie objects
const categories = [
{
title: 'Trending Now',
movies: [
{ id: 1, title: 'Midnight Horizon', posterUrl: 'https://picsum.photos/seed/movie1/200/300', year: 2025 },
{ id: 2, title: 'Crimson Depths', posterUrl: 'https://picsum.photos/seed/movie2/200/300', year: 2024 },
{ id: 3, title: 'The Last Signal', posterUrl: 'https://picsum.photos/seed/movie3/200/300', year: 2026 },
{ id: 4, title: 'Shadow Protocol', posterUrl: 'https://picsum.photos/seed/movie4/200/300', year: 2025 },
{ id: 5, title: 'Neon Divide', posterUrl: 'https://picsum.photos/seed/movie5/200/300', year: 2026 },
{ id: 6, title: 'Beyond the Veil', posterUrl: 'https://picsum.photos/seed/movie6/200/300', year: 2024 },
{ id: 7, title: 'Iron Cascade', posterUrl: 'https://picsum.photos/seed/movie7/200/300', year: 2025 },
{ id: 8, title: 'Frozen Meridian', posterUrl: 'https://picsum.photos/seed/movie8/200/300', year: 2026 },
],
},
{
title: 'Top Rated',
movies: [
{ id: 9, title: 'Echoes of Tomorrow', posterUrl: 'https://picsum.photos/seed/movie9/200/300', year: 2025 },
{ id: 10, title: 'The Quiet Storm', posterUrl: 'https://picsum.photos/seed/movie10/200/300', year: 2024 },
{ id: 11, title: 'Starfall', posterUrl: 'https://picsum.photos/seed/movie11/200/300', year: 2026 },
{ id: 12, title: 'Obsidian Heart', posterUrl: 'https://picsum.photos/seed/movie12/200/300', year: 2025 },
{ id: 13, title: 'Drift City', posterUrl: 'https://picsum.photos/seed/movie13/200/300', year: 2024 },
{ id: 14, title: 'Apex Predator', posterUrl: 'https://picsum.photos/seed/movie14/200/300', year: 2026 },
{ id: 15, title: 'Glass Empire', posterUrl: 'https://picsum.photos/seed/movie15/200/300', year: 2025 },
{ id: 16, title: 'Velvet Revolver', posterUrl: 'https://picsum.photos/seed/movie16/200/300', year: 2024 },
],
},
What does this data do?
Each category object holds a title (like "Trending Now") and a movies array.
Each movie has an id for React's key prop, a title, and a posterUrl that uses picsum.photos to serve a consistent placeholder image based on a seed value.
- Add the remaining two categories directly below the code you just pasted, still inside the same file:
{
title: 'Action Movies',
movies: [
{ id: 17, title: 'Thunder Force', posterUrl: 'https://picsum.photos/seed/movie17/200/300', year: 2025 },
{ id: 18, title: 'Rogue Element', posterUrl: 'https://picsum.photos/seed/movie18/200/300', year: 2026 },
{ id: 19, title: 'Steel Rain', posterUrl: 'https://picsum.photos/seed/movie19/200/300', year: 2024 },
{ id: 20, title: 'Warpath', posterUrl: 'https://picsum.photos/seed/movie20/200/300', year: 2025 },
{ id: 21, title: 'Ground Zero', posterUrl: 'https://picsum.photos/seed/movie21/200/300', year: 2026 },
{ id: 22, title: 'Dead Reckoning', posterUrl: 'https://picsum.photos/seed/movie22/200/300', year: 2024 },
{ id: 23, title: 'Firestorm', posterUrl: 'https://picsum.photos/seed/movie23/200/300', year: 2025 },
{ id: 24, title: 'Blackout', posterUrl: 'https://picsum.photos/seed/movie24/200/300', year: 2026 },
],
},
{
title: 'Comedy Movies',
movies: [
{ id: 25, title: 'The Awkward Age', posterUrl: 'https://picsum.photos/seed/movie25/200/300', year: 2025 },
{ id: 26, title: 'Plan B Squared', posterUrl: 'https://picsum.photos/seed/movie26/200/300', year: 2024 },
{ id: 27, title: 'Oops Again', posterUrl: 'https://picsum.photos/seed/movie27/200/300', year: 2026 },
{ id: 28, title: 'My Robot Roommate', posterUrl: 'https://picsum.photos/seed/movie28/200/300', year: 2025 },
{ id: 29, title: 'Love and WiFi', posterUrl: 'https://picsum.photos/seed/movie29/200/300', year: 2024 },
{ id: 30, title: 'Disaster Chef', posterUrl: 'https://picsum.photos/seed/movie30/200/300', year: 2026 },
{ id: 31, title: 'The Intern Returns', posterUrl: 'https://picsum.photos/seed/movie31/200/300', year: 2025 },
{ id: 32, title: 'Accidental Hero', posterUrl: 'https://picsum.photos/seed/movie32/200/300', year: 2024 },
],
},
];
export default categories;
Why four categories?
Four rows (Trending Now, Top Rated, Action Movies, Comedy Movies) give you enough content to test horizontal scrolling and see how multiple rows stack vertically beneath the banner.
- Save movies.js.
- Confirm the file exists by running this in your terminal:
ls src/data/
You should see movies.js listed in the output.
Don't see movies.js?
Make sure the file is inside src/data/, not directly inside src/. The folder data must exist as a subfolder of src.
help me find where my movies.js file ended up
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
The file is longer than what fits in a single view. Scroll through your src/data/movies.js and confirm these sections are present, in order:
- The opening line with the Trending Now category (8 movies, IDs 1 through 8).
- The Top Rated category (8 movies, IDs 9 through 16).
- The Action Movies category (8 movies, IDs 17 through 24).
- The Comedy Movies category (8 movies, IDs 25 through 32).
- A closing ]; followed by export default categories; at the very end.
Build the MovieRow component
Each movie row needs two things: a CSS file that enables horizontal scrolling and hides the scrollbar, and a React component that renders a title and a row of poster images.
- Create a new file called MovieRow.css inside src/components/.
- Add the base layout styles by pasting the following code:
/* Outer wrapper adds horizontal padding to the row */
.movie-row {
padding: 20px 40px;
}
/* Category heading above each row of posters */
.movie-row-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 12px;
}
/* Flexbox container that scrolls horizontally */
.movie-row-posters {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 20px;
scroll-behavior: smooth;
}
What does this CSS do?
The key property is overflow-x: auto on .movie-row-posters. This creates a horizontal scrollbar when the poster images overflow the container width.
Combined with display: flex, the posters line up in a single row and the learner can scroll through them.
- Add the scrollbar-hiding styles and responsive rules below the code you just pasted, still inside MovieRow.css:
/* Hide the scrollbar in Chrome, Safari, Edge */
.movie-row-posters::-webkit-scrollbar {
display: none;
}
/* Hide the scrollbar in Firefox and IE */
.movie-row-posters {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Shrink padding and font on smaller screens */
@media (max-width: 768px) {
.movie-row {
padding: 15px 20px;
}
.movie-row-title {
font-size: 1.1rem;
}
}
Why hide the scrollbar?
Netflix-style rows let users scroll by dragging or swiping, but the visible scrollbar breaks the clean dark aesthetic. The ::-webkit-scrollbar pseudo-element hides it in Chromium-based browsers, while scrollbar-width: none covers Firefox.
- Save MovieRow.css.
- Confirm the file was created by running:
ls src/components/MovieRow*
You should see src/components/MovieRow.css in the output.
- Create a new file called MovieRow.jsx inside src/components/.
- Build the MovieRow component by pasting the following code:
import './MovieRow.css';
// Renders one horizontal-scrolling row of movie posters
function MovieRow({ title, movies }) {
return (
<div className="movie-row">
<h2 className="movie-row-title">{title}</h2>
<div className="movie-row-posters">
{/* Map each movie to a poster image */}
{movies.map((movie) => (
<img
key={movie.id}
src={movie.posterUrl}
alt={movie.title}
style={{ width: '200px', height: '300px', objectFit: 'cover', borderRadius: '4px', flexShrink: 0 }}
/>
))}
</div>
</div>
);
}
export default MovieRow;
What does this component do?
MovieRow receives a title and a movies array as props. It renders a heading for the category name and then maps over the movies array to produce an <img> tag for each poster.
The inline style on each image sets fixed dimensions so every poster card looks uniform. The flexShrink: 0 prevents the browser from squishing posters to fit the viewport.
- Save MovieRow.jsx.
Seeing a red error overlay in the browser?
If Vite shows an error after you save, check that MovieRow.css is in the same folder as MovieRow.jsx. The import path ./MovieRow.css is relative, so both files must live inside src/components/.
help me debug a Vite import error for MovieRow
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete MovieRow.jsx:
import './MovieRow.css';
function MovieRow({ title, movies }) {
return (
<div className="movie-row">
<h2 className="movie-row-title">{title}</h2>
<div className="movie-row-posters">
{movies.map((movie) => (
<img
key={movie.id}
src={movie.posterUrl}
alt={movie.title}
style={{ width: '200px', height: '300px', objectFit: 'cover', borderRadius: '4px', flexShrink: 0 }}
/>
))}
</div>
</div>
);
}
export default MovieRow;
For MovieRow.css, scroll through and confirm these sections are present in order:
- The .movie-row and .movie-row-title base styles.
- The .movie-row-posters flexbox container with overflow-x: auto.
- The ::-webkit-scrollbar and scrollbar-width: none rules.
- The @media (max-width: 768px) responsive block at the bottom.
Render all movie rows in App
MovieRow and the movie data are ready but nothing shows on the page yet. You need to import both into App.jsx and map over the categories to render a row for each one.
- Switch back to src/App.jsx in VS Code.
- Find the existing import lines at the top of the file:
import Navbar from './components/Navbar.jsx';
import Banner from './components/Banner.jsx';
- Add two new import lines directly below them, so all four imports look like this:
import Navbar from './components/Navbar.jsx';
import Banner from './components/Banner.jsx';
import MovieRow from './components/MovieRow.jsx';
import categories from './data/movies.js';
Now you need to render the rows inside the JSX.
- Find the <Banner movie={featuredMovie} /> line inside the return statement.
- Add the following code directly below it, before the closing </div>:
<div className="movie-rows">
{/* Loop through each category and render a MovieRow */}
{categories.map((category) => (
<MovieRow
key={category.title}
title={category.title}
movies={category.movies}
/>
))}
</div>
What does this JSX do?
The categories.map() call loops through the four category objects from your data file. For each one, it renders a <MovieRow> and passes the category's title and movies as props.
The key={category.title} prop helps React efficiently track which rows change when the data updates.
- Save App.jsx.
Before you look at the browser, what do you think you'll see below the hero banner?
- Check your browser at http://localhost:5173.
You should see four horizontal rows of movie posters stacked below the banner. Each row has a category heading (Trending Now, Top Rated, Action Movies, Comedy Movies) and eight poster images you can scroll through by dragging or using a trackpad.
The rows scroll and the posters display, but notice that hovering over a card does nothing. The cards are completely static. That flatness is the gap the next step closes.
Don't see any movie rows?
Check that your App.jsx has all four imports at the top, including MovieRow and categories. A missing import means the data or component is undefined.
Also confirm that the <div className="movie-rows"> block sits inside the <div className="app"> wrapper, after the <Banner /> line.
help me debug my missing movie rows
✔️ Awesome, I've got everything!
Great. Double check you have saved all files before moving on.
ⓧ I'd like to double check the full code
Here is the complete App.jsx at the end of this step:
import Navbar from './components/Navbar.jsx';
import Banner from './components/Banner.jsx';
import MovieRow from './components/MovieRow.jsx';
import categories from './data/movies.js';
const featuredMovie = {
title: 'Midnight Horizon',
description:
'When a deep-space signal reveals an ancient alien structure on the edge of the solar system, a crew of astronauts must race against time to uncover its secrets before a rival faction reaches it first.',
backdropUrl: 'https://picsum.photos/seed/netflix-banner/1600/900',
};
function App() {
return (
<div className="app">
<Navbar />
<Banner movie={featuredMovie} />
<div className="movie-rows">
{categories.map((category) => (
<MovieRow
key={category.title}
title={category.title}
movies={category.movies}
/>
))}
</div>
</div>
);
}
export default App;
Your page is packed with content now. Four scrollable rows of movie posters fill the space below the hero banner. Next up, you'll make those flat, static cards come alive with hover effects and overlays.
Add Hover Effects and Interactivity
Your movie rows are in place, with poster images you can scroll through in each category. The page structure is solid.
Try hovering over a poster right now. Nothing happens. The cards sit there with no feedback, making the whole page feel static. In this step, you'll create a dedicated MovieCard component with CSS transitions that bring the cards to life.
In this step, get ready to:
- Build a MovieCard component with hover effect styles.
- Update MovieRow to render MovieCard components instead of plain images.
Build the MovieCard component
Each movie poster needs its own component to handle hover interactions. Right now, MovieRow renders plain <img> tags with inline styles.
- Create a new file src/components/MovieCard.css and add the base card styles by pasting this code:
.movie-card {
position: relative;
flex-shrink: 0;
width: 200px;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease;
}
.movie-card:hover {
transform: scale(1.15);
z-index: 10;
}
.movie-card-image {
width: 100%;
height: 300px;
object-fit: cover;
display: block;
}
What does this CSS do?
- .movie-card sets up the card as a relatively positioned container with a fixed width, rounded corners, and a transition on transform so hover effects animate smoothly.
- .movie-card:hover scales the card up to 1.15x its original size and bumps its z-index so it floats above neighboring cards.
- .movie-card-image stretches the poster image to fill the card with object-fit: cover.
- Continue in MovieCard.css and add these overlay styles below the existing code:
.movie-card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.85) 0%,
transparent 50%
);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding: 15px;
opacity: 0;
transition: opacity 0.3s ease;
}
.movie-card:hover .movie-card-overlay {
opacity: 1;
}
.movie-card-play-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.movie-card-title {
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
What do these overlay styles do?
- .movie-card-overlay covers the entire card with a dark gradient that fades from opaque at the bottom to transparent at the top. Its opacity starts at 0, making it completely invisible.
- .movie-card:hover .movie-card-overlay sets opacity to 1 when the card is hovered, creating a smooth fade-in effect.
- .movie-card-play-icon and .movie-card-title style the play triangle and movie name inside the overlay.
- Save MovieCard.css.
- Create a new file src/components/MovieCard.jsx and add the component code by pasting this:
import './MovieCard.css';
function MovieCard({ movie }) {
return (
<div className="movie-card">
<img
className="movie-card-image"
src={movie.posterUrl}
alt={movie.title}
/>
<div className="movie-card-overlay">
<span className="movie-card-play-icon">▶</span>
<span className="movie-card-title">{movie.title}</span>
</div>
</div>
);
}
export default MovieCard;
What does this component do?
- MovieCard receives a movie object as a prop and renders its poster image inside a movie-card container.
- Below the image, a movie-card-overlay div contains a play icon (unicode triangle ▶) and the movie title. The CSS you wrote hides this overlay by default and fades it in on hover.
- Save MovieCard.jsx.
- Confirm both new files exist by running:
ls src/components/
You should see MovieCard.css and MovieCard.jsx listed alongside your existing component files.
Don't see the new files?
- Make sure you created the files inside src/components/, not in the project root or in src/ directly.
- Check the file names are exactly MovieCard.css and MovieCard.jsx (capital M and C).
Need more help? Why aren't my MovieCard files showing up in the components folder?
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the full MovieCard.jsx file:
import './MovieCard.css';
function MovieCard({ movie }) {
return (
<div className="movie-card">
<img
className="movie-card-image"
src={movie.posterUrl}
alt={movie.title}
/>
<div className="movie-card-overlay">
<span className="movie-card-play-icon">▶</span>
<span className="movie-card-title">{movie.title}</span>
</div>
</div>
);
}
export default MovieCard;
Update MovieRow to use MovieCard
MovieRow currently renders plain <img> tags for each poster. With MovieCard ready, you can swap those out to get the hover effects.
- In src/components/MovieRow.jsx, find this block inside the movies.map() call:
<img
key={movie.id}
src={movie.posterUrl}
alt={movie.title}
style={{ width: '200px', height: '300px', objectFit: 'cover', borderRadius: '4px', flexShrink: 0 }}
/>
- Replace the entire <img> tag with this:
<MovieCard key={movie.id} movie={movie} />
- Save MovieRow.jsx.
Before you check the browser, what do you expect to see when you hover over a movie poster?
Check your browser at http://localhost:5173. Each poster card now scales up smoothly when you hover over it. A dark gradient overlay fades in, revealing the movie title and a play icon at the bottom of the card.
Cards not scaling or showing the overlay?
- Make sure you replaced the entire <img> tag (including the style attribute) with <MovieCard key={movie.id} movie={movie} />.
- Check that MovieCard.css has both the base styles and the overlay styles. Open the file and confirm you have .movie-card-overlay with opacity: 0.
- Verify that MovieCard.jsx starts with import './MovieCard.css'; so the styles are loaded.
Still stuck? Why aren't my movie cards showing hover effects when I mouse over them?
✔️ Awesome, I've got everything!
Great. Double check you've saved MovieRow.jsx before continuing.
ⓧ I'd like to double check the full code
Here is the full MovieRow.jsx file:
import MovieCard from './MovieCard.jsx';
import './MovieRow.css';
function MovieRow({ title, movies }) {
return (
<div className="movie-row">
<h2 className="movie-row-title">{title}</h2>
<div className="movie-row-posters">
{movies.map((movie) => (
<MovieCard key={movie.id} movie={movie} />
))}
</div>
</div>
);
}
export default MovieRow;
- Add responsive media queries at the bottom of MovieCard.css by pasting this:
@media (max-width: 768px) {
.movie-card {
width: 140px;
}
.movie-card-image {
height: 210px;
}
}
@media (max-width: 480px) {
.movie-card {
width: 110px;
}
.movie-card-image {
height: 165px;
}
.movie-card-overlay {
padding: 8px;
}
.movie-card-title {
font-size: 0.7rem;
}
}
What do these media queries do?
- On screens narrower than 768px, the card width shrinks to 140px and the image height drops to 210px.
- On screens below 480px, cards shrink further to 110px wide. The overlay padding and title font size also decrease so the content fits.
- Save MovieCard.css.
- Open your browser DevTools by pressing Ctrl+Shift+I (Windows) or Cmd+Option+I (macOS).
- Click the device toolbar toggle (or press Ctrl+Shift+M / Cmd+Shift+M) to enter responsive mode.
- Set the viewport width to 375px.
The movie cards should appear noticeably smaller than at desktop width. Each poster shrinks proportionally to fit the narrower screen.
Cards still look the same size?
- Make sure the media queries are at the very bottom of MovieCard.css, after the .movie-card-title selector.
- Confirm the viewport width in DevTools is set to 375px or narrower. The first breakpoint kicks in at 768px.
Need help? Why aren't my responsive media queries changing the card sizes in DevTools?
✔️ Awesome, I've got everything!
Great. Double check you've saved MovieCard.css with all three sections (base styles, overlay styles, and media queries).
ⓧ I'd like to double check the full code
Your MovieCard.css combines three sections you've added. Scroll through the file and confirm you have:
- Base card styles at the top: .movie-card, .movie-card:hover, and .movie-card-image.
- Overlay styles in the middle: .movie-card-overlay, .movie-card:hover .movie-card-overlay, .movie-card-play-icon, and .movie-card-title.
- Responsive media queries at the bottom: one @media (max-width: 768px) block and one @media (max-width: 480px) block.
Your movie cards now feel alive with smooth scaling and an overlay that reveals the title on hover. Next up, you'll make the entire app responsive so it looks great on any screen size.
Make It Responsive
Your movie cards now scale up and reveal overlays on hover. The app feels polished and interactive on a desktop screen.
On a phone, though, those navbar links would crowd the screen and the poster cards would barely fit. Your CSS files already include media queries that adapt the layout at different screen widths. In this step, you'll walk through those breakpoints and verify the app looks right on any device.
In this step, get ready to:
- Understand the responsive breakpoints in your CSS files.
- Test the layout across desktop, tablet, and mobile viewports using DevTools.
Review the responsive navbar styles
When a browser window shrinks below a certain width, elements that looked fine on desktop can start overlapping or overflowing. CSS media queries let you define style overrides that activate at specific screen widths, called breakpoints.
- In VS Code, open src/components/Navbar.css.
- Scroll to the bottom of the file. You'll find this media query block:
@media (max-width: 768px) {
.navbar {
padding: 12px 20px;
}
.navbar-links {
display: none;
}
.navbar-logo {
font-size: 1.4rem;
}
}
What does this media query do?
- @media (max-width: 768px) targets any screen 768 pixels wide or narrower. This is a common tablet breakpoint.
- .navbar-links { display: none; } hides the navigation links entirely, keeping the navbar clean on small screens.
- The logo font size and padding both shrink to save horizontal space.
- Switch to your browser where the app is running at http://localhost:5173.
- Drag the right edge of your browser window to make it narrower than about 768 pixels.
The Home, Movies, Series, and My List links disappear. Only the NETFLIX logo and the profile icon remain.
Nav links still visible at narrow widths?
- Make sure the @media block in Navbar.css uses max-width: 768px and that .navbar-links is set to display: none.
- Confirm you saved the file. Unsaved changes won't trigger a hot reload.
Why aren't my navbar links hiding on smaller screens?
Test the responsive layout across all devices
The navbar is only one piece. Your other CSS files use the same pattern to adapt the banner, movie cards, and row spacing at smaller widths.
What are the other responsive breakpoints?
- Banner.css at 768px: reduces the banner height from 80vh to 60vh and shrinks the title, description, and button font sizes.
- MovieCard.css at 768px: shrinks card width from 200px to 140px. At 480px: shrinks further to 110px with tighter overlay padding.
- MovieRow.css at 768px: reduces the row padding and title font size so everything fits on narrower screens.
Dragging the browser edge is imprecise. Your browser's DevTools has a device toolbar that lets you test at exact pixel widths.
- Open DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
- Toggle the device toolbar by pressing Cmd+Shift+M (macOS) or Ctrl+Shift+M (Windows) while DevTools is focused.
- In the viewport bar at the top, set the width to 768 to simulate a tablet screen.
At 768px, the navbar links are gone, the hero banner is shorter, and the movie cards are noticeably narrower. The horizontal rows still scroll normally.
- Change the viewport width to 375 to simulate a mobile phone screen.
Before you look at the result, what do you expect to change compared to the 768px view?
The movie cards shrink further to 110px wide. The banner title text is smaller and easier to read on the narrow screen. Everything fits without any horizontal overflow on the page itself.
- Click the device toolbar toggle again (or press Cmd+Shift+M / Ctrl+Shift+M) to switch back to your normal desktop view.
Why two breakpoints for movie cards?
A single breakpoint would jump straight from 200px cards to tiny cards, which looks jarring. Using two breakpoints (768px for tablets, 480px for phones) creates a smoother visual transition across screen sizes.
Cards or banner not changing size?
- Check that the @media blocks in MovieCard.css and Banner.css are placed outside the closing brace of the last regular rule, not nested inside another selector.
- Make sure all CSS files are saved. Hot reload only picks up saved changes.
- If the device toolbar is not showing, make sure DevTools is open and focused before pressing the shortcut.
My responsive styles aren't applying at the right screen width. Help me debug.
Your Netflix clone is fully responsive and looks great on any screen size, from widescreen monitors to phone screens. Up next, the Secret Mission challenges you to add a "My List" feature that lets users save their favorite movies.
Secret mission
Add a "My List" Feature
Your Netflix clone looks great, but users can only browse. Real streaming apps let you save movies to watch later. In this mission, you'll add a "My List" feature using React state and prop callbacks, so users can click a button on any card to save it to a personal collection.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Vite dev server running at http://localhost:5173.
- The netflix-clone project folder on your Desktop (including all source files and node_modules).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your Netflix clone.
- Your dev server stays running at http://localhost:5173.
- Your netflix-clone folder remains on your Desktop, ready for you to keep editing.
Pause - I'll come back to this later
Stop the dev server to free up your terminal, but keep your project files for later.
- Switch back to the terminal where the dev server is running.
- Press Ctrl+C to stop the Vite dev server.
- Your netflix-clone folder stays on your Desktop. When you are ready to resume, open a terminal, navigate into the folder, and run npm run dev to start the server again.
Delete - I don't want to use this again
Remove the project entirely and start fresh.
- Switch back to the terminal where the dev server is running.
- Press Ctrl+C to stop the Vite dev server.
- Navigate to your Desktop and delete the project folder by running these commands:
macOS
cd ~/Desktop
rm -rf netflix-clone
Windows
cd ~\Desktop
Remove-Item -Recurse -Force netflix-clone
- Confirm the folder is gone by running ls (macOS) or dir (Windows). You should no longer see netflix-clone in the output.
Nice Work!
Nice Work!
Nice work! You just built a fully functional Netflix-style movie browsing interface from scratch using React and Vite.
You've learned how to:
- Built reusable React components (Navbar, Banner, MovieRow, MovieCard) with props-driven data flow to compose a complete page from small, focused pieces.
- Styled a dark-themed UI with CSS including horizontal-scrolling rows, gradient overlays, and smooth hover effects using CSS transitions for scale transforms and fade-in overlays.
- Implemented responsive design with CSS media queries so the navbar, hero banner, and movie cards adapt cleanly across mobile, tablet, and desktop.
- Secret Mission: Added a "My List" feature using lifted state and prop callbacks to let users save and remove movies from a dedicated row.
Ready to quiz yourself?