In this tutorial, you’ll build a fun and interactive guessing game using Svelte 5, the latest version of the lightweight JavaScript framework designed for creating fast and reactive web applications. This game will challenge users to guess a randomly generated number within a certain range, offering real-time feedback on whether their guesses are too high or too low. By leveraging Svelte’s powerful reactivity and efficient state management, you’ll create a smooth user experience with minimal code, making it an ideal project to explore Svelte 5’s new features and capabilities.
Svelte 5 has introduced runes, the equivalent of signals in other frameworks, including Solid, Vue, and Angular. Signals notify functions when updated, making the experience real-time by making the system more dynamic. This can reduce the complexity of your app, allow the focus to be more on the components, and avoid such opinionated strict syntax.
Svelte 5 has now officially been released. I have ported my vanilla JavaScript Guessing game to use Svelte 5 for a learning experience.
Prerequisites
An active node installation. If you run MacOS I recommend using NVM which can be installed using homebrew. The guessing game uses TypeScript, Daisy UI and Tailwind. All installation steps are detailed below.
Download
The latest Svelte 5 Guessing Game has been released on 1st November 2024.
Also available on GitHub.
Demo
You can find a demo on my GitHub page.
Installation & Svelte 5 Code
Step 1 – Install Svelte 5
Install the latest version of Svelte and run through the installation setup.
npm create svelte@latest
For this tutorial, I haven’t enabled Vitest or Playwright.
Step 2 – Install Tailwind
Install Tailwind and create a tailwind config file.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Add the following to the tailwind.config.js file.
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}
Step 3 – Setup CSS File
Create a CSS file called src/app.css and add the following to it.
@tailwind base;
@tailwind components;
@tailwind utilities;
Now we have a CSS file we need to load it in a layout file. Create a +layout.svelte file inside the routes folder and add the following.
<script>
import "../app.css";
</script>
<slot />
Step 4 – Install Daisy UI
Now let’s add Daisy UI to our Tailwind installation.
pnpm add -D daisyui@latest
Include Daisy in the tailwind config file. Your config file should look like the following.
import daisyui from 'daisyui';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [daisyui]
};
I have included 2 themes in my build. If you want to choose your own, visit Daisy UI themes.
Your tailwind config file now looks like this.
import daisyui from 'daisyui';
export default {
content: ['./src/**/*.{svelte,js,ts}'],
plugins: [daisyui],
daisyui: {
themes: ['light', 'sunset']
}
};
In src/app.html add your default theme to your main file.
<!doctype html>
<!-- The following line needs changing -->
<html data-theme="sunset" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
Step 5 – Layout Page
Add the following code to the src/routes/+layout.svelte file to create the initial layout.
<script lang="ts">
import '../app.css';
</script>
<div class="hero bg-base-200 min-h-[95vh]">
<div class="hero-content">
<div class="w-screen">
<div class="mb-6 text-5xl font-extrabold font-mono">
<h1 class="bg-gradient-to-r from-cyan-500 to-blue-500 bg-clip-text
text-transparent">
Svelte 5 Guessing Game
</h1>
</div>
<slot />
</div>
</div>
</div>
<footer class="w-screen bg-cover bg-base-200 min-h-[5vh]">
<p class="text-center">Made by Tracy Ridge</p>
</footer>
Step 6 – Edit Main Page
Open src/routes/+page.svelte and remove any code. From here we will go through the main code.
I used the JoyOfCode code to connect and sync to the browser’s local storage to store the user-selected level.
Inside the src/lib folder create a new file called localStore.svelte.ts and add the following code.
/*With thanks to JoyOfCode*/
import { browser } from '$app/environment';
export class LocalStore<T> {
value = $state<T>() as T;
key = '';
constructor(key: string, value: T) {
this.key = key;
this.value = value;
if (browser) {
const item = localStorage.getItem(key);
if (item) this.value = this.deserialize(item);
}
$effect(() => {
localStorage.setItem(this.key, this.serialize(this.value));
});
}
serialize(value: T): string {
return JSON.stringify(value);
}
deserialize(item: string): T {
return JSON.parse(item);
}
}
export function localStore<T>(key: string, value: T) {
return new LocalStore(key, value);
}
Step 7 – Import and Initialise Variables
Import localStore.ts.svelte into your +page.svelte and initialise the variables.
import { localStore } from '$lib/localStore.svelte';
Now we will initialise all of our variables using the new Svelte 5 $state variable.
// Syncs the current level with browser local storage
let currentLevel = localStore('currentLevel', 5);
// Adds a modal when the game ends
let modal: boolean = $state(false);
// To focus on the input
let ref: HTMLElement;
// The user's guess
let guess: number | string = $state('');
// Generates the random number
let randomNumber: number = $state.raw(Math.floor(Math.random() * (1 - 100) + 100));
// The initial message to display
let message: string = $state('Pick a number');
// Keeps track of the user guesses
let guesses: number = $state(0);
// Array to hold previous guesses
let badges: number[] = $state([]);
Step 8 – Input & Message HTML
Whilst in +page.svelte add the code for the input and the message
<!-- Message -->
<div class="my-6">
<h2 class="text-4xl font-bold">{message}</h2>
</div>
<!-- User input -->
<div class="my-6">
<div class="join w-full">
<input
type="number"
min="1"
max="100"
maxlength="3"
bind:this={ref}
bind:value={guess}
placeholder="Enter number"
class="input input-bordered join-item w-5/6"
onkeydown={handleKeydown}
/>
<button onclick={checkNumber} class="btn btn-primary join-item rounded-r-full">Submit</button>
</div>
</div>
<!-- Informational message -->
<p class="my-2">Hit <kbd class="kbd kbd-md">Enter</kbd> to submit.</p>
Step 9 – Input Functions
Let’s focus on the functions handleKeyDown()
and checkNumber()
//Fires when the user presses enter
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
checkNumber();
}
};
Most of the work is done during the checkNumber()
function. This is fired after pressing the submit button or hitting the enter key. See comments inside the code for details.
const checkNumber = () => {
guess = Number(guess);
ref.focus();
//Checks to see if the number is within the boundaries
if (guess < 1 || guess > 100) {
message = 'Please enter a number between 1 and 100';
return;
}
//Checks to see if the guess has already been checked
if (badges.includes(guess)) {
message = 'You have already guessed that number';
return;
}
//Increment the guesses
guesses++;
/*If you go over your guesses limit your game will come to an end.*/
if (guesses >= currentLevel.value) {
message =
'You have reached the maximum number of guesses. The number was ' + randomNumber;
modal = true;
return;
}
//If you are within your limit display a badge
badges.push(guess);
if (guess !== randomNumber) {
if (guess > randomNumber) {
message = 'You need to go lower 👇';
} else {
message = 'You need to go higher 👆';
}
} else {
message = `Super, you guessed right. You guessed it in ${guesses} tries.`;
modal = true;
}
//reset guess
guess = '';
};
Step 10 – Game Over Code
Once the game is over the modal will display so you can restart the game.
const restartGame = () => {
randomNumber = Math.floor(Math.random() * (1 - 100) + 100);
guesses = 0;
badges = [];
message = 'Pick a number';
guess = '';
modal = false;
ref.focus();
};
Step 11 – Displaying Badges
Displaying the badges goes underneath the informational message (Step 8).
<!--Displays the badges -->
<div class="my-6">
{#if badges.length > 0}
<h2 class="text-xl">Guesses</h2>
{#each badges as badge}
<div class="badge badge-secondary mx-2">{badge}</div>
{/each}
{/if}
</div>
Step 12 – Game Over Modal HTML
Creating the model when the game is finished
{#if modal}
<dialog class="modal {modal ? 'modal-open' : ''}">
<div class="modal-box">
<h3 class="text-lg font-bold">Game Over</h3>
<p class="py-4">{message}</p>
<div class="modal-action">
<form method="dialog">
<button onclick={restartGame} class="btn btn-info">Play Again</button>
</form>
</div>
</div>
</dialog>
{/if}
Step 13 – Focus Input
To focus on the input when it loads we use the new $effect rune which has replaced onMount. This is placed at the bottom of the closing script tag.
$effect(() => {
ref.focus();
});
Step 14 – Level Select
Now we focus on the level select feature. Create a ButtonGroup.svelte file inside of the lib/components.
<script lang="ts">
type Props = {
levelSwitch: (val: number) => void;
currentLevel: number;
};
//Sets the values and name for the buttons
const buttons = [
{ level: 'Easy', val: 10 },
{ level: 'Medium', val: 5 },
{ level: 'Hard', val: 2 }
];
//These are passed from the root
let { currentLevel, levelSwitch }: Props = $props();
</script>
<!-- Displays the buttons -->
{#each buttons as b}
<button
onclick={() => levelSwitch(b.val)}
class="btn mr-4 btn-sm {currentLevel === b.val ? 'btn-secondary' : 'btn-accent'}"
>
{b.level}
</button>
{/each}
Step 15 – Once the Button is Fired
There are three buttons and once a button is pressed the value of that button is passed to the levelSwitch()
function in +page.svelte. The button colour changes to an active state as a result.
const levelSwitch = (value: number) => {
if (guesses >= value) {
message = 'You will lose, please switch to a different level';
return;
} else {
currentLevel.value = value;
}
};
If the number of guesses is greater than or equal to the value of the button a message will be displayed otherwise the status of the currentLevel in local storage changes.
Step 16 – Import Button Group
Finally, add the ButtonGroup component inside the HTML. I have placed it below the closing script tag.
<ButtonGroup currentLevel={currentLevel.value} {levelSwitch} />
Conclusion
If you compare the code to the vanilla JavaScript guessing game you can see there isn’t as much code. This is because front-end frameworks do a lot of the heavy lifting. I hope you have liked this tutorial. What would you do differently if you were to create your version?
Discover more from WorldOWeb
Subscribe to get the latest posts sent to your email.