Coding a responsive Navbar with Rails

Pablo Curell Mompo
9 min readFeb 13, 2021

In this article, we will learn how to code a reactive navbar using rails.

If you want to follow along, here is the point in time where I start writing this article: https://github.com/pablocm90/learning-port/tree/pre-navbar.

To do so, we want to imagine at least roughly how our navbar should look like when seeing it from a computer and a tablet, or phone.

Then we will code the style for four pre-determined breakpoints.

Finally, we will add some interactivity.

Rough sketch of what we want our navbar to look

When sketching the first pass of an element, especially for a personal project, I like to do it with pencil and paper.

It usually does the trick, and I can go to a more detailed sketch with Figma or a similar tool later.

I usually won’t use any other tools in practice, and I will try out different options directly on the browser.

Small sketch handmade
handmade sketch

As you can see above, we are not looking to do anything fancy. The navbar will feel like many navbars you have seen. I know at this point that:

  1. I want some movement when you clock on the trigger on mobile.
  2. I want some fancy font for the links.

Coding, the fun begins!

Quick disclaimers:

  1. For the CSS and the class naming, we will be using the BEM methodology. I will not bother you with the explanation, but by all means, if you are interested, do visit their website :)
  2. We code our views using .slim. You can accomplish the same in .erb
  3. We will aim to write our code using TDD.
  4. To test painlessly, we use guard, in a terminal type: bundle exec guard -g green_red_refactor

This is the situation at the top of our website right now.

Image of the top of the website before coding
before we start coding

To see it in your browser, navigate to http://localhost:3000.

Let’s start by listing the different Blocks and Elements that we will use:

  • One `navbar` Block that will contain the rest of the elements
  • One `navbar__logo` element
  • One `navbar__links` element
  • Several `navbar__link` elements
  • One `navbar__close` element

We might find that we have forgotten some later, but these should be the big ones.

To test them quickly in all pages that will need a navbar, we can create an RSpec shared example group.

➜ learning-port: mkdir spec/features/shared && touch spec/features/shared/it_has_a_navbar_spec.rb

in this file, lets code our first test

# spec/features/shared/it_has_a_navbar_spec.rbshared_examples_for ‘it has a navbar’ do it ‘should have a navbar’ do  expect(page).to have_selector(‘.navbar’) endend

For it to do something, we will test it on the only page we have.

# spec/features/visiting_home_spec.rb
# frozen_string_literal: true
require ‘rails_helper’require_relative ‘shared/it_has_a_navbar_spec’RSpec.describe ‘Visiting home’, type: :feature do describe ‘Visiting home page’ do before(:example) do visit root_path end it_behaves_like ‘it has a navbar’ [rest of the code] endend

On saving the file, we should have one error:

Image of a console with a failing test
failing test

That is good, and now we can ‘make it green.’

let us add the navbar class to the navbar partial

# app/views/shared/_navbar.html.slim
.navbar

And

# app/assets/stylesheets/navbar.scss
@import ‘colors’;.navbar { height: 100px; background-color: $dark;}

If we save now our spec file, we will have all passing tests.

Image of a console with passing tests
All tests are passing!

And in our browser:

Image of top of website with a blue strip
Here is our blue bar!

Let’s now get a logo in there!

# spec/features/shared/it_has_a_navbar_spec.rb
shared_examples_for ‘it has a navbar’ do it ‘should have a navbar’ do expect(page).to have_selector(‘.navbar’) end it ‘should have a logo’ do expect(page).to have_selector(‘.navbar__logo’) endend

Right now, we should again have a failing test. Let’s add our logo. I am not rich enough to have a professionally made logo, but for now, let’s use an icon from fontawesome(link to fontawesome)

# app/views/shared/_navbar.html.slim.navbar i.navbar__logo.fas.fa-blog# app/assets/stylesheets/navbar.scss@import ‘colors’;.navbar { height: 7vh; background-color: $dark; display: flex; align-items: center;} .navbar__logo {  color: $accent;  font-size: 3rem;  margin-left: 2rem; }

As it stands now, we have a passing test (so we will not remove the logo by mistake, and if you go to your browser, you will see that it is responsive as well:

It is time to start adding the links. Here I will use the same process (writing a failing test, making it pass, adding the style), but I will skip ahead to the resulting files for your benefit:

# spec/features/shared/it_has_a_navbar_spec.rb# frozen_string_literal: true  
shared_examples_for ‘it has a navbar’ do
it ‘should have a navbar’ do expect(page).to have_selector(‘.navbar’) end it ‘should have a logo’ do expect(page).to have_selector(‘.navbar__logo’) end context ‘with links’ do it ‘should have space for some links’ do expect(page).to have_selector(‘.navbar__links’) end it ‘should have link for sign_in’ do if page.current_path == new_writer_session_path expect(page).not_to have_selector(‘a.link.navbar__link’, text: ‘Sign in’) expect(page).to have_selector(‘p.link.navbar__link.navbar__link — current’, text: ‘Sign in’) else within(‘.navbar__links’) do click_link(‘Sign in’) expect(page.current_path).to eq(new_writer_session_path) end end end it ‘should have link for home’ do if page.current_path == root_path expect(page).not_to have_selector(‘a.link.navbar__link’, text: ‘Home’) expect(page).to have_selector(‘p.link.navbar__link.navbar__link — current’, text: ‘Home’) else within(‘.navbar__links’) do click_link(‘Home’) expect(page.current_path).to eq(root_path) end end
end
end
end
# app/views/shared/_navbar.html.slim.navbar i.navbar__logo.fas.fa-blog .navbar__links = link_to_unless_current ‘Home’, root_path, class: ‘link link — white navbar__link’ do p.link.link — white.navbar__link.navbar__link — current Home - if current_writer = link_to_unless_current ‘Sign out’, destroy_writer_session_path, class: ‘link link — white navbar__link’, method: :delete do p.link.link — white.navbar__link.navbar__link — current Sign out -else = link_to_unless_current ‘Sign in’, new_writer_session_path, class: ‘link link — white navbar__link’ do p.link.link — white.navbar__link.navbar__link — current Sign in

Here I would like to point out rails’ helper method link_to_unless_current. This helper method will print out the link in the method unless you are on it already. In that case, if you don’t provide a block, it will not return anything. If you provide a block, it will return the result of the block.

# app/assets/stylesheets/navbar.scss
@import ‘colors’;
.navbar { height: 6rem; background-color: $dark; display: flex; align-items: center; justify-content: space-between;} .navbar__logo { color: $accent; font-size: 3rem; margin-left: 2rem; } .navbar__links { display: flex; margin-right: 2rem; } .navbar__link { font-family: ‘Permanent Marker’, cursive; margin: 0 1rem; font-size: 2rem; } .navbar__link — current { text-decoration: underline .3rem; &:hover { color: $white !important; } }# app/assets/stylesheets/text/link.scss
@import ‘colors’;.link { color: $link; &:hover { color: $accent; }} .link — white { color: $white; }

Here is the state of the navbar at the moment:

gif of the website at diferent breakpoints
testing the responsiveness with the links

You will see that right now, it is entirely responsive, but I am feeling fancy. I would like to have a selector to open a covering once we are on mobile. To accomplish that, we need to start by hiding the links after a certain screen-width threshold has been met and making the selector appear at that point.

Let’s start by making sure we can test up to that threshold. After searching a bit, I came across this article . their approach of making a helper seems sound; let’s create ours in :

# spec/features/helpers/responsivity.rb
# frozen_string_literal: true
module Responsivity def resize_window_to_mobile resize_window_by([640, 480]) end def resize_window_to_tablet resize_window_by([960, 640]) end def resize_window_default resize_window_by([1024, 768]) end private def resize_window_by(size) return unless Capybara.current_session.driver.browser.respond_to? ‘manage’ Capybara.current_session.driver.browser.manage.window.resize_to(size[0], size[1]) endend

we will also have to change our spec file:

# spec/features/shared/it_has_a_navbar_spec.rb
# frozen_string_literal: true#We add our new helperrequire_relative ‘../helpers/responsivity’RSpec.configure do |config| config.include Responsivityendshared_examples_for ‘it has a navbar’ do it ‘should have a navbar’ do expect(page).to have_selector(‘.navbar’) end it ‘should have a logo’ do expect(page).to have_selector(‘.navbar__logo’) end context ‘with links’ do context ‘while on a big screen’ do # The good old code we had before to check our ‘computer examples end context ‘when smaller than a tablet’ do before(:context) do Capybara.current_driver = :selenium resize_window_to_tablet end after(:context) do Capybara.use_default_driver resize_window_default end it ‘should not have the links’ do expect(page).not_to have_selector(‘.navbar__links’) end end endend

At this point, you should have one failing test and eight passing once you run the specs.

You will also notice that we have before and after blocks. We do that because:

  1. We want to change capybara’s driver since the default rack_test driver considers a ‘hidden’ element to be present on the page
  2. to change our screen’s width to test it.

We want to use as much as possible rack_test since you will notice that it is a fair bit faster than selenium.

We are now ready to make our tests pass:

# app/assets/stylesheets/navbar.scss[previous code]@media only screen and (max-device-width: 960px ) { .navbar__links {  display: none; }}

As you will see from making the specs run (and from the gif below), the links disappear when we reach 960 px.

Links disappear ! Winning !

Let’s now code the selector to open our link pane appear.

# spec/features/shared/it_has_a_navbar_spec.rb
# Inside our ‘when smaller than a tablet’ contextit ‘should have a selector to open a link pane’ do expect(page).to have_selector(‘.navbar__pane-opener’)end# app/views/shared/_navbar.html.slim.navbar i.navbar__logo.fas.fa-blog .navbar__links = link_to_unless_current ‘Home’, root_path, class: ‘link link — white navbar__link’ do p.link.link — white.navbar__link.navbar__link — current Home - if current_writer = link_to_unless_current ‘Sign out’, destroy_writer_session_path, class: ‘link link — white navbar__link’, method: :delete do p.link.link — white.navbar__link.navbar__link — current Sign out -else = link_to_unless_current ‘Sign in’, new_writer_session_path, class: ‘link link — white navbar__link’ do p.link.link — white.navbar__link.navbar__link — current Sign in .navbar__pane-toggler.link.link — white.fas.fa-hamburger#pane-opener# app/assets/stylesheets/navbar.scss@import ‘colors’;[…].navbar__pane-toggler { display: none;}@media only screen and (max-device-width: 960px ) {[…] .navbar__pane-toggler { margin-right: 2rem; font-size: 3rem; cursor: pointer; }}

Now to the interactive part, when we click our hamburger, we want:

  1. To cover the screen in blue and make our links appear
  2. Hide the burger
  3. Show a closing ‘X’.

Since there is nothing too complicated there, we can do it with plain old javascript.

lets write our first test, within the previous scope lets add:

# spec/features/shared/it_has_a_navbar_spec.rbcontext ‘when clicking on the toggler’ do before(:context) do  Capybara.current_driver = :selenium  resize_window_to_tablet end after(:context) do  Capybara.use_default_driver  resize_window_default end before(:example) do  find(‘#navbar__pane-toggler’).click end it ‘should flip between a cross and a burger’ do  expect(page).not_to have_css(‘.fas.fa-hamburger’)  expect(page).to have_css(‘.far.fa-times-circle’)  find(‘#navbar__pane-toggler’).click  expect(page).not_to have_css(‘.far.fa-times-circle’)  expect(page).to have_css(‘.fas.fa-hamburger’) endend

It should fail. Let’s make it work.

# app/views/shared/_navbar.html.slim
[The rest of the navbar code]javascript: document.getElementById(‘navbar__pane-toggler’).addEventListener(‘click’, e => toggleBurger(e.target)) function toggleBurger(element) { [‘fas’, ‘fa-hamburger’, ‘far’, ‘fa-times-circle’].forEach((togledClass) => { element.classList.toggle(togledClass) })}

There is only one step left, congrats on coming this far! We now want to extend (or hide) a blue screen with the links after we click on the toggler.

Here is our test:

Within the same context as before:

# spec/features/shared/it_has_a_navbar_spec.rbit ‘should toggle a panel with links’ do expect(page).to have_css(‘.navbar__mobile-link-pane’)end

There are a few changes in our navbar file:

# app/views/shared/_navbar.html.slim[previous navbar code].navbar__mobile-link-pane#mobile-link-pane = link_to_unless_current ‘Home’, root_path, class: ‘link link — white navbar__link’ do  p.link.link — white.navbar__link.navbar__link — current Home- if current_writer = link_to_unless_current ‘Sign out’, destroy_writer_session_path, class: ‘link link — white navbar__link’, method: :delete do  p.link.link — white.navbar__link.navbar__link — current Sign out-else = link_to_unless_current ‘Sign in’, new_writer_session_path, class: ‘link link — white navbar__link’ do p.link.link — white.navbar__link.navbar__link — current Sign injavascript: document.getElementById(‘navbar__pane-toggler’).addEventListener(‘click’, e => toggleBurger(e.target)) function toggleBurger(element) {  [‘fas’, ‘fa-hamburger’, ‘far’, ‘fa-times-circle’].forEach((togledClass) => {  element.classList.toggle(togledClass) }) document.getElementById(‘mobile-link-pane’).classList.toggle(‘navbar__mobile-link-pane — active’)}

and in our navbar.css file

# app/assets/stylesheets/navbar.scss[...].navbar__pane-toggler { display: none !important;}.navbar__mobile-link-pane { background-color: $dark; visibility: hidden; width: 0; height: 0; position: absolute; top: 6rem;}.navbar__mobile-link-pane — active { visibility: visible; height: calc(100vh — 6rem); width: 100vw; transition: height 500ms, width 300ms;}@media only screen and (max-device-width: 960px ) { .navbar__links {  display: none; }.navbar__pane-toggler {  margin-right: 2rem;  font-size: 3rem;  cursor: pointer;  display: inline-block !important; }}

That is it, friends! We did it!

We have a fancy navbar that is regression proof :)

Please do let me know if you have questions and or things that were not clear!

Also, if you did not want to use rails, most of what we did here could be done with pure HTML | CSS | JavaScript

You can find a live version of the website here.

And the code at the end of the article here.

Till next time!

Pablo

--

--

Pablo Curell Mompo

Full Stack developer @ Seraphin, learned how to code @ Le Wagon. I love coding and plan to keep learning as much as I can about it :)