Build Themeable React Product Cards
Build React product cards with styled-components, themes, and variants.
Introduction
30 Second Summary
Product pages quickly feel inconsistent when every card follows separate design rules. A simple theme change can become a trail of repeated edits.
In this project, you will build a local product-card gallery with React and styled-components. Product data will drive a featured variant while one control switches shared colors across the interface.
What You'll Build
The finished gallery lets you switch three responsive cards between light and dark themes while the featured product keeps its standout treatment.
By the end of this project, you'll have:
- A reusable three-card gallery that turns JavaScript product data into consistent product cards.
- A featured visual variant where the $featured transient styling prop changes the card border, surface, shadow, and button.
- A live theme switch that updates the page, cards, text, badges, and buttons without reloading.
- Secret Mission: Add a compact and comfortable density control that changes the gallery gap, card padding, and card-art height.
Are there any prerequisites?
You need basic JavaScript fundamentals. This project assumes Node.js, npm, Visual Studio Code, Google Chrome, or Microsoft Edge are already installed on Windows.
Before We Start
This checkpoint helps you commit to the Themeable Product Card Lab before the hands-on work begins. You are building a product-card gallery where React data controls scoped styled-components, featured variants, and shared light and dark themes.
Set Up the React Styling Lab
Your styling lab needs a predictable runtime before any component work begins. Node.js executes Vite, so its version determines whether the scaffold can run.
This step creates a React JavaScript starter in Visual Studio Code. You will pin every project dependency before loading the starter page in Google Chrome or Microsoft Edge.
In this step, get ready to:
- Verify that Node.js meets Vite's compatibility requirement.
- Create the css-in-js-theme-lab React project.
- Install the pinned dependencies to launch the starter page.
Check Node.js compatibility
Vite requires Node.js 20.19+ or 22.12+. A version check confirms that your computer can run the pinned toolchain.
- Press the Windows key to open search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Select View from the top menu.
- Select Terminal from the View menu.
- Check your installed Node.js version by running this command:
node --version
What Does This Check?
The --version flag prints the installed Node.js version. You can compare that output with Vite's supported ranges.
- Choose the tab that matches your terminal output.
✔️ I see a compatible version
That compatibility check is complete. Vite now has a supported runtime.
ⓧ I see an older version
Your current Node.js version falls below Vite's requirement. A current LTS release provides a compatible runtime.
- Open the official Node.js download page.
- Install the current LTS release for Windows.
- Restart Visual Studio Code after the installation finishes.
- Press Ctrl+` to reopen the integrated terminal.
- Repeat the version command shown above.
You should now see a version that satisfies 20.19+ or 22.12+.
ⓧ Command not found
The terminal cannot find a Node.js installation. Installing a current LTS release adds the runtime required by Vite.
- Open the official Node.js download page.
- Install the current LTS release for Windows.
- Restart Visual Studio Code after the installation finishes.
- Press Ctrl+` to reopen the integrated terminal.
- Repeat the version command shown above.
You should now see a Node.js version in the terminal.
Create the Vite React project
Vite's scaffolder creates the starter files that React needs. Its interactive prompts let you choose the project name plus the exact template.
- Move to your Desktop by running this command:
cd ~/Desktop
Why Start on the Desktop?
This command changes the terminal location to your Desktop. The new css-in-js-theme-lab folder will be easy to find there.
The scaffolder asks five questions after it starts. Use the following choices to generate the correct React starter.
- Enter css-in-js-theme-lab for Project name:.
- Choose React for Select a framework:.
- Choose JavaScript for Select a variant:.
- Keep Oxlint for Which linter to use?.
- Choose No for Install with npm and start now?.
- Start the Vite scaffolder by running this command:
npm create vite@latest
What Does This Command Create?
The command starts Vite's official scaffolding flow. Your prompt choices create a React JavaScript project named css-in-js-theme-lab.
Choosing No leaves dependency installation for the pinned manifest later in this step.
- Confirm that the project folder exists by running:
ls
What Does This Check?
The command lists the folders on your Desktop. You should see css-in-js-theme-lab in the output.
Project Folder Missing?
Check whether the scaffolder is still waiting for a prompt response. Run the scaffolding command again if the process closed before creating the folder.
Confirm that you entered css-in-js-theme-lab as the project name.
Help me troubleshoot why the Vite project folder was not created.
Pin dependencies and launch Vite
A project manifest records the packages that the lab uses. Exact version pins keep your local setup aligned with every later code change.
- Inspect the new project's top-level files by running these commands:
cd css-in-js-theme-lab
ls
What Do These Commands Show?
The first command moves the terminal into css-in-js-theme-lab. The second command lists the scaffolded files.
You should see package.json in the output. You should also see the src folder plus vite.config.js.
- Expand the src folder in the Visual Studio Code Explorer sidebar.
- Confirm that main.jsx is listed inside src.
- Confirm that vite.config.js is listed at the top level of css-in-js-theme-lab.
- Select package.json in the Explorer sidebar.
- Press Ctrl+A inside the editor to select its current contents.
- Replace the selected contents with this pinned manifest:
{
"name": "css-in-js-theme-lab",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0",
"styled-components": "6.5.3"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
What Does This Manifest Pin?
- The react package plus react-dom use version 19.3.0.
- Styled Components supplies the component-scoped styling API through styled-components version 6.5.3.
- The development toolchain uses vite version 8.3.0.
- The React integration uses @vitejs/plugin-react version 6.1.1.
- The scripts section defines development plus build commands.
- Press Ctrl+S to save package.json.
- Install the pinned dependencies by running:
npm install
What Does the Install Do?
The command reads package.json. It installs the exact dependencies into the project.
The installation also writes package-lock.json so future installs can resolve the same package versions.
You should see the installation finish without a dependency error. The terminal prompt returns when the packages are ready.
Dependency Installation Failed?
Confirm that the terminal path ends with css-in-js-theme-lab. The install command needs to run beside package.json.
Check that every quote plus comma in package.json matches the reference below.
Help me debug the dependency installation.
✔️ Awesome, I've got everything!
Great. Your pinned package.json is saved with the dependencies this lab needs.
ⓧ I'd like to double check the full code
Compare your complete package.json file with this reference:
{
"name": "css-in-js-theme-lab",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0",
"styled-components": "6.5.3"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
Why Compare the Whole File?
A complete comparison catches missing commas plus changed version pins. The saved file should match this reference character for character.
The development server turns the scaffolded files into a live browser page. It stays active in the terminal while you work.
Before you run the server, do you expect the starter page to come from a saved browser file or the live Vite process?
- Start the development server by running:
npm run dev
What Happens When Vite Starts?
Vite starts a foreground development process. The terminal remains occupied until you press Ctrl+C.
The terminal prints a Local URL for the running React page. Later edits can update that page while the process stays active.
- Record the Local URL from the terminal: your Vite Local URL.
- Press the Windows key to open search.
- Type Google Chrome or Microsoft Edge into the search field.
- Press Enter to open the browser.
- Paste your Vite Local URL into the address bar.
- Press Enter to load the running project.
You should see the Vite and React starter page. This page proves that the pinned project can build plus run locally.
Starter Page Not Loading?
- Confirm that the development server is still running in the Visual Studio Code terminal.
- Use the Local URL from the current terminal output because Vite can choose a different port when its default is busy.
- Confirm that the terminal path ends with css-in-js-theme-lab.
Help me get the Vite starter page running.
Your React styling lab is running with its pinned toolchain. Next, you will replace the starter page with your first styled product card.
Style Your First Product Card
The starter page proves your local Vite app can update in the browser. Its default visuals still hide the component styling pattern you came here to learn.
One product card gives you a small surface for connecting React markup to CSS-in-JS. styled-components keeps each visual rule beside the element it styles.
In this step, get ready to:
- Disconnect the app from the scaffolded global stylesheet.
- Build one fixed product card with styled elements.
- Polish the card with responsive sizing and interactive styling.
Remove the scaffolded stylesheet
The src/main.jsx entry point controls which global files enter the app. It still needs React DOM to mount App into the page.
- Replace all contents of src/main.jsx with this code:
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(<App />)
What does this entry point do?
- The createRoot import provides the browser entry point for the React interface.
- The App import loads the component you are about to replace.
- Removing the stylesheet import stops src/index.css from applying its scaffolded global rules.
- Save src/main.jsx.
- Switch back to the app in Chrome or Edge.
You should see the starter content shift as the scaffolded global layout rules disappear.
Did the page stop loading?
- Check that the import path is exactly ./App.jsx.
- Confirm that the final line closes both sets of parentheses.
- Check that the Vite development server from the previous step is still running.
Ask for help with debugging the React entry point.
✔️ Awesome, I've got everything!
Great. Your React entry point now renders App without loading src/index.css.
ⓧ I'd like to double check the full code
Compare your complete src/main.jsx file with this reference:
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(<App />)
This file mounts the imported App component without importing the scaffolded stylesheet.
Build the card structure
The styled helper turns each tagged template literal into a reusable element. Defining those elements outside the component keeps them stable while JSX renders the card content.
- Replace all contents of src/App.jsx with this first card structure:
import styled from 'styled-components'
const Page = styled.main`width: min(1080px, calc(100% - 2rem)); margin: 0 auto; padding: 3rem 0;`
const Card = styled.article`display: flex; width: min(100%, 22rem); flex-direction: column; overflow: hidden;`
const CardArt = styled.div`display: grid; min-height: 8rem; place-items: center;`
const CardBody = styled.div`display: flex; flex: 1; flex-direction: column; padding: 1.25rem;`
export default function App() {
return (
<Page>
<Card>
<CardArt aria-hidden="true">AU</CardArt>
<CardBody>
<span>Audio</span>
<h2>Focus Headphones</h2>
<p>A distraction-free headset for deep work sessions.</p>
<div><strong>$129</strong><button type="button">View details</button></div>
</CardBody>
</Card>
</Page>
)
}
How is the card assembled?
- The Page element gives the interface responsive horizontal space.
- The Card element creates a vertical container that stays within the available width.
- The CardArt element reserves a visual area for the category initials.
- The CardBody element groups the product information inside consistent padding.
- Save src/App.jsx.
- Return to the running app in your browser.
You should see one fixed product card structure replace the Vite starter page. It displays AU, Focus Headphones, its description, $129, and a button.
Still seeing the starter page?
- Confirm that you replaced the contents of src/App.jsx.
- Check that every styled element closes with a backtick.
- Confirm that export default function App() remains at the bottom of the file.
Ask for help with finding why the product card is not rendering.
Finish the card styling
The structure now has a clear job for every section. Scoped visual rules can turn those sections into a polished card without adding global class names.
- Replace all contents of src/App.jsx with the finished single-card version:
import styled from 'styled-components'
const Page = styled.main`width: min(1080px, calc(100% - 2rem)); margin: 0 auto; padding: 3rem 0;`
const Card = styled.article`display: flex; width: min(100%, 22rem); min-height: 100%; flex-direction: column; overflow: hidden; border: 2px solid #dce2ee; border-radius: 1.25rem; background: #ffffff; box-shadow: 0 10px 30px rgba(31, 41, 55, 0.08); transition: transform 180ms ease, background 180ms ease, border-color 180ms ease; &:hover { transform: translateY(-4px); }`
const CardArt = styled.div`display: grid; min-height: 8rem; place-items: center; background: #eef2ff; color: #5b5bd6; font-size: 2.5rem; font-weight: 900; letter-spacing: -0.08em;`
const CardBody = styled.div`display: flex; flex: 1; flex-direction: column; padding: 1.25rem;`
const CardTopLine = styled.div`display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;`
const Category = styled.span`color: #5b6475; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;`
const ProductName = styled.h2`margin: 1rem 0 0.5rem; font-size: 1.3rem;`
const Description = styled.p`margin: 0; color: #5b6475; line-height: 1.55;`
const CardFooter = styled.div`display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: auto; padding-top: 1.4rem;`
const Price = styled.strong`font-size: 1.15rem;`
const ActionButton = styled.button`border: 0; border-radius: 0.75rem; padding: 0.7rem 0.85rem; background: #eef2ff; color: #172033; cursor: pointer; font-weight: 800;`
export default function App() {
return (
<Page>
<Card>
<CardArt aria-hidden="true">AU</CardArt>
<CardBody>
<CardTopLine><Category>Audio</Category></CardTopLine>
<ProductName>Focus Headphones</ProductName>
<Description>A distraction-free headset for deep work sessions.</Description>
<CardFooter><Price>$129</Price><ActionButton type="button">View details</ActionButton></CardFooter>
</CardBody>
</Card>
</Page>
)
}
What creates the polished result?
- The responsive widths keep the page inset from the browser edge while limiting the card to a readable size.
- The border, rounded corners, background, and shadow separate the product from the page.
- The &:hover rule lifts the card when the pointer moves over it.
- The category and card-art styles create a clear visual hierarchy above the product details.
- The footer pushes the price and action button to the bottom of the card.
Before you save, which parts of the plain card do you expect to change most visibly?
- Save src/App.jsx.
- Switch back to the running app in Chrome or Edge.
You should see one polished product card with a purple category-art block, a description, a price, and a styled action button. The Vite starter page is gone.
- Hover over the card with your pointer.
You should see the complete card lift slightly from the page.
- Narrow the browser window until the page approaches mobile width.
You should see the card shrink with the available space while remaining inside the page edges. Nice work. Your first reusable styled card now responds to the browser.
Does the card look unstyled?
- Check that the first line imports styled from styled-components.
- Confirm that every CSS declaration sits inside its component's opening and closing backticks.
- Check that the JSX uses names such as CardArt and ActionButton with matching capitalization.
Ask for help with debugging the styled product card.
✔️ Awesome, I've got everything!
Your browser now shows the finished single-card result. Double check that src/App.jsx is saved before continuing.
ⓧ I'd like to double check the full code
Compare your complete src/App.jsx file with this reference:
import styled from 'styled-components'
const Page = styled.main`width: min(1080px, calc(100% - 2rem)); margin: 0 auto; padding: 3rem 0;`
const Card = styled.article`display: flex; width: min(100%, 22rem); min-height: 100%; flex-direction: column; overflow: hidden; border: 2px solid #dce2ee; border-radius: 1.25rem; background: #ffffff; box-shadow: 0 10px 30px rgba(31, 41, 55, 0.08); transition: transform 180ms ease, background 180ms ease, border-color 180ms ease; &:hover { transform: translateY(-4px); }`
const CardArt = styled.div`display: grid; min-height: 8rem; place-items: center; background: #eef2ff; color: #5b5bd6; font-size: 2.5rem; font-weight: 900; letter-spacing: -0.08em;`
const CardBody = styled.div`display: flex; flex: 1; flex-direction: column; padding: 1.25rem;`
const CardTopLine = styled.div`display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;`
const Category = styled.span`color: #5b6475; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;`
const ProductName = styled.h2`margin: 1rem 0 0.5rem; font-size: 1.3rem;`
const Description = styled.p`margin: 0; color: #5b6475; line-height: 1.55;`
const CardFooter = styled.div`display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: auto; padding-top: 1.4rem;`
const Price = styled.strong`font-size: 1.15rem;`
const ActionButton = styled.button`border: 0; border-radius: 0.75rem; padding: 0.7rem 0.85rem; background: #eef2ff; color: #172033; cursor: pointer; font-weight: 800;`
export default function App() {
return (
<Page>
<Card>
<CardArt aria-hidden="true">AU</CardArt>
<CardBody>
<CardTopLine><Category>Audio</Category></CardTopLine>
<ProductName>Focus Headphones</ProductName>
<Description>A distraction-free headset for deep work sessions.</Description>
<CardFooter><Price>$129</Price><ActionButton type="button">View details</ActionButton></CardFooter>
</CardBody>
</Card>
</Page>
)
}
This complete file defines every styled element outside App before composing the fixed product card inside the component.
Your first product card is styled and responsive. Next, you will reuse this component to turn one fixed product into a gallery.
Build a Reusable Product Gallery
Your running Vite app now shows one polished product card. Repeating that card by copying its JSX would make every future update harder.
You will move the product details into a JavaScript array. One React component will receive each record through props.
In this step, get ready to:
- Store three product records in a shared array.
- Render every record through one reusable product-card component.
- Compare the Featured label with the fixed card styling.
Create the gallery data and helpers
A shared array gives every product the same set of fields. The featured boolean records which product has priority.
- In src/App.jsx, add these declarations below the existing styled elements:
const Badge = styled.span`
border-radius: 999px;
padding: 0.3rem 0.55rem;
background: #5b5bd6;
color: #ffffff;
font-size: 0.72rem;
font-weight: 800;
`
const products = [
{
id: 1,
name: 'Focus Headphones',
category: 'Audio',
description: 'A distraction-free headset for deep work sessions.',
price: '$129',
featured: true,
},
{
id: 2,
name: 'Desk Organizer',
category: 'Workspace',
description: 'A modular tray that keeps everyday tools within reach.',
price: '$42',
featured: false,
},
{
id: 3,
name: 'Ambient Lamp',
category: 'Lighting',
description: 'A compact lamp with a warm, comfortable evening glow.',
price: '$68',
featured: false,
},
]
How Is the Gallery Prepared?
- Each object follows the same field structure, so one component can read every product.
- The unique id gives React a stable reference for each rendered card.
- The featured value stores a true or false decision alongside the product data.
- The Badge styled component gives the featured label its compact pill shape.
Render each product through one component
The ProductCard component turns one product object into a complete card. React uses each product's id to track the rendered items.
- In src/App.jsx, replace the existing App component with the reusable component and gallery below:
function ProductCard({ product }) {
const { category, description, featured, name, price } = product
return (
<Card>
<CardArt aria-hidden="true">{category.slice(0, 2).toUpperCase()}</CardArt>
<CardBody>
<CardTopLine>
<Category>{category}</Category>
{featured && <Badge>Featured</Badge>}
</CardTopLine>
<ProductName>{name}</ProductName>
<Description>{description}</Description>
<CardFooter><Price>{price}</Price><ActionButton type="button">View details</ActionButton></CardFooter>
</CardBody>
</Card>
)
}
export default function App() {
return (
<Page>
<Grid aria-label="Product gallery">
{products.map((product) => <ProductCard key={product.id} product={product} />)}
</Grid>
</Page>
)
}
What Does This Code Do?
- The destructuring line pulls the six product fields into local values that the component can render.
- The category abbreviation takes the first two letters before converting them to uppercase.
- The conditional expression renders Badge only when featured is true.
- The products.map call creates one ProductCard instance for every object in the array.
Before you save, predict whether the featured value changes only the label or the card styling too.
- Save src/App.jsx.
- Switch back to the browser from earlier.
You should see cards for Focus Headphones, Desk Organizer, and Ambient Lamp. Focus Headphones also shows the Featured badge.
All three card containers still share the same surface, border, and shadow. Every View details button also has the same visual treatment.
You have the reusable core working. One component now renders the complete three-product gallery.
Why Does Featured Only Change the Label?
The featured value controls the conditional badge. Card still uses fixed CSS for its visual treatment.
The same fixed styling applies to ActionButton. The visible shortfall proves that data must be connected explicitly to component styles.
Only Seeing One Card?
Confirm that the old fixed App component was fully replaced. Check that the new component includes products.map inside Grid.
Check that every object in products has a closing brace. Check that the array has a closing square bracket.
Ask for help with the component structure: help me debug my reusable product gallery
✔️ Awesome, I've got everything!
Great. Double check that you saved src/App.jsx with all three products before continuing.
ⓧ I'd like to double check the full code
Compare your complete src/App.jsx file with this version:
import styled from 'styled-components'
const Page = styled.main`
width: min(1080px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0;
`
const Grid = styled.section`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 1.25rem;
`
const Card = styled.article`
display: flex;
min-height: 100%;
flex-direction: column;
overflow: hidden;
border: 2px solid #dce2ee;
border-radius: 1.25rem;
background: #ffffff;
box-shadow: 0 10px 30px rgba(31, 41, 55, 0.08);
transition:lis transform 180ms ease, background 180ms ease,
border-color 180ms ease;
&:hover {
transform: translateY(-4px);
}
`
const CardArt = styled.div`
display: grid;
min-height: 8rem;
place-items: center;
background: #eef2ff;
color: #5b5bd6;
font-size: 2.5rem;
font-weight: 900;
letter-spacing: -0.08em;
`
const CardBody = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 1.25rem;
`
const CardTopLine = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
`
const Category = styled.span`
color: #5b6475;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
`
const ProductName = styled.h2`
margin: 1rem 0 0.5rem;
color: #172033;
font-size: 1.3rem;
`
const Description = styled.p`
margin: 0;
color: #5b6475;
line-height: 1.55;
`
const CardFooter = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 1.4rem;
`
const Price = styled.strong`
color: #172033;
font-size: 1.15rem;
`
const ActionButton = styled.button`
border: 0;
border-radius: 0.75rem;
padding: 0.7rem 0.85rem;
background: #eef2ff;
color: #172033;
cursor: pointer;
font-weight: 800;
`
const Badge = styled.span`
border-radius: 999px;
padding: 0.3rem 0.55rem;
background: #5b5bd6;
color: #ffffff;
font-size: 0.72rem;
font-weight: 800;
`
const products = [
{
id: 1,
name: 'Focus Headphones',
category: 'Audio',
description: 'A distraction-free headset for deep work sessions.',
price: '$129',
featured: true,
},
{
id: 2,
name: 'Desk Organizer',
category: 'Workspace',
description: 'A modular tray that keeps everyday tools within reach.',
price: '$42',
featured: false,
},
{
id: 3,
name: 'Ambient Lamp',
category: 'Lighting',
description: 'A compact lamp with a warm, comfortable evening glow.',
price: '$68',
featured: false,
},
]
function ProductCard({ product }) {
const { category, description, featured, name, price } = product
return (
<Card>
<CardArt aria-hidden="true">
{category.slice(0, 2).toUpperCase()}
</CardArt>
<CardBody>
<CardTopLine>
<Category>{category}</Category>
{featured && <Badge>Featured</Badge>}
</CardTopLine>
<ProductName>{name}</ProductName>
<Description>{description}</Description>
<CardFooter>
<Price>{price}</Price>
<ActionButton type="button">
View details
</ActionButton>
</CardFooter>
</CardBody>
</Card>
)
}
export default function App() {
return (
<Page>
<Grid aria-label="Product gallery">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
</Page>
)
}
The reusable gallery is ready. Next, the featured value can take control of the card's appearance.
Style the Featured Variant
Your gallery already turns React data into three reusable cards. The Featured badge identifies the promoted product.
The featured value still has no effect on the card's presentation. This step connects that value to styled-components through a transient styling prop.
In this step, get ready to:
- Pass the product's featured value into the styled card elements.
- Choose featured or standard styles through interpolation functions.
- Confirm the styling prop stays out of the rendered HTML.
Connect the transient styling prop
A transient prop starts with a dollar sign. Styled-components uses the value while generating CSS without forwarding it to the underlying HTML element.
- Change the opening Card tag inside ProductCard to this line:
<Card $featured={featured}>
What does this line do?
The featured value comes from the current product object. The $featured prop makes that value available inside the Card template.
- Change the opening ActionButton tag inside ProductCard to this line:
<ActionButton type="button" $featured={featured}>
Why pass the prop twice?
Each styled component receives its own props. Passing $featured to the button lets its colors respond independently from the card container.
- Save src/App.jsx.
- Return to the running app in Chrome or Edge.
- Inspect the Focus Headphones card with your browser's page inspector.
The card's HTML omits $featured. Good. Styled-components has consumed the styling-only value without creating a custom HTML attribute.
Seeing a custom featured attribute?
Check that both prop names begin with $. A prop named featured does not use the transient prop convention.
Confirm that the prop is attached to Card or ActionButton. These are the styled components that consume it.
Help me check my transient prop setup.
Style the card from its featured value
Interpolation functions receive the styled component's props. The Card function can now select a border, surface, or shadow for each product.
- Replace the current Card styled component in src/App.jsx with this version:
const Card = styled.article`
display: flex;
min-height: 100%;
flex-direction: column;
overflow: hidden;
border: 2px solid
${({ $featured }) => ($featured ? '#5b5bd6' : '#dce2ee')};
border-radius: 1.25rem;
background: ${({ $featured }) =>
$featured ? '#fff4d6' : '#ffffff'};
box-shadow: ${({ $featured }) =>
$featured
? '0 18px 45px rgba(31, 41, 55, 0.18)'
: '0 10px 30px rgba(31, 41, 55, 0.08)'};
transition: transform 180ms ease, background 180ms ease,
border-color 180ms ease;
&:hover {
transform: translateY(-4px);
}
`
How does the card variant work?
- The border interpolation chooses the accent color when $featured is true.
- The background interpolation gives the featured card a highlighted surface.
- The shadow interpolation adds more depth to the featured card.
- The standard values preserve the original treatment for the other products.
- Save src/App.jsx.
- Return to the running gallery in your browser.
The Focus Headphones card now has a purple border, a warm highlighted surface, and a stronger shadow. That first visual split proves the product data is controlling scoped CSS.
Does the featured card still look unchanged?
Check that Card receives $featured={featured} inside ProductCard.
Check each interpolation for the exact $featured spelling. The dollar sign is part of the prop name.
Help me debug the featured card styles.
The card container now communicates which product is featured. The action button needs the same visual priority.
- Replace the current ActionButton styled component in src/App.jsx with this version:
const ActionButton = styled.button`
border: 0;
border-radius: 0.75rem;
padding: 0.7rem 0.85rem;
background: ${({ $featured }) =>
$featured ? '#5b5bd6' : '#eef2ff'};
color: ${({ $featured }) =>
$featured ? '#ffffff' : '#172033'};
cursor: pointer;
font-weight: 800;
`
How does the button respond?
The featured button uses the accent background with light text. Standard buttons keep the softer background with dark text.
Both choices come from the same ProductCard markup. The prop changes the generated CSS for each instance.
- Save src/App.jsx.
- Return to the running gallery in your browser.
The Focus Headphones button now uses the accent treatment. The Desk Organizer and Ambient Lamp buttons keep their standard appearance.
Does every button look featured?
Confirm that $featured={featured} is inside the mapped ProductCard markup. A fixed true value would feature every button.
Check that only the Focus Headphones object has featured: true in the products array.
Help me fix the action button variant.
✔️ Awesome, I've got everything!
Great. Double check that you saved src/App.jsx before inspecting the finished variant.
ⓧ I'd like to double check the full code
Compare your complete src/App.jsx file with this version:
import styled from 'styled-components'
const Page = styled.main`
width: min(1080px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0;
`
const Header = styled.header`
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
@media (max-width: 620px) {
align-items: flex-start;
flex-direction: column;
}
`
const Eyebrow = styled.p`
margin: 0 0 0.35rem;
color: #5b5bd6;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
`
const Heading = styled.h1`
margin: 0;
color: #172033;
font-size: clamp(2rem, 6vw, 3.5rem);
line-height: 1;
`
const Intro = styled.p`
max-width: 42rem;
margin: 0.8rem 0 0;
color: #5b6475;
line-height: 1.6;
`
const Grid = styled.section`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 1.25rem;
`
const Card = styled.article`
display: flex;
min-height: 100%;
flex-direction: column;
overflow: hidden;
border: 2px solid
${({ $featured }) => ($featured ? '#5b5bd6' : '#dce2ee')};
border-radius: 1.25rem;
background: ${({ $featured }) =>
$featured ? '#fff4d6' : '#ffffff'};
box-shadow: ${({ $featured }) =>
$featured
? '0 18px 45px rgba(31, 41, 55, 0.18)'
: '0 10px 30px rgba(31, 41, 55, 0.08)'};
transition: transform 180ms ease, background 180ms ease,
border-color 180ms ease;
&:hover {
transform: translateY(-4px);
}
`
const CardArt = styled.div`
display: grid;
min-height: 8rem;
place-items: center;
background: #eef2ff;
color: #5b5bd6;
font-size: 2.5rem;
font-weight: 900;
letter-spacing: -0.08em;
`
const CardBody = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 1.25rem;
`
const CardTopLine = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
`
const Category = styled.span`
color: #5b6475;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
`
const Badge = styled.span`
border-radius: 999px;
padding: 0.3rem 0.55rem;
background: #5b5bd6;
color: #ffffff;
font-size: 0.72rem;
font-weight: 800;
`
const ProductName = styled.h2`
margin: 1rem 0 0.5rem;
color: #172033;
font-size: 1.3rem;
`
const Description = styled.p`
margin: 0;
color: #5b6475;
line-height: 1.55;
`
const CardFooter = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 1.4rem;
`
const Price = styled.strong`
color: #172033;
font-size: 1.15rem;
`
const ActionButton = styled.button`
border: 0;
border-radius: 0.75rem;
padding: 0.7rem 0.85rem;
background: ${({ $featured }) =>
$featured ? '#5b5bd6' : '#eef2ff'};
color: ${({ $featured }) =>
$featured ? '#ffffff' : '#172033'};
cursor: pointer;
font-weight: 800;
`
const products = [
{
id: 1,
name: 'Focus Headphones',
category: 'Audio',
description: 'A distraction-free headset for deep work sessions.',
price: '$129',
featured: true,
},
{
id: 2,
name: 'Desk Organizer',
category: 'Workspace',
description: 'A modular tray that keeps everyday tools within reach.',
price: '$42',
featured: false,
},
{
id: 3,
name: 'Ambient Lamp',
category: 'Lighting',
description: 'A compact lamp with a warm, comfortable evening glow.',
price: '$68',
featured: false,
},
]
function ProductCard({ product }) {
const { category, description, featured, name, price } = product
return (
<Card $featured={featured}>
<CardArt aria-hidden="true">
{category.slice(0, 2).toUpperCase()}
</CardArt>
<CardBody>
<CardTopLine>
<Category>{category}</Category>
{featured && <Badge>Featured</Badge>}
</CardTopLine>
<ProductName>{name}</ProductName>
<Description>{description}</Description>
<CardFooter>
<Price>{price}</Price>
<ActionButton type="button" $featured={featured}>
View details
</ActionButton>
</CardFooter>
</CardBody>
</Card>
)
}
export default function App() {
return (
<Page>
<Header>
<div>
<Eyebrow>CSS-in-JS Theme Lab</Eyebrow>
<Heading>Products with personality</Heading>
<Intro>
Reusable React cards whose variants and shared colors are
controlled from JavaScript.
</Intro>
</div>
</Header>
<Grid aria-label="Product gallery">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
</Page>
)
}
Verify the featured prop stays out of HTML
The visual result proves that $featured changes generated styles. The browser inspector can confirm that the implementation also keeps the styling prop away from the DOM.
Before you inspect the card, do you expect $featured to appear among its HTML attributes?
- Open the browser inspector for the Focus Headphones card.
- Select the card's outer article element in the markup view.
- Review the attributes rendered on the selected element.
- Select the featured card's View details button in the markup view.
- Review the attributes rendered on the selected button.
You should see generated class names on both elements. You should not see a $featured HTML attribute.
That completes the prop-to-style connection. Your featured product now controls its container and button presentation without leaking a custom attribute into the page.
Your gallery now supports a reusable visual variant. Next, you will replace the fixed colors with shared theme values and add a live light and dark switch.
Add a Live Theme Toggle
Your gallery now lets React data control the featured treatment through a transient styling prop. Hard-coded colors still lock the complete interface to one appearance.
This step moves shared colors into a theme context provided by styled-components. A state-driven control will switch every connected color without reloading the page.
In this step, get ready to:
- Define shared light and dark theme objects.
- Connect component colors to theme values.
- Add a state-driven theme toggle.
Define shared theme tokens
A theme object gives related colors one shared home. Components can request colors by purpose instead of storing separate hard-coded values.
- In src/App.jsx, replace the import area with this import group:
import { useState } from 'react'
import styled, { ThemeProvider } from 'styled-components'
import { createGlobalStyle } from 'styled-components'
What do these imports provide?
- The useState hook stores the current theme preference.
- The ThemeProvider component makes the active theme available to styled components beneath it.
- The createGlobalStyle helper connects page-wide styles to the same theme.
- Save src/App.jsx.
- Check the browser from earlier. The product gallery should remain rendered without a compilation error.
- Add the light color tokens below the imports by copying this object:
const lightTheme = {
colors: {
page: '#f5f7fb',
surface: '#ffffff',
elevated: '#eef2ff',
featured: '#fff4d6',
text: '#172033',
muted: '#5b6475',
border: '#dce2ee',
accent: '#5b5bd6',
accentText: '#ffffff',
},
}
How is the light theme organized?
Each key describes a color's job in the interface. For example, surface controls standard cards while featured controls the highlighted card.
- Add the dark color tokens directly below lightTheme by copying this object:
const darkTheme = {
colors: {
page: '#111827',
surface: '#1f2937',
elevated: '#312e81',
featured: '#312e1a',
text: '#f9fafb',
muted: '#c7d2fe',
border: '#374151',
accent: '#a5b4fc',
accentText: '#111827',
},
}
Why do the keys match?
Both objects expose the same color keys. Switching themes can therefore replace the complete palette without changing any component code.
- Save src/App.jsx.
- Return to the running app. The gallery should still render while both theme objects compile successfully.
Seeing a compilation error?
Check that each theme contains one colors object. Confirm that every opening brace has a matching closing brace.
Compare the commas after each color value. A missing comma can stop the module from compiling.
Ask for help with the theme objects.
Connect component colors to the theme
Styled-component interpolations receive the active theme through their props. Each hard-coded color can now point to a shared token with the same visual purpose.
- Update the Eyebrow color to read theme.colors.accent.
- Update the Intro color to read theme.colors.muted.
- Save src/App.jsx.
The light theme uses the original accent and muted colors. The browser should keep the heading area rendered with the same familiar palette.
- Update the Card border interpolation to choose theme.colors.accent for featured cards or theme.colors.border for standard cards.
- Update the Card background interpolation to choose theme.colors.featured for featured cards or theme.colors.surface for standard cards.
- Save src/App.jsx.
The Focus Headphones card should retain its highlighted surface. The other two cards should retain their standard surfaces.
- Add background 180ms ease to the Card transition.
- Add border-color 180ms ease to the same transition.
- Save src/App.jsx.
Hover over a card in the browser. It should still rise smoothly while its theme-ready colors remain stable.
- Update the CardArt background to read theme.colors.elevated.
- Update the CardArt color to read theme.colors.accent.
- Save src/App.jsx.
Each category block should remain softly tinted with an accent-colored abbreviation.
- Update the Category color to read theme.colors.muted.
- Update the Badge background to read theme.colors.accent.
- Save src/App.jsx.
The category labels should remain muted. The Featured badge should retain its accent background.
- Update the Badge color to read theme.colors.accentText.
- Update the Description color to read theme.colors.muted.
- Save src/App.jsx.
The badge text should remain readable. Each product description should retain its quieter text color.
- Update the ActionButton background interpolation to choose theme.colors.accent for featured cards or theme.colors.elevated for standard cards.
- Update the ActionButton color interpolation to choose theme.colors.accentText for featured cards or theme.colors.text for standard cards.
- Save src/App.jsx.
The featured action button should keep its accent treatment. The standard buttons should retain their softer surfaces.
What has changed under the surface?
The light theme deliberately reproduces the gallery's existing colors. The visible transformation arrives when the same components receive darkTheme through the provider.
Add the live theme control
Global styles connect the page background and inherited text color to the theme. React state then selects which complete theme object the provider shares.
- Add GlobalStyle below the theme objects by copying this styled component:
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
font-family: Inter, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", sans-serif;
background: ${({ theme }) => theme.colors.page};
color: ${({ theme }) => theme.colors.text};
transition: background 180ms ease, color 180ms ease;
}
button {
font: inherit;
}
`
What does the global style control?
- The universal selector gives every element predictable box sizing.
- The body reads its background and text colors from the active theme.
- The transition smooths each page-wide color change.
- Buttons inherit the page font for a consistent interface.
- Save src/App.jsx.
- Confirm the running gallery still compiles in the browser.
- Inside App(), place these state and theme-selection lines before the return statement:
const [darkMode, setDarkMode] = useState(false)
const activeTheme = darkMode ? darkTheme : lightTheme
How does the theme state work?
The initial false value selects the light theme. Updating darkMode triggers another render with the matching theme object.
- Save src/App.jsx.
- Confirm the browser continues to display the complete gallery.
The provider needs to sit above every themed component. These two reference snippets show the new outer structure around the existing Page markup.
- Replace the opening of the return statement with this provider structure:
return (
<ThemeProvider theme={activeTheme}>
<GlobalStyle />
<Page>
What does the opening wrapper do?
The provider shares activeTheme with every styled component inside Page. GlobalStyle consumes the same object for page-wide colors.
- Replace the end of the return statement with these closing lines:
</Page>
</ThemeProvider>
)
Why close both components?
The closing tags preserve the same nesting established above the interface. Every gallery component now remains inside ThemeProvider.
- Save src/App.jsx.
- Check the browser. The page should display the light theme across its background and product cards.
- Add ThemeButton between Intro and Grid by copying this styled component:
const ThemeButton = styled.button`
flex: 0 0 auto;
border: 0;
border-radius: 999px;
padding: 0.75rem 1rem;
background: ${({ theme }) => theme.colors.accent};
color: ${({ theme }) => theme.colors.accentText};
cursor: pointer;
font-weight: 800;
&:hover {
transform: translateY(-1px);
}
&:focus-visible {
outline: 3px solid ${({ theme }) => theme.colors.text};
outline-offset: 3px;
}
`
What does the toggle styling provide?
The control reads its background and text colors from the active theme. Its focus outline gives keyboard users a clear visual indicator.
- Inside Header, add this button immediately after the closing tag for the text container:
<ThemeButton
type="button"
aria-pressed={darkMode}
onClick={() => setDarkMode((current) => !current)}
>
{darkMode ? 'Switch to light theme' : 'Switch to dark theme'}
</ThemeButton>
How does the button switch themes?
- The click handler flips the current Boolean value through its state updater.
- The label describes the theme available on the next click.
- The aria-pressed value exposes the control's current state to assistive technology.
- Save src/App.jsx.
Theme button missing or unresponsive?
Confirm that ThemeButton sits inside Header. Check that the click handler calls setDarkMode with the inverted current value.
If colors stay fixed, confirm that ThemeProvider receives activeTheme.
Ask for help debugging the theme toggle.
✔️ Awesome, I've got everything!
Great. Double check that you saved src/App.jsx before testing both themes.
ⓧ I'd like to double check the full code
Compare your complete src/App.jsx file with this version:
import { useState } from 'react'
import styled, { ThemeProvider } from 'styled-components'
import { createGlobalStyle } from 'styled-components'
const lightTheme = {
colors: {
page: '#f5f7fb',
surface: '#ffffff',
elevated: '#eef2ff',
featured: '#fff4d6',
text: '#172033',
muted: '#5b6475',
border: '#dce2ee',
accent: '#5b5bd6',
accentText: '#ffffff',
},
}
const darkTheme = {
colors: {
page: '#111827',
surface: '#1f2937',
elevated: '#312e81',
featured: '#312e1a',
text: '#f9fafb',
muted: '#c7d2fe',
border: '#374151',
accent: '#a5b4fc',
accentText: '#111827',
},
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
font-family: Inter, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", sans-serif;
background: ${({ theme }) => theme.colors.page};
color: ${({ theme }) => theme.colors.text};
transition: background 180ms ease, color 180ms ease;
}
button {
font: inherit;
}
`
const Page = styled.main`
width: min(1080px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0;
`
const Header = styled.header`
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
@media (max-width: 620px) {
align-items: flex-start;
flex-direction: column;
}
`
const Eyebrow = styled.p`
margin: 0 0 0.35rem;
color: ${({ theme }) => theme.colors.accent};
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
`
const Heading = styled.h1`
margin: 0;
font-size: clamp(2rem, 6vw, 3.5rem);
line-height: 1;
`
const Intro = styled.p`
max-width: 42rem;
margin: 0.8rem 0 0;
color: ${({ theme }) => theme.colors.muted};
line-height: 1.6;
`
const ThemeButton = styled.button`
flex: 0 0 auto;
border: 0;
border-radius: 999px;
padding: 0.75rem 1rem;
background: ${({ theme }) => theme.colors.accent};
color: ${({ theme }) => theme.colors.accentText};
cursor: pointer;
font-weight: 800;
&:hover {
transform: translateY(-1px);
}
&:focus-visible {
outline: 3px solid ${({ theme }) => theme.colors.text};
outline-offset: 3px;
}
`
const Grid = styled.section`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 1.25rem;
`
const Card = styled.article`
display: flex;
min-height: 100%;
flex-direction: column;
overflow: hidden;
border: 2px solid
${({ $featured, theme }) =>
$featured ? theme.colors.accent : theme.colors.border};
border-radius: 1.25rem;
background: ${({ $featured, theme }) =>
$featured ? theme.colors.featured : theme.colors.surface};
box-shadow: ${({ $featured }) =>
$featured
? '0 18px 45px rgba(31, 41, 55, 0.18)'
: '0 10px 30px rgba(31, 41, 55, 0.08)'};
transition: transform 180ms ease, background 180ms ease,
border-color 180ms ease;
&:hover {
transform: translateY(-4px);
}
`
const CardArt = styled.div`
display: grid;
min-height: 8rem;
place-items: center;
background: ${({ theme }) => theme.colors.elevated};
color: ${({ theme }) => theme.colors.accent};
font-size: 2.5rem;
font-weight: 900;
letter-spacing: -0.08em;
`
const CardBody = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 1.25rem;
`
const CardTopLine = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
`
const Category = styled.span`
color: ${({ theme }) => theme.colors.muted};
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
`
const Badge = styled.span`
border-radius: 999px;
padding: 0.3rem 0.55rem;
background: ${({ theme }) => theme.colors.accent};
color: ${({ theme }) => theme.colors.accentText};
font-size: 0.72rem;
font-weight: 800;
`
const ProductName = styled.h2`
margin: 1rem 0 0.5rem;
font-size: 1.3rem;
`
const Description = styled.p`
margin: 0;
color: ${({ theme }) => theme.colors.muted};
line-height: 1.55;
`
const CardFooter = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
padding-top: 1.4rem;
`
const Price = styled.strong`
font-size: 1.15rem;
`
const ActionButton = styled.button`
border: 0;
border-radius: 0.75rem;
padding: 0.7rem 0.85rem;
background: ${({ $featured, theme }) =>
$featured ? theme.colors.accent : theme.colors.elevated};
color: ${({ $featured, theme }) =>
$featured ? theme.colors.accentText : theme.colors.text};
cursor: pointer;
font-weight: 800;
`
const products = [
{
id: 1,
name: 'Focus Headphones',
category: 'Audio',
description: 'A distraction-free headset for deep work sessions.',
price: '$129',
featured: true,
},
{
id: 2,
name: 'Desk Organizer',
category: 'Workspace',
description: 'A modular tray that keeps everyday tools within reach.',
price: '$42',
featured: false,
},
{
id: 3,
name: 'Ambient Lamp',
category: 'Lighting',
description: 'A compact lamp with a warm, comfortable evening glow.',
price: '$68',
featured: false,
},
]
function ProductCard({ product }) {
const { category, description, featured, name, price } = product
return (
<Card $featured={featured}>
<CardArt aria-hidden="true">
{category.slice(0, 2).toUpperCase()}
</CardArt>
<CardBody>
<CardTopLine>
<Category>{category}</Category>
{featured && <Badge>Featured</Badge>}
</CardTopLine>
<ProductName>{name}</ProductName>
<Description>{description}</Description>
<CardFooter>
<Price>{price}</Price>
<ActionButton type="button" $featured={featured}>
View details
</ActionButton>
</CardFooter>
</CardBody>
</Card>
)
}
export default function App() {
const [darkMode, setDarkMode] = useState(false)
const activeTheme = darkMode ? darkTheme : lightTheme
return (
<ThemeProvider theme={activeTheme}>
<GlobalStyle />
<Page>
<Header>
<div>
<Eyebrow>CSS-in-JS Theme Lab</Eyebrow>
<Heading>Products with personality</Heading>
<Intro>
Reusable React cards whose variants and shared colors are
controlled from JavaScript.
</Intro>
</div>
<ThemeButton
type="button"
aria-pressed={darkMode}
onClick={() => setDarkMode((current) => !current)}
>
{darkMode ? 'Switch to light theme' : 'Switch to dark theme'}
</ThemeButton>
</Header>
<Grid aria-label="Product gallery">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
</Page>
</ThemeProvider>
)
}
Before you test the control, which parts of the interface do you expect to change together?
- Click Switch to dark theme in the browser.
You should see the page, cards, text, badge, category blocks, and buttons switch to the dark palette without a reload.
- Click Switch to light theme.
You should see the complete interface return to the light palette. You did it. One state value now coordinates colors across the full gallery.
Secret mission
Add a Compact Density Mode
Add a second interface preference that switches the gallery between compact and comfortable spacing. Your density control will compose with the existing theme toggle and featured card variant.
Clean Up Your Resources
Clean Up Your Resources
Everything here runs on your Windows computer. There are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- A local Vite development server for the enhanced product gallery.
- The local css-in-js-theme-lab folder containing your source files and installed dependencies.
Keep everything running
Choose this option if you are still testing your theme and density combinations. Your gallery remains available in the browser.
- Keep the Vite development server running to continue testing the gallery.
- Keep the css-in-js-theme-lab folder to preserve the finished project.
Pause - I'll come back to this later
Pause the development server to free its terminal session. Your project folder stays intact for your next session.
- Press Ctrl+C in the Visual Studio Code terminal to stop the Vite development server.
- Leave the css-in-js-theme-lab folder on your computer so you can continue later.
Delete - I don't want to use this again
Choose this option when you want a clean reset. Deleting the folder is permanent, so make sure you no longer need the code.
The terminal profile choice can be a little fiddly on Windows. The commands below use Command Prompt syntax.
- Press Ctrl+C in the terminal to stop the Vite development server.
- Use the profile menu in the Terminal panel in Visual Studio Code to select Command Prompt.
- Create a fresh terminal in Visual Studio Code.
- Confirm the new terminal prompt ends in css-in-js-theme-lab.
- Permanently delete the project folder by running these commands:
cd ..
rmdir /s /q css-in-js-theme-lab
What does this cleanup do?
The first line moves the terminal to the folder that contains your project.
The second line permanently removes the css-in-js-theme-lab directory and everything stored inside it.
- Close Visual Studio Code after the command completes.
- Use File Explorer to confirm the css-in-js-theme-lab folder is no longer in its former location.
Folder Still Present?
- Check that the new terminal opened inside the css-in-js-theme-lab folder before you ran the commands.
- Close any app that still has a project file open.
- Help me troubleshoot this cleanup.
Nice Work!
Nice Work!
You made it! You built a reusable React product gallery whose styles respond instantly to interface state.
You've learned how to:
- Build a data-driven gallery that renders reusable ProductCard components.
- Create component-scoped CSS-in-JS with styled-components. Connect the $featured transient prop to card surfaces. Connect the same prop to action buttons.
- Apply shared theme tokens through ThemeProvider. Set global page colors with createGlobalStyle. Switch between light and dark modes with React state.
- Secret Mission: Drive a compact and comfortable density mode with a second transient styling prop. Keep the density mode compatible with both themes. Keep the featured variant working at either density.
Ready to quiz yourself?