Tutorials

Hands-on walkthroughs, in order. Getting started covers the install tags once; this page shows what to actually build with them.

1. Your first page

A minimal page that loads Joy and renders one card. Save this as an HTML file and open it — no server, no build step:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
    <script src="https://unpkg.com/@phosphor-icons/web"></script>
    <link rel="stylesheet" href="https://cdn.vaneltonmedia.com/joy/styles/joy-tokens.css">
    <link rel="stylesheet" href="https://cdn.vaneltonmedia.com/joy/styles/joy.css">
    <link rel="stylesheet" href="https://cdn.vaneltonmedia.com/joy/styles/joy-utilities.css">
</head>
<body style="padding:40px;">
    <div class="joy-card" style="max-width:360px;">
        <div class="joy-card-icon"><i class="ph ph-hand-waving"></i></div>
        <h3>Hello, Joy</h3>
        <p class="joy-text-muted">This card is one file, no build step.</p>
    </div>
</body>
</html>

Open it in a browser and you already have a styled card with the Poppins font and a Phosphor icon — that's the entire dependency chain.

2. Building a layout with utility classes

Say you need a header row (logo left, actions right), a 3-column feature grid that stacks on mobile, and consistent spacing between sections. No custom CSS required — just combine utilities from Flexbox & grid and Sizing & spacing:

Acme Inc.

Fast

Secure

Reliable

<div class="joy-flex joy-justify-between joy-items-center joy-pb-4">
    <strong>Acme Inc.</strong>
    <div class="joy-flex joy-gap-2">
        <button class="joy-btn joy-btn-ghost">Log in</button>
        <button class="joy-btn joy-btn-solid">Sign up</button>
    </div>
</div>
<div class="joy-grid-cols-3 sm:joy-grid-cols-1 joy-gap-4">
    <div class="joy-card"><div class="joy-card-icon"><i class="ph ph-rocket-launch"></i></div><h3>Fast</h3></div>
    <div class="joy-card"><div class="joy-card-icon"><i class="ph ph-shield-check"></i></div><h3>Secure</h3></div>
    <div class="joy-card"><div class="joy-card-icon"><i class="ph ph-heart"></i></div><h3>Reliable</h3></div>
</div>

Walk through what each class is doing:

ClassJob in this layout
.joy-flex .joy-justify-between .joy-items-centerlays the header out in a row, pushes the two groups to opposite ends, centers them vertically
.joy-pb-4adds breathing room below the header before the grid starts
.joy-grid-cols-3three even columns for the feature cards
.sm:joy-grid-cols-1collapses to one column under 600px — see Layout & position for the responsive prefix
.joy-gap-4consistent spacing between the cards, both directions

3. Wiring up the JS components

Once joy.js is on the page, most components are one function call — but a few (the topbar auth slot, the user menu) look for specific ids. Here's a save button that shows a real loading state and a toast when it's done:

<button class="joy-btn joy-btn-solid" id="tutorial-save-btn"><span>Save profile</span></button>
<script>
document.getElementById("tutorial-save-btn").addEventListener("click", async function () {
    var btn = this;
    JoyButton.loading(btn);
    try {
        await new Promise(function (resolve) { setTimeout(resolve, 1200); }); // stand-in for a real fetch() call
        JoyToast.show("Profile saved.", "success");
    } catch (err) {
        JoyToast.show("Could not save. Try again.", "error");
    } finally {
        JoyButton.reset(btn);
    }
});
</script>

The pattern to reuse: JoyButton.loading(btn) right before the request, JoyButton.reset(btn) in a finally block so it always runs, and JoyToast.show(...) to report the outcome either way. See JavaScript API for every method available.

4. A confirm-before-you-delete flow

Combining JoyModal with a network call — the shape you'd use for any destructive action:

async function deleteItem(id, btn) {
    const ok = await JoyModal.confirm({
        title: 'Delete this item?',
        message: "This can't be undone.",
        confirmText: 'Delete',
        cancelText: 'Cancel',
        danger: true
    });
    if (!ok) return;

    JoyButton.loading(btn);
    try {
        await fetch('/api/items/' + id, { method: 'DELETE' });
        JoyToast.show('Item deleted.', 'success');
    } catch (err) {
        JoyToast.show('Delete failed.', 'error');
    } finally {
        JoyButton.reset(btn);
    }
}

Next

Browse Components for every piece of markup Joy ships, or Utilities for the full class reference.