Converting my HTML, CSS and JavaScript site to use Svelte 5 with TypeScript and SvelteKit

Runes used in this project:

  • $props()
  • $state()
  • $effect()
  • $derived()

Grim Manor

[grim manor landing page]

Don't be scared—the original Grim Manor project is from a Halloween Hackathon I did with Samuel Anderson, Armando Urquiola Cabrera, Emma Lamont, and Jorgen Lovbakke. You are a stranger who wandered into an empty old mansion to escape the storm. On arriving, you're greeted by a pale young man, whom you deduce is a ghost. You must solve a few challenges to help him discover why he is stuck there.

Repos and deploy links

Repo - Original Grim Manor

Deployed site - Original Grim Manor

Repo - Svelte 5 version of Grim Manor

Deployed site - Svelte 5 version of Grim Manor

Objective

This is the first project that I have converted to TypeScript and I wanted to see if there were any differences with the TypeScript in Svelte 5. The good news is there's actually not that much difference at all. Many of the Svelte 5 Runes just use standard typing. For example, let count: number = 0 would be let count: number = $state(0) using the $state() Rune. I'll discuss the other instances later.

Setup

Like in the other two projects mentioned in Converting my static site to use SvelteKit and Svelte 5 and Migrating HTML, CSS and JS site to Svelte 5 and SvelteKit, I needed to change the layout of the app.

The way we had set up the app was to break each minigame into its own JavaScript file and then display the minigame's code in a <dialog> element when a certain door was clicked. With Svelte 5 and SvelteKit, I wanted to use a different approach that is less prone to quirky behaviour. I set up a (minigames) folder (the brackets are around it so that it isn't included in the URL path), and then inside that I made a [game_name] folder that would be used as the URL param for each game. I then added the +page.svelte file and +page.ts file to that folder. The code inside each of those is pretty short, so here it is: +page.svelte

<script lang="ts">
	import GameContainer from '$lib/components/GameContainer.svelte';

	let { data } = $props();

	let gameName: string = $derived(data.gameName);
</script>

<GameContainer {gameName} />

+page.ts

import type { PageLoad } from './$types';

export const load = (async ({ params }) => {
	return {
		gameName: params.game_name
	};
}) satisfies PageLoad;

I created a <GameContainer /> component. It takes the name of the game from the data object, which is automatically available to the +page.svelte file after making the load function in the +page.ts. There, we set the gameName to the value of the params.game_name, which is the URL param.

I also created a +layout.svelte file in which I used the children() snippet to render the content of the pages in the routes folder. This is where I found TypeScript to be a little different for Svelte 5. There is a Snippet type in Svelte that can be imported, so it looks like this:

<script lang="ts">
  import type { Snippet } from 'svelte';

  let { children }: { children: Snippet } = $props();
</script>

LocalStorage and Audio objects

In the original game, we used localStorage, but from what I learned from the Joy of Code LocalStorage object using Svelte Runes tutorial to create a LocalStorage object for create state using the $state Rune and updating it using $effect.

In the original code for Grim Manor, we also had some logic for controlling audio, but I was able to use some similar logic as the LocalStorage object to create an Audio Manager object. I could then load the initial userData and the audioManager objects in the +layout.svelte file, then pass them to the children pages using setContext and getContext.

While I created the setUserContext and setAudioManagerContext methods, I couldn't pass the values in there directly because I also needed the objects to be available in the +layout.svelte files. As it turns out, context only makes an object available to the children. If you use setContext to set the context of a userData object directly, that userData object can't be referenced within the +layout.svelte file - at least that's my experience! If you know otherwise, please leave a comment.

In my case, the audio toggles live in the +layout.svelte file, so the audio context needed to be available there and passed down to the children.

New content

We only had 5 days to brainstorm ideas, design, create content, develop and document the project. While I took a lot of the ideas from the original game, there were opportunities to update some of the content.

Ghostly orbs

On the mansion landing page, where the ghost appears, you could see some wands that indicated actions. These were good placeholders, but I wanted to add something ghost-like here. I ended up using a HTML element, and styling it with a background colour, opacity, and box shadow, and applying an animation to move along the SVG path.

Updated quiz

I used the quiz questions from the original project, bar 2 questions, to make it fit more with that olden-time theme. I also changed the background image to an elderly man to add to the story.

The structure of the quiz itself has also changed, as it was a great opportunity to use the $state Rune. Like I mentioned earlier, there is nothing special we have to do in TypeScript for these Runes. Here's how they look:

	let currentQuestionIndex: number = $state(0);
	let score: number = $state(0);
	let answerState: 'correct' | 'incorrect' | null = $state(null);
	let showScore: boolean = $state(false);
	let showEndCard: boolean = $state(false);
	let endText: string = $state('');
	let quizStarted: boolean = $state(false);

As you can see, we simply add the typing as if there were no$state there are all.

New dialogue

I like the dialogue of the original project, so it's the same for the most part. However, I did add some more for the quiz to be in line with the new background image. I also added dialogue for each minigame for when you win or lose and whether it's your first time playing or returning. Finally, there is some new dialogue for the endgame, but I won't give any spoilers.

The dialogues themselves did live in a dialogues.js file and were exported. I thought it might be better in a Svelte project to keep everything encapsulated, so I moved each of the dialogues into their corresponding components.

Removed unnecessary code

Every developer's favourite thing to do. I got to remove a lot of JavaScript code that was made to work with the original <dialog> popup for the minigames, or the many, many element selectors.

The complex logic we had for managing the audio and local storage could also be replaced by my LocalStorage and AudioManager spells.

Other issues faced

I faced a few challenges throughout the migration:

  1. The initial splash screen was always showing, even when the localStorage value meant it shouldn't. It would appear then disappear. As it turns out, this was caused by the fact that it was server-side rendered but then updated by localStorage. To stop this (and a few other annoying things happening), I made the app fully client side by adding this code to the +layout.server.ts file:
export const ssr = false;
  1. In my dialogues.js dialogues, which I had originally added in a way that the user could make choices, which would then trigger JavaScript functions to update the DOM. One of the things it did was redirect to the homepage. While that works in Svelte, it still triggers a page reload, seemingly because the button is created after the DOM has already loaded. To tackle that, I instead made a game-end card that would only show up when the showEndCard state was true. That meant it wasn't generated using JavaScript and kept the page navigation nice and smooth.

  2. That darn audio. Even though the audio logic was simplified, I still ran into issues with playing or stopping music in the correct place. I knew I always wanted the same music in the minigames and different music playing on the mansion landing. To do that, I tracked the currently playing music in the userData object, updated it as the user navigated and then played the correct music when the toggle was enabled.

Project update on Stackportfolio

As usual, I am now able to add TypeScript, Svelte and SvelteKit to the Grim Manor project's Stackportfol.io page. I also updated the links for the repo and the deployed site.

[grim manor project on stackportfolio]

Conclusion

The migration took longer than I expected, but it was more due to the fact that I was removing old code and making updates. TypeScript with Svelte didn't add much complexity at all. As usual, I learned a lot and look forward to the next one!

FrontendLocalstorageSveltekitTypeScriptSvelte
Avatar for Stephen

Written by Stephen

I am a fullstack developer who likes to build useful things.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.