Skip to content

Bootstrap 6 support - #802

Draft
pardeyke wants to merge 42 commits into
bootstrap-ruby:mainfrom
pardeyke:v6
Draft

pardeyke wants to merge 42 commits into
bootstrap-ruby:mainfrom
pardeyke:v6

Conversation

@pardeyke

Copy link
Copy Markdown

Summary

Ports bootstrap_form to Bootstrap 6 (currently 6.0.0-alpha.1) and bumps the gem to 6.0.0.alpha1. Opened as a draft because Bootstrap 6 is still in alpha; the aim is to get the port in front of maintainers early and align on direction. Happy to split this into smaller PRs if that's preferred.

Markup changes

  • Form groups render the Bootstrap 6 form-field grid; horizontal and inline layouts keep mb-3. Labels come before controls, help text uses form-text.
  • Checks, radios and switches use the new check, radio and .switch wrapper markup. switch: accepts classes or attributes for the wrapper; card: true renders form-field-card.
  • Ranges render the form-range wrapper with form-range-input; range: sets wrapper classes and bubble: true.
  • Floating labels, input groups and form-adorn adornments follow the v6 docs, including floating labels nested inside input groups.
  • Validation: invalid_feedback:, valid_feedback:, tooltip: true and valid: true on every helper; is-valid/is-invalid placement per the v6 docs; form[data-bs-validate] client-side validation covered in the demo.
  • Help text gets an id and controls reference it with aria-describedby.
  • Buttons use btn-solid theme-*, alerts use theme-danger, legacy form-select is gone (v6 folds it into form-control).

New helpers

  • combobox (single/multi, search, groups, icons, descriptions, disabled items, combobox: hash for the data-bs-* options)
  • datepicker_field (popup, inline: true with a bound hidden input, button: trigger)
  • chip_input
  • otp_field
  • Password strength meter via strength: on password_field, including static level:

Demo app

Screenshots and README

The README examples and screenshot baselines were regenerated because the generated markup changed throughout; the screenshot diffs are intentional. Note the screenshot-commit step in the workflow is currently commented out, so CI won't add a screenshot commit here.

Checklist (CONTRIBUTING.md)

  • Tests added for every change (424 runs on Rails 8.0/8.1/edge, 419 on 7.2; all green locally with Ruby 4.0.7)
  • rubocop reports no offences
  • README updated (install notes for Bootstrap 6, all new options and helpers)
  • CHANGELOG entry under Pending Release
  • Demo app bin/rails test:all green (18 runs)

Notes for reviewers

  • demo/.ruby-version is set to 4.0.7 rather than the oldest supported Ruby; CI still runs the matrix down to 3.2.
  • Rails 7.2 doesn't apply the form namespace to an explicit label for (Rails 8 does), so the group label namespaces it itself on Rails 7.
  • The turbo_submits_with option that was on this branch earlier has been removed from it; that change lives in add option submits_with to button #783.

🤖 Generated with Claude Code

pardeyke and others added 30 commits October 16, 2026 10:33
Update generated markup for the upcoming Bootstrap 6 release, per the
v5-to-v6 migration guide and the v6 forms documentation:

- Selects: use form-control instead of form-select
- Checkboxes/radios: form-field wrapper with check/radio input classes,
  labels without form-check-label; inline via d-inline-grid utilities
- Switches: input wrapped in div.switch with role="switch" and the
  native switch attribute
- Range fields: div.form-range wrapper with form-range-input class
- Floating labels: label rendered before the control
- Buttons: btn-solid theme-primary/theme-secondary instead of
  btn btn-primary/btn-secondary
- Grid: new breakpoint prefix syntax (sm:col-2, sm:offset-2, sm:me-2)
  for default label/control cols and offset calculation
- Alerts: alert theme-danger instead of alert alert-danger

Update test suite and README examples to the new markup, add
UPGRADE-6.0.md, and bump version to 6.0.0.alpha1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ecdd35f)
Bootstrap 6 ships a datepicker component activated by
data-bs-toggle="datepicker" on a text input. Add a datepicker_field
helper that delegates to text_field and renders the :datepicker option
hash as data-bs-* attributes (Date values as ISO 8601, arrays as JSON).
autocomplete="off" is set by default per the Bootstrap docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e4275d5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 37015a1)
Bootstrap 6's form-field class is a CSS-grid wrapper that lays out a
control with its label, help text, and validation feedback — exactly
what bootstrap_form's form groups contain. Emit it by default:

- Vertical form groups: form-field mb-3 (was mb-3)
- Grouped collections (group_around_collections): form-group mb-3,
  Bootstrap 6's wrapper for multiple form-fields with a shared label
- Horizontal, inline, and floating-label groups are unchanged since
  they lay out via grid columns, utilities, or form-floating, which
  would conflict with the form-field grid

Also drop text-muted from help text (removed in Bootstrap 6; form-text
styles itself).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit abe45e3)
Bootstrap 6's combobox is a native select-like control with optional
search and multiple selection — a plain-Bootstrap replacement for
plugins like use-bootstrap-select. The new combobox helper generates
the toggle button, menu items with data-bs-value, pre-selected state
(from the object value or the :selected option), the search input and
no-results block, and the multiple-selection check icons. Bootstrap's
JavaScript handles the hidden input for form submission via
data-bs-name.

The chips component was considered for multiple-value display, but as
of 6.0.0-alpha1 it has no form-submission integration, so no chip
mark-up is generated; this is documented in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c33f6a5)
Thorough pass over the remaining v6 forms documentation pages:

- form-adorn: new `adorn` option on text-style helpers renders an icon
  or text decoration inside a form-control form-adorn wrapper with a
  form-ghost input (leading or trailing, with custom wrapper classes)
- otp-input: new otp_field helper renders the .otp[data-bs-otp]
  container around a single text input (slots from maxlength, config
  via the :otp option as data-bs-* attributes, is-invalid on the
  container on errors, autocomplete="one-time-code" by default)
- password-strength: new `strength` option on password_field renders
  the segmented meter or strength-bar, with optional strength-text and
  data-bs-min-length
- validation: `validate: true` form option emits data-bs-validate and
  novalidate on the form; checkbox/radio/switch feedback (and new help
  text support) now nests in form-field-content per the v6 validation
  docs; input-group feedback moves outside the input-group, a sibling
  in the form-field wrapper
- input-group: document that buttons in input groups need the v6
  input-group-btn class

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 8605392)
(cherry picked from commit dae8c29)
The demo resolves Rails through the root Gemfile's open-ended
REQUIRED_RAILS_VERSION constraint and now runs on Rails 8.1.4. Its config
was already generated by Rails 8.x and loads framework defaults
dynamically, so only the README heading needed to change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 22aed46)
The demo builds its JavaScript with esbuild from app/javascript/application.js
and its CSS with Sass. The Webpacker config, webpack runner scripts, an unused
webpack.config.js, the packs/ and channels/ entry points, the PostCSS config
and a webpack license artifact were all dead. Drop the turbolinks, Action
Cable and Active Storage npm packages that only those files referenced, and
the public/packs ignore rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit a652def)
Pin bootstrap to the v6 alpha so the demo renders the markup this branch
generates. v6 replaces Popper with Floating UI and adds vanilla-calendar-pro
for its datepicker, so swap the peer packages accordingly. Its stylesheets
use the Sass module system and need Sass 1.103+, so bump sass and switch the
demo stylesheets from @import to @use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit c16eb94)
Bring @floating-ui/dom, @rails/actiontext, bootstrap-icons, esbuild and trix
up to their latest releases. The build scripts are unchanged and the demo
builds and renders as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 8e9abae)
(cherry picked from commit a34d0eb)
Add a demo section per page of the Bootstrap 6 forms documentation: form
field, form controls, disabled form, checkboxes, radios, switches, range,
input groups, extended floating labels, form adorn, chips, combobox,
datepicker, OTP input, password strength, layout variants, client-side
validation and server-side validation states for every supported element.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 5cfa3d7)
Rails prefixes a form's namespace onto label `for` attributes and input ids,
but the combobox toggle is rendered directly, so its id was left unprefixed
and the label pointed at "ns_ns_user_status". Keep the id Rails sees
unprefixed and prefix it only on the toggle button. Also key the error
feedback off the same id so aria-describedby matches.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 58f7a25)
The hidden field that collection_check_boxes prepends shared its id with
the group's label div, so every collection produced duplicate ids. Render
it with hidden_field_tag and no id, as Rails does for its own helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 1a7b127)
The id passed to collection_check_boxes or collection_radio_buttons was
deleted before rendering. Use it for the group's label id and
aria-labelledby, and derive the shared error feedback id from it, so two
collections of the same attribute on one page can be given distinct ids.
The inputs themselves keep their value-based ids.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 10f8644)
Help text now carries an id derived from the control (or the group for
collections), and the control's aria-describedby lists it alongside any
validation feedback id, as Bootstrap's accessibility guidance recommends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 27c6ec8)
Render the attribute's values as dismissible chips inside a chip-input
container with a form-ghost text input, configured through a `chips` hash
of data attributes like the OTP helper. Bootstrap has no submission
integration for chips, which the docs now spell out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 9b30d6c)
Bootstrap 6 puts theme colors, sizes and custom widths on the `switch`
wrapper rather than the checkbox. Accept a string of classes or a hash of
attributes in the `switch` option so they can be set from the helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit e79f065)
Bootstrap 6 puts theme classes and `data-bs-bubble` on the `form-range`
wrapper. Accept `range: "theme-primary"` or
`range: { bubble: true, class: "..." }` on range_field to set them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 979b940)
Support option groups rendered as menu headers, per-item icons,
descriptions and disabled state, optional check icons for single select,
a disabled toggle via the html options, and a `combobox` hash for
placement, offset, boundary and accent-insensitive search.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit abfc784)
`inline: true` renders an always-visible calendar in a div with a hidden
input that Bootstrap binds for form submission, pre-selecting the
attribute's value. `button:` renders Bootstrap's button trigger with a
display span, optionally with an icon.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 8ac394b)
`strength: { level: :good }` renders Bootstrap's CSS-only strength meter
with the level in data-bs-strength and the matching segments active, and
`text:` accepts a string for a static message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 1841dac)
Combining `floating: true` with `prepend` or `append` now renders the
form-floating wrapper (label and input) inside the input-group, with the
validation feedback as a sibling in the form field, matching the docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 5666781)
Every helper now accepts `invalid_feedback:` and `valid_feedback:` for
browser-validation messages (a model error replaces the static invalid
one), `tooltip: true` for Bootstrap's tooltip-style feedback, and
`valid: true` for the is-valid success state. Feedback rendering is
centralised in generate_feedback so text fields, selects, checks, radios,
OTP, combobox, chip and datepicker fields all behave the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 08a4d89)
The form-adorn wrapper is now a label with the adornment aria-hidden, as
in the Bootstrap docs, and it carries the is-invalid/is-valid state along
with the ghost input.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 71a3760)
`card: true` renders Bootstrap's form-field-card variant for a checkbox or
radio, which with `help` gives the docs' group cards. A form_group label
whose `for` matches no element in the group (e.g. above a set of radios
or a date_select) now has the attribute removed instead of pointing at
nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 27bbd71)
…trap's JS

Bootstrap auto-initialises every strength meter and re-evaluates the
neighbouring password field, which overwrote a static `level:`; a static
meter now points data-bs-input at a selector that matches nothing.
Rendering an inline calendar into the datepicker element wiped the hidden
input Bootstrap binds for submission, so the calendar is now mounted on a
child element through data-bs-position-element.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 031a52e)
Drive the combobox, chips, OTP, datepicker, strength, range, switch, card
and validation examples like a user and assert on the resulting values.
Add Bootstrap's client-side validation starter script to the demo layout
so invalid forms don't submit, and fix the README adorn example that
referenced a helper the demo doesn't have.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit ae2b25e)
Apply RuboCop's corrections to the component system test, give the range
bubble examples room above the track, and size the datepicker button
triggers to their content.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit 6c723ea)
pardeyke and others added 12 commits October 16, 2026 10:33
Bootstrap's floating label is positioned against the full height of its
wrapper, so help text or validation feedback inside `.form-floating`
pushed the label onto the value. Floating fields now render a regular
form-field group with a form-floating wrapper around only the label and
control (inside the input group, if any), and the label is generated
before the group strips the label options so custom text is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit d16ab30)
Run the demo's README and index screenshot system tests against the
updated gem and demo: the README's generated HTML blocks now reflect the
current mark-up (help ids, floating label structure, chips, combobox,
datepicker and validation helpers), and the index baselines cover all 29
demo sections. The index test grows the browser window to each section's
height so tall sections can be cropped, and the combobox reads the
attribute value only when the object responds to it, so README examples
with non-model attributes render.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwMXFtvXUNo65gxKB2X2nG
(cherry picked from commit bd82efb)
The demo layout carried an inline copy of the datetime-select rule the
gem ships in app/assets/stylesheets/rails_bootstrap_forms.css. Add the
gem's stylesheet directory to the Sass load path and @use the stylesheet
from the demo's bundle instead, which also exercises the install path
the README documents.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 68e4dd1)
Every other npm package is already on its latest release; Bootstrap
stays on the 6.0.0-alpha.1 "next" tag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 9c08aa8)
(cherry picked from commit f1ca36f)
The JavaScript bundle imports Trix, but its CSS was never loaded: the
two Action Text stylesheets left over from the Sprockets setup were not
part of the Sass bundle, so the rich text editor rendered unstyled.

Rewrite actiontext.scss for the Sass module system, @use it from the
bundle entry, and drop the redundant actiontext.css. Update the two
screenshots that show the editor.

The datepicker system test now closes the popup before using the inline
calendar: Bootstrap keeps the popup open after a pick, and dismissing it
with a click elsewhere refocuses and scrolls to its input mid-click.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit bd32281)
Replace the hand-written Sass wrapper with the stylesheet that
`rails action_text:install` generates for Rails 8.1, which inlines the
Trix styles and the attachment gallery overrides as plain CSS. Sass
loads it into the bundle through the existing @use.

The install generator also produced Active Storage and Action Text
migrations; those tables already exist in db/schema.rb, so the
migrations were dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f213b3e)
…iables

Bootstrap 6 writes its custom properties unprefixed in Sass and relies on
postcss-prefix-custom-properties to add the `bs-` prefix at build time.
The demo compiled the Sass directly, so the CSS read `--range-fill`
while Bootstrap's range plugin wrote `--bs-range-fill`: the track never
filled and the value bubble stayed pinned at the start of the slider.

Follow cssbundling-rails's Bootstrap recipe: split the CSS build into a
Sass compile and a PostCSS pass (prefixing plus Autoprefixer), and watch
with nodemon in Procfile.dev. The range system test now asserts that the
bubble moves with the value, and the range screenshots are regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7eb80ae)
Wrap the spinner markup and a long script line, use a hash assignment
instead of merge!, and order the gems in the 8.0 gemfile, so that
`rubocop` reports no offences and CI passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 4fc558b11aea4aea06830461ef4e33ad76c0334f)
Point the install section at the Bootstrap 6 pre-release and its peer
dependencies, explain the PostCSS prefixing step that Bootstrap 6
requires when compiling its Sass, and note that the bootstrap rubygem
doesn't package Bootstrap 6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 1951c2894b3a481d7ca0f4cde99edb5a4b8d08e8)
Rails 7.2 leaves an explicit label `for` untouched while the control's id
gets the form namespace, so the group label pointed at a non-existent id
and was detached. Namespace it ourselves on Rails 7.

The new tests also expected hidden fields without `autocomplete="off"`,
which only Rails 8.1 drops; use the existing helper for that attribute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 6cbfa677f646a0c32b6465c2af5cc071edee15be)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lcreid

lcreid commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Super interesting work, @pardeyke . Thank you. I will need some time to digest this. I've always felt that the implementation of bootstrap_form was tied too much to the structure of Bootstrap 3, and was thinking that Bootstrap 6 would be the time to change it. But let me look at Bootstrap 6 and think about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants