npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

templa-js

v0.10.1

Published

A tiny HTML template loader using <template src>. Read as tempura.

Downloads

253

Readme

templa

🍤 A tiny HTML template loader. Pronounced "tempura".

Wraps your data in <template src> like batter wraps ingredients.

templa is a tiny dependency-free script that lets you split HTML into reusable partials, pass parameters, and use Handlebars-like syntax — all powered by the native <template> element.

It works in two modes:

  • Runtime (templa.js) — partials are fetched and inlined in the browser
  • Build (npx templa-js build) — partials are inlined ahead of time into static HTML
<!-- index.html -->
<body>
  <template src="_partials/header.html" title="Home" logged-in="yes"></template>
  <main>...</main>
  <template src="_partials/footer.html"></template>

  <script src="templa.js"></script>
  <script type="module">await templa.start();</script>
</body>
<!-- _partials/header.html -->
<header>
  <h1>{{title}}</h1>
  <template if="logged-in">
    <a href="/logout">Logout</a>
  </template>
  <template unless="logged-in">
    <a href="/login">Login</a>
  </template>
</header>

Why templa?

  • No build step — drop in a <script> tag
  • No dependencies — pure vanilla JavaScript
  • Standard HTML — uses the native <template> element, not custom tags
  • Tiny — ~240 lines of source, ~3.5KB gzipped
  • HTML-native{{var}} for values; <template if> / <template unless> for conditionals
  • Recursive — partials inside partials just work
  • Resource-aware — waits for <link rel="stylesheet"> and <script src> inside partials before resolving

Install

Via <script> tag (CDN)

<!-- minified (auto-generated by jsDelivr) -->
<script src="https://cdn.jsdelivr.net/npm/templa-js/templa.min.js"></script>

<!-- or unminified, for debugging -->
<script src="https://cdn.jsdelivr.net/npm/templa-js/templa.js"></script>

jsDelivr serves templa.min.js by minifying the source on the fly, so there is no separate minified file to maintain. Pin a version with [email protected] if you want immutable URLs.

Via npm

npm install templa-js

Init

Bootstrap a new project in the current directory:

npx templa-js init           # minimal src/ tree
npx templa-js init --ai      # also write AGENTS.md and PLANNER.md
npx templa-js init --force   # overwrite existing files

The result is a buildable project: run npx templa-js build immediately afterwards and dist/ will be produced. The --ai flag adds two project-root markdown files that brief AI coding agents on templa's conventions and a planning prompt for site skeletons.

Usage

Basic

Mark any place you want a partial with a <template> element:

<template src="_partials/nav.html"></template>

Then call templa.start() to expand all of them:

<script src="templa.js"></script>
<script type="module">
  await templa.start();
  // any post-init: Alpine.initTree(document.body), AOS.init(), Swiper, …
</script>

templa.start() returns a Promise that resolves once head + body partials have been expanded. The module-script form is required for top-level await. Anything written after the await runs once the DOM has been fully assembled — perfect for plugins like Alpine.js, AOS, Swiper, etc.

Passing data

Each attribute on the calling <template> becomes a string-valued data key inside the partial.

<template src="_partials/card.html" title="Hello" body="Welcome."></template>

Conditionals (<template if="key">) are existence-based, so any non-empty string is truthy — featured="yes" is enough to enable a block.

Reserved attributes — these are not collected as data: src (the partial path), slot (slot filler name), if / unless (conditional markers). Any data-* attribute is also skipped, by HTML metadata convention.

Keys are case-insensitive. HTML attribute names are case-insensitive in the spec and the browser DOM lowercases them, so templa normalises both the attribute name and the {{var}} lookup to lowercase. <template ctaLabel="X"> and {{ctaLabel}} both resolve via ctalabel and behave identically in runtime and build mode. Use kebab-case (cta-label, og-image, hero-bg-color) — it survives every layer unchanged and reads as idiomatic HTML.

Template syntax

| Syntax | Effect | |---|---| | {{key}} | HTML-escaped variable | | {{{key}}} | Raw variable (no escape) — use only for trusted HTML | | <template if="key">…</template> | Block kept when data[key] is truthy | | <template unless="key">…</template> | Block kept when data[key] is falsy |

Conditionals can be nested. Variables fall through unchanged when the key is missing from data.

Layouts and slots

A partial can declare insertion points with <slot>. Pages fill those slots by writing content inside the calling <template src>.

Keep layouts as body fragments (no <!DOCTYPE> / <html> / <head> / <body> wrapper) so they work in both runtime and build modes. Each page provides its own document skeleton and embeds the layout where the body content goes.

<!-- _layouts/main.html (body fragment) -->
<header><slot name="nav">Default Nav</slot></header>
<main><slot></slot></main>
<footer><slot name="footer">&copy; 2026</slot></footer>
<!-- page.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Home</title>
</head>
<body>
  <template src="_layouts/main.html">
    <template slot="nav">
      <a href="/">Home</a>
      <a href="/about">About</a>
    </template>

    <h1>Welcome</h1>
    <p>Anything outside &lt;template slot&gt; goes into the default slot.</p>

    <!-- footer slot is omitted, so its fallback renders -->
  </template>

  <script src="https://cdn.jsdelivr.net/npm/templa-js/templa.min.js"></script>
  <script type="module">await templa.start();</script>
</body>
</html>

Rules:

  • <slot> (no name) receives every node from the calling <template> that is not wrapped in <template slot="...">.
  • <slot name="X"> receives the content of the matching <template slot="X"> filler.
  • A slot's own children are the fallback — they render when no filler is supplied.
  • Slot fillers may themselves contain <template src="..."> partials; they are expanded recursively in the call site's directory context.

This pattern works identically with the build CLI — npx templa-js build inlines the layout into the output and you can drop the script tags.

Nested partials

Partials can include other partials. templa keeps expanding until no <template src> remains.

Loading order

templa.start() kicks off head expansion synchronously so its fetches overlap with the rest of HTML parsing — critical when partials in <head> include <link rel="stylesheet">. Body expansion waits for DOMContentLoaded. The returned Promise resolves once both phases complete.

Relative paths in nested partials

<template src> inside a partial is resolved relative to that partial's URL, not the page. So _partials/layout.html can reference <template src="./header.html"> and it will resolve to _partials/header.html correctly.

Other resources (<img src>, <link href>, <script src>) still resolve against the page URL — use absolute or root-relative paths for those.

Co-located styles

A partial can carry its own CSS in a <style> block tagged with data-merge="<file>". The build CLI extracts it once per partial and appends it to the named output stylesheet — even if the partial is used 100 times, its rules are written exactly once.

<!-- _partials/card.html -->
<style data-merge="style.css">
  .card { background: #fff; border: 1px solid #ddd; padding: 1rem; }
</style>
<article class="card">
  <h3>{{title}}</h3>
</article>

At runtime, the same dedupe applies: the <style> block stays in the DOM for the first expansion of a given partial and is stripped on subsequent ones. A <style> without data-merge is treated as plain inline CSS (existing behaviour).

Caching and recursion

  • Identical <template src> URLs are fetched once per page (in-memory cache).
  • A safety guard stops expansion after 50 passes to prevent infinite loops from circular includes.

API

templa.start()

Loads all <template src> elements (head first, then body). Returns a Promise that resolves once everything is mounted. Use with await from a <script type="module">.

templa.run(selector?)

Lower-level: runs a single expansion pass against selector (default: 'template[src]'). Returns a Promise. Useful if you load partials dynamically.

await templa.run('#my-region template[src]');

Content Security Policy

The runtime never calls eval or new Function. There is no 'unsafe-eval' requirement; a plain script-src 'self' works.

For pages that don't need a runtime at all, build with npx templa-js build — the output is plain HTML with no template syntax left.

Caveats

  • {{key}} HTML-escapes its value. If you need to inject HTML, use {{{key}}} and make sure the value is trusted.
  • Original <template src> elements are removed from the DOM after expansion.
  • This project is in early development (0.0.x). API may change.

Build (CLI)

For static deployment, expand all <template src> ahead of time:

npx templa-js build -i ./src -o ./dist

The CLI reads every .html file under the source directory, recursively inlines its partials with the same syntax as the runtime, and writes the result to the output directory.

File convention

Files and directories whose names start with _ are treated as partials and are not copied to the output:

src/
├── index.html              ← entry, written to dist/
├── about.html              ← entry, written to dist/
└── _partials/              ← skipped (partials only)
    ├── header.html
    └── footer.html

Reference partials with a relative path:

<template src="_partials/header.html" title="Home"></template>

Options

| Flag | Default | Description | |---|---|---| | -i <dir> | ./src | Source directory | | -o <dir> | ./dist | Output directory (cleared before each build) | | --version | | Print version | | --help | | Print usage |

Build vs runtime

Both modes share the same template syntax, so a partial works in either:

| | Runtime (templa.js) | Build (npx templa-js) | |---|---|---| | When partials are inlined | At page load, in the browser | Once, ahead of time | | Dependencies | none | none | | Output | dynamic DOM | static HTML files | | Use case | quick prototypes, dev | production deploy, SEO, static hosting |

You can also use both: ship the static HTML for first paint and keep templa.js for any partials you want to load dynamically later.

Recommended companion: Alpine.js

templa handles composition (partials, layouts, variables, conditionals at build or load time). For interactive behaviour (modals, dropdowns, counters, form state), pair it with Alpine.js — also HTML-first, no build step, ~15 KB.

<!-- somewhere in your <head> -->
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>

<!-- in any partial -->
<section x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open">Hidden until clicked.</div>
</section>

Decision rule: templa for everything that can be resolved before the user clicks; Alpine for everything that depends on user interaction.

Working with AI agents

If you use Claude Code, Cursor, Aider, Copilot, or similar AI tools, two files do most of the work:

  • AGENTS.md — file conventions, two-phase workflow, syntax, common pitfalls. Drop it into a templa project root and the agent reads it as the source of truth.
  • PLANNER.md — an instruction prompt that turns a free-form site brief into a concrete plan.md (page set, wireframes, section list, design tokens, file inventory) before any file is written.

Philosophy

templa is not trying to replace HTML. It exists because HTML does not yet have a native way to include partials, compose layouts, and pass small pieces of data between templates.

The biggest competitor is native HTML. That is also the goal. If one day HTML supports this natively, templa has done its job. Until then, templa is a tiny bridge.

Concrete consequences of that stance:

  • Attribute names follow the platform: <template src> mirrors <img src> / <script src> / <iframe src>. We deliberately don't use data-src. The bare src lets editors and IDEs treat it like a real file reference (path completion, jump-to-file, refactor-rename).
  • Conditionals are written as <template if="key">…</template> and <template unless="key">…</template> — no {{#if}} Mustache block, no expressions, no helpers. They are existence-based only.
  • Co-located styles use data-merge="style.css" — a data-* attribute, because that is HTML's documented hook for component-private metadata.
  • Anything beyond block-level conditionals (conditional attributes, dynamic class lists, loops) is out of scope for the core and lives in plugins.

The core stays small enough to read in one sitting. Everything else is a plugin.

License

MIT