Keyboard Navigation

SmarkForm adds keyboard navigation to supported fields. The exact behavior also depends on the form’s markup, native controls, browser, and enabled features.

Enter / Shift+Enter Navigation

Press Enter to move forward to the next field, and Shift+Enter to move backward. This field-navigation shortcut skips buttons and other non-field components. Use Tab and Shift+Tab for the browser’s normal sequential focus order; disabled controls and the form’s authored markup affect that order.

🔗
❓
<div id="myForm">
  <p>
    <label data-smark>First name:</label>
    <input data-smark type="text" name="first">
  </p>
  <p>
    <label data-smark>Last name:</label>
    <input data-smark type="text" name="last">
  </p>
  <p>
    <label data-smark>Email:</label>
    <input data-smark type="email" name="email">
  </p>
  <p>
    <label data-smark>Message:</label>
    <textarea data-smark name="message"></textarea>
  </p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));

Press Enter in a single-line field to move to the next one; Shift+Enter moves backward. In a textarea, plain Enter inserts a newline, Ctrl+Enter moves forward, and Shift+Enter moves backward. The shared JSON editor is disabled so its controls do not add extra steps to this navigation sequence.

Tab-Flow Exclusion for Hotkey-Equipped Buttons

Hotkey-equipped addItem and removeItem triggers inside their list context are removed from sequential Tab order, so a long list does not require tabbing through every row action. Hotkey-equipped controls outside that list context remain in Tab order, including an outer Add button.

  • Inner buttons (inside list context): Removed from Tab flow — users can activate them via their hotkey instead.
  • Outer buttons (siblings of the list): Kept in Tab flow — with min_items: 0, the only way to create the first item is via the outer add button, which must remain keyboard-accessible.

This tab-order behavior is limited to list addItem/removeItem triggers with hotkeys in their list context. SmarkForm sets tabindex="-1" when the trigger has no authored tabindex; an explicit authored value is preserved. If an action is essential, provide a visible control that remains in the normal keyboard order; do not rely on a CSS-generated hotkey hint as its only description.

🔗
❓
<div id="myForm">
  <button data-smark='{"action":"addItem","context":"items","hotkey":"+"}' title="Add">➕ Add</button>
  <button data-smark='{"action":"removeItem","context":"items","hotkey":"Delete","preserve_non_empty":true}' title="Remove empty">🧹</button>
  <ul data-smark='{"type":"list","name":"items","min_items":1}'>
    <li>
      <input data-smark type="text" name="item" placeholder="Item name">
      <button data-smark='{"action":"removeItem","hotkey":"-"}' title="Remove this">➖</button>
      <button data-smark='{"action":"addItem","hotkey":"+"}' title="Insert here">➕</button>
    </li>
  </ul>
</div>
#myForm [data-hotkey] { position: relative; }
#myForm [data-hotkey]::after {
  content: "Ctrl+" attr(data-hotkey);
  position: absolute; top: 0.75em; left: 0;
  font-size: .7em; background: #333; color: #fff;
  padding: 1px 4px; border-radius: 3px; white-space: nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {"items":[{"item":"Review the agenda"},{"item":"Confirm the venue"},{"item":"Send invitations"}]}
});

Press Tab through the form — the outer ➕ Add/🧹 buttons are reachable, but the inner ➖/➕ buttons inside each list item are skipped. Hold Ctrl to reveal their hotkey shortcuts instead.

The shared JSON editor is disabled because its controls would add unrelated stops to the Tab order being demonstrated.

Hold Ctrl to discover available hotkeys — each trigger button with a hotkey can show a visual hint when the form includes the corresponding CSS.

Alt+Enter for Collapsed Sections

When navigating with Enter / Shift+Enter, fields inside a closed <details> element are skipped. To navigate into them, hold Alt while pressing Enter — SmarkForm automatically opens any closed <details> ancestors along the path. When moving forward from a <textarea>, keep Ctrl held as well (Ctrl+Alt+Enter); plain Enter inserts a newline there. For backward navigation into a preceding closed section, use Alt+Shift+Enter.

This allows you to keep optional sections collapsed by default while still being able to navigate into them when you need to fill data.

Sequential Enter navigation follows SmarkForm fields, not the browser’s native <summary> focus stop. Each address row below is a SmarkForm form in the list, with a field in its summary. A bare summary can still toggle the native disclosure, but focus there is not a field in SmarkForm’s navigation tree; the next Enter cannot continue from that point into the section’s fields.

🔗
❓
<div id="myForm">
  <p>
    <label data-smark>Full name:</label>
    <input data-smark type="text" name="name">
  </p>
  <p>
    <label data-smark>Email:</label>
    <input data-smark type="email" name="email">
  </p>
  <fieldset>
    <span data-smark='{"type":"label"}'>Addresses:</span>
    <div class="addresses-list" data-smark='{"type":"list","name":"addresses","of":"form","min_items":1,"max_items":6}'>
      <details data-smark='{"type":"form"}'>
        <summary>
          <input data-smark type="text" name="label" aria-label="Address label" placeholder="Home or work">
          <button data-smark='{"action":"removeItem"}' type="button" title="Remove address">🗑️</button>
        </summary>
        <p>
          <label data-smark>Street:</label>
          <input data-smark type="text" name="street">
        </p>
        <p>
          <label data-smark>City:</label>
          <input data-smark type="text" name="city">
        </p>
        <p>
          <label data-smark>ZIP:</label>
          <input data-smark type="text" name="zip">
        </p>
      </details>
    </div>
    <button data-smark='{"action":"addItem","context":"addresses"}' type="button">➕ Add address</button>
  </fieldset>
  <p>
    <label data-smark>Final comments:</label>
    <textarea data-smark name="final_comments" placeholder="Notes for the whole form"></textarea>
  </p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
  "name": "Taylor Morgan",
  "email": "taylor@example.com",
  "addresses": [
    {"label":"Home","street":"42 River Lane","city":"Brookside","zip":"12345"},
    {"label":"Work","street":"8 Market Street","city":"Brookside","zip":"12346"}
  ],
  "final_comments": "Call before delivery."
}
});

With the Work address collapsed, focus its summary input and press Enter: focus moves to Final comments, skipping the hidden address fields. Shift+Enter moves back to that address’s summary. From there, Alt+Enter opens the address and focuses Street. Shift+Space from an address field folds it and returns focus to the summary input; another Shift+Space opens it without losing that field.

Each list item is a form, and the input in its <summary> keeps sequential navigation in SmarkForm’s component tree. A native summary without a SmarkForm field can still toggle the disclosure, but Enter cannot resume from that native focus stop. The shared JSON editor is disabled so its controls do not extend the keyboard sequence shown here.

Using <details> as List Items

A powerful design pattern is to wrap each list item in a <details> element with the item’s key identifying field placed inside the <summary>. This way, even when collapsed, the key information remains visible so the user can quickly scan the list, while the full details expand on click.

SmarkForm fields inside the <details> work exactly as they would anywhere else — the browser handles the show/hide toggle natively. Shift+Space on a field inside a <summary> toggles its <details> section; on a field in the open body it folds the section and returns focus within its summary (to its first focusable child, or to the summary itself). Alt+Enter provides a separate way to navigate into a collapsed section. Use semantic markup and test focus and announcements with your target assistive technology.

The contacts example below preserves the key field inside each collapsed summary. Its co-located test checks Space/Shift+Space, forward/backward list navigation, and Alt+Enter / Alt+Shift+Enter into hidden fields.

🔗
❓
<div id="myForm">
  <div data-smark='{"name":"contacts","type":"list","min_items":1,"export_empties":false}'>
    <details>
      <summary>
        <input data-smark type="text" name="fullname" placeholder="Full name">
      </summary>
      <div class="contact-details">
        <input data-smark type="email" name="email" placeholder="Email">
        <input data-smark type="tel" name="phone" placeholder="Phone">
        <button data-smark='{"action":"removeItem","failback":"clear","hotkey":"Delete"}' title="Remove">🗑</button>
      </div>
    </details>
  </div>
  <button data-smark='{"action":"addItem","context":"contacts","hotkey":"+"}'>➕ Add contact</button>
  <textarea data-smark name="notes" placeholder="Notes about this contact list"></textarea>
</div>
#myForm {
  font-family: sans-serif;
}
#myForm details {
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-bottom: 6px;
  padding: 4px 8px;
}
#myForm summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
#myForm summary::before {
  content: "▶";
  font-size: .75em;
  transition: transform .15s;
  flex-shrink: 0;
}
#myForm details[open] > summary::before {
  transform: rotate(90deg);
}
#myForm summary input {
  flex: 1;
  min-width: 0;
  font-weight: bold;
  border: none;
  background: transparent;
  outline: none;
}
#myForm .contact-details {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 0 2px 1.2em;
}
#myForm .contact-details input {
  flex: 1;
  min-width: 120px;
}
#myForm .contact-details button {
  margin-left: auto;
}
#myForm textarea[name="notes"] {
  display: block;
  width: 100%;
  min-height: 60px;
  margin-top: 8px;
  padding: 6px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-family: inherit;
  box-sizing: border-box;
  resize: vertical;
}
/* Hold Ctrl to reveal hotkey hints. */
#myForm [data-hotkey] { position: relative; }
#myForm [data-hotkey]::after {
  content: "Ctrl+" attr(data-hotkey);
  position: absolute; top: 0.75em; left: 0;
  font-size: .7em; background: #333; color: #fff;
  padding: 1px 4px; border-radius: 3px; white-space: nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
  "contacts": [
    {"fullname":"Alice Smith","email":"alice@example.com","phone":"+1 555 100 0001"},
    {"fullname":"Bob Jones","email":"bob@example.com","phone":"+1 555 100 0002"},
    {"fullname":"Carol White","email":"carol@example.com","phone":"+1 555 100 0003"}
  ],
  "notes": ""
}
});// Native details plus SmarkForm keyboard navigation; no custom JS is needed.

Maintain a contact list with expandable details

Try it: Edit a contact name in the summary, expand its details, and use Enter, Shift+Enter, Alt+Enter, and Alt+Shift+Enter to test forward/backward navigation across collapsed list items.

The shared JSON editor is disabled because its controls would add extra fields and tab stops to this keyboard-navigation example.