---
title: "Client-Side Tabs in LiveView with JS Commands"
description: "There are some things it really does make sense for our LiveView to do without calling home. Simple things that the browser doesn't need help with. With the Phoenix.LiveView.JS module, we have an assortment of tidy, composable JS utility commands to easily carry out common client-side tasks without breaking into custom JavaScript."
---

# Client-Side Tabs in LiveView with JS Commands

[Annie Ruygt](https://annieruygtillustration.com/)

There are some things it really does make sense for our LiveView to do without calling home. Simple things that the browser doesn’t need help with. Things we’d like to see happen instantly, like hiding a modal—maybe even with a transition animation.

With the [Phoenix.LiveView.JS](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html) module, we have an assortment of tidy, composable JS utility commands to easily carry out common client-side tasks without breaking into custom JavaScript.

Most LiveView JS commands do what can be summed up as manipulating HTML element attributes, with two exceptions. We can dispatch an event to a DOM element using `JS.dispatch`. And beyond purely client-side operations, `JS.push` pushes an event to the server, but with extended options to “[customize targets, loading states, and additional payload values](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html).”

The most excellent thing about LiveView JS commands may be that their work persists across DOM patches: when an update comes down the line from the server, the JS commands are replayed locally so our local changes don’t get wiped out.

Today we’re going to use a practical example to demonstrate a selection of these: `JS.show`, `JS.hide`, `JS.remove_class`, and `JS.add_class`.

## The problem

We’re building a set of tabs, each with its own HTML content to display. Making a tabbed UI work boils down to showing stuff, hiding other stuff, and making one of the tabs look different so we know which one the displayed content belongs to.

None of that needs any information from the server, so it would be great if we didn’t have to push events to it and wait for it to respond.

## The solution

We can build our tab functionality fully client-side, with a few LiveView JS commands. When a user clicks on a tab, we will:

- Show the content that corresponds to that tab, using `JS.show`
- Hide the content belonging to the other tabs, using `JS.hide`
- Indicate which tab is now active by setting classes on the tab elements with `JS.remove_class` and `JS.add_class` (and styling them accordingly)

### Laying the foundation

Planning ahead a bit: the utility functions we want to use all work by changing attributes on elements. `remove_class` and `add_class` are self-explanatory. `JS.hide` and `JS.show` give an element an inline style property of `display: none;` and (by default) `display: block;` respectively.

So let’s build the basis for the UI with that in mind.

```
<div class="container">
  <div class="tab_header">
    <ul>
      <li class="tab_option">
        <a id="tab1" class="tab active-tab"> Tab one </a>
      </li>
      <li class="tab_option">
        <a id="tab2" class="tab"> Tab two </a>
      </li>
      <li class="tab_option">
        <a id="tab3" class="tab"> Tab three </a>
      </li>
    </ul>
  </div>
  <div id="content" class="tab_body">
    <div id="content_tab_1" class="tab-content">
      <img src={ Routes.static_path(@socket, "/images/1.webp") }>
    </div>

    <div id="content_tab_2" class="tab-content hidden">
      <img src={ Routes.static_path(@socket, "/images/2.webp") }>
    </div>

    <div id="content_tab_3" class="tab-content hidden">
      <img src={ Routes.static_path(@socket, "/images/3.webp") }>
    </div>
  </div>
</div>
```

The tabs at the top are a styled unordered list. In the `#content` div below the tabs, we have the content for *all* the tab options, in elements with `id` attributes `"content_tab_1"`, `"content_tab_2"`, and `"content_tab_3"`. We’re using the class `"hidden"` to hide two of the three.

We’re fans of [Tailwind CSS](https://fly.io/phoenix-files/tailwind-standalone/), where an element with `class="hidden"` gets the property `display: none;`. If we’re not using Tailwind, we can add that CSS rule ourselves. This sets the *default* visibility of each piece of content.

Here’s what the result could look like:

![A row of tabs labeled "Tab one" through "Tab three", with an image beneath; "Tab one" has different styling.](https://fly.io/phoenix-files/tabs-with-js-commands/assets/liveview-js-01.webp?2/3&center?card)

Yay! We have constructed something that *looks* like a tabbed interface when the page loads. It looks like one tab is active, thanks to some styling on the `active-tab` class. Only one of the content options is displayed.

But the content doesn’t actually change if a user clicks on a different tab.

### JS.show

To change that, we’ll add a `phx-click` binding to each tab, invoking `JS.show` for the corresponding content-containing element.

```
<li class="tab_option">
  <a id="tab1" class="tab active-tab" 
    phx-click={JS.show(
      to:"#content_tab_1",
      transition: {"ease-out duration-300", "opacity-0", "opacity-100"},
      time: 300
    )}
  > Tab one </a>
</li>
```

The `to` option takes a DOM identifier for the element we want to target. Above, we’re targeting the `#content_tab_1` element when the `#tab1` element is clicked.

Since JS commands and DOM patches are coordinated, we can use CSS transition animations and not worry about them being interrupted if the server happens to send an update. (Fun fact: there is also a `JS.transition` command in case you want a transition animation by itself. )

We’re giving the `transition` option a 3-tuple of classes: `"ease-out"` and `"duration-300"` have CSS styling to provide the properties for the transition (in this case `transition-timing-function` and `transition-duration` ). Class `"opacity-0"` provides styling for the start of the transition, and `"opacity-100"` for the end.

Yes, we’re using Tailwind classes again. You can also customize the transition by using your own classes, as long as the app’s stylesheet provides rules for them.

Finally, there’s a `time` option. It limits the time taken for the transition, and it defaults to 200ms. Here we’re using a transition that takes 300ms, so we have to adjust `time` to 300ms to ensure we don’t get any weird behaviour.

If we do this for each tab, clicking on a tab makes its corresponding content div visible.

Clearly we’re not done yet. We still need to hide the content of the other tab(s). This is where `JS.hide` comes into play.

### JS.hide

`JS.hide` hides the HTML elements we choose by giving them the style property `display: none;`. Let’s make some changes to our code.

We could just stick a `JS.hide` command before `JS.show` in each tab’s `phx-click` binding. But we need the same pattern in all three tabs, so we may as well make a private function to both show the active content and hide the content of the remaining tabs.

We’ll call it `show_active_content/2`:

```
defp show_active_content(js \\ %JS{}, to) do
  js
  |> JS.hide(to: "div.tab_content")
  |> JS.show(to: to)
end
```

First, `show_active_content` does a `JS.hide` on all the divs that have class `tab_content`; then it `JS.show`s the element whose DOM identifier is specified in the `to` argument.

For simplicity, this snippet doesn’t show the `transition` option, but we would use it in just the same way as we did with `JS.show` above. We can also add a transition to `JS.hide` in the same way.

A note here about the `js` argument to this function. The JS commands inside are composable: that is, we can apply them in a series, [piping](https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2) the output of one into the input of the next. Our optional `js` argument makes `show_active_content` composable as well. If there’s no input from a previous command, `js` is just an empty struct `%JS{}` .

Let’s see how to use our function:

```
<li class="tab_option">
  <a id="tab1" class="tab active-tab" 
    phx-click={show_active_content("#content_tab_1")}
  > Tab one </a>
</li>
<li class="tab_option">
  <a id="tab2" class="tab" 
    phx-click={show_active_content("#content_tab_2")}
  > Tab two </a>
</li>
<li class="tab_option">
  <a id="tab3" class="tab" 
    phx-click={show_active_content("#content_tab_3")}
  > Tab three </a>
</li>
```

Each of our tab options will pass the identifier for its own content in the `to` argument of `show_active_content`, so that after `JS.hide` hides all the `content` components, the content for just the desired tab gets unhidden.

### JS.remove_class and JS.add_class

So far we can show the content of any tab with a click, but right now, the first tab always looks like it’s the active one. That’s because it has the class `"active-tab"` and we have some CSS behind the scenes to make it look special.

So all we have to do is remove the class `"active-tab"` from the tab element that has it, and add it to the tab that’s been clicked. For this we define a new composable function:

```
defp set_active_tab(js \\ %JS{}, tab) do
  js
  |> JS.remove_class("active-tab", to: "a.active-tab")
  |> JS.add_class("active-tab", to: tab)
end
```

Here, we’re literally using the class that we want to remove in the DOM identifier to pass to `JS.remove_class`. Then `JS.add_class` adds the `"active-tab"` class to the tab whose identifier was sent in the `tab` parameter.

Incidentally, you can add a transition effect to both `JS.remove_class` and `JS.add_class` just as with `JS.show` and `JS.hide`.

Now let’s see how to use our function:

```
<li class="tab_option">
  <a id="tab1" class="tab active-tab" 
    phx-click={
      set_active_tab("#tab1") |> show_active_content("#content_tab_1")
      }
  > Tab one </a>
</li>
<li class="tab_option">
  <a id="tab2" class="tab" 
    phx-click={
      set_active_tab("#tab2") |> show_active_content("#content_tab_2")
      }
  > Tab two </a>
</li>
<li class="tab_option">
  <a id="tab3" class="tab" 
    phx-click={
      set_active_tab("#tab3") |> show_active_content("#content_tab_3")
      }
  > Tab three </a>
</li>
```

Since we made both functions composable, we can string them together with the pipe operator.

There we have it: a working tab bar.
