Lorem ipsum dolor sit amet, consectetur adipiscing elit lobortis arcu enim urna adipiscing praesent velit viverra sit semper lorem eu cursus vel hendrerit elementum morbi curabitur etiam nibh justo, lorem aliquet donec sed sit mi at ante massa mattis.
Lorem ipsum dolor sit amet, consectetur adipiscing elit ut aliquam, purus sit amet luctus venenatis, lectus magna fringilla urna, porttitor rhoncus dolor purus non enim praesent elementum facilisis leo, vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in ornare quam viverra orci sagittis eu volutpat odio facilisis mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices in iaculis nunc sed augue lacus
At risus viverra adipiscing at in tellus integer feugiat nisl pretium fusce id velit ut tortor sagittis orci a scelerisque purus semper eget at lectus urna duis convallis. porta nibh venenatis cras sed felis eget neque laoreet libero id faucibus nisl donec pretium vulputate sapien nec sagittis aliquam nunc lobortis mattis aliquam faucibus purus in.
Nisi quis eleifend quam adipiscing vitae aliquet bibendum enim facilisis gravida neque. Velit euismod in pellentesque massa placerat volutpat lacus laoreet non curabitur gravida odio aenean sed adipiscing diam donec adipiscing tristique risus. amet est placerat in egestas erat imperdiet sed euismod nisi.
“Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum”
Eget lorem dolor sed viverra ipsum nunc aliquet bibendum felis donec et odio pellentesque diam volutpat commodo sed egestas aliquam sem fringilla ut morbi tincidunt augue interdum velit euismod eu tincidunt tortor aliquam nulla facilisi aenean sed adipiscing diam donec adipiscing ut lectus arcu bibendum at varius vel pharetra nibh venenatis cras sed felis eget dolor cosnectur drolo.
In the first part of this series, we've looked into the purpose component libraries serve and the reasons to use them. Then, we built a mini React app using only native HTML controls and CSS styling, to demonstrate the capabilities of that approach as well as its limitations.
In this second part, we'll rebuild the same app using a component library called Radix UI and see what advantages it offers.
Radix UI is a component library that comes in two layers:
Let’s see how our <SelectCountry>
component can be implemented using Radix’s primitives. We’ll skip the installation process – you can find the instructions for that on the official documentation. We’ll also skip the parts of the app that are the same as our previous version. Let’s see the part of the code that changes:
1import * as Select from "@radix-ui/react-select";
2import * as Label from "@radix-ui/react-label";
3
4export default function SelectCountry({ countries, onCountrySelected }) {
5 return (
6 <Label.Root>
7 Choose country:
8 <Select.Root onValueChange={onCountrySelected} defaultValue={countries[0]}>
9 <Select.Trigger>
10 <Select.Value />
11 <Select.Icon />
12 </Select.Trigger>
13 <Select.Content>
14 <Select.Viewport>
15 {countries.map((country) => (
16 <Select.Item key={country} value={country}>
17 <Select.ItemText>
18 {country}
19 </Select.ItemText>
20 </Select.Item>
21 ))}
22 </Select.Viewport>
23 </Select.Content>
24 </Select.Root>
25 </Label.Root>
26 );
27}
And this is how it looks like:
Oh, no! What happened here? Why does the open list look like that?! That’s worse than our previous version!
Yes, that’s exactly the point – Radix Primitives are unstyled. They aren't using the default styling of the native HTML controls, and they don't contain their own defaults either. It's us who’re responsible for the entire look and feel.
We can use any CSS method we prefer, but for this example, in order to keep our code in one place, we’ll install TailwindCSS and use its utility classes for most of our restyling work.
Also, since we want to demonstrate the benefits of using Radix over the native HTML controls, let’s add flag icons using the country-flag-icons package. Here’s the updated code:
1import * as Select from "@radix-ui/react-select";
2import * as Label from "@radix-ui/react-label";
3import * as flags from "country-flag-icons/react/3x2";
4
5const flagNames = {
6 Britain: "GB",
7 France: "FR",
8 Israel: "IL",
9}
10
11export default function SelectCountry({ countries, onCountrySelected }) {
12 return (
13 <Label.Root className="flex w-full text-sm font-medium text-gray-700 mb-2">
14 Choose country:
15 <Select.Root onValueChange={onCountrySelected} defaultValue={countries[0]}>
16 <Select.Trigger className="grow inline-flex items-center justify-between px-2 py-0 ms-2 text-sm border border-gray-300 rounded-md shadow-sm bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
17 <Select.Value className="text-gray-700" />
18 <Select.Icon className="ml-2 text-gray-500" />
19 </Select.Trigger>
20 <Select.Content className="mt-1 bg-white rounded-md shadow-lg">
21 <Select.Viewport className="p-2">
22 {countries.map((country) => {
23 const Flag = flags[flagNames[country]];
24
25 return (
26 <Select.Item
27 key={country}
28 value={country}
29 className="px-4 py-2 text-sm text-gray-700 rounded cursor-pointer hover:bg-indigo-100 focus:bg-indigo-100"
30 >
31 <Select.ItemText>
32 <Flag className="inline w-4 h-4 mb-1 me-1" />
33 {country}
34 </Select.ItemText>
35 </Select.Item>
36 )
37 })}
38 </Select.Viewport>
39 </Select.Content>
40 </Select.Root>
41 </Label.Root>
42 );
43}
And the result is:
Pretty nice!
Of course, this is just an example, we could adjust dozens of other details – that’s exactly why Radix Primitives provides us all those building blocks.
Yet, this isn’t exactly what we aimed for, is it? We tried to get rid of component complexity so that we can focus on our app, but the complexity didn’t go away. It just took another form, as we now need to manage all these small building blocks. In the code above, we have used 8 different sub-components just for that simple dropdown list, not counting the label and the flag. Sure, it’s great for customization, but we might not need that level of fine-grained control right now. Can’t we get something simpler and more abstract while still benefiting from some degree of customizability?
The good news is: we definitely can.
Radix Themes is a high-level UI library, built on top of Radix’s primitives. It’s opinionated and provides reasonable defaults, but still maintains some level of customization. To use it, besides installing the package, we need to import the dedicated CSS file and wrap our app with the library's <Theme>
provider. But all of that is just the installation process, which you can find in the official documentation. Let’s skip that and see how our component’s code would look once we switched to using Radix Themes:
1import { Select } from "@radix-ui/themes";
2import * as Label from "@radix-ui/react-label";
3import * as flags from "country-flag-icons/react/3x2";
4
5const flagNames = {
6 Britain: "GB",
7 France: "FR",
8 Israel: "IL",
9}
10
11export default function SelectCountry({ countries, onCountrySelected }) {
12 return (
13 <Label.Root className="flex w-full text-sm font-medium text-gray-700 mb-2">
14 Choose country:
15 <Select.Root onValueChange={onCountrySelected} defaultValue={countries[0]}>
16 <Select.Trigger className="grow ms-2" />
17 <Select.Content>
18 {countries.map((country) => {
19 const Flag = flags[flagNames[country]];
20
21 return (
22 <Select.Item
23 key={country}
24 value={country}
25 >
26 <Flag className="inline w-4 h-4 mb-1 me-1" />
27 {country}
28 </Select.Item>
29 )
30 })}
31 </Select.Content>
32 </Select.Root>
33 </Label.Root>
34 );
35}
And this is how it looks like:
Honestly, the <Select>
component is a somewhat bad example here, as it’s inherently a multipart control and even with Themes we still have to deal with multiple subcomponents.
And yet, we’ve just reduced the number of subcomponents by half – to four instead of eigth: Root, Trigger, Content and Item. We no longer have to get down to the resolution of the little arrow on the side, or the distinction between the item and item text.
We also no longer need to handle basic styling, such as setting an opaque background for the open list – we get that with the library. As you can see, we got rid of most of the tailwind styling classes we've used before.
A library like Radix Themes aims to give you ready-to-use utilities, and naturally this trades-off with the ability to control every detail.
For example, the “color” attribute would let you choose between 26 available theme colors like “Red”, “Violet” or “Grass”. Each of them triggers an entire palette of associated shades including dark and light theme variants, a contrasting text color, a different shade for link color, a dedicated “high contrast” variant, and more. This package is designed to give your components a polished appearance with minimal effort. However, if what you want is to forget all that and pick a specific shade of your own – you would need to do some work to override the library defaults, which might be as simple as overriding the value of a CSS variable, but might also be much more complicated, depending on your specific use case.
It's probably fine if you make such overrides from time to time, but if you find yourself doing it all the time, you’re probably not using the right library for your needs.
This is true for any opinionated component library you might choose. For example, Material UI has a different look, different defaults and different customization policy, but the common part between it and Radix Themes is that they both have their opinions and stylistic line. If you use them, you get that line as part of the package.
Is there a way to escape that tradeoff? Is there a way to get fully styled, ready-to-use components, without losing the control over the small details?
Perhaps surprisingly, the answer is again “yes”. But this would have to wait for the next (and last) part in the series.