1. Flexibility vs Consistency in Design Systems
Hey, I'm Sid. I work in the design system team at GitHub. I want to talk about walking the fine line between flexibility and consistency. I have this alert that I've been looking at other design systems for inspiration, for API ideas, all of that. And I was looking at this alert and trying to build it with Shopify's Polaris and Material UI's Component Library. So this is how they differ. With Shopify, it's a tightly controlled config API, while Material UI offers more flexibility. The spectrum of flexibility depends on various factors such as the target audience, design culture, and product maturity. It's important to find the right balance for your component library. Thank you for coming to my talk!
Hey, I'm Sid. I work in the design system team at GitHub. I want to talk about walking the fine line between flexibility and consistency. So let's start directly with code.
I have this alert that I've been looking at other design systems for inspiration, for API ideas, all of that. And I was looking at this alert and trying to build it with Shopify's Polaris and Material UI's Component Library. So this is how they differ.
So with Shopify they call it a banner. So you render a banner and then it takes a status, in this case status is critical, and it takes a title. So it takes these props as config, right? So the title doesn't go in the child, it goes on the component as a prop. And then you have the action and you only say what is the action and what is the contents that go inside the action. You don't really get to pick the action per se. And the component takes care of where it's rendered, how it's rendered, what color, the border, all of that. So this is an example of a config API, which is very tightly controlled.
And in fact, if you try, I played around with this. When I tried to give a style tag, I tried to give it different class names. It doesn't accept any. This is what you get. And then when I tried to build the same thing with Material UI, they had a severity as well. They call it error not critical, which is all the same. But you could give it any icon you want. So I was able to import an icon from the Material icon and pass it. I could also give it different styles. So I could customize the border. And for the contents inside, I had to take the alert title which they export and take a button and try to make the button look exactly the way I want it. Plus the space in between, I had to do it with the margin in between the components. So that's just like a tiny example of how the flexibility can be so different between two components. Like if you compare the API of these, the Shopify one is super tightly controlled versus the Material UI one is you can customize basically anything you want. That kind of brings us to this line or this spectrum of flexibility where Polaris is probably somewhere here. So it's very opinionated. It's built for Shopify. It's not built for everyone. It has a very strict API and that leads to predictable and consistent outcomes. When you use that banner, you know exactly what's going to come out. But then it could become rigid and restricting for experimentation or different use cases.
On the other hand, the Material UI one is probably somewhere on the right edge. Maybe even all the way there. And that's because it's open source. It has an extremely flexible API because it has no context of where it will be used. So it needs to be flexible so that you can fit it in the product that you're building. And then of course, with so much customization, it can become messy and fragmented because, well, the component library doesn't have a lot of design decisions baked in. All of those lie in the product side of things. So taking this, it kind of is like, what is right for you? Where should your component library fall into the spectrum of flexibility? And the answer is that it kind of depends, doesn't it? Always. But this is what it depends on. It depends on is your component library built for a brand or is it built for open source? Which is like everyone. What's the design culture in the brand that you work for? Is it consolidated with a small group of designers or is it a bunch of independent teams that operate pretty much on their own? And even what's the maturity of the products that you're working on? Do you have established patterns or is it still early and it's more experimental, still playing around to find those patterns?
So the answer then basically is that it depends, right? Thank you for coming to my talk. Bye bye. But no, I tried to use these and tried to apply them to this component. This is an ActionList component inside an overlay.
2. ActionList Component and API Design
This is an ActionList component with various variations used across GitHub. The API design journey explores the balance between control and flexibility. The menu can be created by passing items to the action list and defining the on select behavior. For different actions, a divider API is considered, with the option of using a specific text or a type. The ActionList.divider provides a good balance by hiding the implementation detail.
This is an ActionList component inside an overlay. And you'll see this spread across GitHub. There's a lot of variations of this, but this is a pretty involved component. So it has a bunch of variations. And this is some of them. So you see this on a repo page. Just to the side, you see this as options in a dropdown. You see these as in the releases panel. This is the releases one. You also see them as trying to select assignees or reviewers. So there's a bunch of... There's a lot of range in this component.
And I was trying to figure out what should the API look like, how controlled or flexible should it be? And let me take you through that journey, so that I can quantify what it depends means. So let's do this API exercise from the start. I'm building this action list. This is the issues page. And quick, quick warning for you, none of these designs are actually production design. These are all from an exploration. So if it doesn't exactly match the aesthetics of what you see on GitHub.com, that's actually intentional. This is all an exploration.
Okay, that out of the way, this is the menu that you see, and it has some styles. I think it has some keyboard interactions. Yep, there you go. So to make this menu, let's see, the simplest menu over here could be, you have an action list and you just pass it items, and the items are these options that you're going to do. And then on select, you do something with it. So you can call your own action depending on the string. It feels a little weird to do these actions based on just like the string content. Like if you say quote reply, this is what you get in return. So it feels weird to just act on a string. So maybe something like this is better where it's a config, it's an object, and then you can pass in your on select. So depending on the copy of that, you can also pass it what the selection should be. And that feels decent.
So we move on to the next use case where not all of these have the same actions. So some of these are based on action to, you know, like, taking it outside. Some of these are about the content itself. So edit and delete. And this is third, which is, you know, it's a report. It's a moderation thing. So we want to separate them with dividers and now what should the divider API look like? So I thought about it and I was thinking you already have text, should it be like a secret text, like a very specific text that you give. Like if you give me underscore divider, then I know how to put a divider or maybe it shouldn't do that at all. Maybe it should be a type, right? And all the other ones are type row and this is type divider. But then of course, you know, like is it small case divider, upper case divider, all of that. So maybe you should import it from the ActionList and then it's just a value on the objects with ActionList.divider type. And if you're already doing this, then maybe this is ActionList.divider, where, you know, ActionList.divider is an object that is an internal detail that knows how to render the divider. This feels like a good balance where we're hiding the implementation detail of divider. Nobody needs to know there's a type divider. All you do is put an ActionList.divider there. Feels good. OK, moving on. Next use case, some of these are destructive.
3. ActionList Component Customization
So delete comment is a destructive thing. We wanted to have a different, scarier style. Let's move on. On the right side of the issue, you'll see all of these options. Let's start with labels. The difference here is that it has a circle, the label color. Seems smart. But when we talk about assignees, assignees has, doesn't have labels, it has avatar. So seems smart enough. So we have, it could be a label color, or it could be an avatar. Right, and the ActionList component knows how to render both of these. Seems okay. But then you have other things like milestones, which has this icon, you have projects, which has this icon. Maybe we just need to know if it's a label or avatar or an icon. And that probably just works, doesn't it? For an icon use case, maybe we just import an icon from our icon library and pass it on, so milestones know how to render an icon. But what happens if you give it an avatar or an icon port? So this is one of those things where if the API pushes you in a certain direction it should be the direction that you want. You don't want people to make these decisions, to fall into these traps because they're not trying to break the API, they're just trying to create the selection feature. So coming from that realize there should only be one prefix, there should only be one visual be an avatar, it could be a label, it could be the selection icon and you import that and you pass that as a prefix element. So in this case you only get one, you get an avatar, but of course if you get a prefix element then you also need to pass it a prop. So you get something like maybe prefix props where I have to pass the avatar URL. And if you think about it, this is like if you have an element and props, this is a component so we might as well just call it a component. So this is a prefix which accepts any component that you want and it tries to put it here. Now definitely more flexible, definitely more in sync with what we need it to do, but the trade off here is that you could put any component that you want. So this opens up the API more than just three allowed config to put whatever you want, but if you want an icon, then you can. You can import the icon and put the check icon is what I wanted.
So delete comment is a destructive thing. We wanted to have a different, scarier style. Could be as simple as adding a variant. So everything is variant default or variant subtle. And this one is variant danger, seems like. Seems to fit well. Let's move on.
Now, on the right side of the issue, you'll see all of these options. You have labels, you have assignees, you have reviewers. Let's start with labels. So if you look at labels, it kind of is similar. It's an action list with options that you can click. The difference here is that it has a circle, the label color. So to begin with, I'd say let's just put all the items here. There's a text. There's an unselect. And there's a label color. Because I only have text here, I need something to qualify that there should be a circle. I'm putting this special smart key here, which is called label color. And if you pass it a hex value, it knows that it has to render a color, render a circle with that color. And it takes care of what should be the margin, what should be the gap between the color and the text. Seems smart. And you know, so you do it with all of them, and realistically, if this is being used in a product, it will look something like this. You would loop through repo.labels, or like a config of labels, and pull the label color out of it. Seems smart. Okay.
But when we talk about assignees, assignees has, doesn't have labels, it has avatar. So do we just do repo.collaborators, map through them, and then you have text and onSelect, which is the same. But instead of label color, it becomes avatar, and it knows it has to render a circle, how much gap should there be, and you just pass the user.avatar.url. So seems smart enough. So we have, it could be a label color, or it could be an avatar. Right, and the ActionList component knows how to render both of these. Seems okay. But then you have other things like milestones, which has this icon, you have projects, which has this icon. In fact, if I show you all of these samples, there's a bunch of these. Some of them have icons, some of them don't. Some of them have this indentation, but overall, it looks like most of these are either avatars or their icons. Some of the colored icons, but they're still icons. So maybe we just need to know if it's a label or avatar or an icon. And that probably just works, doesn't it? For an icon use case, maybe we just import an icon from our icon library and pass it on, so milestones know how to render an icon. But what happens if you give it an avatar or an icon port? I'm not saying people are actively trying to do this, they're trying to break something, but it's more like if they're trying to achieve something else, like a selection and they want to put a checkmark, does this feel like a feasible solution? So you import the check icon and you want to show checkmark next to the person it's assigned to and you will end up with something like this in this case where there's not enough space for both of them so they just kind of shift everything and then you're thinking about how do I create that margin, how do I get more space, maybe I can overwrite some margin with CSS and this API looks like that is a valid direction even though we don't want you to do that with the component so this is one of those things where if the API pushes you in a certain direction it should be the direction that you want. You don't want people to make these decisions, to fall into these traps because they're not trying to break the API, they're just trying to create the selection feature. So coming from that realize there should only be one prefix, there should only be one visual be an avatar, it could be a label, it could be the selection icon and you import that and you pass that as a prefix element. So in this case you only get one, you get an avatar, but of course if you get a prefix element then you also need to pass it a prop. So you get something like maybe prefix props where I have to pass the avatar URL. And if you think about it, this is like if you have an element and props, this is a component so we might as well just call it a component. So this is a prefix which accepts any component that you want and it tries to put it here. Now definitely more flexible, definitely more in sync with what we need it to do, but the trade off here is that you could put any component that you want. So this opens up the API more than just three allowed config to put whatever you want, but if you want an icon, then you can. You can import the icon and put the check icon is what I wanted.
4. Component Customization and Reviewers
You can pass the leading visual component different elements like the avatar, label color, or milestone icon. The description field allows for different text styles and variants. Reviewers have suggestions at the top and everyone else at the bottom, with the ability to group users. The API uses group metadata and IDs to determine the order and allows for different variants like filled or subtle.
You could do something like that. So we don't call it the prefix, we call it leading visual, so that's in sync with Figma, tiny implementation details, shouldn't matter. And it works. You can pass it the avatar, you can pass it the label color, you could pass it the milestone icon. So it fits all of these use cases without really breaking the API. So I like it.
Let's move on, make it more complicated, of course. So looking at this, so you see labels. Actually, assignees. We show the name of the assignee right next to their handle. And it has a different text style and it shows up in line versus a label over here. You can put a lot more text into it and it shows in the next line. I think this pattern is also there in milestones, in the next line, projects. It shows the repo or the org in the next line. So I'm just calling it description, right? This is another key. I don't want people to change the text color or anything like that. So I'm just accepting a text field here, which is user.name and also works for labels because it could be what's the label description. And then if you wanted in the next line, then you give it a variant of blocks. It could be in line. It could be block and I call it description variant. So this is still pretty lopped up. Like a config API seems to be... There's one escape hatch and there's like the element and element variant ugliness, but overall it seems to be doing well. So let's keep going with this.
Now let's talk about reviewers because that's a slightly more complicated component. So this is what the viewers look like. You'd see that in reviewers you have suggestions at the top and you have everyone else at the bottom. So you have two variations of this. You have groups of folks, which is not something you have in assignees. In assignees it's just one flat list, but in reviewers it's grouped. So how do we deal with groups here? Maybe we just pass the group so you know, if like the user has recently edited this as a function, could be anything. Then it goes into suggestions, otherwise it goes into everyone. So everyone with that title gets grouped and we know we have to render the suggestions before the first one. Kind of icky, but could work. But how do we decide what comes first? Does suggestions come first for everyone? This isn't reliable, right? Because if the first one is everyone, then does that come first? Does that make sense? So that kind of leads us to a slightly different API, where we want group metadata to be on the top so that you can declare which comes first. Then now that you have these, we don't have to rely on strings anymore. We can make this ID. So if it's recently edited, then it should be the zero-th element, which is suggestions. So you have the group ID here instead of group. And if it's otherwise, it's like the other case, which is one, which is of this element, everyone. Seems all right. I also did something else, which is this is filled up instead of being plain. So let me go back to the previous one. This was inline. Inline or subtle. But the next one is actually filled up. And different areas in GitHub use different styles for this. So I think in the reviewers, we definitely use a filled one, but there are other places where we don't where we use a subtle one. So in this case, we kind of have to also say which variant we want. We want a filled variant for reviewers, not the subtle one.
5. Component API Flexibility
And finally, we want this header that says requests up to 100 viewers. We have both in-line and block descriptions. If we don't want to handle specific cases in the component library, we can defer responsibility to the product. However, I believe there is a better approach. We need a more flexible and composable API that lies somewhere between a config-based API and an open-source style library. This can be achieved by using components instead of objects to render items in the action list.
And finally, we want this header, which is it says requests up to 100 viewers. So I put the header, I put title, because that's what group also has. And these are similar APIs. And this is very unsubtle and not this is not filled up, this is always subtlet. So it takes the same props. Looks fine, not super happy with the group metadata and group ID. It's like, you know, you have to create this is kind of like a side effect of config based API where you have this group ID, but then you have to create another another object because you can't fit that in items. It's not items, it's a different hierarchy, but but it's good so.
And finally, we have this, which is we were only dealing with one sort of description, it was either in line or it was block. But here we have both because we actually want to see what's the reason they have been suggested. So we have in line and block both. And so how do you accommodate that, I guess you do this where instead of description, you have descriptions plural. And you can give an array where you have two texts and then you have two variants, one is in line, one is block. And I don't know if you give three, I guess, then it has to fall into one of these and then the last one wins or something. So you can see like I'm forcing this API to kind of do too much. And it is it is struggling a bit like this API isn't the most intuitive anymore. It started off strong, but not so much anymore. And finally, if we don't want to do this, if you want to do something like there is a point where you say, OK, this component has had enough and now we don't want to take care of this in the product, in the component library. This feels like a job for the product. You have to defer responsibility and say, you know, that's this is like inversion of responsibility and say, this was very product specific. This is not something happens across GitHub, this only happens on this one screen. So this should lie in the product and we won't put it in the in the one that's common for everyone. So for that, you need to enable some sort of way that they can do this. And I imagine it kind of looks something like this for the item item list, except something like a render item function. Right. This is like a render prop function. And then you get all the internal props and these are props that are useful for you know, all the behavior and hover styles and keyboard navigation. We have a lot of props for this. So we want all of those to get passed on. But then inside it, as long as you pass all the internal props, you can decide however you want to render this. So as long as you use the right primitives, where you use the same avatar that we use and the same text color and text size for this. Everything's everything would work out. And this is this is the approach that a lot of component libraries take where they have an escape hatch, where if things get too crazy, you are allowed to, you know, fork out of it and everything's fine. Not a super big fan of this approach, because it kind of just deferring the responsibility. I'm a bit scared of what happens inside this internal props if it's applied correctly. And you can end up breaking responsive behavior or accessible behavior, if not done correctly, if not composed correctly. So you can end up building components that don't follow the guidelines, even though you've taken all the building blocks of the guidelines. So all that to say that I don't think deferring the responsibility on the product is always the best idea. It's already like the last resort, but I think we can do better here. Here's the other API style. I think we've clearly seen here that this API needs to be a lot more flexible. The config based API doesn't really suit it well. We need a more composable API. So on the spectrum we need to be a slightly more towards the right side. Maybe not all the way to open source style library, but somewhere in between those two. This is what it would look like with a different API where I have this action list. And then to render all the items we used to do this, where we had this array of objects. Instead you get a different item inside the action list. So which is just a component. Because this is all Java Script, what you can do is create a component action list, create another component called item, and then attach it, because it's all objects, to the action list.
6. Action List Component Composition
And then you only export one, which is the parent. So you end up doing things like action list.item, and it maps to the internal item, which is perfect. So you get action list dot item. You can put the text inside. It's called children. Moving to dividers, you render the action list divider. And just like item, it's a component and you can put it wherever you want. For labels, you can have action list dot leading visual as a component. In this case, leading visual is this area on the left. It knows how much margin to put. And then we could put label color. Going on to description, you could use a stack, you could use a grid for this in the action list, and each one could have its own grid position. So it's basically a layout component at this point. And you could put anything inside description, which is a bit scary because in the previous API you could say, this is only a text. It cannot be anything else.
And then you only export one, which is the parent. So you end up doing things like action list.item, and it maps to the internal item, which is perfect. So you get action list dot item. You can put the text inside. You don't need to say text prop anymore, because react already has a way of putting content inside an element. It's called children. So you can use the children. You just put it inside and pass the onSelect. Seems easy, seems pretty similar, not that different. It seems to work well.
Moving to dividers then, this is the next use case. This is how we did it earlier, where we had a few ways of cryptically telling what is the divider. In this case, I think it can just be a component, because you render the action list divider. And just like item, it's a component and you can put it wherever you want. And that creates the divider. Right there. Seems same. Not very creative. And then you had this variant. I think I'm going to keep that behavior, variant as a prop seems to make sense. The default is inline or subtle and you can pass the variant danger. Alright.
Now, this is where it starts getting interesting. For the labels, whoops, selected. For labels, what we were doing was we had this leading visual, right? And so this is what it would look like by default, you would, inside the action list, you would map through labels and then use the action list item. Give it a key because now it's a loop. We're mapping through it. And then have the on select and then, you know, the children is where the label name goes. So we could just put something before this, like we could put the label color component, but then we would be responsible for, you know, what is the margin towards the right and stuff. Instead, what you could do is have action list dot leading visual as a component, right? So you see what we're doing here. We're creating all of the child components that can be composed together to create this style. And the children know where they should go. Like the styles are already inside the component, so they always end up at the right position. So in this case, leading visual is this area on the left. It knows how much margin to put. It knows that it has to center vertically all the content inside, and then we could put label color. If it's the collaborators list, if it's if it's this list, then we know that, you know, we have to put the avatars inside and if we have, let's see.
Okay, so if you have the milestones and projects, it kind of looks very similar. So going on to description, because that seems like the very next. Again, does the description go here? Like, do I say user.name. But then I also know it has to have a different style. So maybe I'd use action list.description. In the previous API it was just a variable here. In this, it's a component and you say action list.description. And you see, as we're laying these down, they all know their place in the row of this component, so they all take up that space, which is already decided. And you could use a stack, you could use a grid for this in the action list, and each one could have its own grid position. So it's basically a layout component at this point. And you could put anything inside description, which is a bit scary because in the previous API you could say, this is only a text. It cannot be anything else. Over here you have children, so can children be something else? Not necessary, you can actually...
7. Component Slot-Based API and Flexibility
You can make the description or children typed, allowing for different variants and types. The component uses a slot-based API with predefined slots for leading visual, text, inline description, and block description. It dynamically fills the slots based on the child components used. The middle area is free-form, allowing for flexibility. The component renders the slots using Flexbox or grid layout. Milestones, projects, and labels can be added to the slots.
Oh, I skipped a slide. Okay. Let me come back to that point. But labels, remember, they have to be block variants? We already have a description. So we have a place where we can put that prop, which would be variant block. So that's done. Now, coming back to the thing I was saying about type, you can actually make description or make children be typed, right? There's a way to do that. Which is, you say using type script in this case, but you could do it with proper types as well. Where the description type, the variant can be inline or block, but the children have to be a string. They don't have to be children as a node, right? Which is the default with React. It could be anything you want. You could have a number if you wanted. So in this case, we just type children a string. And if you'd try to give it something else, you got a warning that, you know, please don't do that. It's not allowed. And to show you how this component actually works, how do things fall exactly into their spaces? What I have in this place, or at least this one that's rendered on the left, is I have something of like a slot-based API. So I have these slots where I have this is the leading visual. This is the text. This is the space for inline description. And then I have space for block description. And what I'm doing is, if you use the children from ActionList, if you use the components that we ship with it, we look at the child.type, so I'm looping through all of these children, looking at the child.type. And if there are things that we already identify, then I fill the slots. So, for example, the leading visual, if the component type is ActionList.leadingVisual, which we ship, it goes into the slot of leading visual. So if you try to pass multiple leading visuals, well, I guess the last one wins, and that's totally fine. It kind of shows that it only accepts one. And we have the same thing for description. If it's description and it's a variant block, it goes into block description, otherwise into inline and everything else goes into the text, right? So the text part over here is something I have in an array. So you could put multiple text blocks over there if you wanted. And that's the only free form here. Everything else is typed and has a place. But the middle area is something that you could put anything. So that's the one we keep free. And then I have some selection state as well, which, you know, I don't need to show you just yet. But other than that, it's your good old component. It renders a slot. If there is nothing in the slot, it renders nothing. It just goes empty. But I'm using Flexbox. You can use grid. It's layout thing. So slots as an API choice. Very cool. I like it. Let's see if the milestone and project things work. So we have the leading visuals. We have the name. We have the description. And you can see that I can switch what goes in the middle. What goes inside the slot. And I can get milestones, projects, labels, all of them fit in the slot.
8. Component Composition and Milestone Selection
So I'm only changing the slot. The rest of the structure is the same. Now, let's talk about this review. In reviewers, we're kind of flipping the paradigm. Inside the action list, we want to put two groups of elements. So I just created an action list group, same style as before. It composes very nicely. We've been able to take all the parts without breaking the API and keep composing them. Moving on from this, milestone is a leading visual, but it also has this cool effect where you can select something. There are a bunch of things you could do. You could probably just show the check mark as a check icon. But we also want this space to be opened up, right? This kind of doesn't work. You need to know that one of the slots is filled.
So I'm only changing the slot. The rest of the structure is the same. It's perfect. All right. Now, let's talk about this review. So in reviewers, remember we had the metadata thing, which I didn't like? In this case, we're kind of flipping the paradigm. Inside the action list, we want to put two groups of elements. So I just created an action list group, same style as before. And it takes a title. It takes a variant. And honestly, this title is not a text, because I want to fill this group with all the items. So the group has children and the children are actually action list items. So I can map through suggested folks and put them here. And I can map through everyone else and put them in the second group. So then, you know, there's no mapping, there's no metadata. It's all you think of one group at a time, and then you fill what the folks that go inside it. And inside that you have the same thing that we just saw. You have a leading visual, which has an avatar, you have the text, you have the description, nothing, nothing too wild. It kind of just works. It composes very nicely, like we don't have to learn anything new. There were no extra props on the item. The only new API was for group and it's all self-contained. So you see that we're taking different paths and we're composing them with each other. And they seem to be seem to fit really well from an API standpoint. Finally, remember, we had these two descriptions. One is in line, one is block. How do we fit these? Well, we just put two descriptions, I guess. So you have actionless description variant in line and we have actionless description variant block. And both of these just sit next to each other and you know, it doesn't really matter what order you give them in because we have the slots and the slots have the specific space to go in. So yeah, you can just put two descriptions if you wanted. It works pretty nicely. All right. Finally, I made a header component, which takes the title and the variant. Again, I wanted to put the title inside because you know, React has a way of putting children, but so that it stays consistent with the group API, I kind of follow title and variant instead instead of putting inside. Tiny, tiny choice though. All right. So now if you compare, this is what it looked like in the group metadata in the previous config API where you had a group metadata, you had to match it with the group ID, we had to kind of do this escape hatch render for the descriptions. And over here, it's definitely, you know, it looks a lot longer, but it's also more composable. So, we've been able to take all the parts without breaking the API and keep composing them and it kind of really works nicely. All right. So, moving on from this, let's see if the other things work. So, milestone is a leading visual, but it also has this cool effect where you can select something you know? And this we didn't even talk about in the, actually you know what, there you go. So, we didn't even talk about this in the config API because well, I kind of didn't really have space for it anymore. But in this case, there are a bunch of things you could do. You could probably just show the check mark as a check icon, you know. So, if this milestone is selected, then put a check icon here. But we also want this space to be opened up, right? Because we want these to be aligned. We don't want these two to be left aligned and then have no gap. So, this kind of doesn't work. You need to know that one of the slots is filled.
9. ActionList Selection and API Scaling
You can use the ActionList.selection component to handle single and multiple selection behavior. This simplifies the API by automatically rendering the appropriate selection style based on the selection variant. The slots-based API ensures the component knows where to render the selection.
So, maybe do this what we've been doing till now. You have action list dot check. And then you pass checks if it's checked true, then it renders this. If it's checked false, then it knows it has to put an empty space here. Kind of works. In the case of labels, actually, let's look at assignees. In the case of assignees, though, you'll notice that it's not a check anymore. It's a checkbox because it's multiple selection, not just one. So, you know, we don't want to close this with a tick. We actually want to keep this open, put all the check marks. So, maybe instead of check we just do check box and then it ships to. Kind of. But I think there's a simpler way because, to be honest, based on if it's a single selection or a multi selection, we want to change the behavior, which is, does it close instantly or not? And we want to change what the selection looks like. So, I'm going to replace this with ActionList.selection. And the ActionList knows which selection to put based on the selection variant or maybe a different name for this, you know, single select, multi select. I've chosen selection variant where you can make it single and multiple. And based on this, the action select knows which selection style to render, right? So then it becomes slightly less of an API. You don't have to pick between a check box or a check mark, a check icon. You just have to say, I have selection in this. So, you put that component against slots based API. It knows ... What did I do? Oh, I clicked. I went into her profile. Give me a second. I need to go back. Go back. No? Oh, there we go. So, yeah, because of slots, it knows exactly where to render it and it picks based on the selection variant. So the API is scaling well.
10. Action Menu Component and Composable APIs
With the action menu component, you can use the action menu dot anchor to define the anchor element. Anything else you put inside the component is rendered inside the overlay. You can add different components like a menu, an emoji picker, or an action list with a selection variant. To implement a search pattern, you can render a text input inside the overlay and use an action menu divider or an action list divider for the desired behavior. The action menu dot text input component provides focus styles and allows you to control the state and behavior while the product controls the layout and behavior. Composable APIs enable you to combine components from the product and the component library, providing flexibility and customization options.
And finally, we haven't really talked about how this gets rendered in the menu. So, till now we've been looking at this, the contents inside. But what about the action menu? The overlay? What you click to get this? So that's not action list, that's a different component called action menu. Names are similar because we want to couple them a lot of times. So let me see. Okay, this is not the example. So with the action menu, what you do get is an action menu dot anchor. So for example, these three dots are an anchor for that case. But in this case, this whole thing is an anchor. So you have assignees and you have this gear, and the whole row is an anchor. So you can put basically put anything you want. I have assignees and gear and they're aligned and whatnot. But other than that, anything else that I put here is, is rendered inside the overlay. So the action menu essentially goes against slots, knows what an anchor is, knows everything else is up to you. So you could put a menu, you could put an emoji picker. I should have the example, you should put the emoji picker here. Or you can put an action list with a selection variant and that goes into the overlay. So what about this pattern that you see sometimes where you can have a search? So I can search for Mike and this list filters. So we want to put something else here. Now, in a config-based API, you would have to declare this, that user filter, or filter true, and then it would render the input. You would have to give this placeholder, find a user, you would have to say what happens when it's on change. So you're looking at a bunch of config variables, a bunch of list of new props that would be added to the top-level API. In this case, I think what we can get away with is render a text input. If you render it here in the open area, it will go inside the overlay. Right? Go inside the body of it, and just before action list, and you can add an action menu divider or an action list divider, they're kind of the same to get this behavior. And because we want some focus styles, like check this out. If I am selecting, I can move my keyboard around, but my selection state would still remain here so that I can keep typing. This behavior is very specific. So to do that, we actually ship action menu dot text input. And this is like, it renders the text and it renders the divider. It renders all the behavior, but then you basically can give it whatever placeholder you want. Oh, see, it's still locked on that. You can give it whatever placeholder you want, like this one. This one says, find a user. And then in the on chain, you say, filter users, and then you're responsible for filtering. So you control all the state in the product, which makes sense. And the component controls the layout and the behavior, but not the state. Makes perfect sense. So this is just an example of how you can use Composable APIs to compose components from the product and compose components from the component library, put them all together. And it kind of still works, because we've thought of all those use cases where we don't really know what people will put here, but we know that they want to put things in the overlay. So we give them that slot, which is kind of an open slot, and you can fill it with the things you want.
11. Flexibility and Building for People
Experiment to find your spot on the flexibility-consistency spectrum. Design systems and component libraries are for people building products, not just about the API. Composition allows flexibility. Build for people, remember you're building for people. Check out Primer and the design system on GitHub.
Okay. Let's see. So to summarize, experiment to find out your spot on that flexibility consistency spectrum, right? Like somebody else can't really tell you that. You have to know your company culture. You kind of have to know the kind of use cases that you serve. In our case, I thought we were more on the consistent side, but we're actually in a lot of cases, we have to be, GitHub is big enough to be, you know, like, there are a lot of use cases. So we probably slightly more towards the composable size, more flexible. But you can still use composition to allow flexibility without ejecting, right? And which brings me to the last point, like design systems are for people, component libraries are for people. Brilliant quote. Love this. This is by Gina. And the summary there is that people using our components are just trying to build the products, right? They're trying to build products for their customers. They're not so much interested in what the API is or they're not super interested in, the correctness of the API. It's nice when the API feels good. It's nice when the API is consistent. All of those things are important, of course. But that's not the main thing. The main thing is what can you build with the component so that you can build something for the customer. So, and this is like, I really like the idea of composition, allowing that flexibility because like the folks have to get the job done. If your component library doesn't do it, they'll do it with something else. They'll build their own custom components, right? Like they still have to, there are jobs to be done. So that's the idea. Experiment to know where you are on the spectrum, use composition instead of ejecting out of systems. And finally, remember that you're building for people. Thank you. That's it. If you want to look at Primer. If you want to look at the design system, it's right here, it's on github.com. And that's my Twitter. If you like what you heard, if you want more of that, that's kind of it. So ciao.
Q&A Session
I'm doing well. How are you? Good. Good. I love the talk. One of the first questions is about deciding if a component should be part of the design system based on its usage. Another question is how to decide if a component should be split up to support different use cases without making the API too complex. Dealing with design exceptions can be done using composite patterns or nested styles. The decision between creating your own design system or using a components library depends on the level of customization required and the stage of the project. Finally, there's a question about the color-coordinated books in the background.
I'm doing well. How are you? Good. Good. I love the talk. I was really enjoying that. And I'm going to go back and finish it off later. But I've got some questions that have come in. And I'm just going to pose them to you.
One of the first one is in a nutshell, is there a thumb role to decide if a component should be part of the design system? How many times do you need to use it? Is there a number of usages enough for you to make that decision? That's a very good question. So I guess the thumb role is something that you hinted at, which is how many times is it used, especially across pages and products. So the moment you see something is a pattern that repeats a lot, but of course each product or each team would develop it for their own use case. It's a sign that across teams and across products, it would be just a tiny bit inconsistent or it could be accessible in one but not in the other. So that's usually a sure shot sign that this could be put in the system, polished, made consistent and then released back to the teams to use that one true version. That's awesome. Thank you.
And one thing I really loved as you were going through the talk was you were slowly building it and we watched this design system get more and more complex. And there's a relevant question to this where someone asks how do you decide whether a component should be split up into multiple components to support different use cases without that API becoming too complex? Yeah, that's a good question again. I don't know if there's an easy answer for it other than you're trying to build a few examples with it and you can like, the thing I tried to do in the talk, you can start telling when it starts becoming icky to use. When it starts becoming a little too, you have to do too much guesswork or copy from the docs exactly. So when it's not predictable anymore, you use the API, you can't really tell what's going to happen. That's usually a sign that this component is doing too much and you might benefit from either a specialized component or the API is just going in the wrong direction and you have one to split it. In the second half you'll see I'll take a more composable approach. So it's the same amount of components, one with a bunch of children, but it's a more composable API, so you can actually do more by keeping the API smaller. Thank you.
And another one as well, because there will always be design exceptions. When you're trying to deal with those, is composite patterns the only way to be flexible enough but still stay abstract? At least in my experience, that's the way that I like the most. Some folks also have nested styles, so you basically pass it to the root, and you can reference each child and customize them. So maybe in the top-level list, I can say all of the images, they should actually have this extra border, because I chose so. But I like to co-locate them, right? As close as I can to them. So I usually prefer composite, but it's CSS. You can actually start from the top and cascade it down. Nice. And another thing I want to get your opinion on, what's your view on creating your own design system versus using a components library? I know, like you said, it depends. But how do you usually make that decision? I actually don't have a good answer, so I'm going to give the answer that I personally do. Which is you can start off with a library, which is hopefully an unstyled one. There are a few now. For that, you can at least control the styling, but eventually I always hit a point where I want to customize it more and have to go inside the internal. So the moment you find yourself customizing the behavior and not just the aesthetics of it, that's a sign that you have to pull this component out and build it from scratch. Make it your own. But honestly, if you look at a component library and you're like, we are happy with the behavior that this component gives us, especially if you're like early stage, you don't have to get everything exactly the way you want. And you're slightly more flexible. Shipping is more important than... It's my opinion, I think. Then sure, use a component library. Tried to find an unstyled one, cause you at least want it to feel like your own company. You don't want it to feel like somebody else's. And we've run out of time, but I want to put one more question for you. Did you deliberately color coordinate the books you've got in the background? Yeah, yeah. It's true. All of these are color coordinators. Some of these are not even mine. I haven't read them. They're my wife's. But I borrowed them just so that I can still decode. Nice. I put it there because it makes the transition smoother. That's nice. Thank you so much Sid.
Comments