SUPER BONUS: Create a Reusable Ribbon Component
Introduction
We've built this pretty awesome ribbon component, and it looks great, but we can't reuse it because everything is hardcoded.
What we want to do in this lesson is try to build a ribbon component as opposed to a hardcoded series of ribbons. We want a component that lets us choose the
Transcript
0:01 We've built this pretty awesome ribbon component, and it looks great, but right now, we can't reuse it because everything is hardcoded. You can see I've added one on the bottom-right for completion purposes. We've got four times very similar block of markup.
0:16 What we want to do in this lesson is try to build a ribbon component as opposed to a hardcoded series of ribbons. We want a component that lets us choose the position, the color, the size to make it reusable, and then we can also pass the text that we want.
0:33 Whatever you choose to use to create components might change. You may want to use React, Svelte, Vue, or maybe some PHP templates. It doesn't matter. What I'm going to show you here is transferable skills. In this case, let me let you on a little secret. This project is using Astro.
0:50 Astro comes with these components out-of-the-box that look a lot like JSX syntax in React. This is what I'm going to use to build these components, but hopefully, you should be able to follow along, no matter what framework you choose to use.
1:02 Our four ribbons are currently being imported in a page. You can see here we import HardcodedRibbons from that file, and then we have a bit of HTML to have the background and the white card. Here, we inject our HardcodedRibbons.
1:17 What we want to do instead is build a ribbon component that we can use multiple times and pass options like size, position, color. You can see up here, I have a commented out import that I will uncomment, and it imports a ribbon file.
1:32 If I show you that file, it's completely empty. This is what we're going to use to build a component. What we want to be able to do is use the ribbon component like so. Ribbon, and then pass the text inside the opening and closing brackets. Let's call that Dynamic Ribbon.
1:49 Then, we should be able to pass some options or props, like position, top-left, color, purple, and size, medium. Nothing's going to happen with this code because the ribbon file has no code at all. Let's go and first of all, delete this HardcodedRibbons here, which will remove all four of our ribbons.
2:12 Let's begin our journey of building a reusable ribbon component. Like I mentioned, the ribbon file is completely empty, but we'll grab one of the ribbons. Let's take the first one here as a blueprint and paste it here.
2:24 You can see it appear here. We're passing props like position, color, or size, and these do nothing at the moment, but it's still rendering the HTML that we have in that file.
2:35 Let's get our hands dirty now, but first, let me get some energy. Like I mentioned, we're going to use Astro. There is a little bit of domain-specific knowledge here, but you don't have to pay too much attention to it. Try to think of how you would do the same implementation with your framework.
2:57 In Astro, the way you can define props and access them or do any JavaScript thing is inside a front matter at the top of the file. We're going to use TypeScript here. You don't necessarily have to, and don't let that scare you away, because it's going to help us with the developer experience and make our components more resilient.
3:16 I will define a type for our ribbon. Export, type, RibbonProps. The first prop or option that we want our ribbon to receive is a position prop. This can be top-left, or top-right, or bottom-left, or bottom-right. The next prop we want available is a size prop, and it can be small, medium, or large.
3:44 Finally, we want a color pop. Amber, green, purple, and cyan. That should do. The way we're going to receive these props to use them in our components is by destructuring them. Const, position, size, and color.
4:01 We're going to grab that from Astro.props, which is essentially the mechanism to receive props in an Astro component. Let me remove that comment. To test that our props are passing through, remember that in our page, we're using that ribbon for now, and we pass the color purple prop. Let's see if we receive that color here.
4:20 I'll change the text of the ribbon to say the name of the color prop, and it should say purple. It works. If I change this to green, you can see that it works properly. Great. Instead of showing the color, we want the ribbon to show the text that is passed inside of it.
4:37 Here, it's DynamicRibbon. In React, you would use props.children. In Astro, you can use these slot element. Here, I can go slot. Basically, what is between the opening and closing tag will be injected in that slot. Now, we have a dynamic ribbon.
4:54 This is a pretty good start. Now, obviously, what we want to do is handle the styles and update them based on the combination of position, size, and color. Before we do that, we should set default props for the ribbon component so we can use just ribbon with some text without any props and it will still use sensible defaults.
5:13 The position will be top-right, by default, the size will be medium, and the color will be amber. Since I've set default values for each, I can make all of these props optional so we don't have to pass them. We got solid foundations in place. Let's now start handling the styling.
5:33 We'll start with the size prop because it's probably the easiest to handle. If we look at the entire component code, the only element that is affected by the size prop is the wrapper here, and particularly this w-36 class.
5:47 Basically, we want to have a different class based on the size. If it's small, we want 24. If it's medium, we want 28. If it's large, we want 36. We could, of course, add more values.
5:59 One thing we can do in Astro like you could do in React is use template tags for the class attribute. I will change the syntax to this, which now will allow me to interpolate some JavaScript here.
6:11 Here, we could do something like size = medium?, and then do some dynamic styling. Instead, we're going to do the work and logic outside of the class attribute. Let me add width-36 again so we can see our ribbon. Let me add a comment here.
6:29 We have the prop types, and here, we're going to have something called styles lookup. We're going to create style lookup objects that let us handle different versions of styles for different props. The first one we're going to do is for the size classes. I'll call this object const sizeClasses.
6:46 Here, we want the keys of this object to represent each value possible in the size prop. We would have small, medium, and large. The reason we do this is we can then use the size prop passed to the component to reach for the matching key object and use the classes matching that.
7:07 This is only going to work if your sizeClasses object has a key that matches each prop, because if it doesn't find it, it's going to break the styles. This is why I chose to use TypeScript here. We can enforce the shape of this object to be what's called a record.
7:24 We want the keys of the object to be our RibbonProps, and more specifically, the size for the keys, and then the values can be a string. What's happening here is TypeScript is making sure that our object will have one key representing each possible size.
7:41 If I type small with only one L, it will immediately tell me, "Hey, 'smal' doesn't exist. Did you mean to write small?" This is pretty helpful. Also, if I don't have one of the keys, if the key is missing, now you can see that TypeScript will highlight an error on the object itself saying that the property is small is missing.
7:59 Essentially, this record here is enforcing the correct object shape for things to work as intended with our styling approach. That's really, really useful. Let's bring back small with two Ls so TypeScript is happy.
8:11 Now, here we're going to define the classes we want in each scenario. Small we said it's w-24, medium is w-28 and large is W-36. With that in place inside of here instead of the hardcoded w-36, I can reach for the sizeClasses object, and then reach for the key that matches the size prop that we're passing to the component.
8:35 You'll notice that the component changed size slightly, and notice that we've passed the medium size here. I'll remove all these props since we've passed default values, and so it should still use the medium size, which you can see here.
8:49 Check this out. If I duplicate this component and make the first one size = small, and the second one size = large, you can see now our two ribbons are getting the size dynamically based on the prop passed to it. Let me call that one Small and this one could be Large, but let's go with Not Small so the text is a bit longer.
9:10 Look at it. This is looking pretty good. That styling approach here seems pretty simple, but it scales well, even in more complex situations. You create a lookup object that has a key for each possible prop, and then you apply styles that you need in each scenario, and then compose the class with the prop value inside the class attribute.
9:28 Next, let's tackle the position prop. This is going to be slightly more complicated, because the position affects more than one element. The position is going to affect the parent, specifically these classes here, but it's also going to affect the first and second darkest squares that do the wrap-around effect.
9:48 It's also going to affect our anchor elements as well. The position, the rotation, the transform origin. We can't have simple strings like this. We need to have a more structured object with multiple possible values for each key. Let's build that together.
10:05 Const positionClasses is going to be an object. Once again, we want this to be a TypeScript record, this time for ribbon props, position values. Remember how we had just string for the values in our first object? This time, we want something slightly more complicated.
10:24 We want the values of these primary keys to be an object. Remember, we want the wrapper, shade one, shade two, and banner itself to have styles. Our object is going to have a wrapper, which can be a string.
10:41 It will have ShadeOne, which will be a string as well ShadeTwo, string, and finally, banner, string. This is the expected shape of our record. The primary keys are the positions, so top-right, top-left, bottom-right, bottom-left, but then the values of these are objects with these four different keys.
11:03 That may feel a bit intimidating, but check this out. Now, when I go to populate my object, I can literally go Ctrl+Space and TypeScript is going to tell me, "The first key needs to be one of these." Then, TypeScript is going to say, "OK, now you can have one of these four values"
11:18 We'll start with wrapper and we'll have a string. What else do we have, TypeScript? ShadeOne. You can see that I can autocomplete my way through populating this object and banner. That bottom-left is satisfied.
11:33 If I had wrapper with three Ps here, the error would tell me straight away, "This doesn't exist. You made a mistake, did you mean wrapper?" If it's correct, this section is OK, but then the positionClass, it's going to tell me that I'm missing top-left, top-right, and bottom-right.
11:49 I like Matt Pocock always mentioning that it's like your English teacher looking over your shoulder while you do your homework and say, "Ah, I think you forgot this. You forgot that." That person means well, they want you to succeed.
12:00 TypeScript is going to be a little bit passive-aggressive about you filling up that object properly, but then you will have a very robust lookup object that matches exactly what it should.
12:12 Let me copy that bottom-left object. I'll duplicate it four times, and then instead of bottom-left, the second is going to be...Actually, let's go in the same order, top-left, top-right. You can see I'm typing stuff here, but again, I could use Ctrl+Space and pick from the values that TypeScript knows I need.
12:34 Now, there should be no squiggles because our object satisfies the shape that it should have. Here, I'm going to wing it a bit. I might make some mistakes, but there's a few values. The wrapper, to start with, is this one. Basically, this is that offset by two.
12:53 Remember, this invisible square container that we had, it was in bg-amber-100 for the first part of the tutorial. In case of top-right, it's -top-2 -right-2. Top-left is going to be -top 2 -left-2.
13:08 Let's go and fill that up. Top-left, we want -top-2 and -left-2. For the top-right, we want -top-2 -right-2. Bottom-left, we want -bottom-2 -left-2. For the bottom-right, we want -bottom-2 and -right-2.
13:29 That alone is not going to do much, but let's already go and implement these dynamic classes. In our wrapper here, just like we've done, we're going to replace this hardcoded position offset with positionClasses, which is the object we've created.
13:45 Then, we need the value that reflects the position prop, but then we can't use that because we're now inside an object. We need to reach for the wrapper property inside there. That wrapper, let's try that.
14:01 I'm going to create yet another ribbon. Top-left, and boom, there it is. Obviously, we need to work on the rotation, but I can already have bottom-left and bottom-right.
14:21 At this point, you can already see the potential in how scalable and powerful this approach is. Anyway, let's keep going. The next thing we want to do is the ShadeOne and ShadeTwo. You can see we have top-left- and bottom-right- here. This should be dynamic.
14:36 Again, let's try wing it and see what happens. Top-left, because the banner is going to rotate, we want top-, right-, and then ShadeTwo two is bottom-, left-. For the second one, top-right, that's the default top-, left-, and the bottom-, right-.
14:55 For the bottom-left, we are going to want top-, right-, and bottom-, left-. I might get these wrong, but we can quickly fix them if that happens. The bottom-right, I'm trying to see things laughs in my brain, is going to be top-, right-. I think I made a mistake on the other one. Bottom-, left-.
15:23 This one should be top-left and bottom-right, but let's try it out. We've populated ShadeOne, ShadeTwo, and by now, you should know the approach. We turn this into a template string, and then I'll select both of these to make a dynamic interpolation. This one will be positionClasses, position, and ShadeOne.
15:48 The second one is going to be the same, but with ShadeTwo. Let's save and see if we nailed it. We did. Obviously, it's not working here, but imagine the banner rotated the other way, it would align like this and like that. So far, so good. Next step, we're going to work on the rotation.
16:13 Let's look at what we need to change. Padding, color doesn't matter. Uppercase, text tracking block. Square diagonal is the width. Text center, absolute. Bottom-right, rotation, angle, and origin, these are the dynamic classes that we want to handle in our positionClasses.
16:33 Absolute positioning, rotation, and transform origin. Let me copy that string because this is going to be the value for the top-right, which is our default, which is what we copied the snippet from originally. Then, we're going to try nail it for the other positions.
16:51 Top-left, we want the absolute positioning to happen on the bottom-left and also the origin, bottom-left. We're going to change these two right values to left, and we need to rotate the other way with - 45 degrees. I think that should work.
17:10 For the bottom-left, let's visualize it. We want if the banner is on the bottom-left, we want it to be aligned on the top-left, and then rotate from the top-left origin. Let's try that. Change the bottom values to top. The rotation again should be 45 degrees is right. This might very well be messed up, but let's go with this.
17:38 It might be better to see them on screen, and then try to update. Bottom-right, I want the elements to be aligned to...I have no idea. laughs Let's go top-right for this one and - 45 degrees. For this one, I'm pretty sure I've made a mess, but that's part of the process. Let's go with it, and then fix it.
17:59 Process is still always the same. These are the classes that we want to make dynamic. Before we can do this, we need to turn this into a template string. You should already get pretty comfortable and familiar with that approach.
18:12 Here, we're going to reach for positionClasses, and then get the position prop, which decides what slice of the object to look at. Finally, the styles we want here are banner, because remember, this is how we called that last key in the object. Let's save.
18:28 I'm sure that it's not going to be all perfect. laughs We nailed top-left and bottom-right. We just did not nail bottom-left. Three out of four is pretty good. Let's think of what happens if we have our square like this. We want maybe it's bottom-left, so what did I have? No, we want the top-left to align, and then it to rotate.
18:53 I had top-right, but it's top-left and origin, top-left. Is that all that was wrong? laughs Check this out. By eyeballing it in my head, I had almost everything right. As you can see, if you're not sure, just throw stuff on the screen, throw some spaghetti on the wall and see what sticks, and then do some adjustments.
19:17 I'm happy because this was probably the most complex approach we had this record with a nested object. Hopefully, it shows you how powerful the approach is. I really, really like it. Maintaining the stars up here with a series of strings organized in lookup objects, and then keeping your styles simple is scalable and setting you up for success.
19:39 Let's do the last thing, we're going to do the colors props now. We want our top-left object to have a color of green, the bottom-left can be purple, and the bottom-right can be, what's our last color, cyan. Nothing is going to happen here, but let's go and implement that.
19:58 You know the drill, sizeClasses, positionClasses, and you guessed it, const colorClasses. By the way, you can name these objects whatever you like. Let's make a record which is ribbon props, and we'll reach for the color this time. Again, should the value just be strings? What should they be?
20:21 We need to look at what is affected. The wrapper? No. ShadeOne and three are, but this is the same color, so we can have just shades maybe as a key, and then also here, this color. We have our shades, and then we have our banner.
20:38 We want here the shape of this object to be an object with a, let's go shades, which can be a string, and banner which, can be a string. Now, we let autocompletion do our work. I'll start with amber. Inside of it, we need shades string and banner string. This is our amber slice of the lookup object.
21:03 We have four colors, so one, two, three, four. It's going to tell me this exists already, but I can progressively replace. I like this autocomplete coding where you rely on TypeScript telling you what values are available instead of trying to remember them and type them. That's our object shape.
21:21 Let's do the shades first. Amber is going to be bg-amber. Green is going to be green. That's pretty simple here. Again, I might suggest to use semantic values like primary, secondary, danger, things like this, but for this tutorial, this will do.
21:37 What I'm teaching you is the styling approach with this lookup object. We can go here and replace both of these with the same class, which is colorClasses, color.shades. You might notice, let me go in the browser where I can zoom.
21:57 You can see that the shades already have the colors of the color prop. Obviously, we need to change the actual banner itself now. If we look here, we have bg-amber-200, text 800, and I'll cut these. We'll paste them here for the time being so I can copy the three.
22:15 We want background color, text color, and hover background color. I'll cut these three. Let's leave them in here for now. Amber is our default is going to be that. The green banner, we're going to replace number with green.
22:28 Remember, we had emerald for the hover styles, and then purple is going to be purple, and we were using Violet on hover. Finally, the cyan is going to be cyan, and we used sky. You know the drill.
22:46 We are now going to replace these classes with our dynamic. You tell me, what do I type? ColorClasses. We will reach for the color, and then the banner property, because remember, our object has colors and then a banner key.
23:08 When I hit save here, I'm expecting everything to fall into place and our ribbon to look absolutely gorgeous. Three, two, one, Boom. laughs Hey, look at this. Let's zoom in, because it's worth looking at them.
23:26 We have our top-left ribbon with the nice little shades effect, bottom-left, bottom-right. We now have a totally reusable component where you can create as many ribbons as you want with sensible default values.
23:39 If you don't pass any values, it will be medium size, amber color, top-right. Obviously, these clashes with the other one here, but you get the idea. Then, you could take the top-left and make it larger with size, large. I think this is a really, really nice approach.
23:57 We've defined what prop types we could have on these components, and then we've defined lookup objects for each of these props. We separate the starting concerns into different prop concerns, and by using TypeScript, we can have the exact shape that we should to make things work properly.
24:14 That approach makes sure that you always have entire class strings so it plays nicely with the just in time engine. This is, to me, scalable, especially when you start bringing TypeScript along. I have a lot of fun and success using that.
24:28 Hopefully, that opened your eyes and your minds to some approach that you can use to create multistyle variants UI components using Tailwind CSS. Thank you so much for watching. I hope you enjoyed this tutorial and this bonus. Have a great day. See you later.