Single-File Components All
WildflowerJS does not have a custom file format like .vue or .svelte. It does not need one. Two patterns built entirely from standard HTML, CSS, and JavaScript deliver the same colocation and ergonomics, with no compiler.
.js file with all the component's logic, or an .html fragment file containing the markup, styles, and registration script that you load with a tiny helper.
The Simplest Case: One Page
If a component belongs to one page rather than being reused across several, none of what follows applies. Put the markup and a <script> in the same file and you are done, without a helper and without choosing a pattern:
<div class="counter" data-component="counter">
<button data-action="decrement">-</button>
<span data-bind="count"></span>
<button data-action="increment">+</button>
</div>
<script>
wildflower.component('counter', {
state: { count: 0 },
increment() { this.count++; },
decrement() { this.count--; }
});
</script>
That is a single file holding markup and logic together, which is what the term usually means. The two patterns below exist for reuse, meaning one component that several pages can load.
Why a Component File Cannot Load Itself
The browser loads JavaScript declaratively through <script src> and offers nothing equivalent for a fragment of HTML. There is no <include src="counter.html">. HTML Imports were the platform's proposal for exactly this and were removed from it. So a component written as an .html file has no way into a page on its own, and something has to fetch it. That is the entire reason Pattern 2 carries a helper.
A second platform rule explains the helper's length. A <script> inserted through innerHTML never executes, by specification, so the helper recreates each script element to make the registration run.
A compiled format does not escape this. A browser cannot load a .vue or .svelte file either. A build step turns it into a JavaScript module first, which the browser does load natively, so the compiler is what buys the single-file ergonomics. The choice here is between a component in a .js file the browser loads on its own, and an .html file you fetch yourself.
Pattern 1: JS File With Use-Site Markup
The most common pattern. A single .js file holds the component definition. The HTML markup the component binds to lives where the component is used, inside the data-component element. Loaded via a normal <script src> tag.
The .js file is the single-file component: it owns the state, methods, computed properties, and lifecycle. Styles can also live here via injection on first init(), or sit in a separate stylesheet, whichever fits the project. Markup is content the host page provides, the same way <input> takes attributes from the page that uses it.
Every component used by the documentation site you are reading follows this pattern: www/js/components/*.js. Most of them lean on global stylesheets; one (code-example.js) injects styles in init() as the example below does. Both variants are valid Pattern 1.
The component file. State, methods, lifecycle, and (optionally) component-scoped styles all live here.
// counter.js: the single-file component
wildflower.component('counter', {
state: { count: 0 },
increment() { this.count++; },
decrement() { this.count--; },
init() {
// Inject component styles once. Idempotent across multiple instances.
if (document.getElementById('counter-styles')) return;
const style = document.createElement('style');
style.id = 'counter-styles';
style.textContent = `
.counter { display: flex; gap: 12px; align-items: center; }
.counter button { padding: 8px 16px; }
`;
document.head.appendChild(style);
}
});
The page that uses it. Loads the component file via a normal <script src>, then declares a data-component element wherever the component should appear.
<script src="counter.js"></script>
<div class="counter" data-component="counter">
<button data-action="decrement">-</button>
<span data-bind="count"></span>
<button data-action="increment">+</button>
</div>
What you get: state, methods, lifecycle, and styles all in one .js file. Browser executes the script tag with no special handling. Multiple instances cost nothing extra and the style injection is idempotent; drop more data-component="counter" elements wherever you need them.
When to reach for it: default for almost everything. If you only need one of the two patterns, this is the one.
Trade-off: the markup lives separately from the JS, which some teams prefer (designers can edit HTML without touching JS) and some prefer to colocate. If you want template + script + style in one file, see Pattern 2.
Pattern 2: HTML Fragment File With Helper
Template, styles, and registration script colocated in a single .html file. A small helper fetches the file, hoists its styles into <head>, re-executes its script (so the component registers), and inserts the markup wherever you want the component to appear. Familiar to anyone arriving from Vue or Svelte.
The .html fragment looks like this.
<!-- counter-fragment.html -->
<style>
.sfc-frag-counter { display: flex; gap: 12px; align-items: center; }
.sfc-frag-counter button { padding: 8px 16px; }
</style>
<div class="sfc-frag-counter" data-component="sfc-frag-counter">
<button data-action="decrement">-</button>
<span class="count" data-bind="count"></span>
<button data-action="increment">+</button>
</div>
<script>
wildflower.component('sfc-frag-counter', {
state: { count: 0 },
increment() { this.count++; },
decrement() { this.count--; }
});
</script>
The reusable helper, which exists because nothing in the browser will fetch and mount an HTML fragment for you. Copy it into your project once.
async function mountComponent(url, target) {
const html = await fetch(url).then(r => r.text());
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
// Move <style> tags into <head>
for (const style of wrapper.querySelectorAll('style')) {
document.head.appendChild(style);
}
// Re-create <script> elements so the browser executes them.
// Scripts inserted via innerHTML are inert; this is the standard fix.
for (const oldScript of [...wrapper.querySelectorAll('script')]) {
oldScript.remove();
const newScript = document.createElement('script');
for (const attr of oldScript.attributes) {
newScript.setAttribute(attr.name, attr.value);
}
newScript.textContent = oldScript.textContent;
document.head.appendChild(newScript);
}
// Move remaining markup into the target and scan
target.innerHTML = '';
target.append(...wrapper.children);
wildflower.scan(target);
}
// Usage
await mountComponent('/components/counter.html', document.getElementById('mount'));
What you get: one file per component. Template, style, and script side by side. Familiar layout for users coming from Vue or Svelte. Lazy-loadable: the file is fetched only when needed.
When to reach for it: you want the visual layout of a .vue file, you want components shipped as standalone HTML fragments without a build step, or you want code-split components fetched on demand.
Trade-off: the helper is twenty lines you maintain. The component instantiates only after a network round-trip. CSS scoping is by class convention, not enforced.
Choosing Between Them
| Pattern | File format | Loading | Best for |
|---|---|---|---|
| 1. JS file with use-site markup | .js |
Plain <script src> |
Default. Apps you control end-to-end. |
| 2. HTML fragment with helper | .html |
Helper (fetch + parse + script re-create + scan) |
.vue-style file layout, or lazy-loaded components. |
Neither is "the WildflowerJS way." Both are first-class. Use the one that matches your project's needs. You can mix them within a single app without friction.
What You Don't Get
Honest list of what other frameworks' SFC formats provide that these patterns don't:
- Compile-time CSS scoping with auto-generated attribute selectors. Vue and Svelte rewrite selectors at build time. Standards-only patterns rely on class conventions or attribute prefixes; nothing the browser enforces.
- Hot module replacement for SFCs. WildflowerJS does not run a dev server. Editing a component file requires a page reload.
- TypeScript or Sass inside the file. Pre-processors require a compile step. If you want them, run them as a separate step that outputs plain JS and CSS, then load that output normally.
- A unique editor mode for the framework's file format. The patterns above are valid HTML, CSS, or JavaScript, so any editor with standard syntax highlighting works without a plugin.
The trade is real and worth understanding. Standards-only means no compiler dependency, no version skew between the framework and your build tools, and no weeks lost to debugging your toolchain. It also means you opt into pre-processing yourself when you want it, instead of getting it free.