Styling and Accessibility

Styling an Application


Learning Objectives

  • You can style forms and components using Tailwind CSS and Skeleton.
  • You know how to use Skeleton’s pre-built Svelte components.
  • You understand how to apply transitions and animations.

In the previous chapters, we built a basic layout and learned how to make it responsive. Now we’ll look back into styling the book application that we’ve worked with previously.

We’ll start by styling the layout to display user authentication information, then move to forms for login and adding books, and finally style the book display with cards.

Layout with user authentication

Let’s start with our layout file, which currently displays user information without any styling. Here’s what we’re working with:

<script>
  import { useAuthState } from "$lib/states/authState.svelte.js";
  let { children } = $props();
  const authState = useAuthState();
</script>

<header>
  {#if authState.user}
    <div>
      <span>
        Hello, {authState.user.email}!
        {#if authState.user.roles?.length}
          (Roles: {authState.user.roles.join(", ")})
        {/if}
      </span>
      {#if authState.user.roles?.includes("ADMIN")}
        <ul>
          <li><a href="/admin">Admin</a></li>
        </ul>
      {/if}
      <button onclick={() => authState.logout()}>Logout</button>
    </div>
  {:else}
    <ul>
      <li><a href="/auth/login">Login</a></li>
      <li><a href="/auth/register">Register</a></li>
    </ul>
  {/if}
</header>

<main>
  {@render children()}
</main>

When we open up the application and navigate to the path auth/register, we see a very basic layout with a form without any styling. This is shown in Figure 1 below.

Fig 1. The registration page has no styles at the start.

Fig 1. The registration page has no styles at the start.

Let’s apply what we learned in the previous chapters about building layouts with Tailwind.

Header component

First, we’ll create a component for the header to keep things organized. Create a component called Header.svelte to src/lib/components/layout (creating the folder if it does not yet exist), and place the following content to it. The header will show a title “Booksy” and the user’s email if they are logged in, while otherwise it will show login and registration links.

<script>
  import { useAuthState } from "$lib/states/authState.svelte.js";
  const authState = useAuthState();
</script>

<header class="flex items-center bg-primary-100 p-4 mb-6">
  <span class="text-lg text-primary-900">
    Booksy
    {#if authState.user}
      ({authState.user.email})
    {/if}
  </span>
  <nav class="ml-4">
    <ul class="flex space-x-4">
      {#if authState.user}
        <li>
          <button class="anchor" onclick={() => authState.logout()}>
            Logout
          </button>
        </li>
      {:else}
        <li><a class="anchor" href="/auth/login">Login</a></li>
        <li><a class="anchor" href="/auth/register">Register</a></li>
      {/if}
    </ul>
  </nav>
</header>

For handling authentication state, we import and use the useAuthState function from our shared state — this way, the header can reactively update based on whether the user is logged in or not, and the layout does not need to know about authentication details.

Layout file

Then, modify the layout file to use this new Header component and add styling to it. Update src/routes/+layout.svelte as follows:

<script>
  import "../app.css";
  import Header from "$lib/components/layout/Header.svelte";

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

<div class="flex flex-col h-full">
  <Header />

  <main class="container mx-auto max-w-4xl px-4 grow">
    {@render children()}
  </main>
</div>

Now, the layout file is cleaner, and the header is encapsulated in its own component. Furthermore, we added some Tailwind classes to the main element to center the content and limit its maximum width.

At this point, if you refresh the registration page, you should see a more structured layout with a styled header, as shown in Figure 2 below.

Fig 2. The navigation bar is now styled, and the shown content changes based on whether the student is authenticated.

Fig 2. The navigation bar is now styled, and the shown content changes based on whether the student is authenticated.
Loading Exercise...

Styling the authentication forms

Next, let’s style the login and registration forms. Here’s our starting point:

<script>
  /* logic */
</script>

<h2>
  {data.action === "login" ? "Login" : "Register"}
</h2>

{#if message}
  <div>
    <p>{message}</p>
  </div>
{/if}

{#if errorMessage}
  <div>
    <p>{errorMessage}</p>
  </div>
{/if}

<form onsubmit={handleForm}>
  <label>
    <span>Email</span>
    <input
      id="email"
      name="email"
      type="email"
      placeholder="user@example.com"
      required
    />
  </label>
  <br />
  <label>
    <span>Password</span>
    <input
      id="password"
      name="password"
      type="password"
      placeholder="Enter your password"
      required
    />
  </label>
  <br />
  <button type="submit" disabled={isLoading}>
    {isLoading
      ? "Please wait..."
      : data.action === "login"
        ? "Login"
        : "Register"}
  </button>
</form>

{#if data.action === "login"}
  <p>
    Don't have an account? <a href="/auth/register">Register here</a>
  </p>
{:else}
  <p>
    Already have an account? <a href="/auth/login">Login here</a>
  </p>
{/if}

We can use Skeleton’s utility classes for the form elements to give them a polished look, and also apply Tailwind classes for layout and spacing.

Let’s style this form using Tailwind utilities and Skeleton’s CSS classes:

<script>
  /* logic */
</script>

<div class="max-w-md mx-auto">
  <h2 class="text-2xl font-bold text-primary-900 mb-6">
    {data.action === "login" ? "Login" : "Register"}
  </h2>

  {#if message}
    <div class="card preset-filled-success p-4 mb-4">
      <p>{message}</p>
    </div>
  {/if}

  {#if errorMessage}
    <div class="card preset-filled-error p-4 mb-4">
      <p>{errorMessage}</p>
    </div>
  {/if}

  <form onsubmit={handleForm} class="space-y-4">
    <label class="label">
      <span class="label-text">Email</span>
      <input
        class="input"
        id="email"
        name="email"
        type="email"
        placeholder="user@example.com"
        required
      />
    </label>

    <label class="label">
      <span class="label-text">Password</span>
      <input
        class="input"
        id="password"
        name="password"
        type="password"
        placeholder="Enter your password"
        required
      />
    </label>

    <button
      class="w-full btn preset-filled-primary-500"
      type="submit"
      disabled={isLoading}
    >
      {isLoading
        ? "Please wait..."
        : data.action === "login"
          ? "Login"
          : "Register"}
    </button>
  </form>

  <p class="text-center mt-6 text-sm">
    {#if data.action === "login"}
      Don't have an account?
      <a class="anchor" href="/auth/register"> Register here </a>
    {:else}
      Already have an account?
      <a class="anchor" href="/auth/login"> Login here </a>
    {/if}
  </p>
</div>

Now, the form is centered, has proper spacing, and uses consistent theming throughout. Figure 3 below shows the styled registration page.

Fig 3. The registration (and login) form is now styled.

Fig 3. The registration (and login) form is now styled.
Loading Exercise...

Styling the book form

Now let’s style the form for adding books. Here’s the unstyled version of BookForm.svelte:

<script>
  import { useBookState } from "$lib/states/bookState.svelte.js";

  let bookState = useBookState();

  const addBook = (e) => {
    e.preventDefault();

    const book = Object.fromEntries(new FormData(e.target));
    bookState.addBook(book);
    e.target.reset();
  };
</script>

<form onsubmit={addBook}>
  <label>
    Title
    <input
      id="title"
      name="title"
      type="text"
      placeholder="Book title"
    />
  </label>
  <br />
  <label>
    Description
    <textarea
      id="description"
      name="description"
      placeholder="Enter book description"
    ></textarea>
  </label>
  <br />
  <label>
    Published at
    <input id="published_at" name="published_at" type="date" />
  </label>
  <br />
  <label>
    Page count
    <input id="page_count" name="page_count" type="number" />
  </label>
  <br />
  <input type="submit" value="Add Book" />
</form>

With the existing styles, the page at /books looks similar to the Figure 4 below.

Fig 4. Unstyled books page without books and the form for adding a book.

Fig 4. Unstyled books page without books and the form for adding a book.

Let’s apply similar styling to what we used for the authentication form:

<script>
  import { useBookState } from "$lib/states/bookState.svelte.js";

  let bookState = useBookState();

  const addBook = (e) => {
    e.preventDefault();

    const book = Object.fromEntries(new FormData(e.target));
    bookState.addBook(book);
    e.target.reset();
  };
</script>

<form onsubmit={addBook} class="space-y-4">
  <label class="label">
    <span class="label-text">Title</span>
    <input
      class="input"
      id="title"
      name="title"
      type="text"
      placeholder="Book title"
      required
    />
  </label>

  <label class="label">
    <span class="label-text">Description</span>
    <textarea
      class="input"
      id="description"
      name="description"
      placeholder="Enter book description"
      rows="4"
    ></textarea>
  </label>

  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
    <label class="label">
      <span class="label-text">Published Date</span>
      <input
        class="input"
        id="published_at"
        name="published_at"
        type="date"
        required
      />
    </label>

    <label class="label">
      <span class="label-text">Page Count</span>
      <input
        class="input"
        id="page_count"
        name="page_count"
        type="number"
        min="1"
        placeholder="e.g., 250"
        required
      />
    </label>
  </div>

  <button class="w-full btn preset-filled-primary-500" type="submit">
    Add Book
  </button>
</form>

Now, the input fields are styled using Skeleton’s label, label-text, and input classes, and the form has proper spacing and layout. Figure 5 below shows the styled book addition form.

Fig 5. The book form has is now styled. Otherwise, the (empty) books list is still unstyled, similar to the "Add a book" heading.

Fig 5. The book form has is now styled. Otherwise, the (empty) books list is still unstyled, similar to the “Add a book” heading.
Loading Exercise...

Displaying Books with Cards

Now let’s adjust the functionality used display the list of books. Let’s start with the following BookList.svelte component.

<script>
  import { useBookState } from "$lib/states/bookState.svelte.js";

  let bookState = useBookState();
</script>

<ul>
  {#each bookState.books as book}
    <li>
      <a href={`/books/${book.id}`}>{book.title}</a>
      <button onclick={() => bookState.removeBook(book)}>Remove</button>
    </li>
  {/each}
</ul>

We’ll use Skeleton’s card class to create visually distinct book entries with links and delete functionality. Modify the BookList.svelte to match the following:

<script>
  import { useBookState } from "$lib/states/bookState.svelte.js";

  let bookState = useBookState();
</script>

{#if bookState.books.length === 0}
  <div class="text-center py-12">
    <p class="text-4xl mb-4">📚</p>
    <p class="text-lg text-surface-600 mb-2">The library is empty</p>
    <p class="text-sm text-surface-500">Add a book to get started!</p>
  </div>
{:else}
  <ul class="space-y-4">
    {#each bookState.books as book (book.id)}
      <li class="card border-2 border-surface-300">
        <div class="flex items-center justify-between p-4 gap-4">
          <div class="flex-grow min-w-0">
            <a href={`/books/${book.id}`} class="anchor text-lg font-semibold">
              {book.title}
            </a>
            {#if book.description}
              <p class="text-sm text-surface-600 mt-1 line-clamp-2">
                {book.description}
              </p>
            {/if}
            <div class="flex flex-wrap gap-3 mt-2 text-sm text-surface-700">
              {#if book.published_at}
                <span>📅 {new Date(book.published_at).toLocaleDateString()}</span>
              {/if}
              {#if book.page_count}
                <span>📖 {book.page_count} pages</span>
              {/if}
            </div>
          </div>
          <button
            class="btn"
            onclick={() => bookState.removeBook(book)}
          >
            🗑️ Remove
          </button>
        </div>
      </li>
    {/each}
  </ul>
{/if}

Now, when the book list is empty, the user sees a friendly empty state message with an icon. This is shown in Figure 6 below.

Fig 6. When there are no books in the database, the user sees a message indicating the the library is empty.

Fig 6. When there are no books in the database, the user sees a message indicating the the library is empty.

When books are added, the list displays them as cards with a title, description, publication date, and the number of pages. A sample card is shown in Figure 7 below.

Fig 7. Books are shown using cards, which are styled containers that group related information together.

Fig 7. Books are shown using cards, which are styled containers that group related information together.

Finally, let’s adjust the page that uses this component to display the list of books. Update the styles of the in the heading elements in src/routes/books/+page.svelte to match the following:

<script>
  import { initBooks } from "$lib/states/bookState.svelte.js";
  import BookForm from "$lib/components/books/BookForm.svelte";
  import BookList from "$lib/components/books/BookList.svelte";

  $effect(() => {
    initBooks();
  });
</script>

<h1 class="text-2xl font-bold text-primary-900 mb-4 mt-4">Books</h1>

<BookList />

<h2 class="text-2xl font-bold text-primary-900 mb-4 mt-4">Add a book</h2>

<BookForm />

Now, the headings are styled consistently with the rest of the application. Figure 8 below shows the styled books page with one book entry.

Fig 8. The books page is now styled.

Fig 8. The books page is now styled.
More to style..

Here, we just got started with styling, and there would be plenty of other places in the application that could use some styling as well, such as the book details page, chapter listing, and chapter forms. Feel free to explore and apply similar styling techniques to those parts of the application!


Loading Exercise...

Summary

In summary:

  • Component organization improves maintainability. Extract reusable UI elements like headers into separate components to keep layout files clean and focused.
  • Skeleton provides pre-built utility classes for common UI elements like forms, buttons, and cards. Use classes like input, label, btn, and card for consistent styling across your application.
  • Tailwind utility classes control layout and spacing. Combine Skeleton’s component styles with Tailwind classes like flex, space-y-4, max-w-md, and mx-auto for responsive, well-structured interfaces.
  • Empty states enhance user experience. Display friendly messages with icons when lists or collections are empty to guide users toward their next action.
  • Cards group related information visually. Use Skeleton’s card class to create distinct visual containers for displaying collections of data like book entries.
  • Consistent styling creates cohesive applications. Apply the same design patterns across forms, headers, and content sections to maintain visual harmony throughout your application.
Loading Exercise...