Converting Laravel Spark navigation to Tailwind CSS
Heads up: this post is a bit old. Caveat emptor.
I recently converted the navigation of my Laravel Spark based app, MemberScore, to use Tailwind CSS. Spark uses Bootstrap out of the box, which I wanted to move away from for two reasons:
- I find it a bit clunky and restricting, personally
- I’d built a sales site for MemberScore (at memberscore.io), and I wanted the actual app (at app.memberscore.io) to match the sales site appearance.
I haven’t converted the entire app to use Tailwind (yet?), but the nav is now converted and fully functional.
Here’s how I did it
Essentially, we’re going to recreate all the nav-related Spark views, and swap out the various css classes to take advantage of Tailwind, removing Bootstrap-related ones and instead using Tailwind classes to get the nav looking how we want.
The first step is to install Tailwind. If you’re not sure whether you’ll want to make this change permanently, I highly recommend including Tailwind via CDN as referenced in the docs, rather than going through the relative hassle of including it via npm.
If you’re ready to commit, you’ll need to include tailwindcss
in your package.json
file. I also needed to bump the version of laravel-mix
I was loading, from ^0.12.0
to ^2.0
. I also included laravel-mix-tailwind.
In my webpack.mix.js
file, I was then able to follow the instructions provided by laravel-mix-tailwind, and get Tailwind compiling ok after adding the tailwind directives to the end of my app.less
file:
@import "docs"; | |
@import "automation"; | |
@import "bootstrap-overrides"; | |
@tailwind preflight; | |
@tailwind components; | |
@tailwind utilities; |
You’ll notice the bootstrap-overrides.less
file there; I had to make a couple of additions to override some default Bootstrap files that didn’t play well with Tailwind:
html { | |
font-size: 16px; | |
} | |
body { | |
font-size: 16px; | |
font-weight: inherit; | |
} |
That may not be the Right Way™ to do this, but it’s working ok for me.
Next, I had to figure out how to get two things working:
- The toggling of the main nav menu on small screens
- The account switcher/settings/kiosk etc. dropdown in the nav
I ended up creating a single file Vue template called MainNav.vue
and using it as an inline template; this let me still use blade helpers within it. Here’s the template:
And where it’s used in resources/views/nav/user.blade.php
:
<!-- NavBar For Authenticated Users --> | |
<main-nav | |
:user="user" | |
:teams="teams" | |
:current-team="currentTeam" | |
:has-unread-notifications="hasUnreadNotifications" | |
:has-unread-announcements="hasUnreadAnnouncements" | |
:nav-open="navOpen" | |
:dropdown-open="dropdownOpen" | |
inline-template> | |
<div class="bg-white mb-8"> | |
<nav class="container mx-auto flex items-center justify-between flex-wrap py-3 px-4"> | |
@include('nav.brand') | |
<div class="block lg:hidden"> | |
<button @click="toggleNav" class="flex items-center px-3 py-2 border rounded text-grey-dark border-grey-dark"> | |
<svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg> | |
</button> | |
</div> | |
<div :class="navOpen ? 'block': 'hidden'" class="w-full text-right flex-grow lg:flex lg:items-center lg:w-auto relative"> | |
@include('nav.user-left') | |
<a @click="showNotifications" class="has-activity-indicator"> | |
<div class="navbar-icon py-6 lg:py-2"> | |
<i class="activity-indicator" v-if="hasUnreadNotifications || hasUnreadAnnouncements"></i> | |
<i class="icon fa fa-bell"></i> | |
</div> | |
</a> | |
@include('nav.dropdown-toggle') | |
</div> | |
</nav> | |
</div> | |
</main-nav> |
Toggling visibility
Notice the navOpen
and dropdownOpen
props, and associated toggleNav()
and toggleDropdown()
methods; those handle showing/hiding the two items mentioned previously. You can see there’s a button that calls toggleNav()
, and the main div containing the nav items applies a class, either block
or hidden
, depending on the current value of navOpen
(which is what toggleNav()
alters).
The dropdown toggling is done exactly the same; you can see dropdown-toggle.blade.php
is included in the main nav file. For some reason, I ended up with dropdown.blade.php
being @include()
-ed in dropdown-toggle.blade.php
instead other way around, but the names don’t really matter.
<div class=""> | |
<!-- User Photo / Name --> | |
<a href="#" @click.prevent="toggleDropdown" class="px-4 hover:no-underline focus:no-underline" role="button" aria-expanded="false"> | |
<img :src="user.photo_url" class="spark-nav-profile-photo m-r-xs"> | |
<span class="caret"></span> | |
</a> | |
@include('nav.dropdown') | |
</div> |
<ul :class="dropdownOpen ? 'block': 'hidden'" class="absolute pin-r z-10 bg-white rounded list-reset text-left border-grey-light border shadow" role="menu"> | |
<!-- Impersonation --> | |
@if (session('spark:impersonator')) | |
<li class="px-6 pt-4">Impersonation</li> | |
<!-- Stop Impersonating --> | |
<li class="px-6"> | |
<a href="/spark/kiosk/users/stop-impersonating"> | |
<i class="fa fa-fw fa-btn fa-user-secret"></i>Back To My Account | |
</a> | |
</li> | |
<li class="h-px bg-grey-light my-4"></li> | |
@endif | |
<!-- Developer --> | |
@if (Spark::developer(Auth::user()->email)) | |
@include('nav.developer') | |
@endif | |
<!-- Subscription Reminders --> | |
@include('nav.subscriptions') | |
<!-- Settings --> | |
<li class="px-6">Settings</li> | |
<!-- Your Settings --> | |
<li class="px-6"> | |
<a href="/settings"> | |
<i class="fa fa-fw fa-btn fa-cog"></i>Your Settings | |
</a> | |
</li> | |
<li class="h-px bg-grey-light my-4"></li> | |
@if (Spark::usesTeams() && (Spark::createsAdditionalTeams() || Spark::showsTeamSwitcher())) | |
<!-- Team Settings --> | |
@include('nav.teams') | |
@endif | |
@if (Spark::hasSupportAddress()) | |
<!-- Support --> | |
@include('nav.support') | |
@endif | |
<!-- Logout --> | |
<li class="px-6 pb-4"> | |
<a href="/logout"> | |
<i class="fa fa-fw fa-btn fa-sign-out"></i>Logout | |
</a> | |
</li> | |
</ul> |
Wrap up
That’s the general gist of how I did this. I’m sure this isn’t all done as perfectly as it could be, but it’s functional, looks how I want it, and I understand it – and those three things go a long way in production.
If you have any questions, or suggestions, shoot me an email at travis@travisnorthcutt.com. I’d love to hear from you.