Documentation for the Vanilla Toolkit template

Minimalistic, lightning-fast tool collection
Vite + TypeScript + Tailwind – no React, no framework
Create a folder inside src/tools/.
The folder name becomes the tool’s path/URL slug.
src/tools/my-tool/
├── config.json # Name + description + configuration
├── template.html # Your layout
└── index.ts # Your logic (optional)
config.jsonMinimal example:
{
"name": "My Tool",
"description": "Does something useful",
"draft": false,
"example": false
}
Notes:
name and description are shown on the overview page and used for search.draft: true hides the tool from the normal overview (useful while you’re still building it).example: true is intended for template/demo tools (you can ignore it in real projects).Optional fields you can add later:
icon: an icon id (see Tool Icons (Lucide) below)order / sectionId: for sorting & grouping (see next section)template.htmlThis is the tool’s UI. Keep it small and composable (cards, inputs, buttons).
label, input, button)—it improves accessibility quickly.dark: prefix — prefer daisyUI themes or CSS variables for theme-aware styling (examples below).Practical tips:
label, input, button)—it improves accessibility quickly.index.ts (optional)If your tool is interactive, put the logic in index.ts.
Typical responsibilities:
Keep it defensive:
Export style
Your tool entry can be exported either as a default export or as a named init export:
// Default export
export default function init() {
// ...
}
// Named export
export function init() {
// ...
}
Important: cleanup when navigating between tools
Tools can be opened/closed via routing, so your index.ts may run multiple times.
If you attach any global listeners (e.g. document.addEventListener, window.addEventListener), timers (setInterval), observers, etc.,
make sure you return a cleanup function that removes them.
export default function init() {
const onKeyDown = (e: KeyboardEvent) => {
// ...
};
document.addEventListener('keydown', onKeyDown);
// Return cleanup to prevent duplicate listeners when navigating away/back
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}
Rule of thumb:
document / window should be cleaned up.Start the dev server and open the app:
pnpm run dev
Your tool should appear automatically on the overview page. If it doesn’t:
src/tools/<tool-name>/config.json is valid JSON (no trailing commas)"draft": truedescription (it powers search)pnpm-workspace.yaml)Each tool can declare its own dependencies by adding a package.json inside its folder.
This is supported by the project’s pnpm-workspace.yaml setup.
Example:
The tool example-package in this project add its own dependencies:
(demo purpose only with a lightweight dependency)
// src/tools/example-package/package.json
{
"name": "example-package",
"version": "1.0.0",
"dependencies": {
"is-odd": "3.0.1"
}
}
Note: This allows tools to use different libraries or versions as needed, without polluting the main project dependencies.
src/main.ts (custom startup invocation)In addition to per-tool scripts, you can add an optional project-level entry hook: src/main.ts.
If the file exists, it will be auto-imported and executed once on startup — before the initial route (overview/tool) is rendered.
This is useful for global, one-time setup such as:
You can provide either a default export or a named init export. Both may be async:
// src/main.ts
import type { CustomMainContext } from './js/types';
export default function main(ctx: CustomMainContext) {
console.log('Loaded tools:', ctx.tools.length);
// global setup...
}
// alternatively:
export function init(ctx: CustomMainContext) {
// ...
}
ctx)Currently, the context contains the already-discovered tool list:
ctx.tools: all tools (including metadata), as used later for overview + routing.This main.ts invocation is a one-time hook (not a routing lifecycle).
If you register global side effects here
(e.g. window.addEventListener, timers, observers),
you are responsible for managing cleanup yourself — unlike tool index.ts,
which can return a cleanup function.
Tools can be sorted and grouped into sections on the overview page by adding two optional fields to a tool’s config.json:
order (number): controls the position within a section (ascending)sectionId (string): groups tools into a named sectionconfig.json{
"name": "My Tool",
"description": "Does something useful",
"draft": false,
"example": false,
"sectionId": "examples",
"order": 1
}
order (ascending)name (A → Z) as a tie-breakerThis means you can keep the list stable and intentional, even when multiple tools share the same order.
sectionId are rendered under the same section header.sectionId that is not configured, the UI falls back to showing the raw sectionId as the section title.sectionId, it is grouped into a default “other” section.SiteConfigSection titles and descriptions live in the site configuration.
src/config/site.config.template.ts → src/config/site.config.tssectionIds):export const siteConfig = {
// ...
toolSections: {
examples: { title: 'Examples', description: 'Demo tools that show how the template works.', },
general: { title: 'General', description: 'Everyday helpers and utilities.', },
},
};
Section order:
Sections are rendered in the insertion order of toolSections first, followed by any additional sections discovered at runtime.
The default configuration lives in src/config/site.config.template.ts.
To customize the configuration for your project, copy the file to the Name site.config.ts and change any configuration values.
See types in src/config/site.config.ts for possible values.
Each tool can optionally define an icon in its config.json.
Use the icon id syntax from lucide (lower case with dashes)
If icon is missing or unknown, a default icon is used.
per default all lucide icons are included.
You can add additional icons registering them at startup (see src/main.ts).
This template exposes an icon registry so derived projects can add (or override) icon IDs without editing src/js/tool-icons.ts.
1) Import registerToolIcons in your entry file (e.g. src/script.ts).
2) Import any additional icons you want from lucide or any other source follwing the syntax.
3) Register them at once during startup (see main.ts hook above).
import { registerToolIcons } from
'./src/js/tool-icons';
import { ArrowLeft } from '@lucide/icons';
registerToolIcons({
ArrowLeft: ArrowLeft,
// add more icons here
});
Now you can reference your new icon IDs from any tool config.json:
{
"name": "My Tool",
"description": "Does something useful",
"icon": "arrow-left"
}
Notes:
Brief and practical:
{{ key.path }} inside your HTML templates, e.g. {{ config.title }}.replacePlaceholders() (see src/js/utils.ts). In this codebase the header and footer templates are processed before being inserted into the DOM (src/js/render.ts).siteContext in src/config/index.ts. siteContext merges the defaults from site.config.template.ts with an optional src/config/site.config.ts file.replacePlaceholders() uses the regex /\{\{(.+?)\}\}/g, trims the path and resolves the value using dot-notation with getValueByDotNotation().[{{...} NOT FOUND] to make the issue obvious.Example (Template → Result):
<!-- Template -->
<h1>{{ config.title }}</h1>
<!-- After replacement -->
<h1>Vanilla Toolkit</h1>
This project works with Tailwind’s class strategy but also supports daisyUI’s theme system. In practice prefer daisyUI theme tokens and components instead of sprinkling many dark: utilities across your templates.
Why prefer daisyUI tokens?
bg-base-100, text-base-content, border-base-300) that automatically adapt to the active theme.btn, card, input, form-control, etc.) and consistent spacing/colors with minimal classes.data-theme attribute on <html> (or document.documentElement), which is simpler than toggling many dark: variants.Quick daisyUI examples (concise):
<!-- Card -->
<div class="card bg-base-100 shadow-md p-4">
<h3 class="text-lg font-semibold">Card title</h3>
<p class="text-sm text-base-content/70">Card content</p>
</div>
<!-- Button -->
<button class="btn btn-primary">Save</button>
<!-- Input -->
<div class="form-control">
<label class="label"><span class="label-text">Name</span></label>
<input class="input input-bordered" type="text" />
</div>
Theme-aware tokens (preferred replacements for common pairs):
bg-base-100 instead of bg-white / dark:bg-slate-800.text-base-content instead of text-gray-900 / dark:text-white.border-base-300 instead of border-gray-200 / dark:border-slate-700.btn, btn-primary, btn-outline for buttons instead of crafting many color utilities.Toggling theme (simple script):
// set theme to 'dark' or 'light' (or any daisyUI theme name)
document.documentElement.setAttribute('data-theme', 'dark');
// read current theme
const theme = document.documentElement.getAttribute('data-theme');
When to still use dark:
dark: variants and where migration isn’t worth the effort.Rule of thumb:
dark: sparingly for edge-case, one-off style changes.Most components include sensible focus/hover styles. If you need custom behavior, combine tokens with Tailwind utilities:
<input class="input input-bordered focus:ring-2 focus:ring-primary/60" aria-label="Example input" />
<button class="btn btn-primary hover:brightness-90">Action</button>
Add your own custom styles to src/css/styles.css below the marker comment to avoid conflicts with the template styles on merge.
SiteContext (derived projects)This template is meant to be cloned (GitHub template). To allow project-specific context fields without modifying the core template types, SiteContext exposes a TypeScript declaration merging extension point.
SiteContext automatically includes everything you add to the global interface SiteContextCustom.
1) Create a declaration file (any name is fine), for example:
src/site-context.custom.d.ts2) Add your custom fields by extending SiteContextCustom:
declare global {
interface SiteContextCustom {
custom?: { foo: string; bar?: number; };
}
}
export {};
After this, your SiteContext type will include custom, features, etc. everywhere it’s used.
You can now use it in your tool configs and templates.
tsconfig.json should include something like src/**/*.d.ts (or src/**).custom).This template supports automatic updates for derived repositories using a GitHub Actions workflow.
The workflow uses AndreasAugustin/actions-template-sync
to regularly or manually sync changes from the template repository into your project.
The synchronization uses the existing workflow file .github/workflows/template-sync.yml.
This workflow is configured to run automatically at 00:00 UTC on the first day of every month (cron: '0 0 1 * *').
You can also trigger it manually at any time via the GitHub Actions UI.
To prevent certain files or folders from being overwritten, a .templatesyncignore exist in the .github directory.
Use glob patterns to specify files to ignore.
And above all, have fun with this template! 😊