Build a Resume Editor with PDF Export

Build a browser-based resume editor with live preview and PDF export.

Introduction

30 Second Summary

Updating your resume usually means fighting with formatting in a word processor and hoping the result looks professional enough to send. What if you could build your own editor that shows a live preview as you type?

In this project, you will build a browser-based resume editor using HTML, CSS, and JavaScript. As you type into a form, a styled preview updates in real time before you export it as a downloadable PDF.

What You'll Build

A two-panel web application where you enter resume details in a form on the left and see a professional preview update live on the right, with one-click PDF export.

By the end of this project, you'll have:

  • A two-column layout built with Flexbox that places a form panel and resume preview side by side.
  • Live preview updates driven by JavaScript event listeners and DOM manipulation.
  • One-click PDF export using the html2pdf.js library to download a polished resume file.
  • Secret Mission: add a color theme picker that lets you customize the resume's accent color in real time.

Are there any prerequisites?

This project starts from scratch and explains every concept along the way. All you need is a code editor like VS Code and a web browser like Chrome or Safari.

Before We Start

Before We Start...

Set Up Your Project

Every web application starts with three core files: one for structure (HTML), one for styling (CSS), and one for behavior (JavaScript). Your resume editor is no different.

In this step, you'll create a project folder, set up those three files, and confirm everything is connected by opening your page in a browser.

In this step, get ready to:
  • Create a resume-editor folder with three project files.
  • Add the HTML boilerplate that connects your CSS and JavaScript.
  • Open the page in your browser to confirm it works.
Create your project files

Before you can write any code, you need a folder to hold your project and three empty files inside it. Let's create that structure now.

  • 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 built-in terminal by pressing Ctrl+` (backtick). You can also find it under View, then Terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir resume-editor

Now open this folder in VS Code so you can create files inside it.

  • Click File in the VS Code menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the resume-editor folder, and click Open.

VS Code may ask you to trust the folder

If you see a prompt asking whether you trust the authors of the files in this folder, click Yes, I trust the authors. This is a standard security check for new folders.

You should see the folder name RESUME-EDITOR in the Explorer sidebar on the left. The folder is empty for now.

  • Hover over the RESUME-EDITOR heading in the Explorer sidebar. A row of small icons appears.
  • Click the New File icon (it looks like a page with a small + sign).
  • Type index.html and press Enter.
  • Click the New File icon again, type style.css, and press Enter.
  • Click the New File icon one more time, type script.js, and press Enter.

You should now see all three files listed in the Explorer sidebar: index.html, style.css, and script.js.

Don't see the New File icon?

Make sure you opened the resume-editor folder (not just a single file). Go to File > Open Folder and select the folder from your Desktop.

If the Explorer sidebar is hidden, press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to open it.

Help me set up my project files in VS Code.

Add the HTML boilerplate

HTML boilerplate is the standard starting code that every web page needs. It tells the browser what language the page uses, how to display it, and where to find additional files like your stylesheet and scripts.

  • Click on index.html in the Explorer sidebar to open the file.
  • Copy and paste the following code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Resume Editor</title>
    <!-- Connect the CSS stylesheet -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Temporary heading to confirm the page loads -->
    <h1>Resume Editor</h1>

    <!-- Load the html2pdf.js library for PDF export -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
    <!-- Load our own JavaScript file -->
    <script src="script.js"></script>
</body>
</html>

What does this code do?

  • <!DOCTYPE html> tells the browser this is a modern HTML5 page.
  • The <head> section contains metadata the browser reads before displaying anything. The <link> tag connects your style.css file so CSS styles apply to the page.
  • The <body> section holds everything visible on the page. Right now it just has a temporary heading.
  • The first <script> tag loads html2pdf.js from a CDN (a server that hosts popular libraries). You'll use this library later to export your resume as a PDF.
  • The second <script> tag loads your own script.js file, where you'll write all your JavaScript code.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

You won't see any changes yet. The next substep opens this file in your browser to confirm it works.

✔️ 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 index.html file so far. Make sure yours matches exactly.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Resume Editor</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Resume Editor</h1>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
    <script src="script.js"></script>
</body>
</html>
Open the page in your browser

Your files are ready, but you need to see them in action. Opening the HTML file in a browser is how you preview your work throughout this project.

  • Open Finder (macOS) or File Explorer (Windows) and navigate to your Desktop.
  • Open the resume-editor folder.
  • Drag index.html into an open Chrome or Safari browser window.

You should see a page with the text "Resume Editor" displayed as a large heading. The page is mostly blank, which is expected.

The save-and-refresh workflow

From now on, whenever you make changes to your files in VS Code, save the file (Cmd+S or Ctrl+S) and then refresh your browser (Cmd+R or Ctrl+R) to see the updates. This two-step habit keeps your preview in sync with your code.

Don't see Resume Editor in the browser?

  • Make sure you saved index.html in VS Code. A white dot on the file tab means there are unsaved changes.
  • Check that you dragged index.html (not style.css or script.js) into the browser window.
  • If you see raw code instead of a heading, your browser may be displaying the file as plain text. Try right-clicking the file in your file manager and choosing Open With > Chrome or Safari.

Help me open my HTML file in a browser.

Your project structure is ready and your page loads in the browser. Next up, you'll fill in the HTML with a full resume form and preview layout.

Build the Resume Form and Preview with HTML

Your project files are connected and ready to go. Now it is time to build the actual structure of the resume editor app.

Every web app starts with HTML, which defines the structure and content on the page. In this step, you will build a two-panel layout: a form on the left where you type resume details, and a preview on the right that displays the formatted result.

In this step, get ready to:
  • Build a form panel with sections for personal info, summary, skills, and experience.
  • Build a preview panel that mirrors the form with placeholder resume content.
  • Verify the full HTML structure renders in your browser.
Build the form panel

The form panel is the left side of the app where you will type all your resume information. It is built from HTML form elements like text inputs, email inputs, and textareas.

  • In index.html, find this line:
    <h1>Resume Editor</h1>
  • Select and delete that line.
  • In its place, paste the following code to create the app title, the container structure, and the first two form sections (Personal Info and Summary):
    <h1 class="app-title">Resume Editor</h1>
    <div class="container">
        <!-- Form Panel -->
        <div class="form-panel">
            <h2>Edit Your Resume</h2>

            <section class="form-section">
                <h3>Personal Info</h3>
                <label for="fullName">Full Name</label>
                <input type="text" id="fullName" placeholder="Jane Smith">
                <label for="jobTitle">Job Title</label>
                <input type="text" id="jobTitle" placeholder="Software Developer">
                <label for="email">Email</label>
                <input type="email" id="email" placeholder="jane@example.com">
                <label for="phone">Phone</label>
                <input type="tel" id="phone" placeholder="(555) 123-4567">
            </section>

            <section class="form-section">
                <h3>Summary</h3>
                <textarea id="summary" rows="4" placeholder="A brief professional summary about yourself..."></textarea>
            </section>

What does this code do?

The <h1 class="app-title"> replaces the old temporary heading and adds a class so you can style it later. The <div class="container"> wraps the entire app layout.

Inside the form-panel div, each <section> groups related inputs together. The for attribute on each <label> connects it to the matching input's id, so clicking a label focuses the right input.

Notice the different input types: type="email" validates email format and type="tel" shows a phone keyboard on mobile devices.

  • Save index.html and refresh your browser.
  • You should see the "Edit Your Resume" heading, the Personal Info labels and text boxes, and a Summary text area. The page will look messy and unstyled.

Don't see the form inputs?

Make sure you deleted the old <h1>Resume Editor</h1> line completely before pasting the new code. Duplicate tags can cause unexpected layout.

Check that the new code sits between the opening <body> tag and the first <script> tag. Code outside those boundaries will not render correctly.

Help me figure out why my form inputs are not showing up in the browser.

Now add the remaining form sections. The Skills section is a bit different because it includes buttons for adding and removing entries dynamically.

  • In index.html, paste the following code directly below the closing </section> tag of the Summary section you just added:
            <section class="form-section">
                <h3>Skills</h3>
                <div id="skillsList">
                    <div class="skill-entry">
                        <input type="text" class="skill-input" placeholder="e.g. JavaScript">
                        <button class="remove-btn" onclick="removeSkill(this)">X</button>
                    </div>
                </div>
                <button class="add-btn" id="addSkillBtn">+ Add Skill</button>
            </section>

            <section class="form-section">
                <h3>Experience</h3>
                <label for="companyName">Company</label>
                <input type="text" id="companyName" placeholder="Acme Corp">
                <label for="roleTitle">Role</label>
                <input type="text" id="roleTitle" placeholder="Frontend Developer">
                <label for="dates">Dates</label>
                <input type="text" id="dates" placeholder="Jan 2024 - Present">
                <label for="roleDescription">Description</label>
                <textarea id="roleDescription" rows="3" placeholder="Describe what you did in this role..."></textarea>
            </section>

            <button class="pdf-btn" id="downloadPdf">Download as PDF</button>
        </div>

What does this code do?

The Skills section uses a different structure from the other sections. Instead of plain inputs, each skill is wrapped in a skill-entry div that pairs an input with a remove button. The onclick="removeSkill(this)" on the button will call a JavaScript function you will write later.

The addSkillBtn button lets users create new skill entries. The downloadPdf button at the bottom will trigger a PDF export once you wire it up with JavaScript. The closing </div> at the end closes the form-panel div.

  • Save index.html and refresh your browser.
  • You should now see a skill text box with an "X" button next to it, a green "+ Add Skill" button, the Experience input fields, and a "Download as PDF" button at the bottom.

Seeing duplicate headings or missing sections?

Make sure the new code starts right after the Summary section's closing </section> tag. If you pasted it inside the Summary section by accident, the layout will break.

Help me figure out why my Skills or Experience section is not showing up correctly.

Build the preview panel

The preview panel is the right side of the app. It displays a formatted resume that mirrors whatever the user types into the form.

Every element in the preview has a unique id attribute so JavaScript can target it and update the text in real time. These IDs are the bridge between the form and the preview.

  • In index.html, find the closing </div> tag of the form panel (the one right after the Download as PDF button). Paste the following code directly below it to add the preview panel and close the container:
        <!-- Preview Panel -->
        <div class="preview-panel">
            <div id="resumePreview">
                <div class="resume-header">
                    <h1 id="previewName">Jane Smith</h1>
                    <p id="previewTitle" class="preview-job-title">Software Developer</p>
                    <p class="preview-contact">
                        <span id="previewEmail">jane@example.com</span> |
                        <span id="previewPhone">(555) 123-4567</span>
                    </p>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Summary</h2>
                    <p id="previewSummary">A brief professional summary about yourself...</p>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Skills</h2>
                    <div id="previewSkills" class="skills-list">
                        <span class="skill-tag">JavaScript</span>
                    </div>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Experience</h2>
                    <div class="experience-item">
                        <div class="experience-header">
                            <strong id="previewCompany">Acme Corp</strong>
                            <span id="previewDates" class="experience-dates">Jan 2024 - Present</span>
                        </div>
                        <p id="previewRole" class="experience-role">Frontend Developer</p>
                        <p id="previewDescription">Describe what you did in this role...</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

What does this code do?

The resumePreview div wraps the entire resume layout. Inside it, the resume-header section shows the name, job title, and contact line. Each element has a unique id like previewName and previewEmail.

These IDs match the form inputs by name. For example, the form has an input with id="fullName" and the preview has a heading with id="previewName". In a later step, JavaScript will read from one and write to the other.

The final </div> at the bottom closes the container div that wraps both panels.

  • Save index.html and refresh your browser.
  • Below the form inputs, you should now see placeholder resume content: "Jane Smith", "Software Developer", a contact line, a summary paragraph, a "JavaScript" skill tag, and experience details for "Acme Corp".

Don't see the preview content?

Make sure the preview panel code is placed after the form panel's closing </div> but still inside the <div class="container">. If you accidentally pasted it outside the container, the layout will not work correctly in the next step.

Help me figure out why my preview panel is not showing up.

Check your HTML structure

Your page should now display all the form inputs and the preview content stacked vertically. Everything will look raw and unstyled. That is completely expected.

  • Scroll through the page and confirm you can see all form fields: Full Name, Job Title, Email, Phone, Summary, a skill input, Company, Role, Dates, and Description.
  • Confirm the preview section shows placeholder text: "Jane Smith", "Software Developer", "jane@example.com", a "JavaScript" skill tag, and "Acme Corp" experience details.
  • Try typing in one of the form fields. Nothing will happen in the preview yet. JavaScript will connect them in a later step.

Why does it look so messy?

HTML only defines the structure and content of a page. Without CSS, the browser stacks everything in a single column with default styling. The next step adds CSS to create the side-by-side layout and professional resume look.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html file as it should look at this point. Compare it with yours and fix any differences.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Resume Editor</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1 class="app-title">Resume Editor</h1>
    <div class="container">
        <!-- Form Panel -->
        <div class="form-panel">
            <h2>Edit Your Resume</h2>

            <section class="form-section">
                <h3>Personal Info</h3>
                <label for="fullName">Full Name</label>
                <input type="text" id="fullName" placeholder="Jane Smith">
                <label for="jobTitle">Job Title</label>
                <input type="text" id="jobTitle" placeholder="Software Developer">
                <label for="email">Email</label>
                <input type="email" id="email" placeholder="jane@example.com">
                <label for="phone">Phone</label>
                <input type="tel" id="phone" placeholder="(555) 123-4567">
            </section>

            <section class="form-section">
                <h3>Summary</h3>
                <textarea id="summary" rows="4" placeholder="A brief professional summary about yourself..."></textarea>
            </section>

            <section class="form-section">
                <h3>Skills</h3>
                <div id="skillsList">
                    <div class="skill-entry">
                        <input type="text" class="skill-input" placeholder="e.g. JavaScript">
                        <button class="remove-btn" onclick="removeSkill(this)">X</button>
                    </div>
                </div>
                <button class="add-btn" id="addSkillBtn">+ Add Skill</button>
            </section>

            <section class="form-section">
                <h3>Experience</h3>
                <label for="companyName">Company</label>
                <input type="text" id="companyName" placeholder="Acme Corp">
                <label for="roleTitle">Role</label>
                <input type="text" id="roleTitle" placeholder="Frontend Developer">
                <label for="dates">Dates</label>
                <input type="text" id="dates" placeholder="Jan 2024 - Present">
                <label for="roleDescription">Description</label>
                <textarea id="roleDescription" rows="3" placeholder="Describe what you did in this role..."></textarea>
            </section>

            <button class="pdf-btn" id="downloadPdf">Download as PDF</button>
        </div>

        <!-- Preview Panel -->
        <div class="preview-panel">
            <div id="resumePreview">
                <div class="resume-header">
                    <h1 id="previewName">Jane Smith</h1>
                    <p id="previewTitle" class="preview-job-title">Software Developer</p>
                    <p class="preview-contact">
                        <span id="previewEmail">jane@example.com</span> |
                        <span id="previewPhone">(555) 123-4567</span>
                    </p>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Summary</h2>
                    <p id="previewSummary">A brief professional summary about yourself...</p>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Skills</h2>
                    <div id="previewSkills" class="skills-list">
                        <span class="skill-tag">JavaScript</span>
                    </div>
                </div>

                <div class="resume-section">
                    <h2 class="section-heading">Experience</h2>
                    <div class="experience-item">
                        <div class="experience-header">
                            <strong id="previewCompany">Acme Corp</strong>
                            <span id="previewDates" class="experience-dates">Jan 2024 - Present</span>
                        </div>
                        <p id="previewRole" class="experience-role">Frontend Developer</p>
                        <p id="previewDescription">Describe what you did in this role...</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
    <script src="script.js"></script>
</body>
</html>

Your HTML skeleton is complete. The structure is all there, but it looks like a wall of text. Next up, you will add CSS to transform this into a polished two-column layout with professional resume styling.

Style the Layout and Resume Preview

Your HTML structure is in place, but right now everything is stacked in a single column with default browser styling.

In this step, you will use CSS to transform that raw HTML into a polished two-column layout. The form panel goes on the left, the resume preview on the right, and the whole thing looks like a real app.

In this step, get ready to:
  • Add a CSS reset and a two-column Flexbox layout.
  • Style the form inputs and buttons with colors and focus effects.
  • Make the resume preview look like a professional document.
Add base styles and the two-column layout

Browsers apply their own default margins and padding to every HTML element. A CSS reset strips those defaults so your layout starts from a clean slate.

  • Open style.css in VS Code and add the CSS reset, body styles, title styles, and two-column container layout by pasting this code:
/* Reset and base styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f0f2f5;
    color: #333;
    padding: 20px;
}

.app-title {
    text-align: center;
    margin-bottom: 20px;
    color: #2c3e50;
}

/* Two-column layout */
.container {
    display: flex;
    gap: 30px;
    max-width: 1200px;
    margin: 0 auto;
}

What does this code do?

The * selector targets every element on the page. Setting margin: 0 and padding: 0 removes default browser spacing.

The box-sizing: border-box rule makes padding count inside an element's width instead of adding to it. This prevents layout surprises when you add padding later.

The .container uses display: flex to activate Flexbox, which places the form panel and preview panel side by side. The gap: 30px adds space between the two panels.

  • Save style.css and refresh your browser.
  • You should see the form panel and preview panel sitting side by side on a light grey background. The page title is centered at the top.

Still seeing a single column?

  • Make sure you saved style.css in VS Code. Unsaved changes will not appear in the browser.
  • Confirm the <link rel="stylesheet" href="style.css"> tag exists in the <head> of your index.html file.
  • Check that style.css is in the same folder as index.html. If they are in different folders, the browser cannot find the stylesheet.

Why isn't my CSS showing up in the browser?

Style the form panel

The form panel is where your users will type their resume details. Clean input styling and clear visual grouping make the form feel intuitive to use.

  • In style.css, add the form panel structure, input fields, and focus highlight styles below the .container block by pasting this code:
/* Form Panel */
.form-panel {
    flex: 1;
    background: white;
    padding: 25px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    max-height: 90vh;
    overflow-y: auto;
}

.form-panel h2 {
    margin-bottom: 20px;
    color: #2c3e50;
}

.form-section {
    margin-bottom: 25px;
    padding-bottom: 20px;
    border-bottom: 1px solid #eee;
}

.form-section h3 {
    margin-bottom: 12px;
    color: #34495e;
}

label {
    display: block;
    margin-bottom: 4px;
    font-size: 14px;
    color: #555;
    font-weight: 500;
}

input[type="text"],
input[type="email"],
input[type="tel"],
textarea {
    width: 100%;
    padding: 10px;
    margin-bottom: 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    font-family: inherit;
}

input:focus,
textarea:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}

What does this code do?

  • .form-panel gets a white card look with rounded corners and a subtle shadow. The max-height: 90vh keeps it within the screen height, and overflow-y: auto adds a scrollbar if the form is too long.
  • Each .form-section has a light bottom border to visually separate Personal Info, Summary, Skills, and Experience.
  • The :focus styles add a blue border and a soft glow when you click into an input. This gives the user clear feedback about which field is active.
  • Save style.css and refresh your browser.
  • The form panel should now be a white card with rounded corners. Click into any input field and you should see a blue glow appear around it.

No blue glow when clicking into inputs?

  • Make sure the input:focus and textarea:focus block is present at the end of the code you just pasted. CSS selectors are case-sensitive.
  • Check that you do not have a stray curly brace } breaking the previous rule. A missing or extra brace can silently break all the CSS that comes after it.

My CSS focus styles are not working on input fields.

The inputs look clean, but the buttons still have default browser styling. The next chunk handles the skill entry row, the remove button, the add button, and the PDF download button.

  • Add the skill entry and button styles below the focus styles you just added:
/* Skill entries */
.skill-entry {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}

.skill-entry input {
    margin-bottom: 0;
}

.remove-btn {
    padding: 8px 14px;
    background-color: #e74c3c;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-weight: bold;
}

.remove-btn:hover {
    background-color: #c0392b;
}

.add-btn {
    padding: 8px 16px;
    background-color: #27ae60;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
}

.add-btn:hover {
    background-color: #219a52;
}

.pdf-btn {
    width: 100%;
    padding: 14px;
    background-color: #3498db;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 16px;
    font-weight: bold;
    margin-top: 10px;
}

.pdf-btn:hover {
    background-color: #2980b9;
}

What does this code do?

  • .skill-entry uses Flexbox to place the skill input and its remove button on the same row.
  • Each button type gets a distinct color: red for .remove-btn, green for .add-btn, and blue for .pdf-btn. This makes the purpose of each button visually obvious.
  • The :hover rules darken each button when the cursor hovers over it, giving the user instant feedback that the button is clickable.
  • Save style.css and refresh your browser.
  • You should see a red X button next to the skill input, a green + Add Skill button below it, and a full-width blue Download as PDF button at the bottom of the form.
  • Hover over each button to confirm it darkens slightly.

Buttons still look like default grey rectangles?

  • Confirm your button classes in index.html match your CSS exactly. The remove button needs class="remove-btn", the add button needs class="add-btn", and the PDF button needs class="pdf-btn".
  • Check for a missing closing brace } in the CSS above this section. A single missing brace breaks everything below it.

My button styles are not applying even though I added the CSS.

Style the resume preview

The preview panel is the star of the app. It needs to look like a real printed resume so the user can see exactly what their PDF will look like.

  • Add the preview panel and resume header styles below the .pdf-btn:hover block by pasting this code:
/* Preview Panel */
.preview-panel {
    flex: 1;
    position: sticky;
    top: 20px;
    align-self: flex-start;
}

#resumePreview {
    background: white;
    padding: 40px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    min-height: 600px;
}

/* Resume Header */
.resume-header {
    text-align: center;
    padding-bottom: 20px;
    border-bottom: 2px solid #3498db;
    margin-bottom: 20px;
}

.resume-header h1 {
    font-size: 28px;
    color: #2c3e50;
    margin-bottom: 4px;
}

.preview-job-title {
    font-size: 16px;
    color: #3498db;
    margin-bottom: 8px;
}

.preview-contact {
    font-size: 13px;
    color: #777;
}

What does this code do?

  • position: sticky on .preview-panel keeps the preview visible as you scroll through the form. It "sticks" to the top of the viewport instead of scrolling off-screen.
  • The .resume-header centers the name and job title with a blue bottom border, giving it that classic resume look.
  • The job title and contact info use smaller, lighter text so they do not compete with the name for attention.
  • Save style.css and refresh your browser.
  • The preview panel should now be a white card with a centered header showing "Jane Smith" in large text, "Software Developer" in blue below it, and a blue underline separating the header from the rest.

Preview panel not showing the blue header line?

  • Confirm that the .resume-header class in your CSS matches class="resume-header" in your HTML. A typo in either place breaks the connection.
  • Check that the border-bottom: 2px solid #3498db line is inside the .resume-header block and not outside it.

My resume preview header styling is not appearing correctly.

The header looks great. Now add the styles for the resume body: section headings, the summary text, skill tags, and the experience block.

  • Add the resume section, skills, and experience styles below the .preview-contact block by pasting this code:
/* Resume Sections */
.resume-section {
    margin-bottom: 20px;
}

.section-heading {
    font-size: 16px;
    color: #2c3e50;
    text-transform: uppercase;
    letter-spacing: 1px;
    border-bottom: 1px solid #ddd;
    padding-bottom: 6px;
    margin-bottom: 10px;
}

#previewSummary {
    font-size: 14px;
    line-height: 1.6;
    color: #555;
}

/* Skills */
.skills-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.skill-tag {
    background-color: #ebf5fb;
    color: #2980b9;
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 13px;
}

/* Experience */
.experience-item {
    margin-bottom: 12px;
}

.experience-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.experience-dates {
    font-size: 13px;
    color: #777;
}

.experience-role {
    font-size: 14px;
    color: #3498db;
    margin: 2px 0 6px 0;
}

#previewDescription {
    font-size: 14px;
    line-height: 1.5;
    color: #555;
}

What does this code do?

  • .section-heading uses text-transform: uppercase and letter-spacing: 1px to give the section titles (Summary, Skills, Experience) that classic resume formatting.
  • .skill-tag creates rounded pill-shaped badges with a light blue background. The border-radius: 20px makes the corners fully rounded.
  • .experience-header uses Flexbox with justify-content: space-between to push the company name to the left and the dates to the right on the same line.
  • Save style.css and refresh your browser.
  • Your resume preview should now look like a real resume. You should see uppercase section headings with a thin underline, a "JavaScript" skill displayed as a rounded blue pill, and the experience section with "Acme Corp" on the left and "Jan 2024 - Present" on the right.

Skills or experience not styled correctly?

  • Check that class="skill-tag" in your HTML matches .skill-tag in your CSS. Class names are case-sensitive.
  • If the company name and dates are stacked vertically instead of side by side, confirm that .experience-header has display: flex in your CSS.

My resume preview skill tags or experience section look wrong.

That is all the CSS for this project. Double check your full file against the reference below.

✔️ Awesome, I've got everything!

Great. Make sure you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css file. Compare it against yours and fix any differences.

/* Reset and base styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f0f2f5;
    color: #333;
    padding: 20px;
}

.app-title {
    text-align: center;
    margin-bottom: 20px;
    color: #2c3e50;
}

/* Two-column layout */
.container {
    display: flex;
    gap: 30px;
    max-width: 1200px;
    margin: 0 auto;
}

/* Form Panel */
.form-panel {
    flex: 1;
    background: white;
    padding: 25px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    max-height: 90vh;
    overflow-y: auto;
}

.form-panel h2 {
    margin-bottom: 20px;
    color: #2c3e50;
}

.form-section {
    margin-bottom: 25px;
    padding-bottom: 20px;
    border-bottom: 1px solid #eee;
}

.form-section h3 {
    margin-bottom: 12px;
    color: #34495e;
}

label {
    display: block;
    margin-bottom: 4px;
    font-size: 14px;
    color: #555;
    font-weight: 500;
}

input[type="text"],
input[type="email"],
input[type="tel"],
textarea {
    width: 100%;
    padding: 10px;
    margin-bottom: 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    font-family: inherit;
}

input:focus,
textarea:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}

/* Skill entries */
.skill-entry {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}

.skill-entry input {
    margin-bottom: 0;
}

.remove-btn {
    padding: 8px 14px;
    background-color: #e74c3c;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-weight: bold;
}

.remove-btn:hover {
    background-color: #c0392b;
}

.add-btn {
    padding: 8px 16px;
    background-color: #27ae60;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
}

.add-btn:hover {
    background-color: #219a52;
}

.pdf-btn {
    width: 100%;
    padding: 14px;
    background-color: #3498db;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 16px;
    font-weight: bold;
    margin-top: 10px;
}

.pdf-btn:hover {
    background-color: #2980b9;
}

/* Preview Panel */
.preview-panel {
    flex: 1;
    position: sticky;
    top: 20px;
    align-self: flex-start;
}

#resumePreview {
    background: white;
    padding: 40px;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    min-height: 600px;
}

/* Resume Header */
.resume-header {
    text-align: center;
    padding-bottom: 20px;
    border-bottom: 2px solid #3498db;
    margin-bottom: 20px;
}

.resume-header h1 {
    font-size: 28px;
    color: #2c3e50;
    margin-bottom: 4px;
}

.preview-job-title {
    font-size: 16px;
    color: #3498db;
    margin-bottom: 8px;
}

.preview-contact {
    font-size: 13px;
    color: #777;
}

/* Resume Sections */
.resume-section {
    margin-bottom: 20px;
}

.section-heading {
    font-size: 16px;
    color: #2c3e50;
    text-transform: uppercase;
    letter-spacing: 1px;
    border-bottom: 1px solid #ddd;
    padding-bottom: 6px;
    margin-bottom: 10px;
}

#previewSummary {
    font-size: 14px;
    line-height: 1.6;
    color: #555;
}

/* Skills */
.skills-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.skill-tag {
    background-color: #ebf5fb;
    color: #2980b9;
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 13px;
}

/* Experience */
.experience-item {
    margin-bottom: 12px;
}

.experience-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.experience-dates {
    font-size: 13px;
    color: #777;
}

.experience-role {
    font-size: 14px;
    color: #3498db;
    margin: 2px 0 6px 0;
}

#previewDescription {
    font-size: 14px;
    line-height: 1.5;
    color: #555;
}

Your resume editor looks polished. Next up, you will bring it to life with JavaScript so the preview updates in real time as you type into the form.

Make the Preview Update in Real Time

Your resume editor has a polished two-column layout with professional styling. The preview panel already looks like a real resume document.

Right now, the preview shows static placeholder text. In this step, you'll write JavaScript that listens for changes in the form and instantly updates the preview to match what you type.

In this step, get ready to:
  • Store references to all form and preview elements in JavaScript variables.
  • Add event listeners that update the preview in real time as you type.
Get references to your elements

Before JavaScript can update the preview, it needs to know which HTML elements to read from and write to. The document.getElementById() method grabs an element by its id attribute and stores it in a variable for quick access.

  • In script.js, add the following code to grab references to every form input and preview element:
// ===== LIVE PREVIEW =====

// Get all form input elements
var fullName = document.getElementById('fullName');
var jobTitle = document.getElementById('jobTitle');
var email = document.getElementById('email');
var phone = document.getElementById('phone');
var summary = document.getElementById('summary');
var companyName = document.getElementById('companyName');
var roleTitle = document.getElementById('roleTitle');
var dates = document.getElementById('dates');
var roleDescription = document.getElementById('roleDescription');

// Get all preview elements
var previewName = document.getElementById('previewName');
var previewTitle = document.getElementById('previewTitle');
var previewEmail = document.getElementById('previewEmail');
var previewPhone = document.getElementById('previewPhone');
var previewSummary = document.getElementById('previewSummary');
var previewCompany = document.getElementById('previewCompany');
var previewRole = document.getElementById('previewRole');
var previewDates = document.getElementById('previewDates');
var previewDescription = document.getElementById('previewDescription');

What does this code do?

Each line uses document.getElementById() to find an HTML element by its id and store it in a variable. This is like giving JavaScript a shortcut name for each element so it can read from it or change it later.

The first group grabs the form inputs (where the learner types). The second group grabs the preview elements (where the text needs to appear). Each variable name matches the id you set in your HTML.

You won't see any changes in the browser yet. These variables just store references to the elements. The next chunk puts them to work.

Add event listeners for live updates

An event listener tells JavaScript to run a function every time something specific happens to an element. The input event fires every time the user types, deletes, or pastes text into a field.

  • Below the preview element variables in script.js, add event listeners for the personal info fields:
// Update preview when form inputs change
fullName.addEventListener('input', function () {
    previewName.textContent = fullName.value || 'Jane Smith';
});

jobTitle.addEventListener('input', function () {
    previewTitle.textContent = jobTitle.value || 'Software Developer';
});

email.addEventListener('input', function () {
    previewEmail.textContent = email.value || 'jane@example.com';
});

phone.addEventListener('input', function () {
    previewPhone.textContent = phone.value || '(555) 123-4567';
});

What does this code do?

Each listener follows the same pattern. When the user types into a form field, the function copies that field's value into the matching preview element's textContent.

The || operator is a fallback. If the input is empty (the user cleared the field), it shows placeholder text like 'Jane Smith' instead of leaving the preview blank.

  • Save script.js and refresh your browser.
  • Type a name into the Full Name field. The name in the preview header should update instantly to match what you typed.

Preview not updating?

  • Make sure you saved script.js and refreshed the browser. Unsaved changes won't take effect.
  • Open your browser's developer console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows). If you see a red error message, check that every getElementById string matches the exact id in your HTML.
  • Check that the <script src="script.js"></script> tag appears just before </body> in your HTML. If the script loads before the HTML elements exist, the references will be null.

Help me debug why my live preview isn't updating when I type.

The remaining form fields (summary and experience) need the same treatment. The pattern is identical.

  • Below the phone listener in script.js, add event listeners for the summary and experience fields:
summary.addEventListener('input', function () {
    previewSummary.textContent = summary.value || 'A brief professional summary about yourself...';
});

companyName.addEventListener('input', function () {
    previewCompany.textContent = companyName.value || 'Acme Corp';
});

roleTitle.addEventListener('input', function () {
    previewRole.textContent = roleTitle.value || 'Frontend Developer';
});

dates.addEventListener('input', function () {
    previewDates.textContent = dates.value || 'Jan 2024 - Present';
});

roleDescription.addEventListener('input', function () {
    previewDescription.textContent = roleDescription.value || 'Describe what you did in this role...';
});

What's happening here?

These five listeners follow the exact same pattern as the personal info ones above. Each one watches a form field for changes and copies the typed value into the matching preview element.

Notice that each fallback string matches the placeholder text already shown in the preview. This keeps the preview looking complete even when a field is empty.

  • Save script.js and refresh your browser.
  • Type a company name into the Company field. The Experience section in the preview should update to show the name you typed.

Some fields update but others don't?

  • Double check that every variable name in the listener matches the variable you declared at the top of the file. For example, companyName in the listener must match var companyName = document.getElementById('companyName') exactly.
  • Make sure each listener updates the correct preview variable. For example, the companyName listener should update previewCompany, not previewRole.

Help me figure out why some preview fields update and others don't.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js before moving on.

ⓧ I'd like to double check the full code

// ===== LIVE PREVIEW =====

// Get all form input elements
var fullName = document.getElementById('fullName');
var jobTitle = document.getElementById('jobTitle');
var email = document.getElementById('email');
var phone = document.getElementById('phone');
var summary = document.getElementById('summary');
var companyName = document.getElementById('companyName');
var roleTitle = document.getElementById('roleTitle');
var dates = document.getElementById('dates');
var roleDescription = document.getElementById('roleDescription');

// Get all preview elements
var previewName = document.getElementById('previewName');
var previewTitle = document.getElementById('previewTitle');
var previewEmail = document.getElementById('previewEmail');
var previewPhone = document.getElementById('previewPhone');
var previewSummary = document.getElementById('previewSummary');
var previewCompany = document.getElementById('previewCompany');
var previewRole = document.getElementById('previewRole');
var previewDates = document.getElementById('previewDates');
var previewDescription = document.getElementById('previewDescription');

// Update preview when form inputs change
fullName.addEventListener('input', function () {
    previewName.textContent = fullName.value || 'Jane Smith';
});

jobTitle.addEventListener('input', function () {
    previewTitle.textContent = jobTitle.value || 'Software Developer';
});

email.addEventListener('input', function () {
    previewEmail.textContent = email.value || 'jane@example.com';
});

phone.addEventListener('input', function () {
    previewPhone.textContent = phone.value || '(555) 123-4567';
});

summary.addEventListener('input', function () {
    previewSummary.textContent = summary.value || 'A brief professional summary about yourself...';
});

companyName.addEventListener('input', function () {
    previewCompany.textContent = companyName.value || 'Acme Corp';
});

roleTitle.addEventListener('input', function () {
    previewRole.textContent = roleTitle.value || 'Frontend Developer';
});

dates.addEventListener('input', function () {
    previewDates.textContent = dates.value || 'Jan 2024 - Present';
});

roleDescription.addEventListener('input', function () {
    previewDescription.textContent = roleDescription.value || 'Describe what you did in this role...';
});
Test the live preview

All nine form fields are now wired up to the preview. Time to confirm everything works end to end.

  • Save script.js and refresh your browser.
  • Type into every form field one at a time. Watch the preview panel update as you type.
  • Clear one of the fields completely (select all and delete). The preview should show the placeholder text again (for example, Jane Smith for the name field).

What about the skills section?

You might notice the skills input does not update the preview yet. Skills need special handling because users can add and remove multiple entries. You'll build that in the next step.

Your preview now updates in real time for every field except skills. Next up, you'll add dynamic skill management so users can add and remove skills on the fly.

Add Dynamic Skills

Your live preview is working for every field on the resume. Every field except one, that is.

The Skills section needs something different from the other fields. Instead of a single input, skills need to be added and removed dynamically. In this step, you'll write JavaScript that creates new skill inputs on the fly and keeps the preview in sync every time a skill changes.

In this step, get ready to:
  • Add a click handler that dynamically creates new skill input fields.
  • Build functions that remove skills and refresh the preview.
  • Wire up all skill inputs so the preview updates in real time.
Add the "Add Skill" button handler

The other form fields have a fixed number of inputs. Skills are different because the user decides how many they want. Your JavaScript needs to create brand-new HTML elements and insert them into the page when the user clicks a button.

What is DOM manipulation?

The DOM (Document Object Model) is the browser's live representation of your HTML page. DOM manipulation means using JavaScript to create, modify, or delete HTML elements after the page has loaded.

When you call document.createElement('div'), you're telling the browser to build a new element on the fly. This is how dynamic features like "add another skill" work without reloading the page.

  • In script.js, scroll to the bottom of the file (below the last addEventListener for roleDescription).
  • Add the following code to grab references to the skills list and the Add Skill button, then set up a click handler that creates new skill entries:
// ===== DYNAMIC SKILLS =====

var skillsList = document.getElementById('skillsList');
var addSkillBtn = document.getElementById('addSkillBtn');

// Add a new skill input
addSkillBtn.addEventListener('click', function () {
    var skillEntry = document.createElement('div');
    skillEntry.className = 'skill-entry';
    skillEntry.innerHTML =
        '<input type="text" class="skill-input" placeholder="e.g. JavaScript">' +
        '<button class="remove-btn" onclick="removeSkill(this)">X</button>';
    skillsList.appendChild(skillEntry);

    // Add live preview listener to the new input
    var newInput = skillEntry.querySelector('.skill-input');
    newInput.addEventListener('input', updateSkillsPreview);
});

What does this code do?

The two var lines grab references to the skillsList container and the addSkillBtn button from your HTML.

The click handler builds a brand-new <div> containing a text input and a remove button, then appends it to the skills list. It also attaches an input event listener to the new input so the preview will update when the user types.

You won't see the skills feature working yet. The click handler calls two functions (removeSkill and updateSkillsPreview) that you'll write in the next substep.

Add the remove and preview update functions

The click handler above creates new skill inputs, but two pieces are still missing. You need a function that removes a skill entry when the user clicks the X button. You also need a function that reads every skill input and rebuilds the preview's skill tags from scratch.

  • Directly below the click handler you just added, add the removeSkill and updateSkillsPreview functions by copying this code:
// Remove a skill input
function removeSkill(button) {
    button.parentElement.remove();
    updateSkillsPreview();
}

// Update the skills preview
function updateSkillsPreview() {
    var previewSkills = document.getElementById('previewSkills');
    var skillInputs = document.querySelectorAll('.skill-input');
    previewSkills.innerHTML = '';

    skillInputs.forEach(function (input) {
        if (input.value.trim() !== '') {
            var tag = document.createElement('span');
            tag.className = 'skill-tag';
            tag.textContent = input.value.trim();
            previewSkills.appendChild(tag);
        }
    });
}

What does this code do?

  • removeSkill(button) finds the skill entry that contains the clicked X button (button.parentElement) and removes the entire entry from the page. Then it refreshes the preview.
  • updateSkillsPreview() clears the preview's skills section, then loops through every skill input on the page. For each non-empty input, it creates a new <span> with the skill-tag class and adds it to the preview.
  • Save script.js and refresh your browser.
  • Click the green + Add Skill button. A new empty skill input row should appear below the first one.
  • Type Python into the new input. You should see a blue "Python" pill tag appear in the preview's Skills section.

New skill not showing in the preview?

  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and look for red error messages. A typo in a function name like updateSkillsPreview will show as a ReferenceError.
  • Make sure previewSkills inside updateSkillsPreview uses getElementById('previewSkills'). The ID must match the one in your HTML exactly.
  • Typing in the first skill input (the one that was already in your HTML) won't update the preview yet. Only newly added inputs are wired up so far.

Why aren't my dynamic skills showing in the resume preview?

Wire up the initial skill input

You may have noticed that typing into the first skill input (the one already in your HTML) does not update the preview. That input existed before your JavaScript loaded, so the click handler never attached a listener to it. You need to wire it up manually.

  • Below the updateSkillsPreview function, add this line to attach a listener to the first skill input:
// Add live preview listener to the initial skill input
document.querySelector('.skill-input').addEventListener('input', updateSkillsPreview);

What does this line do?

document.querySelector('.skill-input') finds the first element with the class skill-input. That is the skill input that was already in your HTML. Attaching the input listener to it means typing there now triggers updateSkillsPreview, just like the dynamically added inputs.

  • Save script.js and refresh your browser.
  • Type HTML into the first skill input. You should see a blue "HTML" pill tag appear in the preview.
  • Click + Add Skill and type CSS into the new input. You should see both "HTML" and "CSS" pill tags in the preview.
  • Click the red X button next to one of your skills. The skill entry should disappear from the form, and the matching pill tag should vanish from the preview.

First skill input still not updating the preview?

  • Make sure the querySelector('.skill-input') line is outside and below the updateSkillsPreview function, not inside it. It should be a standalone line at the bottom of your file.
  • Check that the class name .skill-input in your JavaScript matches class="skill-input" in your HTML. Class names are case-sensitive.

Help me debug why my initial skill input is not updating the preview.

Double check your full script.js against the code below to make sure everything is in the right place.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js before moving on.

ⓧ I'd like to double check the full code

// ===== LIVE PREVIEW =====

// Get all form input elements
var fullName = document.getElementById('fullName');
var jobTitle = document.getElementById('jobTitle');
var email = document.getElementById('email');
var phone = document.getElementById('phone');
var summary = document.getElementById('summary');
var companyName = document.getElementById('companyName');
var roleTitle = document.getElementById('roleTitle');
var dates = document.getElementById('dates');
var roleDescription = document.getElementById('roleDescription');

// Get all preview elements
var previewName = document.getElementById('previewName');
var previewTitle = document.getElementById('previewTitle');
var previewEmail = document.getElementById('previewEmail');
var previewPhone = document.getElementById('previewPhone');
var previewSummary = document.getElementById('previewSummary');
var previewCompany = document.getElementById('previewCompany');
var previewRole = document.getElementById('previewRole');
var previewDates = document.getElementById('previewDates');
var previewDescription = document.getElementById('previewDescription');

// Update preview when form inputs change
fullName.addEventListener('input', function () {
    previewName.textContent = fullName.value || 'Jane Smith';
});

jobTitle.addEventListener('input', function () {
    previewTitle.textContent = jobTitle.value || 'Software Developer';
});

email.addEventListener('input', function () {
    previewEmail.textContent = email.value || 'jane@example.com';
});

phone.addEventListener('input', function () {
    previewPhone.textContent = phone.value || '(555) 123-4567';
});

summary.addEventListener('input', function () {
    previewSummary.textContent = summary.value || 'A brief professional summary about yourself...';
});

companyName.addEventListener('input', function () {
    previewCompany.textContent = companyName.value || 'Acme Corp';
});

roleTitle.addEventListener('input', function () {
    previewRole.textContent = roleTitle.value || 'Frontend Developer';
});

dates.addEventListener('input', function () {
    previewDates.textContent = dates.value || 'Jan 2024 - Present';
});

roleDescription.addEventListener('input', function () {
    previewDescription.textContent = roleDescription.value || 'Describe what you did in this role...';
});

// ===== DYNAMIC SKILLS =====

var skillsList = document.getElementById('skillsList');
var addSkillBtn = document.getElementById('addSkillBtn');

// Add a new skill input
addSkillBtn.addEventListener('click', function () {
    var skillEntry = document.createElement('div');
    skillEntry.className = 'skill-entry';
    skillEntry.innerHTML =
        '<input type="text" class="skill-input" placeholder="e.g. JavaScript">' +
        '<button class="remove-btn" onclick="removeSkill(this)">X</button>';
    skillsList.appendChild(skillEntry);

    // Add live preview listener to the new input
    var newInput = skillEntry.querySelector('.skill-input');
    newInput.addEventListener('input', updateSkillsPreview);
});

// Remove a skill input
function removeSkill(button) {
    button.parentElement.remove();
    updateSkillsPreview();
}

// Update the skills preview
function updateSkillsPreview() {
    var previewSkills = document.getElementById('previewSkills');
    var skillInputs = document.querySelectorAll('.skill-input');
    previewSkills.innerHTML = '';

    skillInputs.forEach(function (input) {
        if (input.value.trim() !== '') {
            var tag = document.createElement('span');
            tag.className = 'skill-tag';
            tag.textContent = input.value.trim();
            previewSkills.appendChild(tag);
        }
    });
}

// Add live preview listener to the initial skill input
document.querySelector('.skill-input').addEventListener('input', updateSkillsPreview);

Your skills section is fully dynamic. You can add as many skills as you want, remove any of them, and the preview updates instantly. The last feature to build is PDF export, so you can download your finished resume as a file.

Export Your Resume as a PDF

Your resume editor handles live preview updates and dynamic skills. The final feature is the ability to save your finished resume as a downloadable file.

In this step, you'll use a library called html2pdf.js to convert the preview panel into a PDF. One click on the Download as PDF button, and the resume downloads to your computer.

In this step, get ready to:
  • Add the PDF export click handler in script.js.
  • Test the download and verify the PDF output.
Add the PDF export handler

html2pdf.js works by taking a DOM element and converting it into a PDF file. You configure settings like page size, margins, and image quality in an options object, and the library handles the rest.

  • In script.js, scroll to the bottom of the file.
  • Find the last line that starts with document.querySelector('.skill-input').
  • Add the PDF export handler below it by copying this code:
// ===== PDF EXPORT =====

var downloadPdf = document.getElementById('downloadPdf');

downloadPdf.addEventListener('click', function () {
    // Grab the resume preview element to convert
    var resume = document.getElementById('resumePreview');
    // Configure the PDF output settings
    var options = {
        margin: 10,
        filename: 'my-resume.pdf',
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { scale: 2 },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
    };
    // Generate and download the PDF
    html2pdf().set(options).from(resume).save();
});

What does this code do?

  • downloadPdf grabs a reference to the Download as PDF button using its id.
  • When the button is clicked, resume captures the entire preview panel element.
  • margin: 10 adds 10mm of white space around the edges of the PDF page.
  • html2canvas: { scale: 2 } doubles the rendering resolution so text looks sharp instead of blurry.
  • jsPDF sets the page to A4 size in portrait orientation.
  • html2pdf().set(options).from(resume).save() chains everything together: apply settings, point at the element, and trigger the download.
  • Save script.js.

You won't see a change yet. The next substep tests the PDF download.

Seeing an error in the console?

  • Make sure your new code is outside and below the existing event listeners. It should not be inside another function.
  • Check that the ID string downloadPdf in your JavaScript matches the id="downloadPdf" on the button in index.html.
  • Confirm the html2pdf.js <script> tag in index.html appears before your script.js <script> tag. The library must load first.

Still stuck? Ask for help.

✔️ 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 script.js file. Compare it with yours to spot any differences.

// ===== LIVE PREVIEW =====

// Get all form input elements
var fullName = document.getElementById('fullName');
var jobTitle = document.getElementById('jobTitle');
var email = document.getElementById('email');
var phone = document.getElementById('phone');
var summary = document.getElementById('summary');
var companyName = document.getElementById('companyName');
var roleTitle = document.getElementById('roleTitle');
var dates = document.getElementById('dates');
var roleDescription = document.getElementById('roleDescription');

// Get all preview elements
var previewName = document.getElementById('previewName');
var previewTitle = document.getElementById('previewTitle');
var previewEmail = document.getElementById('previewEmail');
var previewPhone = document.getElementById('previewPhone');
var previewSummary = document.getElementById('previewSummary');
var previewCompany = document.getElementById('previewCompany');
var previewRole = document.getElementById('previewRole');
var previewDates = document.getElementById('previewDates');
var previewDescription = document.getElementById('previewDescription');

// Update preview when form inputs change
fullName.addEventListener('input', function () {
    previewName.textContent = fullName.value || 'Jane Smith';
});

jobTitle.addEventListener('input', function () {
    previewTitle.textContent = jobTitle.value || 'Software Developer';
});

email.addEventListener('input', function () {
    previewEmail.textContent = email.value || 'jane@example.com';
});

phone.addEventListener('input', function () {
    previewPhone.textContent = phone.value || '(555) 123-4567';
});

summary.addEventListener('input', function () {
    previewSummary.textContent = summary.value || 'A brief professional summary about yourself...';
});

companyName.addEventListener('input', function () {
    previewCompany.textContent = companyName.value || 'Acme Corp';
});

roleTitle.addEventListener('input', function () {
    previewRole.textContent = roleTitle.value || 'Frontend Developer';
});

dates.addEventListener('input', function () {
    previewDates.textContent = dates.value || 'Jan 2024 - Present';
});

roleDescription.addEventListener('input', function () {
    previewDescription.textContent = roleDescription.value || 'Describe what you did in this role...';
});

// ===== DYNAMIC SKILLS =====

var skillsList = document.getElementById('skillsList');
var addSkillBtn = document.getElementById('addSkillBtn');

// Add a new skill input
addSkillBtn.addEventListener('click', function () {
    var skillEntry = document.createElement('div');
    skillEntry.className = 'skill-entry';
    skillEntry.innerHTML =
        '<input type="text" class="skill-input" placeholder="e.g. JavaScript">' +
        '<button class="remove-btn" onclick="removeSkill(this)">X</button>';
    skillsList.appendChild(skillEntry);

    // Add live preview listener to the new input
    var newInput = skillEntry.querySelector('.skill-input');
    newInput.addEventListener('input', updateSkillsPreview);
});

// Remove a skill input
function removeSkill(button) {
    button.parentElement.remove();
    updateSkillsPreview();
}

// Update the skills preview
function updateSkillsPreview() {
    var previewSkills = document.getElementById('previewSkills');
    var skillInputs = document.querySelectorAll('.skill-input');
    previewSkills.innerHTML = '';

    skillInputs.forEach(function (input) {
        if (input.value.trim() !== '') {
            var tag = document.createElement('span');
            tag.className = 'skill-tag';
            tag.textContent = input.value.trim();
            previewSkills.appendChild(tag);
        }
    });
}

// Add live preview listener to the initial skill input
document.querySelector('.skill-input').addEventListener('input', updateSkillsPreview);

// ===== PDF EXPORT =====

var downloadPdf = document.getElementById('downloadPdf');

downloadPdf.addEventListener('click', function () {
    var resume = document.getElementById('resumePreview');
    var options = {
        margin: 10,
        filename: 'my-resume.pdf',
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { scale: 2 },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
    };
    html2pdf().set(options).from(resume).save();
});
Test the PDF export
  • Refresh your browser (or reopen index.html by dragging it into a browser window).
  • Fill in a few resume fields. For example, type Alex Johnson in the Full Name field and Web Developer in the Job Title field.
  • Click the Download as PDF button at the bottom of the form panel.

A file called my-resume.pdf should download to your computer. Open it and check that the content matches what you see in the preview panel.

Why can't I select text in the PDF?

html2pdf.js works by rendering your resume as an image inside the PDF. This means text in the downloaded file is not selectable or searchable. For a learning project this is perfectly fine. Professional resume tools use server-side rendering to produce selectable text.

PDF not downloading?

  • Check your browser's download bar or downloads folder. Some browsers silently save the file without a visible prompt.
  • Open DevTools by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows). Look for red error messages in the Console tab.
  • Make sure the html2pdf.js CDN script tag in index.html uses the exact URL https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js.

Need more help? Ask here.

Secret mission

Add a Color Theme Picker

Your resume editor works great, but every resume comes out with the same blue accent. What if you could pick any color and watch the entire resume theme change in real time? In this secret mission, add a color picker that lets you customize the accent color across the entire resume.

Before you go

Clean Up Your Resources

This project runs entirely locally in your browser, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, or delete them entirely.

Resources you used:

  • The resume-editor folder on your Desktop.
  • Three project files: index.html, style.css, and script.js.
  • Any downloaded my-resume.pdf files in your Downloads folder.

Keep everything running

No action needed. Choose this if you want to keep tweaking your resume editor or use it as a reference for future projects.

  • Your resume-editor folder stays on your Desktop. Open index.html in your browser any time to use the editor.
  • Since everything is local files, there is nothing running in the background and no cost.

Pause - I'll come back to this later

Since this project is just local files, there is nothing to pause. Your files sit on disk and use virtually no space.

  • Leave the resume-editor folder where it is. It will be ready whenever you come back.
  • If you want to move it off your Desktop, drag the folder to a location like Documents so it stays organized.

Delete - I don't want to use this again

Remove all project files to start fresh.

  • Delete the resume-editor folder from your Desktop. On macOS, right-click the folder and select Move to Trash. On Windows, right-click and select Delete.
  • Check your Downloads folder for any my-resume.pdf files and delete those too if you no longer need them.

Mission accomplished

That's a Wrap!

Nice work! You just built a complete resume editor from scratch using HTML, CSS, and JavaScript. From a blank folder to a working app with live preview and PDF export, every line of code was yours.

You've learned how to:

  • Built a two-column Flexbox layout with a form panel and a professionally styled resume preview.
  • Wired up real-time DOM updates with event listeners so the preview reflects every keystroke, including dynamically added and removed skill tags.
  • Integrated a third-party library (html2pdf.js) via CDN to export a polished, downloadable PDF with one click.
  • Secret Mission: Added a color theme picker using CSS custom properties and JavaScript to customize the resume's accent color in real time.

Ready to quiz yourself?