Build a GitHub Repository Explorer
Build a React app that fetches and displays GitHub repos by username.
Introduction
30 Second Summary
Every developer has a GitHub profile full of projects. Imagine being able to type someone's username and instantly see all their work laid out in a clean, searchable interface.
In this project, you will build a GitHub Repository Explorer using React. You'll fetch real data from the GitHub API and display it as interactive cards showing each repo's name, description, language, and star count.
What You'll Build
A single-page React app where you type a GitHub username, hit search, and see their public repositories displayed as styled cards that link directly to each repo.
By the end of this project, you'll have:
- A working React application scaffolded with Vite that fetches and displays real GitHub data.
- Hands-on experience with React's core building blocks: components, props, state, and effects.
- A polished UI with CSS styling, loading indicators, and error handling for a professional feel.
- Secret Mission: Add a language filter dropdown so users can filter repos by programming language.
Are there any prerequisites?
You need programming experience (a C# or similar backend background works perfectly). No prior HTML, CSS, JavaScript, or React experience is required.
Before We Start
Before We Start...
Set Up Your React Project
To build a React app, you need a project structure with the right dependencies and a development server that updates in real time as you code.
In this step, you will scaffold a React project using Vite, a fast build tool that sets up everything for you in seconds. Then you will clean up the starter files so you have a blank canvas to build on.
In this step, get ready to:
- Scaffold a new React project with Vite.
- Start the development server and confirm it works in your browser.
- Clean up the starter code to create a blank slate.
Scaffold a new React project with Vite
Vite generates a complete project folder with all the configuration files, scripts, and starter code you need. You just run one command and it handles the rest.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, same shortcut on both macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your React project by running this command:
npm create vite@latest github-explorer -- --template react
What does this command do?
npm create vite@latest downloads and runs the create-vite scaffolding tool.
github-explorer is the name of the project folder it will create. -- --template react tells it to use the React template with plain JavaScript.
Prompted to install a package?
npm may ask permission to install the create-vite package. Enter y and press Enter to proceed.
- Move into your new project folder and install its dependencies by running these commands:
cd github-explorer
npm install
What does npm install do?
npm install reads the package.json file that Vite generated and downloads all the libraries your project needs (like React) into a node_modules folder.
- Confirm the project was created by running:
ls
You should see files like package.json, vite.config.js, index.html, and a src folder in the output.
Seeing a compatibility error?
The create-vite tool requires Node.js 20.19+ or 22.12+. Run node --version to check your version. If it is lower, update Node.js from the official site at nodejs.org before retrying.
help me fix a Node.js version error when running npm create vite
Start the development server
Vite includes a development server that serves your app locally and refreshes the browser automatically when you save changes.
- Start the dev server by running:
npm run dev
- Look at the terminal output for a line that says Local: followed by a URL (typically http://localhost:5173).
- Hold Cmd (macOS) or Ctrl (Windows) and click the URL to open it in your browser.
You should see the default Vite + React starter page with a spinning React logo and a counter button.
Page not loading?
Make sure the terminal still shows the dev server running. If you accidentally closed it, run npm run dev again from the github-explorer folder.
If the port is already in use, Vite will automatically pick the next available port. Check the terminal for the actual URL.
help me troubleshoot why my Vite dev server page is not loading in the browser
Clean up the starter code
The scaffold comes with demo content you do not need. You will strip it back to a minimal starting point.
- Open the github-explorer folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the github-explorer folder, and click Open.
Trust the authors?
VS Code may ask if you trust the authors of this folder. Click Yes, I trust the authors to continue.
- In the Explorer sidebar on the left, expand the src folder and click on App.jsx to open it.
- Select all the code in the file by pressing Cmd+A (macOS) or Ctrl+A (Windows).
- Delete it and replace it with the following code:
function App() {
return <h1>GitHub Explorer</h1>
}
export default App
What does this code do?
This defines a React component called App. A component is a function that returns what should appear on screen.
Right now, App returns a single heading element. The export default App line makes this component available to other files that import it.
- Save App.jsx by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Now open src/App.css in the Explorer sidebar.
- Select all the code with Cmd+A (macOS) or Ctrl+A (Windows) and delete it so the file is completely empty.
- Save the file.
- Open src/index.css in the Explorer sidebar.
- Select all and delete the existing content.
- Replace it with the following global reset styles:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
color: #1a1a2e;
line-height: 1.6;
}
What do these styles do?
The * selector resets every element's box model so sizing behaves predictably. It also removes default margins and padding that browsers add.
The body rule sets a clean system font, a light background, and comfortable line spacing for readability.
- Save index.css.
- Finally, delete the file src/assets/react.svg by right-clicking it in the Explorer sidebar and selecting Delete.
- Confirm the deletion when prompted.
Check your browser. You should now see just the text "GitHub Explorer" as a heading on a light grey background, with no other content on the page.
Still seeing the old starter page?
Make sure you saved all three files (App.jsx, App.css, index.css). Unsaved changes will not appear in the browser.
If the browser shows an error, check that your App.jsx matches the code above exactly. A missing closing tag or extra character can break the component.
help me fix my React app after cleaning up the Vite starter code
✔️ 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 what each file should look like at this point.
function App() {
return <h1>GitHub Explorer</h1>
}
export default App
src/App.css should be completely empty with no content at all.
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
color: #1a1a2e;
line-height: 1.6;
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Your blank canvas is ready. Next up, you will build your first custom component: a search bar that accepts a GitHub username.
Build the Search Bar Component
Your project scaffold is running and the browser shows a heading. Now you need a way for users to tell the app which GitHub profile to look up.
In React, you build features by creating components. Each component is a self-contained piece of UI with its own logic and appearance. In this step, you'll create your first custom component: a search bar that captures a username and passes it up to the main App.
In this step, get ready to:
- Create a SearchBar component with a controlled text input and submit button.
- Wire SearchBar into App so the app knows what username to look up.
- Verify the components communicate by testing the search bar in the browser.
Create the SearchBar component
React components communicate through props. A parent component passes data or functions down to a child. The child calls those functions to send information back up.
Your SearchBar will receive an onSearch function from its parent and call it with whatever username the user typed. You'll also use a React hook called useState to track the text inside the input field. This is called a "controlled input" because React owns the value, not the browser.
- In the VS Code Explorer sidebar, hover over the src folder and click the New File icon (the page icon with a plus sign).
- Type components/SearchBar.jsx as the filename and press Enter. VS Code creates the components folder and the file automatically.
- Paste the following code into SearchBar.jsx:
import { useState } from 'react'
function SearchBar({ onSearch }) {
// Track what the user types in the input field
const [input, setInput] = useState('')
// Handle form submission (button click or Enter key)
const handleSubmit = (e) => {
e.preventDefault()
if (input.trim()) {
onSearch(input.trim())
}
}
return (
<form className="search-bar" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Enter a GitHub username"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">Search</button>
</form>
)
}
export default SearchBar
What does this code do?
- useState('') creates a state variable called input that starts as an empty string. Every time the user types a character, setInput updates this value.
- handleSubmit runs when the form is submitted (either by clicking the button or pressing Enter). It calls e.preventDefault() to stop the page from refreshing, then passes the trimmed input to the onSearch function received from the parent.
- The value={input} and onChange pair makes this a controlled input. React is the single source of truth for what text appears in the field.
- Save src/components/SearchBar.jsx.
You won't see a change in the browser yet because no other file imports this component. The next substep wires it into App.
Seeing a red squiggly line in VS Code?
If VS Code shows an error on the import line, make sure the file extension is .jsx (not .js). Vite uses the extension to know how to process React syntax.
Help me debug my SearchBar.jsx file
Import SearchBar into App
With SearchBar ready, you need to render it inside your main App component. You'll also add a username state variable to App so it can remember what the user searched for.
- Switch to src/App.jsx in VS Code.
- Replace the entire contents of the file with this code:
import { useState } from 'react'
import SearchBar from './components/SearchBar'
import './App.css'
function App() {
// Store the username that the user searched for
const [username, setUsername] = useState('')
return (
<div className="app">
<h1>GitHub Explorer</h1>
<SearchBar onSearch={setUsername} />
</div>
)
}
export default App
What does this code do?
- useState('') creates a username state variable in App. When SearchBar calls onSearch, the value flows up and gets stored here.
- <SearchBar onSearch={setUsername} /> renders the SearchBar and passes the setUsername function as a prop. When the user clicks Search, SearchBar calls this function with the typed username.
- The <div className="app"> wrapper gives you a hook for CSS styling later.
- Save src/App.jsx.
- Switch to your browser. You should see the "GitHub Explorer" heading plus a text input and a Search button below it.
Don't see the search bar?
Make sure you saved both files (SearchBar.jsx and App.jsx). The dev server picks up changes automatically, but only after the file is saved to disk.
Check the terminal running npm run dev for error messages. A common mistake is a typo in the import path. The path must be './components/SearchBar' (with a leading ./).
Help me debug why my SearchBar isn't appearing
✔️ 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 src/components/SearchBar.jsx:
import { useState } from 'react'
function SearchBar({ onSearch }) {
const [input, setInput] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (input.trim()) {
onSearch(input.trim())
}
}
return (
<form className="search-bar" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Enter a GitHub username"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">Search</button>
</form>
)
}
export default SearchBar
And here is the full src/App.jsx:
import { useState } from 'react'
import SearchBar from './components/SearchBar'
import './App.css'
function App() {
const [username, setUsername] = useState('')
return (
<div className="app">
<h1>GitHub Explorer</h1>
<SearchBar onSearch={setUsername} />
</div>
)
}
export default App
Verify the search bar communicates with App
The search bar is rendering, but you want to confirm that clicking Search actually sends the username up to the App component. Right now there is no visible change when you click the button because App doesn't do anything with the username yet.
- Type octocat into the text input in your browser.
- Click the Search button.
- Open your browser's Developer Tools by pressing Cmd+Option+I (macOS) or F12 (Windows).
- Click the Components tab at the top of Developer Tools. If you have the React Developer Tools browser extension installed, you'll see your component tree. Click on App to see that username now holds "octocat".
Don't have React Developer Tools?
React Developer Tools is a free browser extension that lets you inspect component state and props. It's not required to continue. You'll get concrete proof the wiring works in the next step when you use the username to fetch real data from the GitHub API.
Your search bar is wired up and ready to trigger data fetches. In the next step, you'll use the username state to call the GitHub API and pull in real repository data.
Fetch Repositories from the GitHub API
Your search bar captures a username, but nothing happens with it yet. The app needs to reach out to GitHub's API and pull back that user's repositories.
In this step, you will use React's useEffect hook to fetch data whenever the username changes. You will also handle loading and error states so the UI stays responsive during the request.
In this step, get ready to:
- Add a useEffect hook that fetches repos from the GitHub API.
- Track loading and error states to give the user feedback.
- Verify the fetch works by searching for a real GitHub user.
Add a useEffect hook to fetch repositories
React's useEffect hook lets you run code in response to state changes. You will use it to call the GitHub API every time the username state updates.
- In src/App.jsx, find the import line at the top of the file:
import { useState } from 'react'
- Add useEffect to the import so it looks like this:
import { useState, useEffect } from 'react'
Now you need state variables to hold the fetched data, a loading flag, and any error messages.
- Find the existing username state declaration inside the App function:
const [username, setUsername] = useState('')
- Add these three new state variables directly below it:
// Stores the array of repository objects from GitHub
const [repos, setRepos] = useState([])
// Tracks whether a fetch is currently in progress
const [loading, setLoading] = useState(false)
// Holds the error message if the fetch fails
const [error, setError] = useState(null)
What do these state variables do?
repos starts as an empty array and will hold the repository objects returned by GitHub.
loading is a boolean flag. When true, the app shows a loading message. When false, the fetch is done.
error starts as null (no error). If the fetch fails, it stores the error message string.
You won't see any change in the browser yet. The next chunk uses these variables.
Now add the useEffect hook that fetches repositories whenever username changes.
- Add this useEffect block between your state declarations and the return statement:
useEffect(() => {
// Skip the fetch if no username has been entered yet
if (!username) return
const fetchRepos = async () => {
setLoading(true)
setError(null)
try {
// Call the GitHub API for this user's public repos
const response = await fetch(
`https://api.github.com/users/${username}/repos?per_page=30&sort=updated`
)
if (!response.ok) {
throw new Error(`User "${username}" not found`)
}
const data = await response.json()
setRepos(data)
} catch (err) {
setError(err.message)
setRepos([])
} finally {
setLoading(false)
}
}
fetchRepos()
}, [username])
What does this useEffect do?
- The [username] dependency array tells React to re-run this effect only when username changes. It does not re-run on every keystroke.
- if (!username) return skips the fetch when the component first mounts (before the user has searched).
- fetchRepos is an async function defined inside the effect because useEffect itself cannot be async.
- The try/catch/finally block handles success, failure, and cleanup (turning off loading) in a single flow.
Why define fetchRepos inside the effect?
useEffect callbacks cannot use the async keyword directly. Defining a separate async function inside and immediately calling it is the standard React pattern for data fetching in effects.
You still won't see a visible change. The next chunk adds the loading and error display to the JSX.
Display loading and error states
The fetch logic updates loading and error, but the browser shows nothing because the JSX doesn't render them yet. React only displays state values that appear in the returned JSX.
- In the same file, find your return statement. Locate this line inside the <div className="app">:
<SearchBar onSearch={setUsername} />
- Add these two lines directly below it, before the closing </div>:
{loading && <p className="status">Loading...</p>}
{error && <p className="status error">{error}</p>}
What is this pattern?
This is conditional rendering in React. The && operator short-circuits: if the left side is true, React renders the right side. If false, nothing appears.
So {loading && <p>Loading...</p>} only shows the paragraph while the fetch is in progress.
- Save src/App.jsx.
- Switch to your browser. You should still see the heading and search bar. No errors in the page.
Seeing a blank page or console error?
- Check that your useEffect block is placed between the state declarations and the return statement. Placing it outside the function body causes a syntax error.
- Make sure useEffect is included in the import at the top: import { useState, useEffect } from 'react'.
- Confirm you have matching curly braces and parentheses. A missing } at the end of the effect will break the file.
Help me fix a blank page after adding useEffect to my App.jsx
Verify the fetch works
Time to confirm that the app actually calls the GitHub API and responds to results.
- In your browser, type octocat into the search field and click Search.
- Watch for the text Loading... to flash briefly below the search bar. It disappears once the repos load.
- Now try searching for a username that does not exist, like thisuserdoesnotexist99999.
- You should see a red error message: User "thisuserdoesnotexist99999" not found.
How do I know repos actually loaded?
When you search for octocat, the loading message appears and then vanishes without an error. That means the fetch succeeded and repos is now full of data. You will display that data visually in the next step.
To peek at the raw data now, open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look at the Network tab. You will see the GitHub API request and its JSON response.
Don't see Loading... at all?
- The loading state can flash very quickly on fast connections. Try throttling your network in DevTools (Network tab, set throttle to Slow 3G) and search again.
- If you never see any loading text or error, confirm the useEffect dependency array is [username] and that SearchBar calls onSearch properly.
Help me debug why my GitHub fetch isn't working
✔️ Awesome, I've got everything!
Great. Double check you have saved src/App.jsx before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
import { useState, useEffect } from 'react'
import SearchBar from './components/SearchBar'
import './App.css'
function App() {
const [username, setUsername] = useState('')
const [repos, setRepos] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!username) return
const fetchRepos = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(
`https://api.github.com/users/${username}/repos?per_page=30&sort=updated`
)
if (!response.ok) {
throw new Error(`User "${username}" not found`)
}
const data = await response.json()
setRepos(data)
} catch (err) {
setError(err.message)
setRepos([])
} finally {
setLoading(false)
}
}
fetchRepos()
}, [username])
return (
<div className="app">
<h1>GitHub Explorer</h1>
<SearchBar onSearch={setUsername} />
{loading && <p className="status">Loading...</p>}
{error && <p className="status error">{error}</p>}
</div>
)
}
export default App
Your app now talks to the GitHub API and gives the user real-time feedback. Next up, you will build the components that display those repositories as cards on the page.
Display Repositories with RepoCard and RepoList
Your app can now fetch repository data from GitHub and store it in state. The next challenge is turning that raw data into something a user can actually see and interact with.
In React, you build UIs by composing small, focused components together. You will create two new components in this step: one to display a single repository card, and another to render a list of those cards.
In this step, get ready to:
- Create a RepoCard component that displays a single repository's details.
- Create a RepoList component that renders a collection of RepoCards.
- Wire RepoList into App so the browser shows repo cards after a search.
Create the RepoCard component
Each repository in the API response is an object with fields like name, description, language, and stargazers_count. The RepoCard component takes one repo object as a prop and renders it as a readable card.
- Create a new file called src/components/RepoCard.jsx in VS Code.
- Paste the following code into the file:
function RepoCard({ repo }) {
return (
<div className="repo-card">
<h3>
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
</h3>
{repo.description && <p className="description">{repo.description}</p>}
<div className="repo-meta">
{repo.language && (
<span className="language">
<span className="language-dot"></span>
{repo.language}
</span>
)}
<span className="stars">★ {repo.stargazers_count}</span>
</div>
</div>
)
}
export default RepoCard
What does this code do?
The function uses destructuring to pull the repo object out of the props parameter. This is the same pattern your SearchBar uses to receive onSearch.
- repo.html_url links the repo name to its GitHub page. The target="_blank" attribute opens the link in a new tab.
- repo.description may be null for some repos, so the && operator only renders the paragraph if a description exists.
- repo.language uses the same conditional pattern. The language-dot span will eventually display a colored circle next to the language name.
- ★ is an HTML entity for a star character, paired with repo.stargazers_count to show how many stars the repo has.
- Save src/components/RepoCard.jsx.
- You won't see a change in the browser yet. The next component will use RepoCard to build the full list.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
function RepoCard({ repo }) {
return (
<div className="repo-card">
<h3>
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
</h3>
{repo.description && <p className="description">{repo.description}</p>}
<div className="repo-meta">
{repo.language && (
<span className="language">
<span className="language-dot"></span>
{repo.language}
</span>
)}
<span className="stars">★ {repo.stargazers_count}</span>
</div>
</div>
)
}
export default RepoCard
Create the RepoList component
A list component takes an array and renders one card per item. In React, you do this with the .map() array method inside JSX.
- Create a new file called src/components/RepoList.jsx in VS Code.
- Paste the following code into the file:
import RepoCard from './RepoCard'
function RepoList({ repos }) {
return (
<div className="repo-list">
{repos.map((repo) => (
<RepoCard key={repo.id} repo={repo} />
))}
</div>
)
}
export default RepoList
What does this code do?
RepoList receives the repos array as a prop. It calls .map() to loop over every repo object and return a RepoCard for each one.
The key={repo.id} prop is required whenever you render a list in React. It gives React a stable identity for each item so it can efficiently update the DOM when the list changes.
- Save src/components/RepoList.jsx.
- You won't see a change in the browser yet. The final piece is connecting this component to your App.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
import RepoCard from './RepoCard'
function RepoList({ repos }) {
return (
<div className="repo-list">
{repos.map((repo) => (
<RepoCard key={repo.id} repo={repo} />
))}
</div>
)
}
export default RepoList
Render the repo list in App.jsx
Both display components are ready. Now you need to import RepoList into your App component and render it when there are repos to show.
- In src/App.jsx, find this line near the top of the file:
import SearchBar from './components/SearchBar'
- Add the following import directly below it:
import RepoList from './components/RepoList'
- Now scroll down to the return statement and find this line:
{error && <p className="status error">{error}</p>}
- Add the following line directly below it:
{!loading && !error && repos.length > 0 && <RepoList repos={repos} />}
What does this line do?
This is conditional rendering. The && operators chain three checks together: the app is not loading, there is no error, and the repos array has at least one item.
Only when all three conditions are true does React render the <RepoList /> component. This prevents showing an empty list while data is still being fetched or when something went wrong.
- Save src/App.jsx.
- Switch to your browser (it should auto-refresh).
- Type octocat in the search bar and click Search.
- You should see a list of repository names appear on the page, each one a clickable link.
Don't see any repo cards?
- Check that the import path in App.jsx is exactly './components/RepoList'. A missing s or wrong folder name causes a module-not-found error.
- Open the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for red error text. A typo in the filename or export usually surfaces here.
- Make sure RepoCard.jsx and RepoList.jsx are both inside src/components/. If they are in the wrong folder, the import will fail silently.
Help me figure out why my repo cards aren't showing up after I search for a username.
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react'
import SearchBar from './components/SearchBar'
import RepoList from './components/RepoList'
import './App.css'
function App() {
const [username, setUsername] = useState('')
const [repos, setRepos] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!username) return
const fetchRepos = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(
`https://api.github.com/users/${username}/repos?per_page=30&sort=updated`
)
if (!response.ok) {
throw new Error(`User "${username}" not found`)
}
const data = await response.json()
setRepos(data)
} catch (err) {
setError(err.message)
setRepos([])
} finally {
setLoading(false)
}
}
fetchRepos()
}, [username])
return (
<div className="app">
<h1>GitHub Explorer</h1>
<SearchBar onSearch={setUsername} />
{loading && <p className="status">Loading...</p>}
{error && <p className="status error">{error}</p>}
{!loading && !error && repos.length > 0 && <RepoList repos={repos} />}
</div>
)
}
export default App
Your app now fetches real data and displays it as a structured list of cards. Next up, you will add CSS styling to turn these plain cards into a polished, professional layout.
Style the Application
Your app fetches real data and renders it through a clean component tree. But right now, everything looks like unstyled browser defaults.
In this step, you will add CSS to transform the app from raw HTML into a polished, professional interface. You will style the search bar, create a card-based grid layout for repositories, and add hover interactions.
In this step, get ready to:
- Style the page layout, heading, and search bar.
- Style the repo cards with borders, hover effects, and a language indicator dot.
- Verify the global resets in index.css and confirm the app looks clean.
Add layout and search bar styles
CSS controls how your components look in the browser. The class names you already added in your JSX (like className="app" and className="search-bar") are the hooks that CSS uses to target each element.
- In VS Code, open src/App.css from the Explorer sidebar.
- Paste the following CSS to style the page container, heading, search bar, and status messages:
/* Center the app in a narrow column */
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
/* Style the page heading */
h1 {
text-align: center;
margin-bottom: 1.5rem;
color: #1a1a2e;
}
/* Lay out the search bar as a horizontal row */
.search-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
/* Make the input fill available space */
.search-bar input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
/* Highlight the input border on focus */
.search-bar input:focus {
outline: none;
border-color: #4a90d9;
}
/* Style the search button */
.search-bar button {
padding: 0.75rem 1.5rem;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
/* Darken button on hover */
.search-bar button:hover {
background-color: #357abd;
}
/* Center loading and error text */
.status {
text-align: center;
font-size: 1.1rem;
color: #666;
}
/* Show errors in red */
.status.error {
color: #e74c3c;
}
/* Stack repo cards in a single-column grid */
.repo-list {
display: grid;
gap: 1rem;
}
What does this CSS do?
The .app class constrains the page to 800px wide and centers it with margin: 0 auto.
The search bar uses display: flex to place the input and button side by side. flex: 1 on the input makes it stretch to fill all remaining space after the button takes what it needs.
The .repo-list uses CSS Grid to stack cards vertically with consistent spacing between them.
- Save src/App.css and switch to your browser.
- Search for octocat. You should see the heading centered at the top, a blue search button beside the input field, and repo names listed below in a single column.
Styles not showing up?
- Confirm you saved the file. Unsaved changes in VS Code show a dot on the file tab.
- Check that App.jsx already has import './App.css' near the top. Without that import, React does not load the stylesheet.
- Make sure your dev server is still running in the terminal. If it stopped, run npm run dev again.
Still stuck? Ask for help.
Style the repo cards and language indicator
Each repository needs its own visual container. You will give the cards a border, rounded corners, and a subtle shadow on hover to create a sense of depth.
- In src/App.css, add the following CSS below the .repo-list block you just wrote:
/* Base card container */
.repo-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.25rem;
transition: box-shadow 0.2s;
}
/* Lift the card on hover */
.repo-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Remove default heading margin */
.repo-card h3 {
margin: 0 0 0.5rem 0;
}
/* Style repo name as a blue link */
.repo-card h3 a {
color: #4a90d9;
text-decoration: none;
}
.repo-card h3 a:hover {
text-decoration: underline;
}
/* Muted description text */
.description {
color: #555;
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
}
/* Metadata row: language + stars */
.repo-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: #666;
}
/* Language label with dot */
.language {
display: flex;
align-items: center;
gap: 0.3rem;
}
/* Colored circle representing the language */
.language-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #3572a5;
}
/* Gold star icon color */
.stars {
color: #f5a623;
}
What does this CSS do?
Each .repo-card gets a light border and rounded corners. The transition: box-shadow 0.2s smoothly animates the shadow when the learner hovers, creating a "lift" effect.
The .language-dot is a tiny 10px circle. Setting border-radius: 50% on a square element turns it into a perfect circle. The blue color (#3572a5) matches GitHub's default color for JavaScript repositories.
- Save src/App.css and refresh your browser.
- Search for octocat. Each repo should appear inside a bordered card with a blue repo name, muted description, a small colored dot next to the language name, and a gold star count.
- Hover over a card. You should see a subtle shadow appear around it.
Cards look cramped or misaligned?
- Make sure you pasted the card CSS below the existing layout CSS, not inside another block. Each selector should be at the top level of the file.
- Check for missing closing braces. Every opening { needs a matching }.
Need a hand? Ask for help.
Verify the global styles
Your app already imports a global stylesheet that resets browser defaults. This file removes inconsistent padding and margins that browsers add by default, and sets a consistent font across the page.
- Open src/index.css and confirm it contains the following global resets:
/* Reset all browser defaults */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Set base typography and background */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
color: #1a1a2e;
line-height: 1.6;
}
Why a separate index.css?
The index.css file applies to the entire page regardless of which components are rendered. It handles baseline resets like removing default margins and setting the font.
The App.css file styles your specific components. Keeping these separate makes your styles easier to manage as the project grows.
- Open src/main.jsx and confirm it has import './index.css' near the top. This line loads the global stylesheet into your app.
- Switch to your browser and do a final check. The page background should be a light gray (#f8f9fa), text should use a clean sans-serif font, and there should be no awkward spacing gaps around the edges.
What is box-sizing: border-box?
By default, browsers add padding and borders on top of the width you set. This means a 200px element with 20px padding actually takes up 240px.
Setting box-sizing: border-box changes this so padding and borders are included inside the declared width. A 200px element stays 200px no matter what padding you add.
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/App.css and src/index.css.
ⓧ I'd like to double check the full code
Here is the complete src/App.css file. Make sure yours matches exactly:
.app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
text-align: center;
margin-bottom: 1.5rem;
color: #1a1a2e;
}
.search-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.search-bar input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
.search-bar input:focus {
outline: none;
border-color: #4a90d9;
}
.search-bar button {
padding: 0.75rem 1.5rem;
background-color: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
.search-bar button:hover {
background-color: #357abd;
}
.status {
text-align: center;
font-size: 1.1rem;
color: #666;
}
.status.error {
color: #e74c3c;
}
.repo-list {
display: grid;
gap: 1rem;
}
.repo-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.25rem;
transition: box-shadow 0.2s;
}
.repo-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.repo-card h3 {
margin: 0 0 0.5rem 0;
}
.repo-card h3 a {
color: #4a90d9;
text-decoration: none;
}
.repo-card h3 a:hover {
text-decoration: underline;
}
.description {
color: #555;
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
}
.repo-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: #666;
}
.language {
display: flex;
align-items: center;
gap: 0.3rem;
}
.language-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #3572a5;
}
.stars {
color: #f5a623;
}
And here is src/index.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
color: #1a1a2e;
line-height: 1.6;
}
Your GitHub Explorer now looks polished and professional. You have built a complete React application from scratch: components, state, API calls, and styling. Next up is an optional Secret Mission where you can push your skills further by adding a language filter.
Secret mission
Add a Language Filter
Your explorer shows all repos at once. What if users want to see only Python repos, or only JavaScript? Add a language filter dropdown that narrows results by programming language.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project folder for future experiments, pause the dev server, or delete the folder entirely.
Resources you used:
- The github-explorer project folder on your Desktop.
- The Vite dev server running in your terminal.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your React app.
- Your dev server stays running at http://localhost:5173. You can keep making changes and see them reflected immediately.
- Try adding new features like pagination, a dark mode toggle, or React Router for multi-page navigation.
Pause - I'll come back to this later
Stop the running dev server to free up your terminal. Your project files stay on disk so you can pick back up anytime.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- When you want to resume, open a terminal in the github-explorer folder and run npm run dev again.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes the entire project folder from your Desktop.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- Navigate to your Desktop and delete the project folder by running these commands:
macOS
cd ~/Desktop
rm -rf github-explorer
Windows
cd ~\Desktop
rmdir /s /q github-explorer
- Confirm the folder is gone by running:
ls
You should no longer see github-explorer in the output.
Nice Work!
Nice Work!
You just built a fully interactive GitHub Repository Explorer from scratch using React and Vite. You can search for any GitHub user and browse their repositories in a polished, responsive interface.
You've learned how to:
- Build a complete React application with reusable components that communicate through props.
- Manage interactive state with useState and fetch live data from the GitHub API using useEffect.
- Handle loading and error states gracefully, render dynamic lists with proper keys, and style the app with CSS for a professional layout.
- Secret Mission: Added a language filter dropdown using derived state and controlled select inputs.
Ready to quiz yourself?