<form-repeatable>

A web component that enables you to control the duplication of fields with native form participation using ElementInternals. Single-instance architecture manages all groups internally. See the README for installation and API documentation.

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>
Trip Information

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>
Phone Numbers

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>
Project Team

Teams can be between 2 and 5 members.

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>
Contact Emails

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>
Event Guests
Guest 1

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>
Items

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>
Schools Attended
School 1

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>
Ingredients

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>
Styled Example

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>
Attendees (Min: 1, Max: 10)

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

Template Definition

Events

CSS Parts

Default Styling

The component uses a CSS Grid layout by default:

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.