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:
- The base layer is the Radix Primitives library, which provides fundamental building blocks for various components. The "primitives" are built from simple HTML elements that are easy to style, and restore the interactive semantics of the controls using ARIA attributes and other methods.
Aside from essential layout styles needed for functionality, the elements are left unstyled. The library leaves the actual styling entirely to the developer.
This approach allows fine-grained control over almost every visual detail, while still ensuring professional-level functionality and accessibility out of the box.
- The second layer is the Radix Themes library. It provides ready-to-use components built from the primitives, already composed and styled, with customization options available but inevitably more limited. We’ll discuss that library in the last part of this series.
Version 2: Using Radix UI
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.
Version 3: Using Radix Themes
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.
Can Radix Themes components be customized?
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.




