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.
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.
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 relative
ly 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:
- The width of the flyout:
400px
. - The
1px
width of the left-side border (which is technically rendered outside of the flyout's width, meaning it's in addition to the400px
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:
- Default values on our component's
state
object which will give us a default visual "state" for our UI. - 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 theis-open
class name on our.flyout
<aside></aside>
and displaying a fun message when aspyName
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:
- The
.setState()
method to update theshowFlyout
value onstate
to betrue
, toggling the.is-open
class down in ourrender()
's HTML. - In the callback function fired after
.setState()
has updated and re-rendered our component, we access the renderedDOMNode
for our component and run a.querySelector()
on it to say "find the element with aname
attribute equal tospyName
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.