JavaScript API

joy.js is a single dependency-free IIFE that exposes a handful of window.Joy* globals. Every one of them is safe to call even with no dock or topbar markup on the page — each builds its own structure when it needs one.

window.JoyToast

A styled replacement for quick notifications. Creates and reuses a toast stack on its own — no existing element required.

MethodSignature
.show(message, type)type is 'success' (default) or 'error'. Dismisses itself after 3s.
<button class="joy-btn joy-btn-solid" onclick="JoyToast.show('Saved successfully!', 'success')">Success toast</button>
<button class="joy-btn joy-btn-ghost" onclick="JoyToast.show('Something went wrong.', 'error')">Error toast</button>

window.JoyModal

A styled replacement for confirm() that returns a Promise, so it works with await.

MethodSignature
.confirm(options)returns Promise<boolean>true on confirm, false on cancel or close

options: { title, message, confirmText, cancelText, icon, danger }danger: true renders the confirm button in red, for destructive actions like deleting something.

const ok = await JoyModal.confirm({
    title: 'Remove this item?',
    message: 'This action can\'t be undone.',
    confirmText: 'Remove',
    cancelText: 'Cancel',
    danger: true
});
if (ok) { /* proceed with removal */ }

window.JoyButton

Puts any button into a loading state: swaps its content for a spinner, disables clicks, and stores the original HTML to restore later. Built for any button that waits on a network response — save, login, submit.

MethodWhat it does
.loading(btn)disables the button and shows a spinner next to the original label
.reset(btn)restores the original content and clickable state
async function save(btn) {
    JoyButton.loading(btn);
    try {
        await fetch('/api/save', { method: 'POST' });
    } finally {
        JoyButton.reset(btn);
    }
}

Don't use JoyButton on bottom dock items — swapping their content clashes with the dock's own navigation animation, which already gives its own feedback.

window.JoyCropper

A circular image cropper — drag to reposition, mouse wheel or slider to zoom — built entirely on canvas, no external library.

MethodSignature
.open(file, options)returns Promise<Blob|null> — resolves a cropped JPEG, or null if cancelled. options.outputSize sets the output side length in pixels (default 512).
input.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    const blob = await JoyCropper.open(file, { outputSize: 512 });
    if (blob) {
        // upload the blob, or build a local preview
    }
});

window.JoyTheme

MethodWhat it does
.toggle()switches between light and dark, and saves the choice
.apply(theme)forces 'dark' or 'light'

Mechanism details (disabled link + localStorage) are in Theme & tokens.

window.JoyAuth

Built for login/register flows over fetch: after authenticating without a reload, it swaps the topbar's auth slot to the logged-in state.

MethodSignature
.renderLoggedIn(user)expects user.profileimg_full_url and user.name; requires a #joy-topbar-auth-slot element on the page (see Navigation)

What runs automatically

On DOMContentLoaded, joy.js already runs on its own: theme, scroll-triggered .joy-reveal animations, the dock, hero interactions, anchor links, and the user menu — each one only acts if it finds the elements it needs, so including the script doesn't require any specific markup.

If your site is an SPA that swaps content without reloading, dispatch window.dispatchEvent(new CustomEvent('spa:navigated')) after the swap so Joy re-runs what it needs to on the new content (details in Getting started).