Coding a responsive Navbar with Rails
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.
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:
- I want some movement when you clock on the trigger on mobile.
- I want some fancy font for the links.
Coding, the fun begins!
Quick disclaimers:
- 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 :)
- We code our views using .slim. You can accomplish the same in .erb
- We will aim to write our code using TDD.
- 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.
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:
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.
And in our browser:
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:
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:
- We want to change capybara’s driver since the default
rack_test
driver considers a ‘hidden’ element to be present on the page - 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.
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:
- To cover the screen in blue and make our links appear
- Hide the burger
- 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