Video Summary and Transcription
This Talk discusses the comparison between Polaris and Material UI component libraries in terms of consistency and flexibility. It highlights the use of the action list pattern and the customization options available for the action list component. The Talk also emphasizes the introduction of a composite component to improve API flexibility. Additionally, it mentions the importance of finding the right balance between flexibility and consistency and the use of types to enforce specific child components.
1. Introduction to Component Libraries
I'm Sid, a maintainer of Primer, the React component library used by GitHub. I'll discuss consistency and flexibility in component libraries by comparing Polaris and Material UI. Polaris provides a descriptive way to create banners with various components and actions. Material UI, on the other hand, uses alerts instead of banners with different severity levels and customizable icons.
So, let's go. I'm Sid. I work on the design system team at GitHub, and I am one of the maintainers of Primer which is the React component library that other teams at GitHub use to build the things that you actually like to use.
So before I start, I kind of want to give a warning. So GitHub is part of Microsoft now, so I have to say this, these opinions are my own and not of my employer yet. I'm trying to change them, I'm working on it. I haven't been there long enough, so give me a while. All right.
So let's talk about consistency and flexibility when it comes to component libraries. So the thing with component libraries is that there's no one true answer, and it really depends on a bunch of context. So when I started, I started looking at other component libraries, so that's big enough. I looked at Polaris, which is Shopify's design system, and I looked at Material UI, which is an open source component library, and this is one component that they have both, like So with Polaris, how you create this banner is you say I want a banner. The status of this banner is critical. And here's the title. And it renders all of that. The icon is there. The cross button is there. All of that is there. And if you want to add an action, you say action object where you say this is the content, and if this is clicked, this is the function that you should call. So very, very descriptive. There's a lot of config here. If I have to build the same thing with Material UI, that looks something like this. I have an alert. They don't call it banner, they call it alert. Doesn't matter. And you give it a severity of error. And then you can pass it an icon. So it could be any icon. It doesn't have to be this icon. You could basically give any icon.
2. Flexibility in Component Libraries
Material UI and Polaris have different levels of flexibility in their component APIs. Polaris is highly opinionated with a strict API, leading to consistent outcomes. Material UI, as an open-source library, offers a highly flexible API that depends on the company and context. When considering a component library, factors such as product type, design decision centralization, and pattern maturity determine the appropriate level of flexibility.
It's very flexible. And then it also lets you overwrite some of the CSS. So I can actually change the border color here like I have. And then finally... Ouch. Okay. Finally, if I want some content inside it, there is an alert title. But if I want a button, I can just bring my own button and put it there. And then I'm responsible for this margin that you see here. Okay.
So if you compare these two component APIs, they're creating the same component, but there's a lot of difference between what the API looks like and how much flexibility do you have. And that brings me to the spectrum of flexibility. So, when I think of Shopify as Polaris, I think Polaris is somewhere here, where it's very opinionated, it has an extremely strict API, and that leads to these predictable, consistent outcomes. You can use the alert wherever you want, you get something similar. But of course, it can get a bit rigid and restricting if you're experimenting with it. On the other hand, Material UI is probably all the way here. It's open source, so there is no assumption of context. It has to have a very flexible API, because the Material team cannot really guess what you'll use it for. Which means it kind of depends on your company, your context. And if you're not careful, if you use Material UI as your design system, it can become messy or fragmented or you just need a lot of guardrails in place.
So, when I was looking at Primer React, I was like, okay, where do we lie? Or if you're thinking about it for your component library? The answer of course always is it depends. And it depends on these factors too. Is it built for a specific product or is it open source? Is the design decision centralized because you have a certain design team or do individual products have their own design team? So is it scattered? How old is the product? How mature are the patterns? If you already have established patterns, then it's easier to be more consistent, versus If you're still finding the patterns, then it's better to be more flexible. And I looked at this and I was like, we built for a specific product. We do have a centralized design team. We have established patterns. So GitHub is like super old. So probably somewhere here, like this is how much flexibility we probably should allow. So to showcase this, I looked at this component. This is a, this is kind of a common pattern that you've seen a bunch of places.
3. Action List Pattern
The action list pattern is used in various places to display lists of actions. To build this pattern, a config-style approach is used, where an array of items is passed as an argument. When an item is selected, the on select function is fired, allowing the user to determine the action. Dividers can be added to separate options by context, eliminating the need for magic strings. Instead, the component has an attached key for dividers. The label remains the same.
What I want you to focus on is just this part, the list of people. And we call this an action list because it's a list. And you can take an action. I am very clever. So this pattern gets used in a bunch of places. This is a user list that I just showed you. But in a bunch of these places, they're all lists of actions that are distributed, sometimes in a menu, sometimes not.
So to build this, I looked at this simplest example. I was like, okay, this seems easy. This is a list of items. You can click them, and that's an action. So I said, okay, it's an action list. It takes an array of items, because I want it to be a very restrictive API, so I went a config-style approach. And if you want to select something, then you can click it, and I will pass you back the item that you pass so that you can figure out what to do. It's a little bit icky, because you're basing this on text. So changed it a bit. It's an object now. So you have text, and then you can do an on select. This is what gets fired, right? Config API. Then, next up, I looked at these dividers. It makes sense. You want to divide your options by context, so if you want to add a divider here, what would the text be? So do you just say this is a divider, or do we have a secret code where, if you say underscore divider, it's a magic string. Maybe it's a type divider. There's no nice way to do it, so what I decided was instead of relying on magic strings, let's bake this into the component. So because React components are functions or JavaScript functions at the end of the day, you can always attach more attributes to them. So basically here I'm attaching another key, which is divider, and internally we can change this. It's a magic string internally, but for the users of our components, they'll say action list or divider.
Now, let's look at this label. So it's the same list.
4. Customizing Action List
The action list component can render different elements based on the props passed. It can render a circle for colors, a tiny image with an avatar URL, or an icon with a specified size. However, if both an icon and an avatar are passed, it can cause issues. To address this, the API now allows for a single prefix element to be passed, providing more flexibility to the component. This change aims to accommodate users' needs without compromising functionality.
So it's the same list. You're probably looping over somewhere like repo.labels. And the text and the onSelect mix sense. The text is there. But then you have this circle that we show for colors. So maybe it's label color and the action list component is smart enough that if you pass label color, then it knows it has to render a tiny circle there. That makes sense. But what about this one, assignEv? So the text and onSelect are still fine, you pass the text. But maybe you pass an avatar and, again, it's smart enough to know that if you pass avatar, then it has to render a tiny image with the URL passed. Good work. But of course there's more. There's milestones and projects. There's an icon here. So I guess we just add an icon prop again. And if it's an icon, it's smart enough to know where to render the icon and what size it is. But this is where it starts getting tricky. But what if somebody passes an icon and avatar both? I'm not saying somebody is trying to break my component, but it makes sense. If somebody is trying to make a selection state, this is how you would do it probably. But of course it kind of messes it up because we don't actually want selection to be this way. There's a different way to do it. But the API kind of misguided you. So change it up and said there's one element allowed in prefix. We're going to call it prefix element. You can pass the component and you can pass what props does it take. Any time you see something like this, which is like here's the element, here's the props, it's a component. Maybe we should just call it prefix and accept the component. Kind of opened up the API a little bit. As I said, people are not trying to break the component. People are trying to build what they have to and then go home. So prefix component.
5. Customizing Action List (Continued)
The API introduces a leading visual prop instead of prefix, allowing for the rendering of different elements such as avatars, label color components, and milestone icons. Additionally, a description field is added, with a description variant to handle inline and block descriptions. The component also includes a group prop to handle suggestions and everyone else, with a group order prop to determine display order.
We don't call it prefix, we call it leading visual. So this kind of works. You can have avatar, you can have a label color component that you can put there, you can have a milestone icon. So it kind of works. The API is looking a bit tricky because the text is a text. I guess this is a string. Then on select is a function and leading visual is a rendered component. It's JSX. So the API is a bit, like you can't guess all this if you look at the docs you know what to do.
Okay moving on. There is description. We show the name over here. So I added description field. But in labels, because label descriptions can be really long, you want it on the next line. So maybe that's like a description, label description but then a description variant because description can be inline or it can be block. I guess it works. This also is starting to feel like a component but we will deal with that later.
And then the final boss of this is this component. Which is like it has groups, because then you want to assign a reviewer, we show you these are the suggestions and this is everyone else. So the way to do this would be like add a group prop, right? Another prop, why not? And then if the user has recently edited then you say suggestions, otherwise it's everyone else. But how do you decide which one shows up first? Do suggestions show up first or everyone? So you have to give me an order. So there's another prop called group order. That's an array, but then we have this variation. In some places, we want to fill it, so I need another thing called variant, a group variant, I guess. So this becomes a config, this title, this variant. And the good thing about this is that now I have an object, which means I can add an ID, so that at least I can say it's ID, like group one or group two. And then, did you notice there are two descriptions here? There's an inline one and a block one. So I guess we're gonna do maybe just an array of descriptions, and then which one is inline, which one is blocks, so maybe there's a description variant array. This feels stupid because there's two of them, so maybe it's an array of objects. And this is how it goes.
6. Flexible API with Composite Component
The API started to become complicated with multiple props, leading to the decision to use a function-based render prop called render item. However, this approach caused a loss of control and flexibility. To address this, a composite component using composition was introduced, allowing for a more flexible API. The composite component, action list, accepts a child component, action list.item, and enables the attachment of additional components. This approach simplifies the API and improves usability.
So, the only nice thing about it is it's very easy to type this. If you write TypeScript, then it's an object, it's very configuration-based, you can type the whole thing. But what ended up happening was folks wanted to put an icon here, put a count here on the right side. In the end, this is what the API started to look like, where there was a render item prop. Where it's a render prop, it's an inversion of control, we pass you the item props, and we say, please pass these to whatever you're going to render here. Because there's accessibility concerns here, there's a lot of magic that happens for focus and keyboard navigation, we want all of that to work, and we ask you to pass it back. I'm really afraid of something like render item, because we kind of lose all of the control or all of the features that we're big in and we're relying on the user to make sure all of that blue code for responsive design, for accessibility, they have set it up correctly, and they pass it down, right? I hate this. And especially this last pattern of render item, the problem with that is not that people would do mistakes, although it does happen, it's not like people are trying to break it. The idea is just that the component became so complicated with all the props that at some point we just had to declare prop bankruptcy and say, here's a function, you do it. I don't care. And that's like ejecting from a component, which kind of defeats the whole point. So all of that is actually a symptom of misunderstanding how flexible did we actually want the component to be, because if we did not want as many variations, it probably could still work, but I learned that we're probably further down here, even though we have all of the things that I said, it depends. But GitHub is big enough that there are so many contexts where the same component can appear. And it changes from component to component. So let's try this again with a slightly more flexible API in mind.
All right, so the answer that I went for is, let me try composition. Let me try to make a composite component. So there's an action list, like before, but instead of receiving an array of items, it accepts a child, which is action list.item. And .item knows how to render things the same way. And like before, you can attach more components onto one component, because it's all functions at the end of the day. So I can say, here's an item, and then action list.item is that component, and then people can use this nice composite API. So the nice thing is, like, on select is already on the item itself. This is probably where you'll guess to add it also, when you're selecting an element, you put the on select or the on click on it, so it makes sense. And then you have all of these items. I really like this so far, because it kind of makes sense. It looks tiny. It's not very confusing. Next step, dividers. If you wanted to if you had to guess what the divider would look like, what would you guess? Okay, three seconds. One, two, three.
7. Customizing Action List Components
The API now allows for baking components and queuing them into action list. Dividers are now simply rendered using action list.divider. The introduction of a leading visual component provides flexibility to customize labels. By changing the slots inside, such as the text of the leading visual, multiple UIs can be built with the same API. The description area now supports the variant prop to switch between block and inline display.
That's a good answer. So we did this before where we had all of these codes for divider. Now it's just action list.divider, because we have the option of baking components and queueing them into action list. And we say action list to divider, it knows how to render. It has accessibility handled already. Perfect.
All right, next one. Let me open labels. So this is how we did that earlier, where there was a leading visual. I kind of like this. I don't want to change too much. I'll just add an action list.leadingVisual. And this leading visual knows where to render this, whatever goes inside. So you can say this is the label color, and this is the area reserved for leading visual. If there is a leading visual, then it shifts the text, handles the margin, spacing, size, all of that. So it almost gives you a slot, if you think about it. You can fill the slot with label color or an avatar, like this one. So if you think about these two APIs, the basic structure is still the same. You're only changing the slots that go inside, like the text of the leading visual. So that's pretty good. If you can build multiple UIs with the same API, you kind of have to learn less, and you're going to get your task done faster and go home faster, which is a win for everyone.
All right. Now let's look at these descriptions. So earlier, we had a key here, labeled our description. I'm just going to do the same slot behavior. We have an action list or description. And you can pop this description right here, whatever you write inside goes into this description area. Now, if I had to do a variant of block versus inline, where would you put this variant prop? Probably put it here. Because this is the description and this is where description goes. So I'm going to put a variant block here, which will do this.
8. Customizing Action List Slots Implementation
To customize the variant on the description, it can be placed on the action list or description. The slots implementation involves looping through children and checking their type. The leadingVisual child is placed in the leadingVisual slot, the description child is placed in the description slot, and the variant prop is used to determine whether it's a block or inline description.
So a lot of these APIs kind of become easier to guess when you want a variant on the description. Where do you put it? You put it on action list or description. Kind of makes sense.
All right. Let's keep moving. All right, quick. I'm running surprisingly low on time, so I'm going to quickly go through how the slots implementation works. There's an object of slot inside item. And then we loop through children. So I say, react children.forEach. This is like a top-level React API. And then while I'm looping through child, while I'm looping through children, I check what is child.type. And the fun thing is, all of this JSX gets converted to React.createElement. And the type is the function that is similar to the component. So if I actually look at child.type, it's going to be one of these, where it's going to be actionList.leadingVisual or actionList.description.
I can actually just check for if child.type is actionList.leadingVisual. And I'm just going to kidnap that child and then put it in a slot. That sounds so wrong. I'm going to fill the slot of leadingVisual with this child. And then I did the same with description. And I can also check the props of the child. So I can say if child.props.variant is block, then slots.blockDescription. Otherwise slots.inlineDescription. And if it's none of these, then I know it's in the free form, the text field over here. So I say slots.freeform.pushChild. I see some eyebrows raised.
9. Using Action List Components and Groups
This API only works when you have control over the children. The available options for action list.item are action list.readingVisual, description, or just text. When using a predictable API, you can use hacks to optimize and customize the API. The syntax for group is action list.group, allowing customization with a title and variant. By combining different components, you can create a variety of UI on GitHub.
Fair, fair. You have to remember that this API will only work when you have full control over or you can expect what will come inside children. So in this case, with action list.item, these are the only things you can put. You can either use an action list.readingVisual or description or just text. If you wanted to put anything else, it wouldn't work because it would just go in the free form slot and we wouldn't identify. And you can get away with optimizations and nice APIs like this or let's just call it what it is, you can like child kidnapping hacks, if you control the API.
And because this isn't a component library setting, we kind of control what the API looks like. And again, people are not really trying to break the components, people are trying to get the job done and go home. So if it's a predictable API, do all the hacks you want. You have my permission if it matters.
Okay. So quickly looking at the milestone. Projects icon. I don't know if that still works. Because, you know, we just fill the same slots. And then finally, let's look at this big boy. Here we have groups. So again, if you had to guess what would be the syntax for group, what would you guess? Okay. I'm so disappointed. But okay. Maybe it's not as obvious as I thought. It's going to be action list.group. Because then you can put two groups, the group expects title, and you can customize it with the variant, kind of how you do with description. And then inside them, you're just putting action list.items. And everything, this all is similar. It's just leading visual description. But it's very intuitive how you can, if you can make this, you can kind of make a lot of UI on GitHub, which is good.
And then finally, if you wanted both of these descriptions, you just put two action list descriptions, one with variant inline, one with variant block. So, they both will identify the slot they have to fill, fill those slots, and render in the right places. So, no errors.
Experimenting with Flexibility and Consistency
Experiment to find your spot on the flexibility and consistency spectrum. Use composition to allow flexibility instead of ejecting. Design systems and component libraries are built for people. Create predictable APIs, even if it involves crimes. Thank you so much. If you want more bad ideas, you can follow me on Twitter.
I mean, it's an error, but we don't see it. All right, finally, I'm low on time, so I'm going to skip this, because it's kind of boring.
All right, finally, let me say this. You have to experiment to find your spot on the flexibility and consistency spectrum. I thought I knew where we were on the spectrum. I was clearly long, so you kind of have to play around with it a little and find your place. Use composition to allow flexibility when you have to, instead of ejecting. So, like, render. Inversion of control is good when you don't have any control, but in lots of cases, you kind of have assumptions, or you kind of can predict what people want to do. So composition over ejection.
And finally, design systems and component libraries are built for people. So, people are not really trying to break your API. I think that's a concern that we have a lot. We're like, no, people will use it this way, and what will happen, then? People are not trying to, like, make a fool of us. They're just trying to get the right output, and they have a thing in mind, and they want to move on. So if you can create predictable APIs, even if it involves crimes, it's fine.
Thank you so much. That's all I have. If you want to look at the slides, they're on this URL. And if you want more bad ideas, you can follow me on Twitter. Thank you. Thank you, thank you. Thank you so much, Sid. I love Chi. Can we have you come over here? We can convene on this nice React logo that I love here. How are you doing? How did the talk go for you? Good. Good, good. So folks, just in case you have some questions, and you haven't yet put them in the Slido, make sure you check out the Slido, and you ask those questions. Let's jump straight to it. We've got one of them...
Building Flexible APIs and Slide Creation
Build a more flexible API at a lower level of abstraction. Consider the trade-offs of using lower-level blocks instead of an opinionated API. Use types to enforce specific child components. No tool used, just a preview on the left and code on the right.
We've got Nicola who asked a question, why not approach it, like build a more flexible API at a lower level of abstraction, and then build an opinionated API as a layer on top. That's a good point. We kind of do that. A lot of the leading visual and description are built on lower-level APIs. So there's like an avatar, and there's like a text field. So if you wanted, you could take the lower-level blocks and build it yourself. I think that's also a way of ejecting, because I think that gets thrown around a lot where if you don't like the opinionated API, just build it yourself with the lower building blocks, but then you kind of lose out on all of the accessibility, responsive... There's a lot of glue code that's hiding under the component that you kind of just bail on. So you can if you want, but it's always nice when you don't have to drop to a lower level.
And this next question that's jumped to the top is one that I was thinking about as well. And is it possible to write a tstype to only allow actionlists or XXX as a child of actionlists? Yes, it is. We have types for this. It's open source if you go to GitHub slash Primer slash React, you can find slots. I'm very proud of... It took me a whole week to get the types right. But I got it finally, and yeah, you can type it, and if you give something else, then it shows the squiggly lines.
Nice, nice. This is a question. I was thinking about... I love how the audience are reading my mind. What awesome tool did you use to create those slides? Because I need it in my life. Like most of the code I write, it's also like crimes and hacks. Okay, this is gonna be annoying. Sorry to disappoint you, but there's no tool. It's just a preview on the left, which is just a list of components that are next between. On the right, it's just code. And I've already pre-written the code, I just next, next with it. But because it's a text area, I can pretend that I'm typing and it's doing something. It's not doing anything. It's just slides.
Handling Valid Children and Looping Through Items
Presenting consumers with valid children. Collaboration and communication are essential. Looping through items and handling user input. Using slots for flexibility in opinionated components. Forward ref integration with material UI.
I'm just going next, next, next. Nice. Faking it till you make it. I know, right? Straight up crimes. And also, like you said, developers sometimes... We don't like to follow the rules all the time. So how do you present consumers of the component library to only use valid children instead of just random divs and taking over?
Yeah. The types help. But you could do a TS, ignore, and move on with your life. We can't stop you. A lot of times this comes down to not just the code, but it comes down to just collaboration. So we're constantly on the lookout for... I'm always searching who's using ActionList. And if they're using it in ways that I hadn't predicted, it probably means they have a pattern I didn't think of, or a pattern that doesn't fit in the API. So unfortunately, the answer is, you got to talk to them, and you've got to figure out what they want.
How dare the answer be talked about. That's a good idea. That was a slide that I skipped. Because you're responsible for looping through items. We give you a text input, which gets rendered at the right spot. It has the right margin, it has a loader, and all that. But the actual logic of what happens when somebody types is passed on to the user. So you give us filtered users, and we'll render that. And if the keystroke, then you change filtered users. And what did we need slots for? Slots are perfect for places where you have an opinionated component. You also have just the tiniest amount of flexibility. Like in this one, it always looks the same, but the leading visual could be anything. Could be a label, could be an icon, could be an avatar, could be something else, but the size and spacing are already controlled. So slots are great for tiny amounts of flexibility that you want to sprinkle without having to redo the entire component. And the next question about refs integrations, like material UI, how would you work with that? Forward ref all the way, so like leading visual forwards the ref, item forwards the ref, description forwards the ref.
Forwarding Ref and Maintaining CSS Consistency
Forwarding the ref for multipurpose API. Utilizing slots to maintain CSS consistency. Wrapping use cases with guardrails in CSS.
There's no nice way to do it. You just got to forward the ref all the way down.
And this last question, because I can see our time has gone red. How do you make sure that nothing breaks from a CSS perspective when you're building API for multipurpose? That's a good question. So I want to say at least in this component, the slots have helped a lot because the slots, they look simple, but they're kind of like very restrictive. There's like a max width and a max height and a margin. And there's a tiny difference in pixels between a label color and an avatar. And for that, there's a min width, max width going on. So there's a lot of like, we tried to build all the use cases that people might use it for and then bake in a lot of guardrails in the CSS so that it doesn't go out.
Awesome. Thank you Sidd so much. See you around. Everyone give him a round of applause. Thank you all. Give him a round of applause. Thank you so much. Have a great day, everyone. Thank you.
Comments