tutorial // Sep 16, 2022

How to Create an Animated Flyout Panel Using HTML, CSS, and JavaScript

How to build a flyout UI panel using CSS transforms and transitions along with JavaScript DOM events to toggle CSS classes dynamically.

How to Create an Animated Flyout Panel Using HTML, CSS, and JavaScript

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Adding Font Awesome

Although technically optional, for this tutorial, we're going to utilize an icon from the Font Awesome library. This library has free and paid options that give you access to a large library of high-quality icons in the form of either an icon font or SVG graphics. To keep things simple, we're going to rely on their icon font option which is served via CDN (content delivery network) link.

If you don't have one already, to start, head over to the Font Awesome site and set up an account. Once you're in, head over to the Kits page and then click the "New Kit +" button at the top of the page.

nkKq28ov98it96Zi/xE9ZQGyhpgXXVMxw.0
Accessing your Kit's Code on Font Awesome.

On the next page, under the "Add Your Kit's Code to a Project" section (#1), click the blue "Copy Kit Code" button to the right of the input with the <script></script> tag in it.

Once you have this copied, open up the app we just created above and locate the /index.html file at the root of the project. Inside, just above the ${css} placeholder, paste in the <script></script> tag you just copied and save the file.

That's it. Now we have Font Awesome installed and can put it to use later in the tutorial.

Building the flyout in HTML and CSS

To get started building our flyout, we're going to open up a file that already exists in our project at /ui/pages/index/index.js (this is automatically rendered via the boilerplate code in /index.server.js when we visit http://localhost:2600 in our browser).

We want to replace the existing code with the following to give us a blank slate to start our work from:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

With this in place, first, we want to build out the HTML part of our flyout. We're going to use an HTML <aside></aside> element to add a bit of semantic context to our flyout.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
        <aside class="flyout">
          <header>
            <h2>Profile</h2>
            <i class="fas fa-xmark"></i>
          </header>
          <form>
            <label>Spy Name</label>
            <input type="text" name="spyName" placeholder="Captain Insano" />
            <button>Save Profile</button>
          </form>
        </aside>
      </div>
    `;
  },
});

export default Index;

Adding in the <aside></aside> that will represent our flyout, inside we've got some presentational elements (as a test, we're going to grab the value from spyName input rendered inside of the form when it's submitted), the important one being the <i class="fas fa-xmark"></i> tag.

This is where we'll put the Font Awesome library we set up earlier to use. Here, we're rendering an "x mark" icon (or just an x or "times") which we'll use to trigger the close of our flyout once it's been open. While we don't have to do this—you could render a plain <button></button> tag or even a <p></p> if you'd like—it gives us a more polished, real-world example of how this pattern would work.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        <aside class="flyout">
          <header>
            <h2>Profile</h2>
            <i class="fas fa-xmark"></i>
          </header>
          <form>
            <label>Spy Name</label>
            <input type="text" name="spyName" placeholder="Captain Insano" />
            <button>Save Profile</button>
          </form>
        </aside>
      </div>
    `;
  },
});

export default Index;

Next, to help us out later, we've added in a <button></button> tag above our <aside></aside> which we'll use to trigger the open event for our flyout. This is just as an example, in your own app this could be any element you'd like.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  css: `
    .flyout {
      /* Positioning */
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      z-index: 999;
      /* Base styles */
      width: 400px;
      background: #fff;
      border-left: 1px solid #eee;
      box-shadow: -3px 0px 5px rgba(0, 0, 0, 0.03);
      /* Animation */
      transform: translateX(401px);
      transition: transform 0.3s ease-in-out;
    }

    .flyout.is-open {
      transform: translatex(0px);
    }
  `,
  render: () => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        <aside class="flyout">
          ...
        </aside>
      </div>
    `;
  },
});

export default Index;

Now for the important part. Above, we've added in the foundational CSS for our flyout. Notice that we're utilizing the .flyout class that we added to our <aside></aside> down in our HTML as the target for our styles.

First, directly on the .flyout class, we're setting up the styles for the flyout panel. To help clarify what's happening, we've added three comments, separating the styles by their intent: "Positioning," "Base styles," and "Animation."

Under "Positioning," we're utilizing the CSS position property, setting it to fixed. This tells the browser that this element should be position relative to the entire browser window (contrast this with position: absolute which positions the element absolutely relative to its closest relatively positioned parent).

Just beneath this, we give the actual "coordinates" (we use that term loosely) for where the flyout will be fixed on the screen. Here, we're saying that we want the flyout to be aligned perfectly with the top, right, and bottom on the screen (i.e., fix the flyout to the right side of the screen and "stretch" it all the way from the top to the bottom).

Beneath this, we take into consideration the "layering" of elements on screen, setting the z-index arbitrarily to 999. The z-index is the position in the visual "stack" on screen. Think of the elements that make up your UI as pieces of paper that can be next to each other, under each other, or on top of of each other—the lower the number, the lower in the stack, the higher the number, the higher in the stack.

Here, 999 is a random high value that should position the flyout above all other elements on your page (there's a bit of voodoo involved here as there's not an official level/index for certain elements). Depending on the UI you implement this pattern in, you will need to play with this value relative to the z-index of other elements like dropdown menus, modals, etc (typically, the highest value will be something like 9999 but make sure to check).

Next, for our "Base styles," we're adding some slightly more obvious styles: fixing the width to 400 pixels, coloring the background to white (important as by default it'd be transparent/see-through), setting a border on the left to a light gray (so the flyout doesn't blend into the page), and a box-shadow off to the left edge so the flyout has the illusion of "floating" on the page.

Now, for the fun part. In order to simulate a fly-in/fly-out effect, we need to be able to animate/transition a property. For the smoothest effect without layout issues, the translateX() transform function is best. Here, we set the transform on the flyout to translateX(401px). What this achieves is "pushing" the flyout horizontally on the x-axis (moving from left-to-right) off screen. The 401px value is accounting for two things:

  1. The width of the flyout: 400px.
  2. The 1px width of the left-side border (which is technically rendered outside of the flyout's width, meaning it's in addition to the 400px width).

With this, when our page loads, we should see nothing. This is because our translateX() has pushed the element over on the x-axis to a greater pixel value than the flyout visually occupies.

This is where the next part of our CSS comes in. First, just beneath the transform: translateX(401px), we have transition: transform 0.3s ease-in-out. This, like the name implies, describes a transition style we want to apply to a specific style property. Here, we want to apply a transition animation to the transform property (the one we defined on the line above). We want that transition animation to last 0.3s, or, 300 milliseconds, and we want to use the ease-in-out timing function (this describes the animation curve of the transition—ease-in-out being a gradual in-out curve that matches the behavior of our flyout).

With this transition set, now, whenever the transform property changes on the .flyout class, this transition will be applied to it. If we look at the next CSS rule we're defining .flyout.is-open, this comes into focus. Here, we're saying when the .flyout class also has the class .is-open, we want to change the transform property to 0px (meaning, "don't shift the flyout horizontally at all, just leave it alone"). The idea being that when we toggle the .is-open class, with our transition, the browser will smoothly transition between 401px and 0px creating a "fly in" effect.

That does it for the core CSS we'll need. Let's go ahead and pop on the styles for the inner-content of the flyout (they're purely for presentation/testing and not circumstantial to the flyout behavior).

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  css: `
    .flyout {
      ...
    }

    .flyout.is-open {
      transform: translatex(0px);
    }

    .flyout p {
      margin: 0;
    }

    .flyout > header {
      display: flex;
      align-items: center;
      padding: 10px 20px;
      border-bottom: 1px solid #eee;
    }

    .flyout > header > h2 {
      font-size: 18px;
      font-weight: 500;
    }

    .flyout > header > .fa-xmark {
      font-size: 22px;
      margin-left: auto;
      color: #aaa;
      cursor: pointer;
    }

    .flyout > header > .fa-xmark:hover {
      color: #333;
    }

    form {
      padding: 20px;
    }

    form label {
      display: block;
      color: #888;
      margin-bottom: 10px;
      font-size: 15px;
    }

    form input {
      display: block;
      width: 100%;
      padding: 15px;
      font-size: 16px;
      border: 1px solid #eee;
      border-radius: 3px;
    }

    form button {
      display: block;
      width: 100%;
      font-size: 16px;
      background: #eee;
      color: #555;
      border: none;
      border-radius: 3px;
      padding: 15px;
      margin-top: 20px;
      cursor: pointer;
    }

    form button:hover {
      color: #fff;
      background: #555;
    }
  `,
  render: () => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        <aside class="flyout">
          ...
        </aside>
      </div>
    `;
  },
});

export default Index;

Again, none of these additional styles are necessary for behavior, just for aesthetics, so we'll skip a play-by-play here.

Adding DOM events to trigger the flyout

While the above gives us all of the CSS we need to make this work, right now, we still don't have a way to trigger our flyout. To do that, we're going to rely on our Joystick component's state feature (a way to temporarily persist data to control the "state" of our component).

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    showFlyout: false,
    spyName: '',
  },
  css: `...`,
  render: ({ state, when }) => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        ${when(state.spyName, `
          <p>Your mission, if you choose to accept it ${state.spyName}, is to acquire a dozen donuts and eat them in a euphoria of gluttony.</p>
        `)}
        <aside class="flyout ${state.showFlyout ? 'is-open' : ''}">
          <header>
            <h2>Profile</h2>
            <i class="fas fa-xmark"></i>
          </header>
          <form>
            <label>Spy Name</label>
            <input type="text" name="spyName" placeholder="Captain Insano" />
            <button>Save Profile</button>
          </form>
        </aside>
      </div>
    `;
  },
});

export default Index;

Before we implement the necessary logic to modify that state, above, we're adding two things:

  1. Default values on our component's state object which will give us a default visual "state" for our UI.
  2. The necessary values in the HTML returned by our render() method that will respond to state changes and update our UI (in this case, toggling the is-open class name on our .flyout <aside></aside> and displaying a fun message when a spyName is entered and submitted via our test form).

Because the render() method on our component just returns a JavaScript string using backticks, we can leverage interpolation to include values in our HTML.

Here, we anticipate the component instance being passed to the render() method and use JavaScript object destructuring to "pluck off" the state and when properties from that object.

The state property we're plucking off refers to the current state value on the component (what we set the defaults for up top) and when is a special function —known as a "render method" in Joystick—which takes a value to test for its truthiness and, when it evaluates to true, renders the string of HTML passed as the second argument.

For our needs, we reference the state.showFlyout value in a ternary which conditionally renders the is-open class that will shift our transform style back to translateX(0px) from its default translateX(401px).

For the when() part, we're just having some fun and rendering the name we type into the form in our flyout onto screen as visual feedback.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    showFlyout: false,
    spyName: '',
  },
  events: {
    'click .open-flyout': (_event, component) => {
      component.setState({ showFlyout: true }, () => {
        component.DOMNode.querySelector('[name="spyName"]').focus();
      });
    },
  },
  css: `...`,
  render: ({ state, when }) => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        ${when(state.spyName, `
          <p>Your mission, if you choose to accept it ${state.spyName}, is to acquire a dozen donuts and eat them in a euphoria of gluttony.</p>
        `)}
        <aside class="flyout ${state.showFlyout ? 'is-open' : ''}">
          ...
        </aside>
      </div>
    `;
  },
});

export default Index;

Getting into our DOM events, now, we've added an event definition to the events object on our component which specifies each event as a type of event to listen for on some CSS selector as a property (here, click .open-flyout), assigned to a function that will be called when the event is detected. That function is passed two arguments: the raw DOM event and the component instance.

Here, we've prefixed the event with an underscore by convention (developer's prefix unused variables with an _ to make maintenance easier later) as we won't use it inside of our function. We do, however, use our component instance to access two things:

  1. The .setState() method to update the showFlyout value on state to be true, toggling the .is-open class down in our render()'s HTML.
  2. In the callback function fired after .setState() has updated and re-rendered our component, we access the rendered DOMNode for our component and run a .querySelector() on it to say "find the element with a name attribute equal to spyName and then call the .focus() method on it.

Here, step two is just for UX/showing off.

With this, now, if we click the "Open Flyout" button we rendered in our HTML above our <aside></aside> in the browser, we should see our flyout...fly out! Before we get too excited, though, let's wrap this up by implementing the "close" functionality and handle the spyName input.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    showFlyout: false,
    spyName: '',
  },
  methods: {
    handleCloseFlyout: (component = {}) => {
      component.setState({ showFlyout: false });
    },
  },
  events: {
    'click .open-flyout': (_event, component) => { ... },
    'click .flyout .fa-xmark': (_event, component) => {
      component.methods.handleCloseFlyout();
    },
    'submit .flyout form': (event, component) => {
      event.preventDefault();
      component.setState({
        spyName: event?.target?.spyName?.value?.trim(),
      }, () => {
        component.methods.handleCloseFlyout();
      });
    },
  },
  css: `...`,
  render: ({ state, when }) => {
    return `
      <div>
        <button class="open-flyout">Open Flyout</button>
        ${when(state.spyName, `
          <p>Your mission, if you choose to accept it ${state.spyName}, is to acquire a dozen donuts and eat them in a euphoria of gluttony.</p>
        `)}
        <aside class="flyout ${state.showFlyout ? 'is-open' : ''}">
          ...
        </aside>
      </div>
    `;
  },
});

export default Index;

First, above, we've added one additional property to our component methods which is set to an object with miscellaneous functions defined on it for our component. Here, we've wired up a method handleCloseFlyout which will take the component instance passed to the method (Joystick does this automatically) and call the .setState() method, setting showFlyout to false.

Putting this to use, back down in our events object, we're adding two additional event listeners: one for a click on the .fa-xmark icon we added via FontAwesome inside of our .flyout and another for the submit event on the form inside of our .flyout.

For the first, we write code similar to what we had for opening our flyout, however, instead of calling component.setState() directly, we just call the handleCloseFlyout() method we just defined (accessed via the component.methods value inside of our event handler function).

Next, for our submit listener, we put our DOM event object to use, first calling event.preventDefault() to prevent the submit of our form triggering a page refresh. Next, we call to component.setState() setting our spyName value to the current value typed into the input with the name attribute spyName. Finally, in the callback for our .setState() call, we call to our handleCloseFlyout() method to hide the flyout when we submit our form.

That's it! If we open up the browser and click around, we should get the fly-in/fly-out behavior we expect based on our interactions.

Wrapping up

In this tutorial, we learned how to build a flyout panel using HTML, CSS, and JavaScript. First we learned how to set up our HTML to display the flyout panel, and then, how to write the necessary CSS to give our flyout panel its appearance and control its positioning/animation based on the classes applied to it. Finally, we learned how to wire up DOM event listeners in a Joystick component to conditionally toggle the visibility of our flyout.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode