Introduction to the Lit Carousel Project
Hey hey, welcome to the second episode of Build it with Lit, where I build something in Lit. And that something is a simple carousel today.
We will build a simple carousel using the Lit starter kit and I'll also show you how to install this carousel into a Create React App project.
We'll be going a little fast, touching on the key concepts required to build this carousel. Our code will not be production grade, but it will be learning grade. Each chapter of this video has a code link in the description below in both TypeScript and JavaScript, so you can pause the video and experiment with the code or just check it out later.
There's a complete GitHub repo of the finished project linked in the description. Okay.
Project Overview and Setup
We'll be making one of these. It's kind of similar to what we built in episode one, but instead of taking in words, use it by passing in a bunch of HTML elements as children, which are shown one at a time, like this. And I'll show you how I animated it using the web animations API.
By the way, if you've never heard of Lit, the first episode gives an intro. It should be pinned in the comments. This is scripted.
To get set up quickly, I'm going to use the Lit TypeScript starter project. There is also a JavaScript starter project that you can use. lit.dev, our documentation site, has a toggle for switching between JavaScript and TypeScript if you want to see comparisons. For this video, I will use TypeScript and decorators.
On lit.dev, search for starter kits. Go to the TypeScript starter kit GitHub template and make a new repository using the template. I'm going to call it simple-carousel. Then we can git clone it onto our computer and run npm install.
While this installs, if you have run into an issue with npm not existing, you'll need to install Node onto your computer. This can be installed from nodejs.org.
There's a bunch of stuff in this starter kit to help with building a component. I'm going to mostly be making changes in the source and dev folder. Delete everything in the source folder. We don't need the example. Add our simple-carousel TypeScript file.
We need LitElement for our component, html for the rendering, and the customElement decorator to tell the browser about the simple-carousel.
Defining the Basic Carousel Component
This is the start of our SimpleCarousel class. It will render that it is alive.
Now we've told the browser about it, invented a new HTML tag, and told TypeScript about it. What I mean when I say we've told TypeScript about it is now if you document.createElement('simple-carousel'), TypeScript knows it's this class that you wrote.
Now in our HTML, we can import this script and use our simple carousel. In one terminal tab, execute npm run build:watch, and in the other, npm run serve. And now we have a preview that reloads as we change stuff.
Displaying Child Elements with Slots
A good carousel needs a way of putting stuff in the carousel. HTML already has a way to do this, and you do it all the time with your divs and p-tags and h1s. Place HTML child elements between the simple-carousel open tag and close tag. But the preview hasn't changed because we've not told the browser where the children should be rendered in our carousel.
Introducing the HTML slot element. The browser knows to put children in a slot element. All we need is to add a slot, and now every one of our elements is showing.
Managing Slide Visibility with State
A great carousel shows one element at a time. A solution is to collect all the child elements and hide all of them except the one we want to show. So this slideIndex property is the child we want to show. Property means changing slideIndex will schedule a reactive update. We can get the array of children using the @queryAssignedElements decorator. It makes this member a getter so we can make it private and read only.
These functions hide and show an element by adding and removing a class which we can later add styles to. And then on the firstUpdated lifecycle and every update after that, we can iterate through our child elements and hide all of them except the one which matches the slideIndex.
To recap: changing the reactive property slideIndex causes Lit to schedule an update. This results in the updated lifecycle method being called, which results in our method changing some classes around. And you can see the classes change as I change the slideIndex around.
This approach is simple and has some problems. We can only get away with it because we're not going to use this in production across different frameworks and renderers.
Why? Because the child elements passed in are not yours and some other code you don't own can change the classes, messing up the display. But for this video, simple is better. And we know that we will control what classes are on these children.
Styling Slotted Children with Shadow DOM
Because a great carousel only shows one child at a time, we should add CSS. And now it works. Although I'll spend a second on this ::slotted() pseudo-selector.
Basically, the CSS we declare in our component is scoped to the component's DOM. This scoped DOM has a really memorable name: it's called the Shadow DOM. It won't leak styles out into the rest of the page and also prevents most styles from getting in. Notice that children are not in the Shadow DOM or simple-carousel's DOM. We can visually see in the developer tools that there is some kind of projection of the children into the slot element.
Here's an analogy: it is like a projector projecting these elements from outside into the Shadow DOM. Because the child element is projected in, a CSS that is scoped to the Shadow DOM cannot select it, unless we use the slotted pseudo-selector that says, "Hey, look at the slotted children with matching selector and put styles on them."
A carousel needs some buttons to change what is being shown. This is a great excuse to show you how to make another Lit component that communicates with the carousel.
We're going to make a second Lit component called slide-button in slidebutton.ts. It has a familiar component skeleton: import what we will need, tell the browser about this button, declare it, tell typescript about it.
A button component automatically emits a click event when clicked, so this stuff is for styles and passing in our own button SVG image. That means slots again. The very last piece is some styles.
This is to make the button a nice circle which centers its content. I'm trying out this neomorphism design thing, since I thought it would be fun. This makes the button respond to mouseovers and tabbing. We're also going to pass an SVG icon into our button, so this sizes the SVG child to look good.
Adding Navigation Controls
How do we use a component in a component? We are writing HTML, and the browser needs to know about our button HTML tag. And how do we tell the browser about our button? By running the define code. This is done with a side effect-full import. Now we can use our button.
And we can set up an event listener with @click. All this event has to do is update our state. This will cause a reactive update and update our visible slide.
changeSlide updates the slideIndex by an offset and does some math to keep us in our array bounds. Then we make the event handler methods which will be called by clicking.
It's not beautiful, it's not animated, it's functional, and that's awesome.
Advanced Styling and Dynamic Sizing
We need this to look better before we add the juicy animation. We're going to add SVG icons and make the carousel height fit the height of the largest slide element. Here's some Bootstrap chevron SVGs that I prepared earlier in this constants file, which we can import and use in our buttons. Recall we've used a slot element in our slide button which will render these SVG children. Nice.
And it won't take much CSS to put the buttons on the left and right. For ease, I'm wrapping our slot in a container div which will contain the currently displayed carousel slide. And boom, all the styles we need.
We're going to animate our child elements, so we want them absolutely positioned and sitting on top of one another. The :host refers to the component itself, i.e. simple-carousel. And then we want to flex up the container, align everything in the center, and add some aesthetic margins, padding, and box shadow. The overflow: hidden is required as we start animating soon and we want the carousel elements to slide out without being visible.
We still have this height problem. The children are an array of HTML elements, so we can map them into a list of height numbers which we can get the max of. We will store this max height as state and we're going to apply it with a styleMap. After the very first update, we can get the max height of our container and finally apply the style by setting the height. This styleMap is nearly identical to the classMap used in the previous video but for efficiently applying individual styles to an element.
The cherry on top is to add some global styles which make this look brilliant. And if we're doing this wild shadow style, we should use Lit brand colors. Mint.
Animating Transitions with Web Animations API
This is literally my favorite part. Elliott, the phenomenal individual who you know from the Lit U series, told me that I have to try out the web animations API. That's how we got here.
Right now we have a hard cut between the element we are showing and the next element. Instead, we want to transition the visible element out either left or right and simultaneously show the next element, teleport it off screen, and then slide it in from either side.
We're going to do this using element.animate. We provide some keyframes and animation options. They look pretty similar to CSS keyframes as well. I've already figured out all the animations required for the two leaving and two entry animations. Each animation is two keyframes: a start and end. Transform with a translateX of zero is the child being shown in the carousel, while negative or positive 100 view width translates the child by the width of the view to the left or right.
These tuples correspond to the first and second argument of the animate method we'll write shortly. Before we apply animations, we want to get rid of the element being reactively updated. So we can delete updated. We still need to be able to cut to a single slide on creation, so let's rename this to initializeSlide. Now nothing works, but the stage is set for animations.
Put on your async and animation glasses. We're going to do things that take time. Import all our animation arguments and create a method called navigateWithAnimation. This is where the magic happens.
Grab the current slide and animate it with the leaving animation arguments. Change the slide. Immediately show this new slide, then animate it in using the entering animation parameters. The element will jump to the first keyframe, which is off the carousel. Then after the two animations have finished, we hide the element that left. And we call this method to navigate between slides.
That feels and looks great.
Integrating the Lit Component in React
We're ready to distribute this component. Let's integrate our component with all the frameworks. Actually, we just have time for React. The point is that it basically works everywhere.
From the starter kit, we need to export the built JavaScript and fix up the package.json a tiny bit, replacing the example element with our simple-carousel script. I also deleted some of the .gitignore file so I could commit all the built files to GitHub. Link to GitHub repo is in the description so you can follow along from this point.
Let's use Create React App. We can install our simple carousel from GitHub, and with an npm install, we have our simple carousel.
Remember, we want the browser to know about our HTML element, so we use a side effect-full import. The image already has an animation on it, so if we don't wrap it then our animation won't play. Now our animation can play on the span, and the image can keep spinning separately. And instead of all this info on one page, I want to view it one at a time using our simple carousel.
This just works.
There's also a Lit labs package for more sophisticated interop between Lit and React. And did you know React now has experimental support for custom elements? Another link for the description.
Conclusion and Next Steps
Did I just pique your interest in animations and carousels? There's an awesome implementation you should check out on the lit.dev website that uses our Labs motion package. There's a link down below, but you can also find it by going to lit.dev, click Playground, scroll down to Motion examples, and then it's the carousel one.
While you're there, give our labs packages some feedback. If you use the motion package and have thoughts, we'd love to hear about it. Write to us on GitHub using Discussions. That's another link you can find below. Talk to us on Slack and Twitters. If you want to leave a comment, you can. Here are some prompts: Is there a video you'd like to see on Lit that doesn't exist?
Next time, we will tackle our greatest challenge ever from scratch. We will build Google SERP.