@zotezica/simple-front
v0.1.0
Published
Minimal JS library for server-rendered apps with interactive islands
Downloads
93
Readme
@zotezica/simple-front
A minimal JS library for server-rendered apps with interactive islands.
Philosophy
Modern full-stack frameworks (Next.js, SvelteKit, Remix) own everything — routing, data fetching, rendering — and couple your backend to their conventions. This is too much when you have a real backend (Hono, Express, Rails) that renders HTML and you just need a little client-side interactivity.
The insight: the only things you actually need from a framework are:
- Smooth navigation between server-rendered pages (no full-page blink)
- A way to mount interactive components onto server-rendered DOM nodes
That's it. Everything else stays on the server.
Core principles:
- Server renders all read-only data — no spinners, no client-side fetches for page content
- Islands own only interactivity: forms, toggles, drawers
- CSP
script-src 'self'compliant — no inline event handlers, no eval - No opinions about your backend or build tool
Install
npm install @zotezica/simple-frontSvelte and React are optional peer dependencies — install whichever you use.
Usage
// Svelte
import { island, start, slideDownFadeIn } from "@zotezica/simple-front/svelte";
// React
import { island, start, slideDownFadeIn } from "@zotezica/simple-front/react";Register islands and call start():
const animate = slideDownFadeIn();
island("search-island", () => import("./islands/Search.svelte"), { animate });
island("product-island", () => import("./islands/Product.svelte"), {
props: (el) => ({ productId: el.dataset.productId }),
animate,
});
start();The server renders mount points as empty divs with data-* props:
<div id="product-island" data-product-id="abc123"></div>simple-front finds them by ID, reads props from data-* attributes, and mounts the component.
API
island(id, importer, options?)
Registers an island.
| Option | Type | Description |
|--------|------|-------------|
| props | (el: HTMLElement) => Record<string, unknown> | Reads props from the element before mounting |
| animate | IslandAnimation | Animation to run when the island mounts |
start()
Attaches the click interceptor, popstate, and app:navigate listeners, then mounts all registered islands found in the current DOM.
navigate(url, options?)
Programmatically navigate to a URL. Options: { replace?: boolean }.
app:navigate event
Islands trigger navigation by dispatching a custom event — no need to import navigate:
window.dispatchEvent(new CustomEvent("app:navigate", {
detail: { url: "/products/abc123" }
}));Animations
Built-in animations, all accepting { duration?, easing? }:
import { slideDown, fadeIn, slideDownFadeIn } from "@zotezica/simple-front/svelte";
island("my-island", () => import("./My.svelte"), {
animate: slideDownFadeIn({ duration: 300 }),
});| Animation | Description |
|-----------|-------------|
| slideDown() | Animates height from 0 to natural height |
| fadeIn() | Animates opacity from 0 to 1 |
| slideDownFadeIn() | Slide, then fade |
You can also pass any custom animation:
island("my-island", () => import("./My.svelte"), {
animate: {
prepare: (el) => { el.style.opacity = "0"; },
run: (el) => el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300 }).finished,
},
});Navigation
simple-front intercepts same-origin link clicks, fetches the new page, diffs the DOM with morphdom, and updates the URL — no full-page reload. View Transitions API is used when available.
The X-Navigation: 1 header is sent on every fetch, letting the server skip the layout if it wants to return a partial response.
