Basic Example
A simple demonstration of repeatable fields. The first child element becomes the template automatically, with automatic ID and label incrementing.
Travel Stops
Add multiple stops to your journey. The component manages all groups internally in its shadow DOM.
<form>
<form-repeatable>
<div>
<label for="stop-1">Stop 1</label>
<input id="stop-1" type="text"
name="stops[]"
placeholder="Enter location">
</div>
</form-repeatable>
</form>
Pre-populated Fields (Progressive Enhancement)
Server-rendered initial groups are preserved. Each group becomes part of the shadow DOM.
Contact Information
Multiple initial groups with values. The component recognizes them all and allows adding/removing.
<form-repeatable>
<div>
<label for="phone-1">Phone 1</label>
<input id="phone-1" type="tel"
name="phones[]"
value="555-0100">
</div>
<div>
<label for="phone-2">Phone 2</label>
<input id="phone-2" type="tel"
name="phones[]"
value="555-0101">
</div>
<div>
<label for="phone-3">Phone 3</label>
<input id="phone-3" type="tel"
name="phones[]"
value="555-0102">
</div>
</form-repeatable>
Min/Max Constraints
Control the number of allowed groups with
min and max attributes.
Team Members (2-5 allowed)
This example requires at least 2 members and allows up to 5. Remove buttons are hidden when at minimum, add button hidden at maximum.
<form-repeatable min="2" max="5">
<div>
<label for="member-1">Member 1</label>
<input id="member-1" type="text"
name="members[]">
</div>
<div>
<label for="member-2">Member 2</label>
<input id="member-2" type="text"
name="members[]">
</div>
</form-repeatable>
Explicit Template Element
Use a <template> element for explicit
template definition with {n} placeholders.
Email Addresses
The template element is removed from the light DOM and
used internally. Use {n} as the placeholder
for numbers.
<form-repeatable>
<template>
<div>
<label for="email-{n}">Email {n}</label>
<input id="email-{n}"
type="email"
name="emails[]"
placeholder="user@example.com">
</div>
</template>
</form-repeatable>
Multiple Fields per Group
Each repeatable group can contain multiple related fields. All elements inside the template/first child are duplicated together.
Guest Information
Add guests with name and dietary requirements.
<form-repeatable>
<fieldset>
<legend>Guest 1</legend>
<label for="guest-1">Name 1</label>
<input id="guest-1" type="text"
name="guests[]">
<label for="diet-1">Dietary Needs 1</label>
<select id="diet-1" name="diets[]">
<option value="">None</option>
<option value="vegetarian">Vegetarian</option>
<option value="vegan">Vegan</option>
<option value="gluten-free">Gluten Free</option>
</select>
</fieldset>
</form-repeatable>
Custom Button Labels
Customize the "Add" and "Remove" button labels for better context.
Shopping List
Using custom button labels.
<form-repeatable
add-label="Add Item"
remove-label="Delete Item">
<div>
<label for="item-1">Item 1</label>
<input id="item-1" type="text"
name="items[]">
</div>
</form-repeatable>
Different Container Elements
The template can be any element type - div, fieldset, p, etc. Works with various input types.
Education History
Using a fieldset as the repeating container.
<form-repeatable>
<fieldset>
<legend>School 1</legend>
<label for="school-1">Institution 1</label>
<input id="school-1" type="text"
name="schools[]">
<label for="degree-1">Degree 1</label>
<select id="degree-1" name="degrees[]">
<option>Select degree</option>
<option value="hs">High School</option>
<option value="bs">Bachelor's</option>
</select>
<label for="notes-1">Notes 1</label>
<textarea id="notes-1"
name="notes[]"></textarea>
</fieldset>
</form-repeatable>
Events
Listen to custom events fired when groups are added or removed.
Event Logging
Watch the output below to see events firing with group count details.
<form-repeatable id="event-repeatable">
<div>
<label for="ingredient-1">
Ingredient 1
</label>
<input id="ingredient-1"
type="text"
name="ingredients[]">
</div>
</form-repeatable>
<script>
const repeatable =
document.querySelector('#event-repeatable');
repeatable.addEventListener(
'form-repeatable:added', (e) => {
console.log('Added:', e.detail.groupCount);
});
repeatable.addEventLinpx servetener(
'form-repeatable:removed', (e) => {
console.log('Removed:', e.detail.groupCount);
});
</script>
Event Log:
Custom Styling with ::part()
Use CSS ::part() to style the component's
shadow DOM parts.
Styled Buttons
This example uses ::part() selectors to
customize button appearance.
<style>
form-repeatable::part(add-button) {
background: green;
color: white;
}
form-repeatable::part(remove-button) {
background: red;
color: white;
}
form-repeatable::part(group) {
padding: 1rem;
background: #f5f5f5;
margin-bottom: 0.5rem;
}
</style>
<form-repeatable>
<div>
<label for="task-1">Task 1</label>
<input id="task-1" type="text"
name="tasks[]">
</div>
</form-repeatable>
Form Participation with Mixed Fields
The component uses ElementInternals API for native form participation. All inputs within the shadow DOM are automatically collected and submitted with the form. This example combines repeatable fields with regular form fields to demonstrate seamless integration.
Event Registration Form
Fill in your details and add attendees. Click Submit to see how the FormData combines regular fields with repeatable groups.
<form id="combined-form">
<!-- Regular form fields -->
<label for="event-name">Event Name</label>
<input id="event-name" name="eventName"
type="text" required>
<label for="event-date">Date</label>
<input id="event-date" name="eventDate"
type="date" required>
<!-- Repeatable fields via component -->
<fieldset>
<legend>Attendees</legend>
<form-repeatable min="1" max="10">
<div>
<label for="attendee-1">Attendee 1</label>
<input id="attendee-1" type="text"
name="attendees[]"
placeholder="Full name" required>
<label for="email-1">Email 1</label>
<input id="email-1" type="email"
name="emails[]"
placeholder="email@example.com">
</div>
</form-repeatable>
</fieldset>
<button type="submit">Register</button>
</form>
How It Works
When you submit the form, notice how the FormData includes:
- Regular fields (eventName, eventDate, eventLocation) from the light DOM
- Repeatable fields (attendees[], emails[]) collected from the shadow DOM via ElementInternals
- All values are properly combined into a single FormData object
The component automatically syncs its shadow DOM inputs with the parent form, so no special handling is needed. It works just like native form controls!
API Reference
Attributes
-
min- Number (default: 1). Minimum number of groups required. Must be a positive integer. Remove buttons are hidden when at minimum. -
max- Number (optional). Maximum number of groups allowed. Must be greater than min. Add button is hidden when at maximum. -
add-label- String (default: "Add Another"). Custom label for the add button. -
remove-label- String (default: "Remove"). Custom label for the remove buttons.
Template Definition
-
First Child: By default, the first
child element becomes the template. Numbers in IDs,
labels, and text are automatically converted to
{n}placeholders. -
Explicit Template: Use a
<template>element with{n}placeholders for explicit control. The template element is removed from light DOM.
Events
-
form-repeatable:added- Fired when a new group is added. Detail contains{groupCount}. -
form-repeatable:removed- Fired when a group is removed. Detail contains{groupCount}.
CSS Parts
-
::part(groups)- Container for all groups (default: CSS grid with 2-column layout). -
::part(group)- Each repeatable group wrapper (default: subgrid spanning both columns). -
::part(content)- Container for the group's fields (default: column 1). -
::part(group-controls)- Container for group-level controls, remove button (default: column 2). -
::part(controls)- Container for main controls, add button (default: below groups). ::part(add-button)- The add button.-
::part(remove-button)- The remove buttons.
Default Styling
The component uses a CSS Grid layout by default:
-
Two-column grid:
minmax(min-content, 2fr)andminmax(min-content, 1fr) -
Each group uses
subgridto align content with controls - Content appears in column 1, remove buttons in column 2
- Add button appears below all groups
- Global stylesheets are automatically adopted into the shadow DOM, so your page styles will apply to elements inside the component
Form Participation
This component uses the
ElementInternals API for native form
participation. All inputs within the shadow DOM are
automatically collected and submitted with the form using
FormData. Supports form reset and disabled
states. Browser support: 95% (Chrome 77+, Firefox 93+,
Safari 16.4+).
For complete documentation, see the README.