Building the User Interface
The User Interface, also known as the front end, is the layer of our application that users interact with.
In a headless setup, the front end is responsible for displaying/rendering the content stored in the backend (WordPress) using an API.
In this section we'll:
Create a reusable component
Create and style our grid
Components
Components are the building blocks of our User Interface.
They are modular and reusable which makes the UI easier to maintain and scale
In SvelteKit, component files are saved wi the .svelte extension
For this tutorial, we'll make a card component.
1.1 Card.Svelte
This component renders a single card in the collection. It receives the card data through a prop 'Card'
The prop stores the card information that's retrieved from the WordPress backend and passes it to the component to render it.
Required properties:
slug (string) — URL-friendly identifier for the card
title (string) — Card title
Optional properties:
featuredImage (object, optional)
sourceUrl (string) — Image URL
categories (object, optional)
name (string) — Category name
excerpt (string, optional) — Short description
rating (number, optional) — Rating from 0 to 10
Follow these steps to create the card component:
In you project directory, create:
A components folder inside the src folder
A Card.svelte file inside the components folder
Structure: src/components/Card.svelte
<script lang="ts">
import type { Card } from '$lib/types';
export let card: Card;
</script>
<article class="trading-card">
<div class="card-content">
<div class="image-wrapper">
{#if card.featuredImage?.node?.sourceUrl}
<img src={card.featuredImage.node.sourceUrl} alt={card.title} loading="lazy" />
{:else}
<div class="no-image">No Image</div>
{/if}
</div>
<div class="card-body">
<h3>{card.title}</h3>
<div class="card-meta">
{#if card.categories?.nodes?.length}
<span class="card-set">
{card.categories.nodes[0].name}
</span>
{/if}
{#if card.rating !== undefined && card.rating !== null}
<span class="rating">
{card.rating}/10
</span>
{/if}
</div>
<div class="excerpt">{@html card.excerpt}</div>
</div>
</div>
</article>
<style>
.trading-card {
background: #ffffff;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e5e5e5;
transition: transform 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.trading-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
}
.image-wrapper {
aspect-ratio: 3 / 4;
background-color: #f3f3f3;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-image {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 0.9rem;
}
.card-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
h3 {
margin: 0;
font-size: 1.05rem;
line-height: 1.3;
}
.card-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.card-set {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
color: #991b1b;
background-color: #fee2e2;
padding: 0.2rem 0.6rem;
border-radius: 999px;
width: fit-content;
}
.rating {
display: inline-block;
font-size: 0.8rem;
font-weight: 600;
color: #1f2937;
background-color: #fef3c7;
padding: 0.2rem 0.6rem;
border-radius: 999px;
width: fit-content;
}
.excerpt {
font-size: 0.85rem;
color: #555;
line-height: 1.5;
margin-top: auto;
}
<
Note:{@html} renders raw HTML without escaping it
This means if the content contains malicious JavaScript, it will execute
This is safe here because WordPress sanitizes content, but avoid using it with unvalidated user input.
Layout and styling
Now that we have a reusable Card component, we'll design a layout to display multiple cards.
2.1 +page.svelte
In SvelteKit, +page.svelte is the route component for the homepage, It's created automatically when you set up Sveltekit.
SvelteKit uses a file-based routing system, the file structure in your src/routes folder determines your app's URLs.
+page.svelte automatically becomes the homepage that users see when they visit your site at /
We'll modify it to:
Import the Card component
Render the cards in a grid
Navigate to src/routes/+page.svelte and replace the existing code with the code below:
<script lang="ts">
import { onMount } from 'svelte';
import { getCards } from '$lib/api';
import Card from '../components/Card.svelte';
import type { Card as CardType } from '$lib/types';
let cards: CardType[] = [];
let loading = true;
let error = '';
onMount(async () => {
try {
const result = await getCards();
cards = result.posts.nodes;
} catch (err) {
console.error('Error fetching cards:', err);
error = 'Failed to load cards';
} finally {
loading = false;
}
});
</script>
<div class="container">
<h1>Trading Card Collection</h1>
{#if loading}
<p class="message">Loading cards...</p>
{:else if error}
<p class="message error">{error}</p>
{:else if cards.length === 0}
<p class="message">No cards found.</p>
{:else}
<div class="cards-grid">
{#each cards as card}
<Card {card} />
{/each}
</div>
{/if}
</div>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
background-color: #f9fafb;
min-height: calc(100vh - 100px);
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 2rem;
color: #1a1a1a;
text-align: center;
}
.message {
text-align: center;
font-size: 1.1rem;
color: #666;
padding: 2rem;
}
.message.error {
color: #dc2626;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 2rem;
width: 100%;
}
@media (max-width: 1024px) {
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.container {
padding: 1.5rem 1rem;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
}
@media (max-width: 480px) {
.cards-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
</style>