Bootstrap 4


div is block element and by default it will be stacked. Bootstrap grid is mobile first, so three .col-md-4 will be stacked until desktop, so on desktop, large and extra large desktop it will be like three equal width columns. You do not need to define all 12, .col will occupy rest of place. .col-auto will occupy only that is needed (based on it’s contents, it uses max-width: 100%). When you specify for sm, you do not need to specify for md or lg. col-xs-12 is width: 100% which is default so no need to write that. Write only when you need columns. Gutter between columns (padding) is 30px, ie 15px on each side, so when nesting column inside column, first column will get blank space on its left (unneeded indent from 15px padding from first column and 15px padding from nested column) so that the reason why row has -15px margin (to fix eventual parent column padding) and you have to wrap row with .container class to compensate that (padding 15px) or to add class px-3, otherwise you will get horizontal scrollbar since body will be larger than 100% by 30px. Also for .container-fluid

Bootstrap Media queries:

  • xs - phones (no need for media queary since this is default)
  • sm - tablets @media (min-width: map-get($grid-breakpoints, 'sm') { // 576px (Bv3 768px) and up
  • md - desktops @media (min-width: @screen-md-min) { // 768px (Bv3 992px)
  • lg - larger desktops @media (min-width: @screen-lg-min) { // 992px (Bv3 1200px)
  • xl - extra lage @media (min-width: @screen-xl-min) { // 1200pxa

With bootstrap 4 you can use mixins for media queries:

// given (sm) and larger
@include media-breakpoint-up(sm) {
  .some-class {
    display: block;

// or other direction (given and smaller)
@include media-breakpoint-down(sm) {}

// only given size
@include media-breakpoint-only(sm) {}

// multiple sizes
@include media-breakpoint-between(md, xl) {}
@media (min-width: 768px) and (max-width: 1199.98px) { ... }

With Bootstrap3 you can use their predefined media queries with min-width (mobile first), for example

  .alert {
    position: absolute;
    bottom: 0px;
    right: 20px;

// or in other direction (given and smaller)
@media(max-width: $screen-sm) { }
// or better do not use boundary
@media(max-width: 575.98px) { }

Bootstrap container has fixed width for big devices.

Difference with bootstrap 3 is that there is new grid tier xl that was lg in B3 (sm is 576px instead of 768px in B3) and all are bumped up one level (so .col-md-6 in v3 is now .col-lg-6 in v4) Boostrap 3 you can use responsive available classes to toggle between display: block, inline, inline-block or to hide, for specific viewport size .visibe-sm-block .hidden-md .hidden-sm-up

In Bootstrap 4 you can use d-*-none class To hide on smaller than md (ie show on md and larger) use d-none d-md-block To show only on md use .d-none .d-md-block .d-lg-none

You can write your own to match all greater (or smaller) sizes:

# when it is max-width than it is Non-Mobile First Method
# so we use le (less or equal then)
# (min-width is Mobile First and we use hide-ge-)
/* Large Devices, Wide Screens */
@media only screen and (max-width : 1200px) {
  .hide-le-lg {
    display: none;
/* Medium Devices, Desktops */
@media only screen and (max-width : 992px) {
  .hide-le-md {
    display: none;
/* Small Devices, Tablets */
@media only screen and (max-width : 768px) {
  .hide-le-sm {
    display: none;
/* Extra Small Devices, Phones */
@media only screen and (max-width : 480px) {
  .hide-le-xs {
    display: none;
  • you can center bootstrap column with offset
  • you can set icons on inputs, easy with generators, just add prepend: 'password'

Bootstrap modal

On B3 you can use ajax and render layout: false and provide only inner html of <div class="modal"><div class="modal-dialog">... ie only .modal-content will be replaced. So you should use same header and footer in initial and ajax version.

On B4 we need to replace the modal content with custom js, so I use jBox modal in this case.

To close modal with escape key you need to add tabindex on modal elemenent: <div class="modal" tabindex="-1">. Note that on Firefox ESC closing modal works without this tabindex attribute. Note also that select2 dows

On activation button you do not need data-keyboard="true" since that is default. But remove data-keyboard="false" if exists and you want to close modal with escape key.

You can use autofocus="true" option to focus input field when modal shows up.

For tooltips you can use html version data-html="true" data-toggle="tooltip" title="<h2>Hi</h2>".

If you use ajax than modal content is cached, you can clear that cache on shown or hidden event (or in ajax response). Note that inline javascript will not be run again, cached content will be cleared (and modal will be loaded again) but new javascript will not be run for the same url… Actually it does not run tags in subsequent runs (only on first fetch).

$('#assign-location-modal').on('', function () {
# or for all, for example in
$(document).on '', '.modal', ->
  console.log "remove boostrap modal"

You can hide close modal with $('#my-modal').modal('toggle');

Usually fade makes some problem with positioning so you should not use fadding for custom modals.

Bootstrap tooltip

To inspect data-toggle="tooltip" you need to find it’s selector and manually call $('.myel').tooltip("show"); or in developer tools when you select <a title= element you can reference it with $0 so command is $($0).tooltip("show") or you can increase delay for hide

      hide: 100000

If it is inside element with overflow-y: scroll; than it won’t be seen if it goes over borders. You need to change container of the tooltip. You can do it simply for all tooptips by overwriting DEFAULTS (if you need more customization, you can overwrite any prototype of the class)

$.fn.tooltip.Constructor.DEFAULTS.container = 'body'
$.fn.tooltip.Constructor.DEFAULTS.placement = 'right'

Bootstrap input group and button group

Buttons and inputs can be grouped, but input group is not advised to group with select. Here is example of input with select and button.


Color helpers Primary colors are link primary, secondary, success, danger, warning, info, dark, light, white. For text there is muted. Background bg-dark text-light. There is a border corols border-white. You can set default body background by defining variable $body-bg: #bcdee3 before importing boostrap. You can use css darken for example for variable $link-hover-color: darken($link-color, 15%) !default;

  • text and links .text-success
  • background .bg-warning
  • buttons .btn-primary
  • borders .border-primary (border border-top-0 to disable, rounded-pill to add radius to border)

Other helpers are:

  • position helpers position-relative, position-absolute
  • .clearfix
  • text size .h1(2.5rem), .h2(2rem), .h3(1.75rem), .h4(1.5rem), .h5(1.25rem), .h6(1rem) so you can use h2 instead defining font-size-2
  • font-weight-bold to use bold font
  • text position.text-center, .text-left, .text-nowrap
  • text-truncate with max-width: 100px
  • .show and .hidden
  • .d-none to hide something. But when I want to show/hide in js I usually create my own .d-none-not-important since bootstrap’s is with important. I used hide-not-important with jQuery $ (hide) or $el.slideDown() (slideUp) but they use display: block which does not work well with display: flex elements, better is to use $(el).addClass('d-none-not-important'). There is also hidden
# app/assets/stylesheets/common/show_hide.sass
  visibility: hidden
  opacity: 0
  transition: visibility 0.5s ease, opacity 0.5s ease
  // display: none
    visibility: visible
    opacity: 1
    // display: initial
  • margin and padding helpers in format <property><sides>-<breakpoint>-<size> property is m margin or p padding sides t top, b bottom, l left, r right, x both l and r, y t and b size 0, 1 ($spacer * 0.25), 2 ($spacer * .5), 3 ($spacer), 4 ($spacer * 1.5), 5 ($spacer * 3), By default $spacer: 1rem 1rem is default font size ie 16px on B4 (1.5 line height) and 14px in B3 (1.428 line height)

    mx-auto for margin: 0px auto. sides or breakpoint not required: p-0 padding none, mt-5 margin top, but you can use mb-lg-5. Note that margins are not applicable for d-inline elements.

    For inline elements you can use float-left (instead of pull-left). For block elements with fixed width ml-auto to align to the right (same as pull-right in B3), mx-auto for horizontal centering (need fixed width). For block elements that are by default 100% width, you can wrap inside d-flex w-100 justify-content-between width 100% is important because you want right element to go right. (also justify-content-around justify-content-center justify-content-start).

    For cross axis it will be scretched so use d-flex align-items-center to center verticaly (or horizontaly if it is column based). You can also align for particular item align-self-center. Grow item with flex-grow-1 or flex-shrink-1.

  • d-flex class to convert to flexbox container. You can change display with d-inline, d-inline-block. Other flex helpers for example to enable wrap use d-flex flex-wrap. You can use those classes instead of row col. For example
    <div class='d-flex flex-column flex-md-row'>
    is similar to
    <div class='row'>
      <div class='col-md-6'></div>
      <div class='col-md-6'></div>
    but it will be justify-content-start(so no padding around columns )and size is depending on the content (not 50% as in col-6)
  • dl-horizontal can be replaces with
    <dl class='row justify-content-start'>
      <dt class='col-sm-2'>Sign In Count</dt>
      <dd class='col'><%= @user.sign_in_count %></dd>
      <dt class='w-100'></dt>
      <dt class='col-sm-2'>Last Sign In at</dt>
      <dd class='col'><%= @user.last_sign_in_at.to_s :short %></dd>
      <dt class='w-100'></dt>
  • to use variable unkown number of columns with multi line you can insert .w-100 where you want on specific responsive (for example on md you can insert after every 6 columnts)
    <div class="container">
      <div class="row">
        <div class="col">col</div>
        <div class="col">col</div>
        <div class="w-100"></div>
        <div class="col">col</div>
        <div class="col">col</div>

    Btw there are helpers for max-width .mw-100, view port .vh-100 (heigh: 100vh).

    another way is to use row-cols

    <div class="container">
      <div class="row row-cols-2">
        <div class="col">Column</div>
        <div class="col">Column</div>
        <div class="col">Column</div>
        <div class="col">Column</div>



There are two files that configure and override bootstrap

# app/stylesheets/application.sass
// default
@import 'common/variable_default_values'

// node_modules
@import 'bootstrap/scss/bootstrap'

// common
@import 'common/variables'
@import 'common/bootstrap_overrides'
# app/assets/stylesheets/common/bootstrap_overrides
// it is not actually overrides, but additional classes that modify existing

// this is used if you want btn inside h1
  font-size: inherit

  white-space: normal

Nabar shows different styles

Here is an example adding style for specific media for alert

// app/assets/stylesheets/common.scss
@import "bootstrap-variables";

.hidden-left {
  position: absolute;
  left: -1000px;

  .alert {
    position: absolute;
    top: 8px;
    padding: 8px;
    padding-right: 30px; // close sign
    z-index: 9999;
    left: 480px; // enought to see nav buttons

Still, default rails form build will render field_with_errors on label and input wrap, so if you need to change for bootstrap error classes. Note that with Bootstrap 3, you have to change control-group to form-group, add form-control to <input> elements, help-inline to help-block, and warning to has-warning or error to has-error. The easiest approach is with bootstrap form.

Bootstrap form

bootstrap_form gem rails-bootstrap-forms git (not old pluralize bootstrap_forms). Just add two lines

cat >> Gemfile << HERE_DOC
# adding bootstrap_form_for
gem 'bootstrap_form'

sed -i app/assets/stylesheets/application.css -e '1i\
 *= require rails_bootstrap_forms\
# or sass
cat >> app/assets/stylesheets/application.sass << HERE_DOC
// gems
@import 'rails_bootstrap_forms'

and use your bootstrap_form_for

You can use horizontal forms with layout: :horizontal (note that it won’t work for string layout: 'horizontal'). You can add options label_col: 'col-sm-4', control_col: 'col-sm-8'. Also you can override control_class, wrapper_class (if it is a hash you can override any option).

If you want all controlls and submit button to be in one line, you can use layout: :inline (note that only symbols works, string does not layout: 'inline'). For text_field maximum size use

<%= bootstrap_form_for @form, layout: :inline do |f|
  <%= f.text_field :name, size: 2 %>
<% end %>

Use placeholders instead of labels

<%= f.text_field :email, label_as_placeholder: true %>

For Select2, you can resolve with to the current value (for ajax you need to populate with prompt: 'Please select'). I need to populate with label so I add this helper

# app/form_builders/my_form_builder.rb
class MyFormBuilder < BootstrapForm::FormBuilder
  def select(method, choices = nil, options = {}, html_options = {}, &block)
    if options.delete :label_as_prompt
      options[:prompt] = form_group_placeholder(options, method)
      options[:prompt] ||= I18n.t('please_select')

you can make checkbox inline with button

  <%= f.check_box :remember_me, inline: true %>
  <%= f.submit t "sign_in" %>

You can change form-group wrapper class so to make two inputs inline you wrap with form-inline. If you need more customisation you can override wrapper class

<div class='form-inline'>
  <%= f.text_input :name, wrapper_class: 'form-group__with_select'

You can also use bootstrap row and cols or d-flex

<%= f.text_field :name %>
<div class='d-flex flex-row justify-content-between'>
    <%= f.text_field :address %>
    <%= f.text_field :city %>

You can use form to have horizontal layout, and some input group can be inline, so you can group input and select (note that is you have horizontal layout you can not use this since col-sm-2 and col-sm-10 will be added, look below for custom form builder).

<%# app/views/todos/_form.html.erb %>
<%= bootstrap_form_for @task, url: todos_path do |f| %>
  <div class="form-inline">
    <%= f.label :name %>
    <div class="input-group input-group__with-select">
      <%= f.text_field :name, input_group_class: 'input-group-under-group-with-select', skip_label: true %>
      <%= :id, 1..5, skip_label: true %>
<% end %>
# app/assets/stylesheets/application.scss
.input-group__with-select {
  .input-group-under-group-with-select,.form-group {
    position: initial;
    display: initial;
    border-collapse: initial;
  .form-control {
    width: 50%!important;

You can prepend or append strings or buttons

  <%= f.text_field :price, append: '$' %>
  <%= f.text_field :body, append: f.primary(t('send')) %>

Checkboxes or radio buttons should be inside a form-group if you want to show them indented (inside wrapper class) and to show label (control class) and help text

  <%= f.form_group label: { text: 'Admin', for: 'admin' }, help: 'This option enables admin' do %>
    <%= f.check_box :admin, hide_label: true, id: 'admin' %>
  <% end %>

To hide label instead of label: '' you need to use hide_label: true

Static text can be displayed with

<%= f.static_control :email %>
<%= f.static_control label: 'Custom Static Control' do %>
  Content here
<% end %>

You can use original rails form builder appending _without_bootstrap

  <%= f.text_field_without_bootstrap :name %>

Form builder

You can write your own form builder that extends for example rails-bootstrap-forms But since bootstrap_form_for (which is defined inside helper) calculate layout class (:inline or :horizontal) we need to use same helper (bootstrap_form_for ... builder: MyFormBuilder instead of form_for ... builder: MyFormBuilder). I noticed huge chrome memory problems (memory chrome is increasing when there is no form-horizontal class). To override some form elements you need to override bootstrap_form_for helper

# app/helpers/form_helper.rb
module FormHelper
  include BootstrapForm::Helper
  def bootstrap_form_for(object, options = {}, &block)
    options.reverse_merge! builder: MyFormBuilder
    super(object, options, &block)
  def bootstrap_form_with(options = {}, &block)
    options.reverse_merge! builder: MyFormBuilder
    super(options, &block)
  def options_for_select(container, attr = {})
    # do not show Const.not_specified option
    container = container.reject { |k, _| k.to_s.match Const.not_specified } unless attr[:show_not_specified]
    super container, attr

Oneliner form is using button_to with: input label, target url, form class…

<%= button_to t('notify'), notify_device_path(device), class: 'btn btn-sm btn-secondary', title: t('send_notification_to_this_device'), form_class: 'd-inline' %>

You can change default col-sm-2 control col class

# config/initializers/bootstrap_form.rb
module BootstrapForm
  class FormBuilder
    def default_label_col

    def default_control_col

Here is example of input with select base od example

# app/form_builders/my_form_builder.rb
class MyFormBuilder < BootstrapForm::FormBuilder
  def my_text_field(method, options = {})
    content_tag :div, class: "my-wrapper" do
      super method, options
  def text_field_with_select(method_for_text_input, options_for_text_input, method_for_select, choices, options_for_select = {}, html_options = {})
    content_tag :div, class: "from-group form-group__with_select" do
      form_group_builder method_for_text_input, options_for_text_input do
        select_without_group = form_group_builder_without_group(method_for_select, options_for_select, html_options) do
          select_without_bootstrap(method_for_select, choices, options_for_select, html_options)
        text_field_without_bootstrap(method_for_text_input, options_for_text_input) +

  # this will overwrite all f.submit form helpers. data-disable-with does not
  # work well when responding with csv so there you should explicitly disable:
  # <%= f.submit "Generate CSV", 'data-disable-with': nil %>
  def submit(name, options = {})
    options.reverse_merge! 'data-disable-with': "<i class='fa fa-spinner fa-spin'></i> #{name}"
    hidden_field_tag(:commit, name) +
      button(name, options)

  def single_image_upload(method, options = {})


  def form_group_builder_without_group(method, options, html_options = nil)
    # copy from original form_group_builder
    # bootstrap_form-2.5.2/lib/bootstrap_form/form_builder.rb
    html_options.symbolize_keys! if html_options

    # Add control_class; allow it to be overridden by :control_class option
    css_options = html_options || options
    control_classes = css_options.delete(:control_class) { control_class }
    css_options[:class] = [control_classes, css_options[:class]].compact.join(" ")

    options = convert_form_tag_options(method, options) if acts_like_form_tag

    wrapper_class = css_options.delete(:wrapper_class)
    wrapper_options = css_options.delete(:wrapper)
    help = options.delete(:help)
    icon = options.delete(:icon)
    label_col = options.delete(:label_col)
    control_col = options.delete(:control_col)
    layout = get_group_layout(options.delete(:layout))
    form_group_options = {
      id: options[:id],
      help: help,
      icon: icon,
      label_col: label_col,
      control_col: control_col,
      layout: layout,
      class: wrapper_class

    if wrapper_options.is_a?(Hash)

    unless options.delete(:skip_label)
      if options[:label].is_a?(Hash)
        label_text  = options[:label].delete(:text)
        label_class = options[:label].delete(:class)
      label_class ||= options.delete(:label_class)
      label_class = hide_class if options.delete(:hide_label)

      if options[:label].is_a?(String)
        label_text ||= options.delete(:label)

      form_group_options.merge!(label: {
        text: label_text,
        class: label_class,
        skip_required: options.delete(:skip_required)
    form_group_without_group(method, form_group_options) do

  def form_group_without_group(*args, &block)
    options = args.extract_options!
    name = args.first

    options[:class] = ["form-group", options[:class]].compact.join(' ')
    options[:class] << " #{error_class}" if has_error?(name)
    options[:class] << " #{feedback_class}" if options[:icon]

    control = capture(&block).to_s
    control.concat(generate_help(name, options[:help]).to_s)
    control.concat(generate_icon(options[:icon])) if options[:icon]


Data confirm modal

When rails use data-confirm='Are you sure?' it opens default browser’s builtin confirm(). Instead of that, you can open bootstrap 3 or 4 modal with

  • navbar is starting class which display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between (so if you have two childs like navbar-brand and navbar-toggler they will be on left and right side, but navbar-collapse has flex-grow: 1; flex-basis: 100% so that’s why it looks like it’s on left, also navbar-expand-lg has justify-content: flex-start).
  • navbar-light bg-light to add colors (it will change color of nav-links)
  • navbar-collapse contains all navbar. add navbar-expand-lg on navbar so on lg navbar-toggler dissapears, navbar-collapse expands and navbar-nav change from column to flex-direction: row. Inside navbar-collapse beside navbar-nav you can also nest one more navbar-nav ml-auto if you want to pull some links to right.


<div class="card" style="width: 18rem;">
  <img class="card-img-top" src=".../100px180/" alt="Card image cap">
  <div class="card-body">
    <h5 class="card-title">Card title</h5>
    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>

Cards occupy full width, so you need to set width manually. You can use card-group or card-deck (it has padding), or you can simply add d-flex flex-wrap to wrapped element. card-body is used for padding.

# app/assets/stylesheets/components/sizes.sass
  @include media-breakpoint-up(md)
    // single card on sm and start with two on a md(minus two margins m-2)
    max-width: map-get($grid-breakpoints, 'md') / 2 - to-px(2 * map-get($spacers, 2))

# app/views/cards/index.html
<div class='d-flex flex-wrap justify-content-center'>
  <% @tasks.all.each do |task| %>
    <div class='card m-2 card--max-width'>
      <div class='card-body'>

To add buttons to card using list group, use flush to remove some borders and render edge to edge, use btn to add hover icon

<div class='card'>
  <div class='list-group list-group-flush text-center'>
    <%= button_tag class: 'list-group-item list-group-item-action btn' do %>
      <i class="fa fa-pencil-alt" aria-hidden="true"></i>
      <%= t('edit') %>
    <% end %>

Login box

On responsive, instead of margin (margin is used with auto so it is centered on bigger screens) you can use max-width: 90vw or better is to wrap inside container with padding: 0.5rem. Since I like to put a logo and text which is larger than box, I use two times max with and mx-auto, one for wrapper (60rem) and one for white box (sm size).

# app/assets/stylesheets/common/sizes.sass
  max-width: 60rem

  max-width: 30rem

# app/views/layouts/application.html.erb
  <% if login_layout? %>
      <div class='text-center'>
        <%= link_to root_path do %>
          <%= image_tag 'cable_crm_logo.png', class: 'login-logo' %>
        <% end %>
        <h2><%= login_title %></h2>
      <div class='container'>
        <div class='login--wrapper-max-width mx-auto'>
          <div class="<%= 'login--max-width mx-auto bg-white shadow p-4' unless controller_name == 'companies' %>">
            <%= yield %>
  <% else %>

# app/assets/stylesheets/components/sidebar.sass
$sidebar-width: 20rem
    right: 0
  min-width: $sidebar-width
  max-width: $sidebar-width
  height: 100vh
  position: fixed
  top: 0
  right: -$sidebar-width
  background: #7386D5
  color: #fff
  transition: right 0.3s
  z-index: 9999

  display: none
  position: fixed
  width: 100vw
  height: 100vh
  background: rgba(0, 0, 0, 0.7)
  z-index: 998
  opacity: 0
  transition: all 0.5s ease-in-out
    display: block
    opacity: 1

  width: 35px
  height: 35px
  position: absolute
  top: 10px
  right: 10px

# app/views/layouts/_sidebar.html.erb
<nav id='sidebar'>
  <div class='sidebar__dismiss' data-toggle-active='#sidebar,.sidebar__overlay'>
    <i class="fa fa-times"></i>

  <ul class='list-unstyled'>
    <p>My App</p>
    <li class='<%= 'active' if controller_name == 'tasks' %>'>
      <%= link_to 'Past tasks', '#' %>
<div class='sidebar__overlay' data-toggle-active='#sidebar,.sidebar__overlay'></div>


  • create classes for text primary for list groups. There is only a color variants list-group-item-primary but I do not see variant with white background and blue text My fix is:
    // by defauls background is white, but when we add
    // text-primary if overrides that, so we need to define again
      color: #ffffff !important
        color: #ffffff !important
  • to check if postcss autoprefixer was applied you need to create <select class='custom-select'></select> and inspect that element. When there is no autoprefixer than it will contain appearance: none, if there is autoprefixed than it will be -mox-appearance: none or -webkit-appearance: none
  • badge label <span class="badge badge-primary">Primary</span>
  • use with webpacker is simply adding few lines of stylesheets
    # app/javascript/stylesheets/application.scss:
    @import "~bootstrap/scss/bootstrap";
    .rails-bootstrap-forms-date-select select,
    .rails-bootstrap-forms-time-select select,
    .rails-bootstrap-forms-datetime-select select {
      display: inline-block;
      width: auto;
    .rails-bootstrap-forms-error-summary {
      margin-top: 10px;