Home View
++ This is the home content. Notice the URL has changed to include + #home. +
+diff --git a/README.md b/README.md index 156d528..62c6d61 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,42 @@ -[](https://github.com/attogram/base/actions/workflows/ci.yml) -[](https://github.com/attogram/base/releases) -[](https://github.com/attogram/base/stargazers) -[](https://github.com/attogram/base/watchers) -[](https://github.com/attogram/base/forks) -[](https://github.com/attogram/base/issues) -[](https://github.com/attogram/base/commits/main/) -[](./LICENSE) +# Getting Interactive with GitHub Pages -Welcome to [base](./docs/base.md). +[](https://github.com/attogram/static-magic/actions/workflows/ci.yml) +[](https://github.com/attogram/static-magic/stargazers) +[](https://github.com/attogram/static-magic/watchers) +[](https://github.com/attogram/static-magic/forks) +[](https://github.com/attogram/static-magic/issues) +[](./LICENSE) -Your starting point for new GitHub projects. +Frontend magic for GitHub Pages - zero backend, pure fun! -- [Documentation](./docs/README.md) +This website is a collection of interactive examples and games that you can run directly on GitHub Pages. No backend, no build tools, just plain HTML, CSS, and JavaScript. -- Repo: [https://github.com/attogram/base](https://github.com/attogram/base) +- **Website**: https://attogram.github.io/static-magic/ +- **Repo**: https://github.com/attogram/static-magic + +## Features + +This site demonstrates how to create rich interactive experiences on a static site, including: + +- Forms without servers +- Live JavaScript runner +- Theme switcher (light/dark mode) +- Persistent notes app +- Filterable lists and search +- Markdown to HTML converter +- JSON viewer/formatter +- Interactive diagrams with Mermaid.js +- And much more! + +## Games + +Check out the `/games` directory for a collection of simple browser games, including: + +- Clicker Game +- Tic Tac Toe +- Memory Match +- And more! + +## For Developers + +This project is a fork of [attogram/base](https://github.com/attogram/base) and serves as a living example of what you can do with GitHub Pages. Feel free to fork this repository and build your own interactive static sites! diff --git a/features/browser-apis.html b/features/browser-apis.html new file mode 100644 index 0000000..54fb8a5 --- /dev/null +++ b/features/browser-apis.html @@ -0,0 +1,230 @@ + + +
+ + +Frontend magic for GitHub Pages - zero backend, pure fun!
++ Modern browsers come packed with powerful JavaScript APIs that can + give your static site capabilities that once required a native + application. These often require user permission for security and + privacy reasons. +
+Here are a few examples of what you can do.
++ Get the user's geographical location. This requires user permission. +
+ + +const geoBtn = document.getElementById('geo-btn');
+const geoOutput = document.getElementById('geo-output');
+
+geoBtn.addEventListener('click', () => {
+ if ('geolocation' in navigator) {
+ navigator.geolocation.getCurrentPosition((position) => {
+ const lat = position.coords.latitude;
+ const lon = position.coords.longitude;
+ geoOutput.textContent = `Latitude: ${lat}, Longitude: ${lon}`;
+ }, (error) => {
+ geoOutput.textContent = `Error: ${error.message}`;
+ });
+ } else {
+ geoOutput.textContent = 'Geolocation is not available.';
+ }
+});
+ + Display desktop notifications to the user. This also requires + permission. +
+ + +const notifyBtn = document.getElementById('notify-btn');
+
+notifyBtn.addEventListener('click', () => {
+ if (!("Notification" in window)) {
+ alert("This browser does not support desktop notification");
+ } else if (Notification.permission === "granted") {
+ new Notification("Hi there!", { body: "This is a notification." });
+ } else if (Notification.permission !== "denied") {
+ Notification.requestPermission().then((permission) => {
+ if (permission === "granted") {
+ new Notification("Hi there!", { body: "This is a notification." });
+ }
+ });
+ }
+});
+ Convert text to speech right in the browser.
+ + + +const speakBtn = document.getElementById('speak-btn');
+const speechText = document.getElementById('speech-text');
+
+speakBtn.addEventListener('click', () => {
+ if ('speechSynthesis' in window) {
+ const utterance = new SpeechSynthesisUtterance(speechText.value);
+ window.speechSynthesis.speak(utterance);
+ } else {
+ alert('Speech synthesis is not available.');
+ }
+});
+ Frontend magic for GitHub Pages - zero backend, pure fun!
+
+ A "Copy to Clipboard" button is a small but incredibly useful
+ feature. The modern way to implement this is with the asynchronous
+ navigator.clipboard API. It's more secure and powerful
+ than the old document.execCommand('copy') method.
+
+ The API is promise-based, making it easy to work with and provide + feedback to the user upon success or failure. Note that this API + requires a secure context (like HTTPS), which is automatically + provided by GitHub Pages. +
++ The JavaScript is straightforward: get the text, call the API, and + handle the promise. +
+ +<textarea id="copy-text">Some text to copy.</textarea>
+<button id="copy-btn" class="btn">Copy Text</button>
+<div id="copy-feedback"></div>
+
+ const copyText = document.getElementById('copy-text');
+const copyBtn = document.getElementById('copy-btn');
+const copyFeedback = document.getElementById('copy-feedback');
+
+copyBtn.addEventListener('click', () => {
+ const textToCopy = copyText.value;
+
+ if (!navigator.clipboard) {
+ copyFeedback.textContent = 'Clipboard API not available.';
+ return;
+ }
+
+ navigator.clipboard.writeText(textToCopy).then(() => {
+ copyFeedback.textContent = 'Copied to clipboard!';
+ copyFeedback.style.color = 'green';
+
+ const originalText = copyBtn.textContent;
+ copyBtn.textContent = 'Copied!';
+
+ setTimeout(() => {
+ copyFeedback.textContent = '';
+ copyBtn.textContent = originalText;
+ }, 2000);
+ }).catch(err => {
+ copyFeedback.textContent = `Failed to copy: ${err}`;
+ copyFeedback.style.color = 'red';
+ });
+});
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ The HTML5 Drag and Drop API provides a native way to create user + interfaces where elements can be moved and reordered. It's a + powerful tool for building interactive applications like Kanban + boards or reorderable lists. +
+
+ The API is event-driven. Key events include
+ dragstart (when you start dragging),
+ dragover (when you're dragging over a valid drop
+ target), and drop (when you release the mouse).
+
Drag the items between the lanes.
++ The logic involves adding event listeners for the various + drag-and-drop events to both the draggable items and the drop + targets (lanes). +
+ +<div class="drop-lanes">
+ <div class="lane" id="lane-1">
+ <div class="draggable" draggable="true" id="item-1">Item 1</div>
+ </div>
+ <div class="lane" id="lane-2"></div>
+</div>
+
+ .draggable.dragging {
+ opacity: 0.5;
+}
+.lane.drag-over {
+ background-color: rgba(0, 123, 255, 0.1);
+ border-style: solid;
+}
+
+ const draggables = document.querySelectorAll('.draggable');
+const lanes = document.querySelectorAll('.lane');
+
+draggables.forEach(draggable => {
+ draggable.addEventListener('dragstart', (e) => {
+ draggable.classList.add('dragging');
+ e.dataTransfer.setData('text/plain', draggable.id);
+ });
+
+ draggable.addEventListener('dragend', () => {
+ draggable.classList.remove('dragging');
+ });
+});
+
+lanes.forEach(lane => {
+ lane.addEventListener('dragover', e => {
+ e.preventDefault(); // Necessary to allow dropping
+ lane.classList.add('drag-over');
+ });
+
+ lane.addEventListener('dragleave', () => {
+ lane.classList.remove('drag-over');
+ });
+
+ lane.addEventListener('drop', e => {
+ e.preventDefault();
+ const id = e.dataTransfer.getData('text/plain');
+ const draggable = document.getElementById(id);
+ if (draggable) {
+ lane.appendChild(draggable);
+ }
+ lane.classList.remove('drag-over');
+ });
+});
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ A fast, responsive search experience is crucial for good UX. For + many sites, you don't need a powerful server-side search engine. If + your dataset is reasonably small (like a list of products, articles, + or users), you can implement a near-instant search feature using + just JavaScript. +
++ The technique is simple: listen for keyboard input in a search box, + and then iterate through your list of items, hiding those that don't + match the search query. +
++ You need an input field and a list of items in your HTML. The + JavaScript will handle the filtering logic. +
+ +<input type="text" id="search-input" placeholder="Search for a fruit...">
+<ul id="item-list">
+ <li>Apple</li>
+ <li>Banana</li>
+ <li>Orange</li>
+ <li>Grape</li>
+ <li>Strawberry</li>
+ <li>Blueberry</li>
+ <li>Mango</li>
+ <li>Pineapple</li>
+</ul>
+
+ const searchInput = document.getElementById('search-input');
+const itemList = document.getElementById('item-list');
+const listItems = itemList.getElementsByTagName('li');
+
+searchInput.addEventListener('input', (event) => {
+ const query = event.target.value.toLowerCase().trim();
+
+ for (let i = 0; i < listItems.length; i++) {
+ const item = listItems[i];
+ const text = item.textContent.toLowerCase();
+
+ if (text.includes(query)) {
+ item.style.display = '';
+ } else {
+ item.style.display = 'none';
+ }
+ }
+});
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ One of the most common myths about static sites is that you can't + handle form submissions. While you can't have a traditional + server-side script to process the data, you can absolutely capture + user input and create interactive experiences entirely on the + client-side with JavaScript. +
+mailto: link.
+ + Here's the full code to make the demo above work. You can copy and + paste this into your own HTML file. +
+ +<form id="greeting-form">
+ <label for="name-input">Enter your name:</label>
+ <input type="text" id="name-input" required>
+ <button type="submit" class="btn">Say Hello</button>
+</form>
+<div id="greeting-output"></div>
+
+ const form = document.getElementById('greeting-form');
+const nameInput = document.getElementById('name-input');
+const outputDiv = document.getElementById('greeting-output');
+
+form.addEventListener('submit', (event) => {
+ // Prevent the form from submitting the traditional way
+ event.preventDefault();
+
+ // Get the value from the input field
+ const name = nameInput.value;
+
+ // Display the greeting
+ outputDiv.textContent = `Hello, ${name}!`;
+
+ // Optional: Clear the input field
+ nameInput.value = '';
+});
+ Frontend magic for GitHub Pages - zero backend, pure fun!
+
+ Working with JSON is a daily task for many developers. A simple
+ in-browser tool to quickly format and inspect JSON can be very
+ handy. This can be built easily using JavaScript's built-in
+ JSON object.
+
+ We'll use JSON.parse() to validate the input and
+ JSON.stringify() to pretty-print it. For a better
+ experience, we'll also add some basic syntax highlighting using
+ regular expressions.
+
+ The core logic is small, with the syntax highlighting being the most + complex part. +
+ +<textarea id="json-input" placeholder="Paste your JSON here..."></textarea>
+<button id="format-btn" class="btn">Format JSON</button>
+<pre id="json-output"></pre>
+
+ .json-key { color: #bb86fc; }
+.json-string { color: #03dac6; }
+.json-number { color: #cf6679; }
+.json-boolean { color: #ffab40; }
+.json-null { color: #b0bec5; }
+
+ const jsonInput = document.getElementById('json-input');
+const formatBtn = document.getElementById('format-btn');
+const jsonOutput = document.getElementById('json-output');
+
+function syntaxHighlight(json) {
+ json = json.replace(/&/g, '&').replace(/<\/g, '<').replace(/>/g, '>');
+ return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+ let cls = 'json-number';
+ if (/^"/.test(match)) {
+ if (/:$/.test(match)) {
+ cls = 'json-key';
+ } else {
+ cls = 'json-string';
+ }
+ } else if (/true|false/.test(match)) {
+ cls = 'json-boolean';
+ } else if (/null/.test(match)) {
+ cls = 'json-null';
+ }
+ return '<span class="' + cls + '">' + match + '</span>';
+ });
+}
+
+formatBtn.addEventListener('click', () => {
+ try {
+ const jsonObj = JSON.parse(jsonInput.value);
+ const prettyJson = JSON.stringify(jsonObj, null, 4);
+ jsonOutput.innerHTML = syntaxHighlight(prettyJson);
+ } catch (error) {
+ jsonOutput.innerHTML = `<span style="color: red;">Invalid JSON: ${error.message}</span>`;
+ }
+});
+
+// Set some default JSON and format it on page load
+jsonInput.value = '{\n "name": "Jules",\n "isAgent": true,\n "skills": ["HTML", "CSS", "JavaScript"],\n "experience": null\n}';
+formatBtn.click(); // Trigger a click to format the default JSON
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ You can build a simple JavaScript playground right in the browser. + This allows users to write and execute code on the fly, seeing the + results instantly. It's a powerful tool for learning and + experimentation. +
+
+ Security Note: Executing arbitrary code from users
+ can be risky. While we're using new Function() which is
+ safer than eval(), this approach should still be used
+ with caution, especially in a production environment with sensitive
+ user data. For a simple educational tool on a static site, the risk
+ is minimal.
+
+ The magic is in capturing the console.log messages and
+ executing the user's code in a controlled way.
+
<textarea id="js-code" placeholder="console.log('Hello, world!');"></textarea>
+<button id="run-btn" class="btn">Run</button>
+<h4>Output:</h4>
+<pre id="js-output"></pre>
+
+ const codeArea = document.getElementById('js-code');
+const runButton = document.getElementById('run-btn');
+const outputArea = document.getElementById('js-output');
+
+runButton.addEventListener('click', () => {
+ const userCode = codeArea.value;
+ let output = '';
+
+ // Temporarily override console.log to capture output
+ const oldLog = console.log;
+ console.log = (...args) => {
+ output += args.map(arg => JSON.stringify(arg, null, 2)).join(' ') + '\\n';
+ };
+
+ try {
+ // Use new Function() to execute the code
+ new Function(userCode)();
+ outputArea.style.color = 'var(--text-color)';
+ } catch (error) {
+ output = `Error: ${error.message}`;
+ outputArea.style.color = 'red';
+ } finally {
+ // Restore the original console.log
+ console.log = oldLog;
+ outputArea.textContent = output || '(No output)';
+ }
+});
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ Markdown is a popular way to write formatted text. With a + client-side JavaScript library, you can easily convert Markdown into + HTML in real-time, creating a live preview editor. +
++ For this demo, we're using a library called + Showdown.js, + included from a CDN (Content Delivery Network). This means we don't + need to host the library ourselves. +
++ You'll need to include the Showdown.js library from a CDN in your + HTML, then write a short script to tie it to your input and output + elements. +
+ +<!-- Include Showdown.js from a CDN -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
+
+<div class="converter-container">
+ <textarea id="markdown-input"></textarea>
+ <div id="html-output"></div>
+</div>
+
+ const markdownInput = document.getElementById('markdown-input');
+const htmlOutput = document.getElementById('html-output');
+
+// Create a new Showdown converter with some options
+const converter = new showdown.Converter({
+ tables: true,
+ strikethrough: true,
+ tasklists: true,
+ simpleLineBreaks: true
+});
+
+// Function to update the output
+const updateOutput = () => {
+ const markdownText = markdownInput.value;
+ const html = converter.makeHtml(markdownText);
+ htmlOutput.innerHTML = html;
+};
+
+// Listen for input events on the textarea
+markdownInput.addEventListener('input', updateOutput);
+
+// Set some default text and do an initial conversion
+markdownInput.value = "# Hello, Markdown!\\n\\nType some **Markdown** here.\\n\\n- List item 1\\n- List item 2";
+updateOutput();
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ Adding diagrams to your documentation or blog posts can make complex + ideas much easier to understand. + Mermaid.js + is a fantastic library that lets you create beautiful diagrams and + charts from simple, text-based syntax, similar to Markdown. +
++ By including the library from a CDN, you can render flowcharts, + sequence diagrams, Gantt charts, and more, right in the browser. +
++ Include the Mermaid.js script from a CDN, then use its API to render + your diagram definition. +
+ +<!-- Include Mermaid.js from a CDN -->
+<script src="https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.min.js"></script>
+
+<textarea id="mermaid-input"></textarea>
+<button id="render-btn" class="btn">Render Diagram</button>
+<div id="mermaid-output"></div>
+<div id="mermaid-error"></div>
+
+ // Initialize Mermaid.js
+mermaid.initialize({ startOnLoad: false });
+
+const mermaidInput = document.getElementById('mermaid-input');
+const renderBtn = document.getElementById('render-btn');
+const mermaidOutput = document.getElementById('mermaid-output');
+const errorDiv = document.getElementById('mermaid-error');
+
+const renderDiagram = async () => {
+ errorDiv.textContent = '';
+ mermaidOutput.innerHTML = 'Rendering...';
+ const definition = mermaidInput.value;
+
+ try {
+ // A unique ID is needed for each render
+ const uniqueId = `mermaid-graph-${Date.now()}`;
+ const { svg, bindFunctions } = await mermaid.render(uniqueId, definition);
+ mermaidOutput.innerHTML = svg;
+ if (bindFunctions) {
+ bindFunctions(mermaidOutput);
+ }
+ } catch (error) {
+ mermaidOutput.innerHTML = '';
+ errorDiv.textContent = error;
+ }
+};
+
+renderBtn.addEventListener('click', renderDiagram);
+
+// Default diagram
+mermaidInput.value = `graph TD
+ A[Start] --> B{Is it interactive?};
+ B -->|Yes| C[Awesome!];
+ B -->|No| D[Add some JS!];
+ C --> E[End];
+ D --> E[End];`;
+
+renderDiagram();
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
+
+ Want to save user data without a server? The browser's
+ localStorage API is the perfect tool for the job. It
+ allows you to save key-value pairs that persist even after the
+ browser window is closed.
+
+ This makes it ideal for creating simple applications like to-do + lists, settings panels, and, as we'll demonstrate here, a persistent + notes app. +
+
+ We need HTML for the interface and JavaScript to manage the notes.
+ The core logic involves reading from and writing to
+ localStorage.
+
<textarea id="note-input" placeholder="Write a new note..."></textarea>
+<button id="add-note-btn" class="btn">Add Note</button>
+<h3>Your Notes:</h3>
+<ul id="notes-list"></ul>
+
+ const noteInput = document.getElementById('note-input');
+const addNoteBtn = document.getElementById('add-note-btn');
+const notesList = document.getElementById('notes-list');
+
+// Load notes from localStorage
+let notes = JSON.parse(localStorage.getItem('notes')) || [];
+
+// Function to render notes
+const renderNotes = () => {
+ notesList.innerHTML = '';
+ // Render in reverse order to show newest first
+ notes.slice().reverse().forEach((noteText, index) => {
+ const li = document.createElement('li');
+ // This is the original index in the 'notes' array
+ const originalIndex = notes.length - 1 - index;
+
+ // Sanitize note text before displaying to prevent HTML injection
+ const sanitizedText = noteText.replace(/&/g, '&').replace(/<\/g, '<').replace(/>/g, '>');
+
+ li.innerHTML = `
+ <span>${sanitizedText}</span>
+ <button class="delete-btn" data-index="${originalIndex}">Delete</button>
+ `;
+ notesList.appendChild(li);
+ });
+};
+
+// Function to save notes
+const saveNotes = () => {
+ localStorage.setItem('notes', JSON.stringify(notes));
+};
+
+// Add note event
+addNoteBtn.addEventListener('click', () => {
+ const newNote = noteInput.value.trim();
+ if (newNote) {
+ notes.push(newNote);
+ saveNotes();
+ renderNotes();
+ noteInput.value = '';
+ }
+});
+
+// Delete note event (using event delegation)
+notesList.addEventListener('click', (event) => {
+ if (event.target.classList.contains('delete-btn')) {
+ const index = parseInt(event.target.getAttribute('data-index'), 10);
+ notes.splice(index, 1);
+ saveNotes();
+ renderNotes();
+ }
+});
+
+// Initial render
+renderNotes();
+ Frontend magic for GitHub Pages - zero backend, pure fun!
++ A great user experience often means not forgetting what the user was + doing. Using the browser's storage APIs, you can easily save the + state of UI elements, so that if the user reloads the page or comes + back later, their settings and input are preserved. +
+localStorage: Stores data with no expiration date. It will persist until the
+ user manually clears their browser cache.
+ sessionStorage: Stores data for one session only. The data is lost when the
+ browser tab is closed.
+
+ For this demo, we'll use localStorage to make the state
+ persistent across visits.
+
+ Change the values in the form below, then reload the page. Your + changes will be remembered. +
+ + +
+ The logic involves listening for changes on the form inputs and
+ saving the values to localStorage. On page load, we
+ check for saved values and apply them.
+
<form id="state-form">
+ <label for="username">Username:</label>
+ <input type="text" id="username">
+
+ <label for="volume">Volume: <span id="volume-value">50</span></label>
+ <input type="range" id="volume" min="0" max="100">
+
+ <label>
+ <input type="checkbox" id="notifications">
+ Enable notifications
+ </label>
+</form>
+<button id="clear-state-btn" class="btn">Clear Saved State</button>
+
+ const form = document.getElementById('state-form');
+const usernameInput = document.getElementById('username');
+const volumeSlider = document.getElementById('volume');
+const volumeValue = document.getElementById('volume-value');
+const notificationsCheckbox = document.getElementById('notifications');
+const clearBtn = document.getElementById('clear-state-btn');
+
+const storageKey = 'formState';
+
+// Function to save state
+function saveState() {
+ const state = {
+ username: usernameInput.value,
+ volume: volumeSlider.value,
+ notifications: notificationsCheckbox.checked
+ };
+ localStorage.setItem(storageKey, JSON.stringify(state));
+}
+
+// Function to load state
+function loadState() {
+ const savedState = JSON.parse(localStorage.getItem(storageKey));
+ if (savedState) {
+ usernameInput.value = savedState.username || '';
+ volumeSlider.value = savedState.volume || 50;
+ notificationsCheckbox.checked = savedState.notifications || false;
+ }
+ volumeValue.textContent = volumeSlider.value;
+}
+
+// Event Listeners
+form.addEventListener('input', (e) => {
+ if(e.target.id === 'volume') {
+ volumeValue.textContent = e.target.value;
+ }
+ saveState();
+});
+
+clearBtn.addEventListener('click', () => {
+ localStorage.removeItem(storageKey);
+ window.location.reload();
+});
+
+// Load the state when the page loads
+loadState();
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
+
+ You can simulate the navigation of a Single-Page Application (SPA)
+ without a complex framework. The secret is using the URL's "hash"
+ (the part after the #). By listening for changes to the
+ hash, you can show and hide content dynamically, creating a fast,
+ app-like experience.
+
+ This technique is perfect for simple SPAs, tabbed interfaces, or + deep-linking to specific sections of a page. The browser's history + is even updated, so the back and forward buttons work as expected. +
++ This is the home content. Notice the URL has changed to include + #home. +
+This is the about page content. The URL now ends in #about.
+This is the contact section. The URL hash is #contact.
+
+ The JavaScript listens for the hashchange event on the
+ window and a DOMContentLoaded event to handle the
+ initial page load.
+
<nav class="spa-nav">
+ <a href="#home">Home</a>
+ <a href="#about">About</a>
+ <a href="#contact">Contact</a>
+</nav>
+
+<div id="router-output">
+ <div id="home" class="page-content">...</div>
+ <div id="about" class="page-content">...</div>
+ <div id="contact" class="page-content">...</div>
+</div>
+
+ document.addEventListener('DOMContentLoaded', () => {
+ const contentPages = document.querySelectorAll('.page-content');
+ const spaNavLinks = document.querySelectorAll('.spa-nav a');
+ const defaultRoute = 'home';
+
+ function handleRouteChange() {
+ const route = window.location.hash.substring(1) || defaultRoute;
+
+ contentPages.forEach(page => {
+ page.classList.toggle('active', page.id === route);
+ });
+
+ spaNavLinks.forEach(link => {
+ link.classList.toggle('active', link.hash === `#${route}`);
+ });
+ }
+
+ window.addEventListener('hashchange', handleRouteChange);
+
+ // Set initial hash if none is present and handle initial load
+ if (!window.location.hash) {
+ window.location.hash = defaultRoute;
+ } else {
+ handleRouteChange();
+ }
+});
+
+ Frontend magic for GitHub Pages - zero backend, pure fun!
+
+ A theme switcher is a fantastic feature that respects user
+ preferences and improves accessibility. It's surprisingly easy to
+ implement on a static site using CSS variables and a little bit of
+ JavaScript to handle localStorage.
+
+ The live demo is right here on this page! Look for the "Dark Mode" / + "Light Mode" button, likely in the top-right corner. Click it to + toggle the theme. Your preference will be saved and applied + automatically on your next visit. +
++ The implementation involves three parts: CSS variables for the + themes, a JavaScript function to toggle the theme and save the + preference, and a button in your HTML. +
+ +
+ In your main stylesheet (style.css), define two sets of
+ color variables. One for the default (light) theme, and one for the
+ dark theme, triggered by a class on the body element.
+
/* Light Theme (Default) */
+:root {
+ --bg-color: #ffffff;
+ --text-color: #333333;
+ --header-bg: #f8f9fa;
+ /* ... other light theme variables */
+}
+
+/* Dark Theme */
+body.dark-mode {
+ --bg-color: #121212;
+ --text-color: #e0e0e0;
+ --header-bg: #1f1f1f;
+ /* ... other dark theme variables */
+}
+
+body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ transition: background-color 0.3s, color 0.3s;
+}
+
+ + Place a button in your HTML file. This button will be used to + trigger the theme change. +
+<button id="theme-switcher" class="btn">Dark Mode</button>
+
+
+ This script checks for a saved theme in localStorage,
+ applies it on page load, and adds an event listener to the button to
+ toggle the theme.
+
document.addEventListener('DOMContentLoaded', () => {
+ const themeSwitcher = document.getElementById('theme-switcher');
+ const currentTheme = localStorage.getItem('theme');
+
+ // Apply saved theme or OS preference
+ if (currentTheme) {
+ document.body.classList.toggle('dark-mode', currentTheme === 'dark');
+ themeSwitcher.textContent = currentTheme === 'dark' ? 'Light Mode' : 'Dark Mode';
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ document.body.classList.add('dark-mode');
+ themeSwitcher.textContent = 'Light Mode';
+ } else {
+ themeSwitcher.textContent = 'Dark Mode';
+ }
+
+ // Theme switcher button event listener
+ themeSwitcher.addEventListener('click', () => {
+ document.body.classList.toggle('dark-mode');
+ let theme = 'light';
+ if (document.body.classList.contains('dark-mode')) {
+ theme = 'dark';
+ }
+ localStorage.setItem('theme', theme);
+ themeSwitcher.textContent = theme === 'dark' ? 'Light Mode' : 'Dark Mode';
+ });
+});
+ Score: 0
+ +Points per click: 1
+
+ This is a simple clicker game. Click the button to increase your
+ score. You can spend your score to buy upgrades, which increase the
+ number of points you get per click. Your score and upgrade level are
+ saved in your browser's localStorage, so you can close
+ the page and come back to it later!
+
<!-- HTML -->
+<div class="game-container">
+ <p id="score">Score: 0</p>
+ <button id="clicker-btn" class="btn">Click Me!</button>
+ <div>
+ <button id="upgrade-btn" class="btn">Upgrade Click (Cost: 10)</button>
+ <p>Points per click: <span id="click-power">1</span></p>
+ </div>
+</div>
+
+<!-- JavaScript -->
+const scoreDisplay = document.getElementById('score');
+const clickerBtn = document.getElementById('clicker-btn');
+const upgradeBtn = document.getElementById('upgrade-btn');
+const clickPowerDisplay = document.getElementById('click-power');
+
+let score = 0;
+let clickPower = 1;
+let upgradeCost = 10;
+
+function loadGame() {
+ const savedGame = JSON.parse(localStorage.getItem('clickerGame'));
+ if (savedGame) {
+ score = parseInt(savedGame.score, 10) || 0;
+ clickPower = parseInt(savedGame.clickPower, 10) || 1;
+ upgradeCost = parseInt(savedGame.upgradeCost, 10) || 10;
+ }
+ updateDisplay();
+}
+
+function saveGame() {
+ localStorage.setItem('clickerGame', JSON.stringify({ score, clickPower, upgradeCost }));
+}
+
+function updateDisplay() {
+ scoreDisplay.textContent = `Score: ${score}`;
+ clickPowerDisplay.textContent = clickPower;
+ upgradeBtn.textContent = `Upgrade Click (Cost: ${upgradeCost})`;
+ upgradeBtn.disabled = score < upgradeCost;
+}
+
+clickerBtn.addEventListener('click', () => {
+ score += clickPower;
+ updateDisplay();
+ saveGame();
+});
+
+upgradeBtn.addEventListener('click', () => {
+ if (score >= upgradeCost) {
+ score -= upgradeCost;
+ clickPower++;
+ upgradeCost = Math.ceil(upgradeCost * 1.5);
+ updateDisplay();
+ saveGame();
+ }
+});
+
+loadGame();
+
+ + An image is divided into a 3x3 grid. The pieces are created as divs + with the `background-image` property set to the source image and the + `background-position` adjusted to show the correct part of the + image. The pieces are then shuffled and placed in a "pool". The user + must drag each piece from the pool and drop it into the correct slot + on the puzzle board. The HTML5 Drag and Drop API is used to manage + moving the pieces. +
+Frontend magic for GitHub Pages - zero backend, pure fun!
++ This section showcases simple browser games built with just HTML, + CSS, and vanilla JavaScript. Each game is self-contained and + includes an explanation of how it works and its full source code. +
+A simple clicker game with persistent scoring.
+ + + +The classic game of Tic Tac Toe for two players.
+ + + +A card matching memory game.
+ + + +Guess a number between 1 and 100.
+ + + +Play against the computer with simple animations.
+ + + +Navigate a simple grid-based maze with the keyboard.
+ + + +Test your typing speed in words per minute.
+ + + +A simple jigsaw-style puzzle.
+ + + +A memory game of repeating color patterns.
+ + + +A simplified version of the popular word guessing game.
+ +Use the arrow keys to navigate from blue to green.
+ + ++ A simple maze is generated using a 2D array where 'W' represents a + wall and 'P' represents a path. This array is rendered as a grid + using CSS. The player's position is tracked with coordinates. An + event listener for the 'keydown' event checks for arrow key presses. + When a key is pressed, the game logic checks if the target cell is a + wall. If not, the player's position is updated, and the grid is + re-rendered. +
+<!-- HTML -->
+<div id="maze-container"></div>
+<button id="restart-btn" class="btn">Reset Game</button>
+
+ Moves: 0
+ + ++ This is a classic card matching memory game. A deck of paired cards + is shuffled and laid out face down. The player flips two cards at a + time. If they match, they remain face up. If not, they are flipped + back over. The goal is to find all the pairs. +
+<!-- HTML -->
+<p id="game-status">Moves: 0</p>
+<div id="memory-game-board"></div>
+<button id="restart-btn" class="btn">New Game</button>
+
+<!-- CSS for card flip -->
+#memory-game-board { perspective: 1000px; }
+.card {
+ transform-style: preserve-3d;
+ transition: transform 0.6s;
+}
+.card.flipped, .card.matched { transform: rotateY(180deg); }
+.card-face {
+ position: absolute;
+ width: 100%; height: 100%;
+ backface-visibility: hidden;
+}
+.card-back { transform: rotateY(180deg); }
+
+<!-- JavaScript -->
+const gameBoard = document.getElementById('memory-game-board');
+// ... game logic ...
+
+ I'm thinking of a number between 1 and 100.
+ + + +
+ The computer generates a random number between 1 and 100 using
+ Math.random(). The player submits their guess via a
+ form. The game then compares the guess to the secret number and
+ provides feedback ('Too high!', 'Too low!', or 'You got it!'). The
+ input form is disabled once the correct number is guessed, and a
+ 'Play Again' button appears.
+
<!-- HTML -->
+<p>I'm thinking of a number between 1 and 100.</p>
+<form id="guess-form">
+ <input type="number" id="guess-input" min="1" max="100" required>
+ <button type="submit" id="guess-submit" class="btn">Guess</button>
+</form>
+<p id="feedback"></p>
+<button id="restart-btn" class="btn" style="display: none;">Play Again</button>
+
+<!-- JavaScript -->
+const guessInput = document.getElementById('guess-input');
+// ... game logic ...
+
+ Choose your weapon!
+Player: 0 - Computer: 0
++ The player clicks one of the three buttons. The computer's choice is + generated randomly from the three options. A function then compares + the two choices to determine a winner based on the classic rules: + Rock beats Scissors, Scissors beats Paper, and Paper beats Rock. The + scores are then updated. +
+<!-- HTML -->
+<div class="choices">
+ <button class="btn" data-choice="Rock">✊</button>
+ <button class="btn" data-choice="Paper">✋</button>
+ <button class="btn" data-choice="Scissors">✌️</button>
+</div>
+<div id="result"></div>
+<p id="score">Player: 0 - Computer: 0</p>
+
+<!-- JavaScript -->
+const choiceButtons = document.querySelectorAll('.choices button');
+// ... game logic ...
+
+ Press "Start" to play.
++ This is a memory game. The computer will light up a sequence of + colored pads. You must repeat the sequence in the same order. Each + round, the sequence gets one step longer. The game ends if you make + a mistake. +
++ This is a classic Tic Tac Toe game. The game state is managed in a + JavaScript array. After each move, the game checks for a win by + comparing the board state against a set of winning combinations. The + game ends when a player wins or all cells are filled, resulting in a + draw. +
+<!-- HTML -->
+<div id="game-status">Player X's turn</div>
+<div id="tic-tac-toe-board">
+ <div class="cell" data-index="0"></div>
+ ... (9 cells) ...
+</div>
+<button id="restart-btn" class="btn">New Game</button>
+
+<!-- JavaScript -->
+const statusDisplay = document.getElementById('game-status');
+const restartBtn = document.getElementById('restart-btn');
+const cells = document.querySelectorAll('.cell');
+
+let currentPlayer = 'X';
+let gameState = ["", "", "", "", "", "", "", "", ""];
+let gameActive = true;
+
+const winningConditions = [
+ [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
+ [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
+ [0, 4, 8], [2, 4, 6] // Diagonals
+];
+
+function handleCellClick(e) {
+ const clickedCell = e.target;
+ const clickedCellIndex = parseInt(clickedCell.getAttribute('data-index'));
+
+ if (gameState[clickedCellIndex] !== "" || !gameActive) return;
+
+ gameState[clickedCellIndex] = currentPlayer;
+ clickedCell.textContent = currentPlayer;
+ handleResultValidation();
+}
+
+function handleResultValidation() {
+ let roundWon = false;
+ for (let i = 0; i < winningConditions.length; i++) {
+ const winCondition = winningConditions[i];
+ let a = gameState[winCondition[0]];
+ let b = gameState[winCondition[1]];
+ let c = gameState[winCondition[2]];
+ if (a === '' || b === '' || c === '') continue;
+ if (a === b && b === c) {
+ roundWon = true;
+ break;
+ }
+ }
+
+ if (roundWon) {
+ statusDisplay.textContent = `Player ${currentPlayer} has won!`;
+ gameActive = false;
+ return;
+ }
+
+ if (!gameState.includes("")) {
+ statusDisplay.textContent = "Game ended in a draw!";
+ gameActive = false;
+ return;
+ }
+
+ currentPlayer = currentPlayer === "X" ? "O" : "X";
+ statusDisplay.textContent = `Player ${currentPlayer}'s turn`;
+}
+
+function handleRestartGame() {
+ gameActive = true;
+ currentPlayer = "X";
+ gameState = ["", "", "", "", "", "", "", "", ""];
+ statusDisplay.textContent = `Player ${currentPlayer}'s turn`;
+ cells.forEach(cell => cell.textContent = "");
+}
+
+cells.forEach(cell => cell.addEventListener('click', handleCellClick));
+restartBtn.addEventListener('click', handleRestartGame);
+
+ Type the text below as quickly and accurately as you can.
+ + + + ++ A random snippet of text is displayed. When the user starts typing + in the text area, a timer begins. On each input, the typed text is + compared to the sample text. When the typed text's length matches + the sample text's length, the test ends. The Words Per Minute (WPM) + is calculated based on the number of correctly typed characters and + the elapsed time. (WPM is conventionally calculated as + `(character_count / 5) / time_in_minutes`). +
+Guess the 5-letter word in 6 tries.
+ + + ++ A 5-letter word is chosen at random. The player types their guess. + When a 5-letter guess is submitted (by pressing Enter), the letters + are colored based on their status: Green for correct letter in the + correct position, Yellow for correct letter in the wrong position, + and Gray for a letter not in the word at all. The game is managed by + listening for `keydown` events. +
++ Start with a solid HTML foundation that works without CSS or + JavaScript. Your content should be readable and your links should + function. Then, layer on CSS for styling and JavaScript for + interactivity. This ensures your site is usable by everyone, + regardless of their browser capabilities or network speed. +
++ An interactive site is only useful if everyone can interact with it. + Keep accessibility in mind: +
+<nav>, <main>,
+ <button>, and
+ <input> correctly. They provide built-in
+ accessibility features.
+ + Client-side interactivity means the user's device is doing all the + work. Keep it fast: +
+async or defer attributes on your
+ <script> tags to prevent them from blocking
+ page rendering.
+ Even without a server, there are security considerations:
+element.innerHTML = userInput, use
+ element.textContent = userInput if you don't need to
+ render HTML.
+ + Markdown is a lightweight markup language with plain-text-formatting + syntax. Its main goal is to be as readable as possible, even in its + raw form. It's a simple way to write and format articles, + documentation, and messages. +
+# This is a heading
+
+This is a paragraph with some **bold** and *italic* text.
+
+- This is a list item.
+- So is this.
+
+ As you've seen on our
+ Markdown Converter
+ page, you can use a JavaScript library to convert Markdown to HTML
+ directly in the browser. This means you can write your content in
+ simple .md files and then use a script to fetch and
+ render them on your site, creating a very simple Content Management
+ System (CMS) without a backend.
+
Frontend magic for GitHub Pages - zero backend, pure fun!
++ This website is dedicated to showcasing the power of static + websites, particularly those hosted on GitHub Pages. Many developers + believe that a "static" site means a non-interactive, boring page. + We're here to prove that wrong! +
++ With just HTML, CSS, and vanilla JavaScript, you can create rich, + interactive, and useful web applications without any backend servers + or complex build tools. This site is a living demonstration of that + philosophy. +
++ We have a collection of pages, each dedicated to a specific + interactive feature. For each feature, you'll find: +
++ Explore the navigation menu to see demos of serverless forms, a live + JavaScript editor, a persistent notes app, client-side search, and + much more. +
+Frontend magic for GitHub Pages - zero backend, pure fun!
++ Many simple developer utilities can be built to run entirely in the + browser. This is fast, secure, and works offline. Here are a few + examples of common tools recreated with vanilla JavaScript. +
+Characters: 0, Words: 0, Lines: 0
+