bh writes the Bootstrap 6 markup your Rails form was asking you to type
Bootstrap Helpers for Ruby
At a glance
- What is it?
- bh is a Ruby gem that dresses Rails forms in Bootstrap 6, adds helpers for the components Bootstrap ships behaviour for rather than markup, and serves its own stylesheet, script and nine palettes, and the version pin it asks for is a warning about its own stability.
- Who is it for?
- bh suits a Rails team that has decided on Bootstrap 6 and would rather not hand write the markup Bootstrap asks for, and It is the wrong choice for a project on another component library, and it is the wrong choice for a team unwilling to read the CHANGELOG, because the gem names three kinds of release and the README says the current line still breaks on a minor rather than a major.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 26 days ago.
- What is it written in?
- Mainly CSS, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Bootstrap asks for a lot of HTML, and bh writes it
The README states the problem plainly: Bootstrap is a fine framework that asks for a lot of HTML. Bh answers with a form builder that dresses every field a page can ask for, helpers for the components Bootstrap ships behaviour for and no markup, and the one stylesheet and one script that carry them, served by the engine. The components named are a combobox, a 6-digit code, a dialog, a toast and a thread of messages, and the point is not that Bootstrap cannot do those things but that Bootstrap provides the behaviour and leaves the markup to you. The gem's contribution is to remove that work from the view layer. The audience is a Rails team on Bootstrap 6 who wants forms and components without the div soup, and the demo page linked from the README draws every component beside the line of Ruby that produces it.
The form builder is one line in the application controller
Adopting the dressed forms is a single declaration in the base controller.
class ApplicationController < ActionController::Base
default_form_builder Bh::FormBuilder
endAfter that, the field helpers in the table become available, and the design rule the README states is that a view should say which kind of field it wants and nothing about how it looks. Every text_field, text_area, select and check is dressed at one size, and the specialised ones add their own behaviour: phone_field is a North American number shaped 555-555-5555 as it is typed, price_field and percentage_field are money and a share of a hundred typed into Bootstrap's adorned control, pin_field is one real field drawn as six slots, and combobox is a select drawn as a searchable menu while remaining the one thing the form submits. The rest of the table covers the furniture, fieldset, label, submit and button, plus the behavioural helpers toasts, chat_with, confirm_button_to and tooltip_data.
The engine serves the assets, and the layout links them
With the gem in the bundle, the app serves the stylesheet and the script itself, at the prefix its engine is mounted at, and a layout links both.
<link rel='stylesheet' href='<%= Bh::Engine::PREFIX %>css/bh.css'>
<script type='module' src='<%= Bh::Engine::PREFIX %>js/bh.js'></script>Nothing has to be vendored or copied into the public directory, which matters because the CSS and the JavaScript are versioned together with the helpers. The JavaScript arrives as a module, and the package.json at the repository root shows where it comes from: @hotwired/stimulus at 3.2 or newer, @hotwired/turbo-rails at 8.0 or newer, sortablejs for the drag behaviour, and esbuild as the build tool behind the node build.mjs script. That last file and the package-lock.json are the sign of the architecture. The gem is not only Ruby. A Stimulus based front end is built with esbuild and shipped as an asset, and the repository lists 22 Stimulus controllers registered by the bundle and called by data-controller.
Nine palettes, contrast ratios and the bootstrap option that declares nothing
The palettes are served at /bh/theme/<name>.css and linked one at a time after the stylesheet, and there are nine: bootstrap, dawn, dracula, gruvbox, monokai, nord, one_dark, solarized and tokyo_night. Each one restates all thirteen steps of every Bootstrap family it repaints, plus the white, black and three text tone variables. The README states two rules about the result: every accent clears 3:1 against its label, and every text tone clears 4:1, in both modes. Those are the WCAG contrast minimums for large text and non-text content respectively, so the palettes are checked rather than eyeballed. The bootstrap option is the odd one out, and the README explains why: it declares nothing, because the upstream palette comes back by dropping the other blocks rather than by writing one, and it is named so a toggle can reach it. The consequence for a user is that rotating through nine palettes and never finding the one the pages started in means you have been shown a door with no handle on the inside.
The scheme controller rotates palettes, and the choice is the reader's
The swap is handled by a Stimulus controller, and the button carries the list of themes as a data attribute.
<button type='button' data-controller='scheme' data-action='scheme#rotate'
data-scheme-themes-value='["bootstrap","dawn","dracula","gruvbox","monokai","nord","one_dark","solarized","tokyo_night"]'
data-scheme-path-value='/bh/theme' data-scheme-storage-value='scheme'>
Another palette
</button>A click moves to another palette and into the mode it is not in, and the selection is stored under the key given by data-scheme-storage-value. The one implementation detail the README insists on is timing. The stored choice has to be put back before the first paint, from the layout's own script, because a controller connects far too late for that and the page would otherwise flash the palette the server chose. That is a real, avoidable bug class, and the fix is a single script in the layout rather than a change to the controller.
The version pin is a statement about the API, not about a bug
Installation is one gem install, and the README shows the bundle entry with a specific constraint.
gem install bhgem 'bh', '~> 6.9.0'The README explains the reasoning directly: the ~> 6.9.0 constraint stops short of 6.10, and that is the pin to hold while this line still breaks on a minor rather than on a major. So the semver promise is inverted from the usual reading. A patch release will not break you, a minor will, and the pin exists to hold you on the last known good minor. The CHANGELOG is described as showing what each release is and which of the three it is, which is a clue that the project tracks three categories of release. There are no GitHub releases, the repository has a Rakefile and a .rubocop.yml, and the gemspec is bh.gemspec. The last push was on 2026-09-27, so the code is being worked on, but the version you get is a decision you have to make deliberately.
Where bh is the wrong choice
The limits are mostly consequences of the scope. The gem is Bootstrap 6 only, so a project on another component library gets nothing from it, and the helpers are written against Bootstrap's class names and DOM structure, so they are not portable. The second limit is the 6.x line itself, and the README is upfront about it: a minor release can break your build, so a team that upgrades on a schedule will find that schedule interrupted, and a team that pins forever accumulates a backlog. The third limit is what a Rails engine cannot do, which is render on the client. The components are server rendered and their behaviour is carried by Stimulus controllers, which is the right call for a server rendered app but means the components are not usable from a React island without reimplementing them. The fourth is the size of the bet. The whole visual language of a site, buttons, fields, palettes, dialogs, toasts, is being taken from a gem that is pre-1.0 in its line semantics, and a rewrite away from bh means revisiting every view.
Editorial conclusion
bh suits a Rails team that has decided on Bootstrap 6 and would rather not hand write the markup Bootstrap asks for, and It is the wrong choice for a project on another component library, and it is the wrong choice for a team unwilling to read the CHANGELOG, because the gem names three kinds of release and the README says the current line still breaks on a minor rather than a major. Before adopting it, pin the version the way the README does, read the page at claudiob.github.io/bh/ to see every component drawn next to the Ruby that produces it, and check the scheme controller's storage behaviour against your own layout, since the palette choice has to be restored before the first paint from the layout's own script. The last push was on 2026-09-27 and the repository publishes no GitHub releases.
Frequently asked questions
How do I install the bh gem in a Rails app?
Install it with gem install bh, then add gem 'bh', '~> 6.9.0' to the Gemfile. The README notes that the constraint stops short of 6.10 and is the pin to hold while the line still breaks on a minor rather than on a major.
How do I enable the Bootstrap dressed form builder in Rails?
Set default_form_builder Bh::FormBuilder in your ApplicationController. The field helpers, from text_field to phone_field, price_field, pin_field and combobox, then follow the form's own furniture rules rather than per view styling.
Which colour palettes does bh ship?
Nine, served at /bh/theme/<name>.css and linked one at a time after the stylesheet: bootstrap, dawn, dracula, gruvbox, monokai, nord, one_dark, solarized and tokyo_night. The README states every accent clears 3:1 against its label and every text tone 4:1, in both modes.
How does the bh scheme controller switch palettes?
A button with data-controller='scheme' and data-action='scheme#rotate' carries the theme list, the theme path and the storage key as data values. A click moves to another palette and into the mode it is not in, and the stored choice must be restored before the first paint from the layout's own script to avoid a flash of the server chosen palette.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/claudiob-bh)Was this useful?