30
loading...
This website collects cookies to deliver better user experience
sidebar.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Collapsible sidebar with Stimulus and Tailwind</title>
</head>
<body>
<aside>
Here's our sidebar, eventually
</aside>
<main>
Here's where our main content lives
</main>
</body>
</html>
Stimulus
. Add these script tags to the head tag, copied right from the Stimulus docs.<script src="https://unpkg.com/stimulus/dist/stimulus.umd.js"></script>
<script>
(() => {
const application = Stimulus.Application.start()
application.register("sidebar", class extends Stimulus.Controller {
static get targets() {
return [ "" ]
}
})
})()
</script>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
sidebar.html
in our browser we should be able to access Stimulus
in the JavaScript console and the defaults applied by Tailwind should be visible on our placeholder text.<body>
of your sidebar.html file with the below to add the basic structure of the sidebar to your page.<body>
<div class="container">
<div class="flex">
<aside class="sm:w-1/5 bg-blue-500 min-h-screen">
<div class="sticky top-0 pt-12 px-2 w-full">
<div class="absolute right-2 top-2 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</div>
<nav>
<ul class="flex flex-col overflow-hidden space-y-2">
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span>
Home
</span>
</a>
</li>
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<span>
Learn
</span>
</a>
</li>
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span>
About
</span>
</a>
</li>
<li class="flex items-center hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<span>
Contact
</span>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<main class="sm:w-4/5 p-4">
Main content goes here.
</main>
</div>
</div>
</body>
min-h-screen
class to the sidebar's container. We want to keep the navigation links on the page at all times, even if the main content scrolls, so add the sticky
class to the element that wraps the navigation menu.toggle() {
if (this.sidebarContainerTarget.dataset.expanded === "1") {
this.collapse()
} else {
this.expand()
}
}
toggle()
function uses a simple if statement that checks a data attribute in the DOM to determine if the sidebar is currently expanded or collapsed and then call the collapse or expand function as appropriate. We'll create this data attribute in the next section.collapse()
function is responsible for collapsing the expanded sidebar, and it looks like this:collapse() {
this.sidebarContainerTarget.classList.remove("sm:w-1/5")
this.iconTarget.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
`
this.linkTargets.forEach(link => {
link.classList.add("sr-only")
})
this.sidebarContainerTarget.dataset.expanded = "0"
}
sr-only
utility class to hide navigation link text from the page unless the user is accessing the site with a screenreader. Last, we update the sidebar's expanded data attribute to 0, so that the next time the collapse/expand link is clicked the toggle
function calls the expand
function.expand()
function comes next, and it looks like this:expand() {
this.sidebarContainerTarget.classList.add("sm:w-1/5")
this.iconTarget.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
`
this.linkTargets.forEach(link => {
link.classList.remove("sr-only")
})
this.sidebarContainerTarget.dataset.expanded = "1"
}
targets
definition at the top of the Stimulus controller:static get targets() {
return [ "sidebarContainer", "icon", "link" ]
}
<script>
tag that adds Stimulus to our page looks like this:<script>
(() => {
const application = Stimulus.Application.start()
application.register("sidebar", class extends Stimulus.Controller {
static get targets() {
return [ "sidebarContainer", "icon", "link" ]
}
toggle() {
if (this.sidebarContainerTarget.dataset.expanded === "1") {
this.collapse()
} else {
this.expand()
}
}
collapse() {
this.sidebarContainerTarget.classList.remove("sm:w-1/5")
this.sidebarContainerTarget.dataset.expanded = "0"
this.iconTarget.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
`
this.linkTargets.forEach(link => {
link.classList.add("sr-only")
})
}
expand() {
this.sidebarContainerTarget.classList.add("sm:w-1/5")
this.sidebarContainerTarget.dataset.expanded = "1"
this.iconTarget.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
`
this.linkTargets.forEach(link => {
link.classList.remove("sr-only")
})
}
})
})()
</script>
controller
data attribute somewhere in the HTML. In general, you want to attach the controller to the parent element for the part of the DOM you plan to change within the controller. Since the Sidebar controller makes changes to the sidebar and the elements inside of the sidebar, it makes sense to attach the controller to the top-level sidebar element:<aside data-controller="sidebar" class="sm:w-1/5 bg-blue-500 min-h-screen">
<!-- Sidebar content -->
</aside>
<aside data-sidebar-target="sidebarContainer" data-expanded="1" class="sm:w-1/5 bg-blue-500 min-h-screen">
<!-- Sidebar content -->
</aside>
<aside>
element is to tell our Stimulus controller that it is the sidebarContainer
target:<aside data-sidebar-target="sidebarContainer" data-expanded="1" data-controller="sidebar" class="sm:w-1/5 bg-blue-500 min-h-screen">
<!-- Sidebar content -->
</aside>
<div data-action="click->sidebar#toggle" data-sidebar-target="icon" class="absolute right-2 top-2 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</div>
data-action
attribute is structured as "UserAction -> ControllerName#FunctionName". For certain elements and actions, the UserAction component is optional but for a div, an action must always be provided. Read more about actions here.toggle()
function defined in our Sidebar
controller will be called. If you refresh the page now you'll notice an error in your JavaScript console. One last addition to the HTML and we'll be all set:<nav>
<ul class="flex flex-col overflow-hidden space-y-2">
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span data-sidebar-target="link">
Home
</span>
</a>
</li>
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<span data-sidebar-target="link">
Learn
</span>
</a>
</li>
<li class="hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span data-sidebar-target="link">
About
</span>
</a>
</li>
<li class="flex items-center hover:text-gray-200 h-8">
<a href="#" class="flex items-center h-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<span data-sidebar-target="link">
Contact
</span>
</a>
</li>
</ul>
</nav>
sidebar-target="link"
to each of the span's that wrap the link text. linkTargets
to toggle the sr-only
class as needed. This ability to easily find and modify any number of elements without relying on classes that may or may not be changed in the future is one of the things that makes Stimulus so pleasant to work with.this.linkTargets.forEach(link => {
link.classList.add("sr-only")
})