Build Google Gemini AI Chatbot with HTML CSS & JavaScript

Introduction

In today’s rapidly evolving digital world, AI-powered chatbots are transforming how we interact online. One such innovation is the Google Gemini AI, known for its dynamic, conversational abilities. In this tutorial, you'll learn how to create your own Gemini AI chatbot using HTML, CSS, and JavaScript.

Build Google Gemini AI Chatbot with HTML CSS & JavaScript


Whether you're a beginner or an experienced developer, this project will deepen your understanding of working with APIs, enhancing UI with JavaScript, and designing accessible user interfaces.

Why Build a Gemini AI Chatbot?

  • API Integration: Learn to work with the free Google Gemini API.
  • Frontend Mastery: Combine HTML, CSS, and JavaScript in a practical project.
  • Light/Dark Mode: Implement a modern theme switcher.
  • File Upload: Enable users to upload images or documents for contextual queries.
  • Real-Time Interaction: Create an engaging user experience.
  • Portfolio Project: Add a trending AI project to your portfolio.

Project Setup

Create a new folder called gemini-chatbot and inside it, create the following files:

  • index.html
  • style.css
  • script.js

Download a Gemini logo and save it in the project directory as logo.png.

1. HTML Structure (index.html)

<!DOCTYPE html>
<!-- Coding By CodingNepal - youtube.com/@codingnepal -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gemini Chatbot | CodingNepal</title>
    <!-- Linking Google Fonts For Icons -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@32,400,0,0" />
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="container">
      <!-- App Header -->
      <header class="app-header">
        <h1 class="heading">Hello, there</h1>
        <h4 class="sub-heading">How can I help you today?</h4>
      </header>
      <!-- Suggestions List -->
      <ul class="suggestions">
        <li class="suggestions-item">
          <p class="text">Design a home office setup for remote work under $500.</p>
          <span class="icon material-symbols-rounded">draw</span>
        </li>
        <li class="suggestions-item">
          <p class="text">How can I level up my web development expertise in 2025?</p>
          <span class="icon material-symbols-rounded">lightbulb</span>
        </li>
        <li class="suggestions-item">
          <p class="text">Suggest some useful tools for debugging JavaScript code.</p>
          <span class="icon material-symbols-rounded">explore</span>
        </li>
        <li class="suggestions-item">
          <p class="text">Create a React JS component for the simple todo list app.</p>
          <span class="icon material-symbols-rounded">code_blocks</span>
        </li>
      </ul>
      <!-- Chats -->
      <div class="chats-container"></div>
      <!-- Prompt Input -->
      <div class="prompt-container">
        <div class="prompt-wrapper">
          <form action="#" class="prompt-form">
            <input type="text" placeholder="Ask Gemini" class="prompt-input" required />
            <div class="prompt-actions">
              <!-- File Upload Wrapper -->
              <div class="file-upload-wrapper">
                <img src="#" class="file-preview" />
                <input id="file-input" type="file" accept="image/*, .pdf, .txt, .csv" hidden />
                <button type="button" class="file-icon material-symbols-rounded">description</button>
                <button id="cancel-file-btn" type="button" class="material-symbols-rounded">close</button>
                <button id="add-file-btn" type="button" class="material-symbols-rounded">attach_file</button>
              </div>
              <!-- Send Prompt and Stop Response Buttons -->
              <button id="stop-response-btn" type="button" class="material-symbols-rounded">stop_circle</button>
              <button id="send-prompt-btn" class="material-symbols-rounded">arrow_upward</button>
            </div>
          </form>
          <!-- Theme and Delete Chats Buttons -->
          <button id="theme-toggle-btn" class="material-symbols-rounded">light_mode</button>
          <button id="delete-chats-btn" class="material-symbols-rounded">delete</button>
        </div>
        <p class="disclaimer-text">Gemini can make mistakes, so double-check it.</p>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

2. CSS Styling (style.css)

/* Import Google Font - Poppins */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}
:root {
  /* Dark theme colors */
  --text-color: #edf3ff;
  --subheading-color: #97a7ca;
  --placeholder-color: #c3cdde;
  --primary-color: #101623;
  --secondary-color: #283045;
  --secondary-hover-color: #333e58;
  --scrollbar-color: #626a7f;
}
body.light-theme {
  /* Light theme colors */
  --text-color: #090c13;
  --subheading-color: #7b8cae;
  --placeholder-color: #606982;
  --primary-color: #f3f7ff;
  --secondary-color: #dce6f9;
  --secondary-hover-color: #d2ddf2;
  --scrollbar-color: #a2aac2;
}
body {
  color: var(--text-color);
  background: var(--primary-color);
}
.container {
  overflow-y: auto;
  padding: 32px 0 60px;
  height: calc(100vh - 127px);
  scrollbar-color: var(--scrollbar-color) transparent;
}
.container :where(.app-header, .suggestions, .message, .prompt-wrapper) {
  position: relative;
  margin: 0 auto;
  width: 100%;
  padding: 0 20px;
  max-width: 990px;
}
.container .app-header {
  margin-top: 3vh;
}
.app-header .heading {
  width: fit-content;
  font-size: 3rem;
  background: linear-gradient(to right, #1d7efd, #8f6fff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.app-header .sub-heading {
  font-size: 2.6rem;
  margin-top: -5px;
  color: var(--subheading-color);
}
.container .suggestions {
  width: 100%;
  list-style: none;
  display: flex;
  gap: 15px;
  margin-top: 9.5vh;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
body.chats-active .container :where(.app-header, .suggestions) {
  display: none;
}
.suggestions .suggestions-item {
  cursor: pointer;
  padding: 18px;
  width: 228px;
  flex-shrink: 0;
  display: flex;
  scroll-snap-align: center;
  flex-direction: column;
  align-items: flex-end;
  border-radius: 12px;
  justify-content: space-between;
  background: var(--secondary-color);
  transition: 0.3s ease;
}
.suggestions .suggestions-item:hover {
  background: var(--secondary-hover-color);
}
.suggestions .suggestions-item .text {
  font-size: 1.1rem;
}
.suggestions .suggestions-item .icon {
  width: 45px;
  height: 45px;
  display: flex;
  font-size: 1.4rem;
  margin-top: 35px;
  align-self: flex-end;
  align-items: center;
  border-radius: 50%;
  justify-content: center;
  color: #1d7efd;
  background: var(--primary-color);
}
.suggestions .suggestions-item:nth-child(2) .icon {
  color: #28a745;
}
.suggestions .suggestions-item:nth-child(3) .icon {
  color: #ffc107;
}
.suggestions .suggestions-item:nth-child(4) .icon {
  color: #6f42c1;
}
.container .chats-container {
  display: flex;
  gap: 20px;
  flex-direction: column;
}
.chats-container .message {
  display: flex;
  gap: 11px;
  align-items: center;
}
.chats-container .message .avatar {
  width: 43px;
  height: 43px;
  flex-shrink: 0;
  align-self: flex-start;
  border-radius: 50%;
  padding: 6px;
  margin-right: -7px;
  background: var(--secondary-color);
  border: 1px solid var(--secondary-hover-color);
}
.chats-container .message.loading .avatar {
  animation: rotate 3s linear infinite;
}
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}
.chats-container .message .message-text {
  padding: 3px 16px;
  word-wrap: break-word;
  white-space: pre-line;
}
.chats-container .bot-message {
  margin: 9px auto;
}
.chats-container .user-message {
  flex-direction: column;
  align-items: flex-end;
}
.chats-container .user-message .message-text {
  padding: 12px 16px;
  max-width: 75%;
  background: var(--secondary-color);
  border-radius: 13px 13px 3px 13px;
}
.chats-container .user-message .img-attachment {
  margin-top: -7px;
  width: 50%;
  border-radius: 13px 3px 13px 13px;
}
.chats-container .user-message .file-attachment {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 10px;
  margin-top: -7px;
  border-radius: 13px 3px 13px 13px;
  background: var(--secondary-color);
}
.chats-container .user-message .file-attachment span {
  color: #1d7efd;
}
.container .prompt-container {
  position: fixed;
  width: 100%;
  left: 0;
  bottom: 0;
  padding: 16px 0;
  background: var(--primary-color);
}
.prompt-container :where(.prompt-wrapper, .prompt-form, .prompt-actions) {
  display: flex;
  gap: 12px;
  height: 56px;
  align-items: center;
}
.prompt-container .prompt-form {
  height: 100%;
  width: 100%;
  border-radius: 130px;
  background: var(--secondary-color);
}
.prompt-form .prompt-input {
  width: 100%;
  height: 100%;
  background: none;
  outline: none;
  border: none;
  font-size: 1rem;
  color: var(--text-color);
  padding-left: 24px;
}
.prompt-form .prompt-input::placeholder {
  color: var(--placeholder-color);
}
.prompt-wrapper button {
  width: 56px;
  height: 100%;
  flex-shrink: 0;
  cursor: pointer;
  border-radius: 50%;
  font-size: 1.4rem;
  border: none;
  color: var(--text-color);
  background: var(--secondary-color);
  transition: 0.3s ease;
}
.prompt-wrapper :is(button:hover, #cancel-file-btn, .file-icon) {
  background: var(--secondary-hover-color);
}
.prompt-form .prompt-actions {
  gap: 5px;
  margin-right: 7px;
}
.prompt-wrapper .prompt-form :where(.file-upload-wrapper, button, img) {
  position: relative;
  height: 45px;
  width: 45px;
}
.prompt-form .prompt-actions #send-prompt-btn {
  color: #fff;
  display: none;
  background: #1d7efd;
}
.prompt-form .prompt-input:valid~.prompt-actions #send-prompt-btn {
  display: block;
}
.prompt-form #send-prompt-btn:hover {
  background: #0264e3;
}
.prompt-form .file-upload-wrapper :where(button, img) {
  display: none;
  border-radius: 50%;
  object-fit: cover;
  position: absolute;
}
.prompt-form .file-upload-wrapper.active #add-file-btn {
  display: none;
}
.prompt-form .file-upload-wrapper #add-file-btn,
.prompt-form .file-upload-wrapper.active.img-attached img,
.prompt-form .file-upload-wrapper.active.file-attached .file-icon,
.prompt-form .file-upload-wrapper.active:hover #cancel-file-btn {
  display: block;
}
.prompt-form :is(#stop-response-btn:hover, #cancel-file-btn) {
  color: #d62939;
}
.prompt-wrapper .prompt-form .file-icon {
  color: #1d7efd;
}
.prompt-form #stop-response-btn,
body.bot-responding .prompt-form .file-upload-wrapper {
  display: none;
}
body.bot-responding .prompt-form #stop-response-btn {
  display: block;
}
.prompt-container .disclaimer-text {
  font-size: 0.9rem;
  text-align: center;
  padding: 16px 20px 0;
  color: var(--placeholder-color);
}
/* Responsive media query code for small screens */
@media (max-width: 768px) {
  .container {
    padding: 20px 0 100px;
  }
  .app-header :is(.heading, .sub-heading) {
    font-size: 2rem;
    line-height: 1.4;
  }
  .app-header .sub-heading {
    font-size: 1.7rem;
  }
  .container .chats-container {
    gap: 15px;
  }
  .chats-container .bot-message {
    margin: 4px auto;
  }
  .prompt-container :where(.prompt-wrapper, .prompt-form, .prompt-actions) {
    gap: 8px;
    height: 53px;
  }
  .prompt-container button {
    width: 53px;
  }
  .prompt-form :is(.file-upload-wrapper, button, img) {
    height: 42px;
    width: 42px;
  }
  .prompt-form .prompt-input {
    padding-left: 20px;
  }
  .prompt-form .file-upload-wrapper.active #cancel-file-btn {
    opacity: 0;
  }
  .prompt-wrapper.hide-controls :where(#theme-toggle-btn, #delete-chats-btn) {
    display: none;
  }
}

3. JavaScript Logic (script.js)

const container = document.querySelector(".container");
const chatsContainer = document.querySelector(".chats-container");
const promptForm = document.querySelector(".prompt-form");
const promptInput = promptForm.querySelector(".prompt-input");
const fileInput = promptForm.querySelector("#file-input");
const fileUploadWrapper = promptForm.querySelector(".file-upload-wrapper");
const themeToggleBtn = document.querySelector("#theme-toggle-btn");
// API Setup
const API_KEY = "PASTE-YOUR-API-KEY";
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`;
let controller, typingInterval;
const chatHistory = [];
const userData = { message: "", file: {} };
// Set initial theme from local storage
const isLightTheme = localStorage.getItem("themeColor") === "light_mode";
document.body.classList.toggle("light-theme", isLightTheme);
themeToggleBtn.textContent = isLightTheme ? "dark_mode" : "light_mode";
// Function to create message elements
const createMessageElement = (content, ...classes) => {
  const div = document.createElement("div");
  div.classList.add("message", ...classes);
  div.innerHTML = content;
  return div;
};
// Scroll to the bottom of the container
const scrollToBottom = () => container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
// Simulate typing effect for bot responses
const typingEffect = (text, textElement, botMsgDiv) => {
  textElement.textContent = "";
  const words = text.split(" ");
  let wordIndex = 0;
  // Set an interval to type each word
  typingInterval = setInterval(() => {
    if (wordIndex < words.length) {
      textElement.textContent += (wordIndex === 0 ? "" : " ") + words[wordIndex++];
      scrollToBottom();
    } else {
      clearInterval(typingInterval);
      botMsgDiv.classList.remove("loading");
      document.body.classList.remove("bot-responding");
    }
  }, 40); // 40 ms delay
};
// Make the API call and generate the bot's response
const generateResponse = async (botMsgDiv) => {
  const textElement = botMsgDiv.querySelector(".message-text");
  controller = new AbortController();
  // Add user message and file data to the chat history
  chatHistory.push({
    role: "user",
    parts: [{ text: userData.message }, ...(userData.file.data ? [{ inline_data: (({ fileName, isImage, ...rest }) => rest)(userData.file) }] : [])],
  });
  try {
    // Send the chat history to the API to get a response
    const response = await fetch(API_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ contents: chatHistory }),
      signal: controller.signal,
    });
    const data = await response.json();
    if (!response.ok) throw new Error(data.error.message);
    // Process the response text and display with typing effect
    const responseText = data.candidates[0].content.parts[0].text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
    typingEffect(responseText, textElement, botMsgDiv);
    chatHistory.push({ role: "model", parts: [{ text: responseText }] });
  } catch (error) {
    textElement.textContent = error.name === "AbortError" ? "Response generation stopped." : error.message;
    textElement.style.color = "#d62939";
    botMsgDiv.classList.remove("loading");
    document.body.classList.remove("bot-responding");
    scrollToBottom();
  } finally {
    userData.file = {};
  }
};
// Handle the form submission
const handleFormSubmit = (e) => {
  e.preventDefault();
  const userMessage = promptInput.value.trim();
  if (!userMessage || document.body.classList.contains("bot-responding")) return;
  userData.message = userMessage;
  promptInput.value = "";
  document.body.classList.add("chats-active", "bot-responding");
  fileUploadWrapper.classList.remove("file-attached", "img-attached", "active");
  // Generate user message HTML with optional file attachment
  const userMsgHTML = `
    <p class="message-text"></p>
    ${userData.file.data ? (userData.file.isImage ? `<img src="data:${userData.file.mime_type};base64,${userData.file.data}" class="img-attachment" />` : `<p class="file-attachment"><span class="material-symbols-rounded">description</span>${userData.file.fileName}</p>`) : ""}
  `;
  const userMsgDiv = createMessageElement(userMsgHTML, "user-message");
  userMsgDiv.querySelector(".message-text").textContent = userData.message;
  chatsContainer.appendChild(userMsgDiv);
  scrollToBottom();
  setTimeout(() => {
    // Generate bot message HTML and add in the chat container
    const botMsgHTML = `<img class="avatar" src="gemini.svg" /> <p class="message-text">Just a sec...</p>`;
    const botMsgDiv = createMessageElement(botMsgHTML, "bot-message", "loading");
    chatsContainer.appendChild(botMsgDiv);
    scrollToBottom();
    generateResponse(botMsgDiv);
  }, 600); // 600 ms delay
};
// Handle file input change (file upload)
fileInput.addEventListener("change", () => {
  const file = fileInput.files[0];
  if (!file) return;
  const isImage = file.type.startsWith("image/");
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = (e) => {
    fileInput.value = "";
    const base64String = e.target.result.split(",")[1];
    fileUploadWrapper.querySelector(".file-preview").src = e.target.result;
    fileUploadWrapper.classList.add("active", isImage ? "img-attached" : "file-attached");
    // Store file data in userData obj
    userData.file = { fileName: file.name, data: base64String, mime_type: file.type, isImage };
  };
});
// Cancel file upload
document.querySelector("#cancel-file-btn").addEventListener("click", () => {
  userData.file = {};
  fileUploadWrapper.classList.remove("file-attached", "img-attached", "active");
});
// Stop Bot Response
document.querySelector("#stop-response-btn").addEventListener("click", () => {
  controller?.abort();
  userData.file = {};
  clearInterval(typingInterval);
  chatsContainer.querySelector(".bot-message.loading").classList.remove("loading");
  document.body.classList.remove("bot-responding");
});
// Toggle dark/light theme
themeToggleBtn.addEventListener("click", () => {
  const isLightTheme = document.body.classList.toggle("light-theme");
  localStorage.setItem("themeColor", isLightTheme ? "light_mode" : "dark_mode");
  themeToggleBtn.textContent = isLightTheme ? "dark_mode" : "light_mode";
});
// Delete all chats
document.querySelector("#delete-chats-btn").addEventListener("click", () => {
  chatHistory.length = 0;
  chatsContainer.innerHTML = "";
  document.body.classList.remove("chats-active", "bot-responding");
});
// Handle suggestions click
document.querySelectorAll(".suggestions-item").forEach((suggestion) => {
  suggestion.addEventListener("click", () => {
    promptInput.value = suggestion.querySelector(".text").textContent;
    promptForm.dispatchEvent(new Event("submit"));
  });
});
// Show/hide controls for mobile on prompt input focus
document.addEventListener("click", ({ target }) => {
  const wrapper = document.querySelector(".prompt-wrapper");
  const shouldHide = target.classList.contains("prompt-input") || (wrapper.classList.contains("hide-controls") && (target.id === "add-file-btn" || target.id === "stop-response-btn"));
  wrapper.classList.toggle("hide-controls", shouldHide);
});
// Add event listeners for form submission and file input click
promptForm.addEventListener("submit", handleFormSubmit);
promptForm.querySelector("#add-file-btn").addEventListener("click", () => fileInput.click());

Configure Your Gemini API Key

Important: Your chatbot won’t generate responses until you provide your API key. Get your free key from Google AI Studio and paste it into the API_KEY variable in script.js.

Example key format: AIzaSyAtbnKGX15bTgmx0l_gQeatYvdWvY_wOTQ

Bonus Ideas for Future Enhancements

  • Add chat history storage with localStorage
  • Include a speech-to-text option
  • Enable copying responses to clipboard
  • Add typing animations for better UX
  • Integrate image generation if available from Gemini

Conclusion

Congratulations! You’ve now built a working Google Gemini AI chatbot using HTML, CSS, and JavaScript. This project is a great way to improve your development skills while learning how to integrate real-world AI APIs. Feel free to expand on this chatbot and enhance its features to fit your needs.

If you need help, download the source code, read the README.md for setup instructions, and explore the official Gemini documentation.

Next Post Previous Post

Cookies Consent

This website uses cookies to analyze traffic and offer you a better Browsing Experience. By using our website.

Learn More