sinaptia waves

React by example: Slider Panel

Enzo Beducci
Jun 14th, 2023

In user interfaces, vertical sliders have diverse applications such as vertical menus, navigation options, content lists, or even forms or configuration panels. That is why these types of components need to be able to adapt to any situation that may arise. In this opportunity, we would like to show a flexible solution to this widely used component. At the end of the post, you will find the code sample built with React and TailwindCSS.

First approach

TailwindCSS, with its utility-first approach and extensive set of CSS classes, enables you to quickly style and build user interface elements. When it comes to more complex components like a vertical slider, the predefined options we may find sometimes do not align perfectly with the desired design or behavior. By developing a custom solution, we could have the freedom to define the structure and appearance of such a menu according to the specific needs of the product.

The idea is to let the component define its transition animations, implement specific scrolling mechanisms, and touch event handling to provide an intuitive and engaging user experience.

Challenge

In addition to opening and closing with a transition, one of the biggest challenges when creating this layout is the positioning of elements. That’s why we’re going to try to create a component that solves this problem for us. With this component, we can pass the content we want to display, and it will know how to position it and, if necessary, allow it to grow in height with vertical scrolling.

To achieve this, we’re going to use some properties of Headless-UI, a set of unstyled and accessible UI components designed to easily integrate with TailwindCSS.

Solution

We can start by using Dialog (Modal) from Headless-UI. A renderless component jam-packed with accessibility and keyboard features. Dialogs are built using the Dialog.Panel, Dialog.Title and Dialog.Description components.

<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  <Dialog.Panel>
    <Dialog.Title>This is my title</Dialog.Title>
      <div className="mt-6 flex-1 px-4 sm:px-6">
        This is my content
      </div>
  </Dialog.Panel>
</Dialog>

This already takes care of showing and hiding our panel, which happens when the user clicks outside our Dialog.Panel or press the Escape key. Notice that we added a few CSS properties on the content to separate it from the title. Keep in mind that the idea is to manage the content from the outside (only positioning from the inside). So this is not a final solution.

Now, we said this is a slider panel so the next thing we want to do is position it on the right of the screen with full height. We can do that with inset-y-0 right-0. Also, for mobile devices we want to make sure that the whole panel takes full width, so let’s do that:

<Dialog as="div" className="relative z-10" open={isOpen} onClose={() => setIsOpen(false)}>
  <div className="fixed inset-y-0 right-0 flex max-w-full pl-10">
    <Dialog.Panel className="w-screen max-w-md">
      <Dialog.Title>This is my title</Dialog.Title>
      <div className="mt-6 flex-1 px-4 sm:px-6">
        This is my content
      </div>
    </Dialog.Panel>
  </div>
</Dialog>

Already looking good! Now we can add a transition to smooth the user experience. Thanks to Headless-UI this is simple to implement. We can use Transition that controls whether the children should be shown or hidden, and a set of lifecycle props (like enterFrom, and leaveTo). For this particular slider, we want to also have a backdrop with a slightly different background color. To do that we want to use Transition.Children and Transition.Root to isolate the animations between the backdrop overlay and Dialog.Panel

<Transition.Root show={open} as={Fragment}>
  <Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}>
    {/* Backdrop overlay */}
    <Transition.Child
      as={Fragment}
      enter="ease-in-out duration-500"
      enterFrom="opacity-0"
      enterTo="opacity-100"
      leave="ease-in-out duration-500"
      leaveFrom="opacity-100"
      leaveTo="opacity-0"
    >
      <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
    </Transition.Child>
    <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
      {/* Sliding Panel */}
      <Transition.Child
        as={Fragment}
        enter="transform transition ease-in-out duration-500 sm:duration-700"
        enterFrom="translate-x-full"
        enterTo="translate-x-0"
        leave="transform transition ease-in-out duration-500 sm:duration-700"
        leaveFrom="translate-x-0"
        leaveTo="translate-x-full"
      >
        <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
          <Dialog.Title>This is my title</Dialog.Title>
            <div className="relative mt-6 flex-1 px-4 sm:px-6">
              This is my content
            </div>
        </Dialog.Panel>
      </Transition.Child>
    </div>
  </Dialog>
</Transition.Root>

Notice how the slider panel enters from translate-x-full which is the starting point (hidden) into translate-x-0 taking its corresponding position.

Good! We are almost there. Now we need to adjust the positioning of the elements inside of the modal. Remember that we want to make this modular so it needs to be able to adapt for whatever content we might send. We can wrap the whole Dialog.Panel and give it some CSS properties such as overflow-y-scroll and flex-col to position elements one underneath the other and allow scrolling if it gets too large.

<Transition.Root show={open} as={Fragment}>
  <Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}>
    {/* Backdrop overlay */}
    <Transition.Child
      as={Fragment}
      enter="ease-in-out duration-500"
      enterFrom="opacity-0"
      enterTo="opacity-100"
      leave="ease-in-out duration-500"
      leaveFrom="opacity-100"
      leaveTo="opacity-0"
    >
      <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
    </Transition.Child>
    <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
      {/* Sliding Panel */}
      <Transition.Child
        as={Fragment}
        enter="transform transition ease-in-out duration-500 sm:duration-700"
        enterFrom="translate-x-full"
        enterTo="translate-x-0"
        leave="transform transition ease-in-out duration-500 sm:duration-700"
        leaveFrom="translate-x-0"
        leaveTo="translate-x-full"
      >
        <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
          <div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl">
            <Dialog.Title>This is my title</Dialog.Title>
            <div className="relative mt-6 flex-1 px-4 sm:px-6">
              This is my content
            </div>
          </div>
        </Dialog.Panel>
      </Transition.Child>
    </div>
  </Dialog>
</Transition.Root>

Good, we can now say that we have our very own Slider Panel, but it’s not fully done until we make it reusable for the whole application. Let’s work on that.

Make it modular

We completed the core functionality of this component which is to show and hide information with its styles. Now, to use it everywhere we can simply set the content using props. For that, let’s define title and children as information to display, and open and onClose handlers.

export default function SliderPanel({title = '', children, open, setOpen})

and we can easily use it like this:

<SliderPanel title='This is my title' open={open} setOpen={setOpen}>
  This is my content
</SliderPanel>

The open and setOpen states are useful to handle how the component behaves. In our code sample below, you will find an additional element, a button that takes care of this situation. But probably your implementation of how to open and close this modal might be different. So it’s good to have these props that let you adapt the component for other circumstances.

Feel free to check our <SliderPanel/> and even play around with it!