Build repeatable form rows, dynamic form fields, and repeatable inputs from a single <template> with zero dependencies, hard reindexing, copy-down, JSON prefill, and clean DOM events.
Rowio is a lightweight vanilla JavaScript library for building repeatable form rows, repeatable inputs, and dynamic form fields.
Rowio is suitable for admin panels, SaaS back offices, invoice editors, settings pages, and complex forms where users need to add, remove, and manage repeatable form sections dynamically.
Initialize Rowio on any element with the .rowio CSS class and configure your JavaScript form repeater using data-* attributes on the wrapper element.
Rowio is designed for forms where one field group must be repeated multiple times without building custom JavaScript every time.
Rowio is a vanilla JavaScript library for repeatable form rows, dynamic form fields, repeatable inputs, JSON prefill, copy-down behavior, and clean DOM events.
Rowio is a strong fit for invoices, quotes, product variant forms, admin CRUD screens, SaaS backoffice tools, and repeatable settings sections.
No. Rowio is framework-agnostic and works as a plain vanilla JavaScript form repeater.
Choose Rowio PRO when your team wants a more complete workflow layer for production forms and less custom engineering around repeatable form behavior.
Use Rowio Free when you want a lightweight repeatable form rows library with explicit setup and zero dependencies. Upgrade to Rowio PRO when your forms become a bigger product surface and you want a faster path to richer workflows.
Rowio Free gives you the core repeatable-row engine. Rowio PRO is for teams building real admin products where speed, polish, and less custom integration work matter.
Many teams start with hand-written JavaScript for dynamic form rows, then spend time fixing indexing, deleting rows, copying values, and serializing data consistently.
Rowio gives you a reusable, explicit, backend-friendly repeatable form row pattern so you can stop rebuilding the same form repeater logic across projects.
Rowio is initialized manually by calling
Rowio.init().
This gives you full control over when and where repeatable rows become active.
For Rowio to initialize successfully, the following HTML structure is required.
.rowio<template> element with class .rowio-template.rowio-rowMinimal example:
When initialized, Rowio will:
.rowio-rows containerprefix__field__indexrowio:ready
Initialization is usually done once after the DOM is ready:
document.addEventListener('DOMContentLoaded', () => Rowio.init());
Rowio is configured entirely through data-* attributes.
No JavaScript configuration objects are required.
This keeps Rowio declarative, predictable, and framework-agnostic.
.rowio)data-rowio-prefixitems__price__0data-rowio-showndata-rowio-max0 or omit for unlimited rows.data-rowio-copy-downdata-rowio-copy-down="price,tax"data-rowio-copy-down-class<template>)data-rowio-keydata-rowio-defaultdata-rowio-htmlcontenteditable="true" elements. When set to 1 or true, Rowio writes and reads raw HTML instead of text.All configuration is intentionally explicit. Rowio never infers behavior from markup structure alone.
Rowio emits DOM events on the .rowio wrapper element.
Each event is a CustomEvent and carries its payload in
event.detail.
rowio:readyrowio:row-addrowio:change.rowio:row-removerowio:change. Note: payload typically has row: null because the row is gone.rowio:copy-downrowio:change.rowio:changerowio:max-rows-reacheddata-rowio-max is reached. No mutation happens. No row is created.Most events expose these keys inside event.detail:
instance - the Rowio instancerow - the affected row element (or null)index - row index (0-based) when applicablefields - map of field name to element or array of elements (when applicable)editors - list of WYSIWYG candidates in the row (when applicable)Listen on a wrapper and react to events:
// wrapper = the .rowio element
const wrapper = document.querySelector('.rowio');
// Re-init plugins / validation after any structural change
wrapper.addEventListener('rowio:change', (e) => {
const { instance, row, index, fields, editors } = e.detail;
// Example: init Select2 / Choices on the affected row only
if (row) {
// init_select2(row.querySelectorAll('.select2-select'));
// init_choices(row.querySelectorAll('.choices-select'));
}
// Example: re-init WYSIWYG (Rowio does not do this automatically)
// editors.forEach(el => tinymce_init(el));
});
// Show alert when maximum number of rows is reached
wrapper.addEventListener('rowio:max-rows-reached', (e) => {
const { max, rows_count } = e.detail;
// alert(`Max rows reached (${rows_count}/${max})`);
});
Template fields use bare names and optionally ids (e.g. opt_value, opt_label).
Rowio rewrites them to prefix___field__0, prefix___field__1 etc.
In this case the prefix is enum, so the full field names become enum__opt_value__0, enum__opt_label__0, enum__opt_value__1, enum__opt_label__1 etc.
+ and × buttons to add/remove rows.
↓ button. It should fill all next rows.
<script type="application/json" class="rowio-data"> inside the template and renders those rows.
Max rows blocks adding beyond the limit.
contenteditable="true" field (HTML mode)contenteditable="true" elements as value carriers.
data-rowio-html="true", Rowio uses innerHTML (not textContent).
Rowio emits events on the wrapper element.
This log shows the sequence you can hook into (e.g. init plugins, re-init editors, custom validation, etc.).
Use Free to evaluate the core engine. Upgrade to PRO when you need a more complete workflow layer and less custom implementation overhead.
Explore Rowio PRO