Build a Glassmorphism Author Showcase
Build a dark-themed author showcase with glassmorphism cards and live API data.
Introduction
30 Second Summary
You know those sleek, frosted-glass interfaces you see in modern apps and websites? The ones that look impossibly polished and make you wonder how they were built? That effect is called glassmorphism, and it's easier to create than you might think.
In this project, you will build a dark-themed Author Showcase webpage that displays famous authors as glowing, animated glass cards. You'll connect it to a live API to fetch real book data and cover art.
What You'll Build
A modern single-page website that combines advanced CSS visual effects with live data from the Open Library API.
By the end of this project, you'll have:
- A responsive CSS Grid layout of author cards styled with glassmorphism (frosted-glass blur and translucent backgrounds).
- Animated rotating gradient borders built with CSS @property and conic-gradient that speed up on hover.
- JavaScript that fetches live author and book data from the Open Library API, displaying real cover art in an interactive slide-in panel.
- Secret Mission: Add a live search bar that lets users search for any author and dynamically adds them to the showcase.
Are there any prerequisites?
This project assumes basic familiarity with HTML and CSS. You don't need any prior JavaScript experience. The project walks you through every line.
Before We Start
You're about to build a visually stunning author showcase that pulls live data from the internet. Before writing any code, you need a local development environment that lets you see changes instantly in your browser.
In this step, you'll install a preview tool, create your project folder and files, and confirm everything connects.
In this step, get ready to:
- Install the Live Server extension for instant browser previews.
- Create the project folder and three starter files.
- Launch Live Server and confirm a blank page loads in Brave.
Install the Live Server extension
Live Server is a VS Code extension that automatically refreshes your browser every time you save a file. This means you can write code and immediately see the result without manually reloading the page.
- Press the Windows key to open your search bar.
- Type VS Code and press Enter to open it.
- Open Quick Open by pressing Ctrl+P.
- Install the Live Server extension by pasting this into the Quick Open bar and pressing Enter:
ext install ritwickdey.liveserver
What does this command do?
This tells VS Code to find and install the Live Server extension (by Ritwick Dey) directly from the marketplace. It's the same as searching for it in the Extensions panel, but faster.
- Look at the bottom status bar of VS Code. You should see a Go Live button appear on the right side.
Don't see the Go Live button?
The button may take a moment to appear after installation. Try closing and reopening VS Code.
If it still doesn't appear, open the Extensions panel with Ctrl+Shift+X and search for "Live Server" to confirm it installed successfully.
Help me troubleshoot my Live Server installation.
Create the project folder and files
Every web project lives in its own folder. You'll create one on your Desktop so it's easy to find, then add the three files every web page needs: HTML for structure, CSS for styling, and JavaScript for interactivity.
- Open the integrated terminal in VS Code by pressing Ctrl+`.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir author-showcase
cd author-showcase
- Create the three project files by running the command for your operating system:
macOS
touch index.html style.css script.js
Windows
New-Item -ItemType File -Name "index.html", "style.css", "script.js"
- Open the author-showcase folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the author-showcase folder, and click Select Folder.
Why open the folder this way?
Opening a folder in VS Code makes it the workspace root. The Explorer panel on the left shows all files inside it, and the terminal opens directly in that folder. This keeps everything organized.
- Confirm your files exist by opening a new terminal with Ctrl+` and running:
ls
- You should see index.html, style.css, and script.js listed in the output.
Don't see all three files?
Make sure your terminal is inside the author-showcase folder. Run cd on its own (Windows) to check your current location. The path should end with author-showcase.
If you see an empty output, re-run the file creation command from the platform tab above. Make sure you're in the correct folder first.
Help me find my project files in the terminal.
Launch Live Server and confirm it works
With your files in place, you can now launch Live Server to start a local preview in your browser.
- Click the Go Live button in the bottom-right of the VS Code status bar.
What should I see?
Your default browser opens with a blank white page. This is expected because your index.html file is empty. The URL bar shows something like 127.0.0.1:5500 or localhost:5500.
If the page opened in a browser other than Brave, copy the URL from the address bar, open Brave, and paste it there.
- Confirm you see a blank page open in Brave. This means Live Server is running and connected to your project.
Browser didn't open?
Make sure you clicked Go Live while you have the author-showcase folder open as your workspace. If VS Code still shows your Desktop or another folder, click File, then Open Folder, and select author-showcase.
If you see a file listing instead of a blank page, click index.html in the VS Code Explorer to open it in the editor first, then click Go Live again.
Help me get Live Server working in my browser.
Your development environment is ready. Next up, you'll build the dark-themed page structure and lay out your author cards with CSS Grid.
Build the Dark Theme and Page Structure
Your project files are ready and Live Server is running. Now it is time to build the visual foundation of your Author Showcase.
Every great webpage starts with solid structure and a strong visual identity. In this step, you will create the HTML skeleton, apply a dark gradient theme, and lay out placeholder author cards in a responsive grid.
In this step, get ready to:
- Write the full HTML structure with a header and 6 placeholder author cards.
- Style the dark theme with a gradient background, custom font, and grid layout.
- Style the base card appearance so each card looks like a dark, polished tile.
Write the HTML structure
The HTML gives your page its bones. You will define a header with a title and subtitle, then create a grid container holding 6 placeholder author cards.
- In the VS Code Explorer sidebar, click index.html to open it in the editor.
- Replace the empty file with the following HTML by copying and pasting this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Author Showcase</title>
<!-- Preconnect to Google Fonts for faster loading -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Import the Inter font family -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<!-- Link to our stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Page header with title and subtitle -->
<header class="header">
<h1 class="header-title">Author Showcase</h1>
<p class="header-subtitle">Discover brilliant minds and their works</p>
</header>
<main>
<!-- Grid container for author cards -->
<div class="authors-grid" id="authors-grid">
<div class="author-card"><h3 class="author-name">Author 1</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 2</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 3</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 4</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 5</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 6</h3><span class="author-work-count">0 works</span></div>
</div>
</main>
<!-- JavaScript will go here later -->
<script src="script.js"></script>
</body>
</html>
What does this code do?
- The <link> tags at the top import the Inter font from Google Fonts and connect your CSS stylesheet.
- The <header> section holds the page title and a subtitle that describe what the page is about.
- The div.authors-grid container holds 6 placeholder cards. Each card has a name heading and a work count span.
- The <script> tag at the bottom links your JavaScript file, which you will fill in later.
- Save index.html by pressing Ctrl+S.
- Switch to your Brave browser. Live Server auto-refreshes, so you should see the text "Author Showcase", the subtitle, and 6 lines of "Author 1" through "Author 6" on a plain white background.
Don't see anything in the browser?
- Make sure you saved the file. Unsaved files have a dot on their tab in VS Code.
- Confirm Live Server is running. You should see a port number (like 5500) in the VS Code status bar. If not, right-click index.html in the Explorer and select Open with Live Server.
Still stuck? Help me figure out why my HTML isn't showing in the browser with Live Server.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Author Showcase</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="header">
<h1 class="header-title">Author Showcase</h1>
<p class="header-subtitle">Discover brilliant minds and their works</p>
</header>
<main>
<div class="authors-grid" id="authors-grid">
<div class="author-card"><h3 class="author-name">Author 1</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 2</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 3</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 4</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 5</h3><span class="author-work-count">0 works</span></div>
<div class="author-card"><h3 class="author-name">Author 6</h3><span class="author-work-count">0 works</span></div>
</div>
</main>
<script src="script.js"></script>
</body>
</html>
Style the dark theme and grid layout
Right now the page has a plain white background. You will transform it into a moody, dark-gradient canvas and arrange the cards in a responsive CSS Grid layout.
- In VS Code, click style.css in the Explorer sidebar to open it.
- Paste the following CSS to set up your dark theme, header styles, and grid layout:
/* Reset default browser margins and padding */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Dark gradient background spanning the full viewport */
body {
font-family: 'Inter', sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 50%, #0a1628 100%);
color: #ffffff;
padding: 2rem;
}
/* Centered header section */
.header {
text-align: center;
margin-bottom: 3rem;
padding-top: 2rem;
}
/* Gradient-colored title text */
.header-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(to right, #a855f7, #6366f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
/* Muted subtitle beneath the title */
.header-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
/* Responsive grid that auto-fills columns at 280px minimum */
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
What does this code do?
- The * reset removes default browser spacing so your layout starts from a clean slate.
- The body selector applies a deep purple-to-navy diagonal gradient and sets white text for contrast.
- The .header-title uses background-clip: text to fill the heading text with a purple-to-indigo gradient instead of a flat color.
- The .authors-grid uses auto-fill with minmax(280px, 1fr) so columns wrap responsively. On a wide screen you see 3 columns. On a narrow screen the cards stack.
- Save style.css and switch to Brave.
- You should see a dark gradient background, the title "Author Showcase" in purple gradient text, and the 6 author names arranged in a grid layout.
Still seeing a white background?
- Check that index.html includes <link rel="stylesheet" href="style.css"> in the <head> section. Without this link, the browser cannot find your styles.
- Confirm the filename is exactly style.css (not styles.css or Style.css). File names are case-sensitive.
Need more help? Help me debug why my CSS file isn't being applied to my HTML page.
Style the base card appearance
The cards are just plain text right now. You will give each card a dark semi-transparent background, rounded corners, and a shadow to make them look like polished tiles floating above the page.
- Return to style.css in VS Code and add the following CSS below your existing code:
/* Dark semi-transparent card with rounded corners */
.author-card {
position: relative;
border-radius: 20px;
padding: 2rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
z-index: 1;
}
/* Author name styling */
.author-name {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
/* Italic display for the author's best-known work */
.author-top-work {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
margin-bottom: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Purple accent color for work count */
.author-work-count {
font-size: 0.85rem;
color: #a855f7;
font-weight: 600;
}
What does this code do?
- The .author-card uses rgba(255, 255, 255, 0.05) for a nearly-transparent white tint. This gives each card a subtle presence against the dark background.
- The box-shadow adds depth by casting a soft, dark shadow beneath each card.
- The transition property means any future hover effects will animate smoothly rather than snapping instantly.
- The .author-work-count uses the purple accent color #a855f7 to draw the eye to the work count stat.
- Save style.css and check Brave.
- You should now see 6 dark, rounded cards arranged in a responsive grid. Each card has white text for the author name and a purple work count. Try resizing the browser window to watch the grid reflow.
Cards not showing rounded corners or shadows?
- Make sure there is no typo in the class name. Your HTML uses class="author-card" and your CSS targets .author-card. A mismatch means the styles do not apply.
- Confirm you pasted the new CSS below the existing code, not inside another selector's curly braces.
Still stuck? Help me debug why my author card styles aren't appearing.
✔️ Awesome, I've got everything!
Great. Make sure both index.html and style.css are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full style.css at this point:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 50%, #0a1628 100%);
color: #ffffff;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-top: 2rem;
}
.header-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(to right, #a855f7, #6366f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.header-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.author-card {
position: relative;
border-radius: 20px;
padding: 2rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
z-index: 1;
}
.author-name {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.author-top-work {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
margin-bottom: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.author-work-count {
font-size: 0.85rem;
color: #a855f7;
font-weight: 600;
}
Your page now has a strong visual identity. The dark gradient, gradient title text, and polished card tiles set the stage for the glassmorphism effects you will add next.
Add Glassmorphism and Animated Gradient Borders
Your cards are styled and sitting in a clean grid layout. Now it's time to make them look like something special.
In this step, you'll layer on a frosted-glass effect and build animated glowing borders that rotate around each card using pure CSS. The result is a premium visual style that looks like it took hours but only needs a few well-chosen properties.
In this step, get ready to:
- Apply glassmorphism (frosted-glass blur) to your author cards.
- Build animated rotating gradient borders using CSS @property and conic-gradient.
- Add hover interactions that lift, glow, and speed up the animation.
Add glassmorphism properties to the cards
Glassmorphism is a design style that makes elements look like frosted glass. The key ingredient is backdrop-filter, which blurs whatever is behind the element.
- In style.css, find the .author-card rule.
- Find the line that says background: rgba(255, 255, 255, 0.05); and add these three new properties directly below it:
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
What do these properties do?
backdrop-filter: blur(16px) saturate(180%) blurs anything behind the card by 16px and boosts the color saturation. This creates the frosted-glass look.
The -webkit-backdrop-filter line is a copy for Safari compatibility. The border adds a faint white edge that simulates light catching the glass.
- Save style.css and check your browser.
- You should see your cards now have a subtle frosted appearance with a faint white border edge.
Cards look exactly the same?
The glassmorphism effect is subtle on a solid dark background. Look closely for the faint white border line on each card's edges. If you don't see it, make sure the border line is inside the .author-card rule (between the curly braces), not outside it.
Help me troubleshoot why my glassmorphism styles aren't showing up on the cards.
Create the animated gradient border
The animated border uses a clever layering trick. A huge rotating gradient sits behind the card, and a dark inner layer masks everything except a thin glowing edge.
First, you need to tell the browser that --angle is a number representing an angle so that CSS can animate it smoothly.
- At the very top of style.css (above the * reset rule), add this @property registration:
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
- Now add the rotating gradient layer and its animation. Place this code directly after the closing } of the .author-card rule:
.author-card::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 250%;
height: 250%;
background: conic-gradient(
from var(--angle),
transparent 0deg,
#a855f7 90deg,
#6366f1 180deg,
#a855f7 270deg,
transparent 360deg
);
transform: translate(-50%, -50%);
animation: rotate-border 6s linear infinite;
z-index: -2;
opacity: 0.4;
}
@keyframes rotate-border {
from { --angle: 0deg; }
to { --angle: 360deg; }
}
What does this code do?
The ::before pseudo-element creates an invisible layer behind each card. It's 250% the size of the card, centered with translate(-50%, -50%), so it overflows on all sides.
The conic-gradient paints a circular sweep of purple-to-transparent color, starting from var(--angle). The @keyframes rule spins --angle from 0 to 360 degrees, making the gradient rotate continuously.
Right now the gradient covers the entire card. You need a dark mask that hides everything except the thin glowing edge.
- Add this ::after rule directly below the @keyframes rotate-border block:
.author-card::after {
content: '';
position: absolute;
inset: 2px;
background: rgba(10, 10, 30, 0.95);
border-radius: 18px;
z-index: -1;
}
Why inset: 2px?
The ::after layer sits 2px inside the card's edges. It covers the rotating gradient everywhere except that 2px gap. The remaining sliver of visible gradient is your animated border.
- Save style.css and check your browser.
- You should see a faint purple gradient slowly rotating around the edges of each card. The center of each card stays dark.
No rotating border visible?
Make sure the @property --angle block is at the very top of your file, above the * { } reset. Without it, the browser can't animate the angle and the gradient stays frozen.
Also check that your .author-card still has overflow: hidden and position: relative from the previous step. Both are needed for the pseudo-elements to display correctly.
Help me debug why my animated gradient border isn't showing up.
Add hover interactions
The cards already have a transition property from the previous step, so any changes on hover will animate smoothly. Now you'll add the hover state that lifts the card and speeds up the border rotation.
- Add these hover rules after the .author-card::after block:
.author-card:hover {
transform: translateY(-8px);
box-shadow: 0 16px 48px rgba(168, 85, 247, 0.2);
}
.author-card:hover::before {
animation-duration: 3s;
opacity: 0.8;
}
What do the hover states do?
- translateY(-8px) lifts the card 8 pixels upward, creating a "pop" effect.
- The purple box-shadow creates a soft glow beneath the lifted card.
- animation-duration: 3s cuts the rotation time in half (from 6s), making the border spin faster on hover.
- opacity: 0.8 doubles the gradient brightness (from 0.4), making the border glow brighter.
Finally, add a utility class that you'll use in the next step when loading author data from an API.
- Add this rule at the bottom of style.css, after the .author-work-count rule:
.loading {
text-align: center;
color: rgba(255, 255, 255, 0.5);
padding: 2rem;
font-size: 1rem;
}
- Save style.css.
- Hover over any card in your browser. You should see the card lift upward, a purple glow appear beneath it, and the gradient border spin noticeably faster and brighter.
Card lifts but the border doesn't speed up?
Make sure you have the .author-card:hover::before rule as a separate selector from .author-card:hover. They target different elements (the card vs. its ::before pseudo-element) and must be in separate rule blocks.
Help me figure out why my hover animation speed change isn't working.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete style.css at this point. Compare yours against it:
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 50%, #0a1628 100%);
color: #ffffff;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-top: 2rem;
}
.header-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(to right, #a855f7, #6366f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.header-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.author-card {
position: relative;
border-radius: 20px;
padding: 2rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
z-index: 1;
}
.author-card::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 250%;
height: 250%;
background: conic-gradient(
from var(--angle),
transparent 0deg,
#a855f7 90deg,
#6366f1 180deg,
#a855f7 270deg,
transparent 360deg
);
transform: translate(-50%, -50%);
animation: rotate-border 6s linear infinite;
z-index: -2;
opacity: 0.4;
}
.author-card::after {
content: '';
position: absolute;
inset: 2px;
background: rgba(10, 10, 30, 0.95);
border-radius: 18px;
z-index: -1;
}
@keyframes rotate-border {
from { --angle: 0deg; }
to { --angle: 360deg; }
}
.author-card:hover {
transform: translateY(-8px);
box-shadow: 0 16px 48px rgba(168, 85, 247, 0.2);
}
.author-card:hover::before {
animation-duration: 3s;
opacity: 0.8;
}
.author-name {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.author-top-work {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
margin-bottom: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.author-work-count {
font-size: 0.85rem;
color: #a855f7;
font-weight: 600;
}
.loading {
text-align: center;
color: rgba(255, 255, 255, 0.5);
padding: 2rem;
font-size: 1rem;
}
Your cards now have a polished, premium look with frosted glass and glowing animated borders. Next up, you'll bring these cards to life with real author data from an API.
Connect to the Open Library API
Your cards look stunning with their animated gradient borders and glassmorphism styling. Right now, though, they only show placeholder text.
In this step, you'll bring those cards to life by connecting to the Open Library API using JavaScript's fetch function. Your cards will display real author names, top works, and total work counts pulled from a live database.
In this step, get ready to:
- Define an array of authors and fetch their details from the Open Library API.
- Generate author cards dynamically from real API data.
- Wire up click handling to prepare for displaying books.
Define the author data and fetch function
Before you can fetch anything, you need to tell your script which authors to look up. You also need references to the HTML elements your script will interact with.
- Switch back to script.js in VS Code.
- Delete the placeholder comment and add the authors array and DOM references by pasting this code:
const AUTHORS = [
{ name: "J.K. Rowling", key: "OL23919A" },
{ name: "George Orwell", key: "OL118077A" },
{ name: "Agatha Christie", key: "OL21594A" },
{ name: "Stephen King", key: "OL2162284A" },
{ name: "Isaac Asimov", key: "OL34221A" },
{ name: "Jane Austen", key: "OL21594A" }
];
// Grab references to the HTML elements we'll manipulate
const authorsGrid = document.getElementById("authors-grid");
const bookPanelOverlay = document.getElementById("book-panel-overlay");
const bookPanel = document.getElementById("book-panel");
const bookPanelClose = document.getElementById("book-panel-close");
const bookPanelTitle = document.getElementById("book-panel-title");
const booksGrid = document.getElementById("books-grid");
What does this code do?
The AUTHORS array holds six objects, each with a display name and an Open Library key (a unique identifier like OL23919A). These keys let you query the API for a specific author's data.
The getElementById lines cache references to DOM elements. Some of these elements (like book-panel-overlay) don't exist in your HTML yet. You'll add them in the next step.
Now you need the function that actually calls the Open Library API to get author details.
- Add the fetch function below your DOM references by pasting this code:
async function fetchAuthorDetails(authorKey) {
// Call the Open Library Author Search API
const response = await fetch(
`https://openlibrary.org/search/authors.json?q=${authorKey}`
);
const data = await response.json();
// Return the first matching author, or null if none found
if (data.docs && data.docs.length > 0) {
return data.docs[0];
}
return null;
}
What does this code do?
The async keyword lets you use await inside the function. await pauses execution until the API responds, rather than blocking the entire browser.
The API returns a docs array containing matching authors. Each doc includes top_work (their most famous book), work_count (total number of works), and other metadata.
- Save script.js.
You won't see a change in the browser yet. The next chunk uses these pieces to generate cards.
Seeing a red error in the console?
Open DevTools with F12 and check the Console tab. A syntax error usually means a missing backtick (`) in the template literal URL or a misplaced bracket.
help me fix a syntax error in my fetchAuthorDetails function
Generate author cards from the fetched data
With the fetch function ready, you need two more pieces. A function to build a card element from API data, and a function to orchestrate the whole loading process.
First, remove the hardcoded placeholder cards from your HTML so JavaScript can take over.
- Switch to index.html in VS Code.
- Find the six hardcoded <div class="author-card"> elements inside div#authors-grid.
- Delete all six lines so that the grid container is empty:
<div class="authors-grid" id="authors-grid">
</div>
Why remove the hardcoded cards?
Your JavaScript will create cards dynamically from API data. Leaving hardcoded cards in the HTML would result in duplicate cards appearing on the page.
- Save index.html.
- Switch back to script.js.
- Add the card-building function below fetchAuthorDetails by pasting this code:
function createAuthorCard(author, authorData) {
// Create a new div element for the card
const card = document.createElement("div");
card.classList.add("author-card");
card.dataset.authorKey = author.key;
// Extract display values from the API response
const topWork = authorData ? authorData.top_work || "Unknown" : "Loading...";
const workCount = authorData ? authorData.work_count || 0 : 0;
// Fill the card with author info
card.innerHTML = `
<h3 class="author-name">${author.name}</h3>
<p class="author-top-work">${topWork}</p>
<span class="author-work-count">${workCount} works</span>
`;
// Attach a click handler for showing books later
card.addEventListener("click", () => openBookPanel(author));
return card;
}
What does this code do?
This function takes an author object and API data, then builds a complete card element. The dataset.authorKey stores the Open Library key directly on the HTML element for later use.
The ternary expressions (authorData ? ... : ...) handle cases where the API returns incomplete data. If top_work is missing, the card shows "Unknown" instead of breaking.
Now add the orchestrator function that loads all authors and populates the grid.
- Add the loading function below createAuthorCard by pasting this code:
async function loadAuthors() {
// Show a loading message while fetching
authorsGrid.innerHTML = '<div class="loading">Loading authors...</div>';
// Fetch details for each author one at a time
const cards = [];
for (const author of AUTHORS) {
const authorData = await fetchAuthorDetails(author.name);
const card = createAuthorCard(author, authorData);
cards.push(card);
}
// Clear the loading message and add all cards
authorsGrid.innerHTML = "";
cards.forEach(card => authorsGrid.appendChild(card));
}
Why fetch one at a time instead of all at once?
The Open Library API has a rate limit of 1 request per second for unidentified users. Using a for...of loop with await sends requests sequentially. This naturally stays under the rate limit without needing a timer.
Firing all 6 requests simultaneously with Promise.all could trigger rate limiting and cause failed responses.
- Save script.js.
You won't see cards yet because nothing is calling loadAuthors(). You'll add that in the next chunk.
Seeing an error about openBookPanel not defined?
This is expected if you saved and reloaded before adding the next chunk. The click listener references openBookPanel which doesn't exist yet. You'll define it next.
help me understand why my script is throwing a reference error
Add click handling and start the app
The createAuthorCard function already attaches a click listener that calls openBookPanel. You need to define that function (as a stub for now) and then kick off the loading process.
- Add the stub function and the startup call at the bottom of script.js by pasting this code:
async function openBookPanel(author) {
// stub — will be completed in next step
}
loadAuthors();
What does this code do?
The openBookPanel function is an empty placeholder. You'll fill it in during the next step when you build the book display panel.
The bare loadAuthors() call at the bottom runs as soon as the browser loads the script. This kicks off the API requests and populates your grid.
- Save script.js and switch to your Brave browser.
- Wait a few seconds for the API calls to complete. You should see 6 author cards appear in the grid, each showing a real author name (like "J.K. Rowling"), their most famous work in italic text, and a purple work count.
Cards not appearing?
Open DevTools (F12) and check the Console tab for errors. Common issues:
- A TypeError: Cannot set properties of null means authorsGrid couldn't find the element. Make sure your HTML still has id="authors-grid" on the grid container.
- A Failed to fetch error means your internet connection might be blocked or the API is temporarily unavailable. Try refreshing.
- If you see "Loading authors..." stuck on screen, the requests are still in progress. The sequential fetching takes a few seconds.
help me debug why my author cards aren't appearing
✔️ Awesome, I've got everything!
Great. Double check you've saved both index.html and script.js.
ⓧ I'd like to double check the full code
Here's what index.html should look like now:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Author Showcase</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="header">
<h1 class="header-title">Author Showcase</h1>
<p class="header-subtitle">Discover brilliant minds and their works</p>
</header>
<main>
<div class="authors-grid" id="authors-grid">
</div>
</main>
<script src="script.js"></script>
</body>
</html>
Here's what script.js should look like now:
const AUTHORS = [
{ name: "J.K. Rowling", key: "OL23919A" },
{ name: "George Orwell", key: "OL118077A" },
{ name: "Agatha Christie", key: "OL21594A" },
{ name: "Stephen King", key: "OL2162284A" },
{ name: "Isaac Asimov", key: "OL34221A" },
{ name: "Jane Austen", key: "OL21594A" }
];
const authorsGrid = document.getElementById("authors-grid");
const bookPanelOverlay = document.getElementById("book-panel-overlay");
const bookPanel = document.getElementById("book-panel");
const bookPanelClose = document.getElementById("book-panel-close");
const bookPanelTitle = document.getElementById("book-panel-title");
const booksGrid = document.getElementById("books-grid");
async function fetchAuthorDetails(authorKey) {
const response = await fetch(
`https://openlibrary.org/search/authors.json?q=${authorKey}`
);
const data = await response.json();
if (data.docs && data.docs.length > 0) {
return data.docs[0];
}
return null;
}
function createAuthorCard(author, authorData) {
const card = document.createElement("div");
card.classList.add("author-card");
card.dataset.authorKey = author.key;
const topWork = authorData ? authorData.top_work || "Unknown" : "Loading...";
const workCount = authorData ? authorData.work_count || 0 : 0;
card.innerHTML = `
<h3 class="author-name">${author.name}</h3>
<p class="author-top-work">${topWork}</p>
<span class="author-work-count">${workCount} works</span>
`;
card.addEventListener("click", () => openBookPanel(author));
return card;
}
async function loadAuthors() {
authorsGrid.innerHTML = '<div class="loading">Loading authors...</div>';
const cards = [];
for (const author of AUTHORS) {
const authorData = await fetchAuthorDetails(author.name);
const card = createAuthorCard(author, authorData);
cards.push(card);
}
authorsGrid.innerHTML = "";
cards.forEach(card => authorsGrid.appendChild(card));
}
async function openBookPanel(author) {
// stub — will be completed in next step
}
loadAuthors();
Your showcase now pulls real data from the internet and displays it in your beautifully styled cards. Next up, you'll make those cards interactive by building a book panel that shows each author's published works with real cover art.
Display Books with Cover Art
Your author cards are live and pulling real data from the Open Library API. Now it's time to make them interactive.
In this step, you'll build a slide-in book panel that appears when you click an author card. The panel will fetch that author's published works and display them as a grid of book cards with real cover art from the Open Library Covers API.
In this step, get ready to:
- Build a glassmorphism book panel overlay with open/close animations.
- Fetch an author's works from the Open Library API and display book covers.
- Wire up close interactions on the button and overlay background.
Add the book panel HTML structure
The book panel needs a full-screen overlay that dims the page, a centered panel container, and slots for book content. Your JavaScript already has DOM references pointing at these element IDs, so the HTML needs to match exactly.
- In index.html, find the closing </main> tag.
- Add the following book panel overlay between </main> and <script src="script.js"></script>:
<div class="book-panel-overlay" id="book-panel-overlay">
<div class="book-panel" id="book-panel">
<button class="book-panel-close" id="book-panel-close">×</button>
<h2 class="book-panel-title" id="book-panel-title">Author's Books</h2>
<div class="books-grid" id="books-grid">
</div>
</div>
</div>
What does this HTML do?
- The outer .book-panel-overlay covers the entire screen with a dark tint when active.
- The inner .book-panel is the centered glassmorphism container that holds the book content.
- The × button renders as an × close icon. The .books-grid div will hold dynamically created book cards.
- Save index.html.
- Refresh your browser. You won't see any visible change yet because the overlay starts hidden with zero opacity.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Author Showcase</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="header">
<h1 class="header-title">Author Showcase</h1>
<p class="header-subtitle">Discover brilliant minds and their works</p>
</header>
<main>
<div class="authors-grid" id="authors-grid">
</div>
</main>
<div class="book-panel-overlay" id="book-panel-overlay">
<div class="book-panel" id="book-panel">
<button class="book-panel-close" id="book-panel-close">×</button>
<h2 class="book-panel-title" id="book-panel-title">Author's Books</h2>
<div class="books-grid" id="books-grid">
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Style the overlay and panel container
The overlay needs to cover the viewport, center the panel, and start invisible. The panel itself uses the same glassmorphism technique as your author cards.
- In style.css, find the .loading rule at the bottom of the file.
- Add the following styles directly above the .loading rule:
.book-panel-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.book-panel-overlay.active {
opacity: 1;
pointer-events: all;
}
.book-panel {
background: rgba(20, 10, 40, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
padding: 2.5rem;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
transform: translateY(30px);
transition: transform 0.3s ease;
position: relative;
}
.book-panel-overlay.active .book-panel {
transform: translateY(0);
}
.book-panel-close {
position: absolute;
top: 1rem;
right: 1.5rem;
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
font-size: 2rem;
cursor: pointer;
transition: color 0.2s ease;
}
.book-panel-close:hover {
color: #ffffff;
}
.book-panel-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #a855f7;
}
What does this CSS do?
- The overlay starts at opacity: 0 with pointer-events: none so it's invisible and unclickable by default.
- When the .active class is added, opacity fades to 1 and pointer events are restored.
- The panel starts offset 30px downward with transform: translateY(30px) and slides up to translateY(0) when active. This creates a smooth "slide up" entrance.
- The backdrop-filter: blur(20px) gives the panel the same frosted-glass look as your author cards.
- Save style.css. You won't see a change yet because the overlay is hidden until JavaScript adds the active class.
Seeing a layout shift or broken styles?
Make sure the new rules are placed above the existing .loading rule, not inside it. Each selector block should be separated by a single blank line.
Check for missing closing braces. Every { needs a matching }.
Help me troubleshoot my book panel CSS.
Now add the styles for the individual book cards that will appear inside the panel.
- Still in style.css, add these styles directly below the .book-panel-title rule you just wrote (still above .loading):
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.book-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1rem;
text-align: center;
transition: transform 0.2s ease;
}
.book-card:hover {
transform: translateY(-4px);
}
.book-cover {
width: 100%;
height: 180px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
}
.book-cover-placeholder {
width: 100%;
height: 180px;
border-radius: 8px;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.3);
font-size: 0.8rem;
}
.book-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.book-year {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
}
What does this CSS do?
- The .books-grid uses the same auto-fill pattern as your author grid, but with a smaller minimum width of 200px per card.
- Each .book-card gets a subtle hover lift effect and translucent styling.
- The .book-cover uses object-fit: contain so cover images display at their natural aspect ratio without cropping.
- The .book-cover-placeholder is a fallback that appears when a book has no cover image available.
- Save style.css.
- Refresh your browser. Still no visible change, because the panel starts hidden and the book cards don't exist until JavaScript creates them.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on to JavaScript.
ⓧ I'd like to double check the full code
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, #0a0a1a 0%, #1a0a2e 50%, #0a1628 100%);
color: #ffffff;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-top: 2rem;
}
.header-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(to right, #a855f7, #6366f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.header-subtitle {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.author-card {
position: relative;
border-radius: 20px;
padding: 2rem;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
z-index: 1;
}
.author-card::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 250%;
height: 250%;
background: conic-gradient(
from var(--angle),
transparent 0deg,
#a855f7 90deg,
#6366f1 180deg,
#a855f7 270deg,
transparent 360deg
);
transform: translate(-50%, -50%);
animation: rotate-border 6s linear infinite;
z-index: -2;
opacity: 0.4;
}
.author-card::after {
content: '';
position: absolute;
inset: 2px;
background: rgba(10, 10, 30, 0.95);
border-radius: 18px;
z-index: -1;
}
@keyframes rotate-border {
from {
--angle: 0deg;
}
to {
--angle: 360deg;
}
}
.author-card:hover {
transform: translateY(-8px);
box-shadow: 0 16px 48px rgba(168, 85, 247, 0.2);
}
.author-card:hover::before {
animation-duration: 3s;
opacity: 0.8;
}
.author-name {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.author-top-work {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
font-style: italic;
margin-bottom: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.author-work-count {
font-size: 0.85rem;
color: #a855f7;
font-weight: 600;
}
.book-panel-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.book-panel-overlay.active {
opacity: 1;
pointer-events: all;
}
.book-panel {
background: rgba(20, 10, 40, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
padding: 2.5rem;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
transform: translateY(30px);
transition: transform 0.3s ease;
position: relative;
}
.book-panel-overlay.active .book-panel {
transform: translateY(0);
}
.book-panel-close {
position: absolute;
top: 1rem;
right: 1.5rem;
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
font-size: 2rem;
cursor: pointer;
transition: color 0.2s ease;
}
.book-panel-close:hover {
color: #ffffff;
}
.book-panel-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #a855f7;
}
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
}
.book-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1rem;
text-align: center;
transition: transform 0.2s ease;
}
.book-card:hover {
transform: translateY(-4px);
}
.book-cover {
width: 100%;
height: 180px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
}
.book-cover-placeholder {
width: 100%;
height: 180px;
border-radius: 8px;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.3);
font-size: 0.8rem;
}
.book-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.book-year {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
}
.loading {
text-align: center;
color: rgba(255, 255, 255, 0.5);
padding: 2rem;
font-size: 1rem;
}
Populate the book panel with live data
Right now, openBookPanel() is a stub that does nothing. You'll replace it with a function that fetches an author's works from the Open Library Authors API and builds book cards showing real cover art.
- In script.js, find the existing stub function:
async function openBookPanel(author) {
// stub — will be completed in next step
}
- Delete the entire stub function and replace it with this full implementation:
async function openBookPanel(author) {
// Set the panel heading and show a loading message
bookPanelTitle.textContent = `${author.name}'s Books`;
booksGrid.innerHTML = '<div class="loading">Loading books...</div>';
bookPanelOverlay.classList.add("active");
// Fetch up to 6 works for this author
const response = await fetch(
`https://openlibrary.org/authors/${author.key}/works.json?limit=6`
);
const data = await response.json();
booksGrid.innerHTML = "";
if (data.entries && data.entries.length > 0) {
data.entries.forEach(work => {
const bookCard = document.createElement("div");
bookCard.classList.add("book-card");
// Use the first cover ID if available, otherwise show placeholder
let coverHtml;
if (work.covers && work.covers.length > 0) {
coverHtml = `<img class="book-cover" src="https://covers.openlibrary.org/b/id/${work.covers[0]}-M.jpg" alt="${work.title}">`;
} else {
coverHtml = `<div class="book-cover-placeholder">No cover available</div>`;
}
// Use first_publish_date if it exists
const year = work.first_publish_date
? work.first_publish_date
: "";
bookCard.innerHTML = `
${coverHtml}
<p class="book-title" title="${work.title}">${work.title}</p>
<p class="book-year">${year}</p>
`;
booksGrid.appendChild(bookCard);
});
} else {
booksGrid.innerHTML = '<div class="loading">No books found.</div>';
}
}
What does this function do?
- It immediately shows the panel (by adding the active class) with a loading message so the user gets instant feedback.
- It fetches from https://openlibrary.org/authors/{key}/works.json?limit=6 to get up to 6 books.
- For each work in data.entries, it checks if work.covers exists. If so, it builds an <img> tag using the Open Library Covers URL pattern. If not, it shows a placeholder.
- The cover URL follows the format https://covers.openlibrary.org/b/id/{cover_id}-M.jpg where M means medium size.
- Save script.js and refresh your browser.
- Click any author card. You should see the dark overlay fade in and the book panel slide up, showing a loading message followed by book cards with cover images.
Panel not appearing when you click?
Open DevTools (F12) and check the console for errors. A common issue is a typo in the element ID that causes bookPanelOverlay to be null.
Verify that the id values in your HTML match exactly: book-panel-overlay, book-panel, book-panel-close, book-panel-title, and books-grid.
Help me debug my book panel.
Add close button and overlay dismiss
The panel opens, but there's no way to close it yet. You'll add a close function and two event listeners: one for the × button and one for clicking the dark overlay background.
- In script.js, find the closing brace of the openBookPanel function you just wrote.
- Add the following code directly below it, above the loadAuthors(); call at the bottom:
function closeBookPanel() {
bookPanelOverlay.classList.remove("active");
}
// Close when the X button is clicked
bookPanelClose.addEventListener("click", closeBookPanel);
// Close when clicking the dark overlay background (not the panel itself)
bookPanelOverlay.addEventListener("click", (e) => {
if (e.target === bookPanelOverlay) {
closeBookPanel();
}
});
What does this code do?
- The closeBookPanel() function removes the active class, which triggers the CSS transitions in reverse (fade out + slide down).
- The overlay click listener checks e.target === bookPanelOverlay so clicks on the panel content itself don't accidentally close it. Only clicks on the dark background behind the panel dismiss it.
- Save script.js and refresh your browser.
- Click an author card to open the panel. Then click the × button in the top-right corner. The panel should fade out smoothly.
- Open the panel again and click the dark area outside the panel. It should close the same way.
- Try clicking different author cards to confirm each one loads different books with cover art.
Panel won't close?
Check that the close button's ID in your HTML is book-panel-close (with hyphens, not underscores). The bookPanelClose DOM reference at the top of your script relies on this exact ID.
If clicking the overlay background doesn't work, make sure the e.target === bookPanelOverlay check references the correct variable name.
Help me fix my close button.
✔️ Awesome, I've got everything!
Great. Double check you've saved all your files before moving on.
ⓧ I'd like to double check the full code
const AUTHORS = [
{ name: "J.K. Rowling", key: "OL23919A" },
{ name: "George Orwell", key: "OL118077A" },
{ name: "Agatha Christie", key: "OL21594A" },
{ name: "Stephen King", key: "OL2162284A" },
{ name: "Isaac Asimov", key: "OL34221A" },
{ name: "Jane Austen", key: "OL21594A" }
];
const authorsGrid = document.getElementById("authors-grid");
const bookPanelOverlay = document.getElementById("book-panel-overlay");
const bookPanel = document.getElementById("book-panel");
const bookPanelClose = document.getElementById("book-panel-close");
const bookPanelTitle = document.getElementById("book-panel-title");
const booksGrid = document.getElementById("books-grid");
async function fetchAuthorDetails(authorKey) {
const response = await fetch(
`https://openlibrary.org/search/authors.json?q=${authorKey}`
);
const data = await response.json();
if (data.docs && data.docs.length > 0) {
return data.docs[0];
}
return null;
}
function createAuthorCard(author, authorData) {
const card = document.createElement("div");
card.classList.add("author-card");
card.dataset.authorKey = author.key;
const topWork = authorData ? authorData.top_work || "Unknown" : "Loading...";
const workCount = authorData ? authorData.work_count || 0 : 0;
card.innerHTML = `
<h3 class="author-name">${author.name}</h3>
<p class="author-top-work">${topWork}</p>
<span class="author-work-count">${workCount} works</span>
`;
card.addEventListener("click", () => openBookPanel(author));
return card;
}
async function loadAuthors() {
authorsGrid.innerHTML = '<div class="loading">Loading authors...</div>';
const cards = [];
for (const author of AUTHORS) {
const authorData = await fetchAuthorDetails(author.name);
const card = createAuthorCard(author, authorData);
cards.push(card);
}
authorsGrid.innerHTML = "";
cards.forEach(card => authorsGrid.appendChild(card));
}
async function openBookPanel(author) {
bookPanelTitle.textContent = `${author.name}'s Books`;
booksGrid.innerHTML = '<div class="loading">Loading books...</div>';
bookPanelOverlay.classList.add("active");
const response = await fetch(
`https://openlibrary.org/authors/${author.key}/works.json?limit=6`
);
const data = await response.json();
booksGrid.innerHTML = "";
if (data.entries && data.entries.length > 0) {
data.entries.forEach(work => {
const bookCard = document.createElement("div");
bookCard.classList.add("book-card");
let coverHtml;
if (work.covers && work.covers.length > 0) {
coverHtml = `<img class="book-cover" src="https://covers.openlibrary.org/b/id/${work.covers[0]}-M.jpg" alt="${work.title}">`;
} else {
coverHtml = `<div class="book-cover-placeholder">No cover available</div>`;
}
const year = work.first_publish_date
? work.first_publish_date
: "";
bookCard.innerHTML = `
${coverHtml}
<p class="book-title" title="${work.title}">${work.title}</p>
<p class="book-year">${year}</p>
`;
booksGrid.appendChild(bookCard);
});
} else {
booksGrid.innerHTML = '<div class="loading">No books found.</div>';
}
}
function closeBookPanel() {
bookPanelOverlay.classList.remove("active");
}
bookPanelClose.addEventListener("click", closeBookPanel);
bookPanelOverlay.addEventListener("click", (e) => {
if (e.target === bookPanelOverlay) {
closeBookPanel();
}
});
loadAuthors();
Your Author Showcase is fully interactive. Clicking any card opens a polished panel with real book covers fetched live from the internet. Next up, you'll take this further in the Secret Mission by adding a live search bar to find any author.
Secret mission
Add a Live Author Search
Your showcase looks great, but it only shows a fixed list of authors. What if visitors could search for any author in the world and instantly add them to the grid? In this secret mission, you'll build a live search bar that queries the Open Library API and dynamically creates new glassmorphism cards from the results.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The author-showcase project folder on your Desktop (contains index.html, style.css, and script.js).
- The Live Server extension installed in VS Code.
- Live Server running in the background, serving your page to Brave.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with the Author Showcase.
- Your project files stay in the author-showcase folder on your Desktop.
- Live Server continues running and will serve your page whenever you open VS Code in that folder.
- The Live Server extension remains installed in VS Code for future projects.
Pause - I'll come back to this later
Stop the running server to free up the port, but keep all your files intact for later.
- In VS Code, click the Port: 5500 button in the bottom status bar to stop Live Server.
- Close the Brave browser tab showing your Author Showcase.
- Your files remain in the author-showcase folder. Reopen the folder in VS Code and click Go Live to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and the extension to start completely fresh.
- Stop Live Server by clicking Port: 5500 in the VS Code status bar.
- Delete the project folder. On macOS, open Terminal and run:
rm -rf ~/Desktop/author-showcase
On Windows, open PowerShell and run:
Remove-Item -Recurse -Force ~\Desktop\author-showcase
- To uninstall the Live Server extension, open the Extensions sidebar in VS Code (Ctrl+Shift+X).
- Find Live Server in your installed extensions list, right-click it, and select Uninstall.
Nice Work!
Nice Work!
You just built a polished, portfolio-worthy Author Showcase from scratch. Your page combines modern CSS visual effects with live data from a real API to create something that looks and feels professional.
Here's what you accomplished:
- Built glassmorphism cards with animated rotating gradient borders using CSS @property and conic-gradient.
- Connected to the Open Library API using JavaScript fetch to pull live author and book data.
- Built an interactive slide-in panel that displays real book covers from the Open Library Covers API.
- Secret Mission: Added a live search feature to find and display any author from Open Library.
Ready to quiz yourself?