Building a Custom Dropdown Menu with Headless UI React and Tailwind CSS

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're going to build a custom drop-down menu component that is fully accessible out of the box for this we'll use tailwind css and headless ui let's get right into it all right so here's the component that we're building we have a menu button here and when we click it it reveals a panel which has multiple menu items within it on the surface it actually looks like it's already working properly but when we start poking around we realize that it's actually lacking some critical pieces of functionality in terms of accessibility and user experience for example i can't click outside to close the panel if i press the escape key also it's not going to close the panel if i try open my button and navigate with the arrows keys down up nothing's happening so if you're only using your mouse it does look like it's working properly but it's actually a very incomplete and inaccessible implementation at this point so let's look at the codes to understand how it's implemented so we have one piece of state here is open which is set by default to false if i scroll down a bit you can see we're using this is open value to conditionally render or not render the menu items finally the only way that we change the value of this is open is in our button so whenever we click on it we set the value of ease open to the opposite of what it was that's literally all we're doing we're not listening to key presses like escape or arrow keys and therefore none of that is supported if we take a look at the design patterns and widgets section of the y area authoring practices website this component that we're building is referred as the menu pattern in there you'll find that there's a lot of recommendations in terms of keyboard interaction and also roles and attributes that should be present in your markup so it's really a ton of work to implement proper keyboard navigation and also support the appropriate attribute management and for that reason it's quite common to want to turn to a ui library that would handle that for you so you don't have to do all the implementation from scratch what ends up happening quite often is you do find a library that has all this support for accessibility but it also comes with some design opinions that you feel that you need to fight against and so that's where headless ui comes into play headless ui is a library that packs all the accessibility goodness all the keyboard navigation supports all the proper area roles and attributes but without making any design decision for you everything is completely customizable and styleable and it plays really nicely with tailwind css so now let's go and convert our existing menu dropdown using headless ui's menu component so let's start by installing headless ui from npm at headless ui and we're gonna go slash react because we want the react package perfect okay so now we're going to import up here the menu component from headless ui import menu from headless ui react so this menu component is going to give us a few different components that we can compose together before we do that i'm going to remove the little state management we're doing so i'll remove the on click to set the ease open value down here i'll remove the conditional based on this ease open and at the top here i'll remove my piece of state which means i can also remove the import of use state so now as a result the items panel is always open and clicking on the button does nothing let's also take a quick look at what's currently rendered in the browser our menu button is a button and you can see the only attribute applied is a class attribute with tailwind's utilities and the same for the panel it's just a div with utility classes applied and the children element once again don't have any other attributes than class attributes so the first element we're going to replace here is this div and we're going to make it a menu now by default the menu component renders as a react fragment which means the element actually disappears in our case we want to preserve it because we want this class of relative to be applied so i'll pass an s prop and i'll set it to div so this s prop lets you decide what elements the component should be rendered into the next thing we're going to implement is this button here i'll use a menu dot button this one defaults to button so i'll leave it like this and let's do one more so down here i'll come and replace this div which is the start of the items panel and replace it with menu dot items and whoop you can see that the items panel has now disappeared you can see that once again we can toggle the menu items panel but now we're letting headless ui manage this logic internally and decide when to show or hide the menu okay we've implemented a menu a menu button and a menu items now let's go and check the markup to see what's changed so here's the menu wrapper up here and then here's a menu button and here you can see that there's some extra attributes applied there's an id a type of button and aria has pop-up true likewise if we look at our menu items we also have new attributes like aria labeled by a unique id a roll of menu tab index equals zero you can see that even the children elements which are not yet using the menu item component have a role of none and you can see this really nice attribute management coming into place without us having to do anything or even think about it speaking of these children elements the next thing we want to do is come here and make these anchor tags use the menu item components so here's one of the link i'm going to convert just one first so we can compare the markup and then do the other ones so i'll create a menu.item singular and wrap my link inside of it by just doing this you can see that now we have a unique id a role of menu item and a tab index of -1 whereas the non-converted links still have a role of none all right i'll do a little bit of multi-cursor magic to transform the other six links to use the menu item component okay so now all seven links are using the menu item component and we can start observing some really cool things so let me open the menu here and i want you to pay attention to the bottom of this div around here so when i start hovering over some of the links you can see this aria active descendant attribute which has an id which gets updated every time i change so basically when i hover over an element it becomes in focus so when i press the arrow key down and up you can see that this active descendant attribute is updating as well nothing in the ui shows that an element is active or not but you can see that it's clearly updating which one is active so once again headless ui doesn't make any assumption on how things should look instead of doing that it's going to make available for you a piece of logic that you can use to then conditionally apply styles headless ui makes use of the render prop pattern to make some pieces of logic available to you so here in my menu item i can define a function and i'll return this jsx inside of here and now i'm able to receive here the active state of the current menu item and so now i can use this in my class names to style things differently whether the item is active or not so let me turn this into a javascript expression here and now essentially we have these classes applied by default and then on hover we apply bg indigo 500 and text white as you can see here but we don't want to do this on the hover we want to use the raw classes not the ones with the hover variants and then let's the active value determine which one should be applied so let me add a few spaces for some clarity here what i want to do is if the current element is active output some classes and if not i'd put some other classes if it is active i want to basically apply bg indigo 500 and text white but without the hover prefixes so i'll remove hover here and hover here and then if the element is not active i want to apply this make sense and so now it still works on hover but if i use the arrow key down you can see that we also have the same styles with the arrow navigation now you can see that the icon is not white like it should be so we should do the exact same thing on the icon itself here so once again i'll make that a javascript expression this time if it's active we want text white and again we don't want the group hover we just want the raw text white and if not we want text gray 400 and now because we're not using the group hover here we can also get rid of group here which was used to be able to achieve the group hover so let's give it a shot hover and down arrow perfect all right so now essentially i'll replicate the same thing for the other six so bear with me and i'll fast forward okay so now i have the same exact thing in place for all the elements and now if i navigate with the arrows up and down you can see that it works beautifully can we use the escape key to close sure we can and now here's a really cool thing i can even search for a menu item name so if i want to go to move i press m and it's gonna jump to move if i want to share i press s and it goes to share if i press ar it's going to go to archive but if i press a d it's going to go to add to favorites if headless u i understand that i'm searching for a string it will interpret the spacebar as a character and not close the panel add space to space favorites and you can see that it hasn't closed the item if i was a bit slower add and await and space then it would have closed it the menu component also has support for disabled items so let's say we wanted to make this duplicate disabled all i have to do is on the menu item element add a disabled attribute now if i navigate with the arrows you can see that it's going to jump over the duplicate element i can't even hover over it and give it the active state because it can never be active now once again there's no visual cues to tell us that this is disabled this is up to us to implement so in the render prop here we can also grab the disabled value and use that accordingly now bear with me i'm going to do something quite questionable here i'm going to have nested ternary operators to handle the logic so let's go on multiple lines just so we understand a bit clearer and now what we want to do is start with checking if it's actually disabled and if it's the case let's say we want the text color to be text gray 300 and if it's not disabled this is where we're going to move into the logic of the active so basically we check first is it disabled yes it is let's apply text gray 300 no it's not so is it active if so apply these classes and finally if it's not apply these classes and you can see that now our duplicate styles which is disabled has a different style so let's do quickly the same logic for the actual icon is it disabled if yes we're going to have some classes otherwise we're gonna have the rest of the logic so here we want the text to be text gray 200 and there you go you can see that now we have a really nice disabled looking menu item and it cannot be active under any circumstances so that's perfect very cool so last thing we can do here to add a bit of delight is add some transitions to the way the menu items panel appears i'll come at the top here and also import the transition component from headless ui and then all we're going to do is wrap our menu items inside a transition component so i'll grab the closing one and put it at the end of the menu items component spoiler alert this is going to break things and the reason it's going to break thing is the transition component expect a show prop to be true or false so up here let's start by just putting show equals true and it should show always the element but it's not showing it it still allows us to toggle it on or off what happens is the internal behavior for headless ui menu component is that when the menu button is clicked this is when we show or hide the component so when you want to break out of this default behavior in headless ui you can use the static prop on the menu items so if i come here on the menu items and i just add a prop called static this is essentially going to say display all the time don't worry about the internal logic just show the menu items and then the user has to take care of what to do to decide when to show or when to hide it so now if i click on the button nothing happens it's always showing and now the show true is actually what determines if it's showing or not so if i go false here it's going to always hide it and there's no way to bring it back so instead of true or false here we want to access headless ui's internal logic once again to be able to determine that and the way we do this is using once again a render prop but this time we're going to use the render prop on the menu component itself so right up here inside the menu component and now we got to be careful because we have multiple same level elements so we need to add a fragment and now we can access this open piece of state that headless ui makes available to us and i will be able to use this open in my show logic here we should now once again be able to show and hide the component and so now it still doesn't transition because the transition component needs some attributes and classes to do this so now we should have a really nice subtle transition coming in and going out looks good alright so now we have this really smooth accessible keyboard navigateable menu drop-down component and we've achieved all of this without compromising any of the styling and the fine grained control that we can apply with utility classes all right that's it for this video thanks for watching and i'll see you in the next one bye for now
Info
Channel: Tailwind Labs
Views: 83,645
Rating: undefined out of 5
Keywords:
Id: qJnIJa-cF2M
Channel Id: undefined
Length: 14min 7sec (847 seconds)
Published: Thu Apr 15 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.