# Your Browser Already Has a Component Library

Recently I was reading the documentation of yet another UI library, and I couldn't help thinking that something went wrong somewhere.

This isn't really about one particular library. Ark UI made me think about it a while ago, and looking recently at projects such as Kumo UI brought the thought back. These are well-made libraries solving real problems, but they also illustrate a broader trend in frontend development: **we have become so used to thinking in components that we sometimes forget the browser already comes with a component library. It's called HTML.**

## So many adapters

If you create a JavaScript UI library and want it to work across ecosystems, you quickly end up maintaining implementations or adapters for React, Vue, Solid, Svelte and whatever comes next.

There are good architectural ways to reduce that duplication. Ark UI, for example, shares much of its underlying behavior rather than implementing everything independently for every framework. Still, it raises a more fundamental question: how much of this abstraction needed to exist in the first place?

When some behavior genuinely needs JavaScript, framework adapters can make perfect sense. Custom elements are another increasingly viable way to expose reusable behavior without binding it to a particular framework. But before solving the difficult problem of making a component work everywhere, perhaps we should first ask whether we needed to create a component at all.

## What's wrong with native HTML?

Take a checkbox.

A component library such as Ark UI may expose the full anatomy of a checkbox:

```typescript
<Checkbox.Root defaultChecked>
  <Checkbox.Control>
    <Checkbox.Indicator />
  </Checkbox.Control>
  <Checkbox.Label>Checkbox</Checkbox.Label>
  <Checkbox.HiddenInput />
</Checkbox.Root>
```

Kumo UI takes a noticeably more compact approach:

```typescript
<Checkbox
  label="Remember me"
  controlFirst={false}
  checked={checked}
  onCheckedChange={setChecked}
/>
```

That's certainly nicer to consume. But it also shows another form of abstraction: instead of exposing several component parts, the library introduces its own vocabulary. A prop such as `controlFirst` now describes whether the control or label comes first.

Compare that with:

```html
<label>
  <input type="checkbox" checked>
  Checkbox
</label>
```

There is nothing inherently wrong with either component API. Kumo in particular makes the common case quite pleasant. The question is simply what the abstraction bought us. In plain HTML, the relationship between the label and control, their order, the checked state, keyboard interaction, form participation and accessibility semantics already exist. For basic styling and layout, adding a component API can sometimes amount to giving new names to concepts the platform already has.

There is a surprising amount of behavior hidden behind those few characters of HTML.

And this is where modern component abstractions can become slightly circular. Once the visible control is no longer the native control, libraries often need to keep or recreate a native input somewhere underneath so form submission, validation and other browser behavior still work correctly.

There is nothing inherently wrong with that, and sometimes the extra abstraction is worth it. But there is a certain irony in replacing a native `checkbox` only to add machinery that recovers the semantics and behavior the browser already provided.

That is not necessarily bad engineering, but it is a useful smell.

> Sometimes component libraries don't remove complexity; they translate platform concepts into library concepts.

## Abstractions should buy something

The question I increasingly ask when looking at a UI abstraction is simple: **what did we get in exchange?**

For a checkbox, perhaps the answer is substantial visual freedom, complex animations, controlled state integration or a consistent API across a large React design system. Those can all be legitimate requirements. But if all I wanted was a checkbox with my colors, spacing and border radius, I would much rather start with the browser control and CSS.

The same reasoning applies to other primitives. A button should generally start as a `<button>`, a link as an `<a>`, and a disclosure can often start as `<details>` and `<summary>`. A modal can now build on `<dialog>` instead of inventing its own concept of a modal element, top-layer rendering and backdrop. Popovers can use the Popover API instead of every library implementing its own transport, light-dismiss behavior and escape handling from scratch.

This is especially relevant because the web platform has changed significantly. Some abstractions that were perfectly reasonable ten years ago are now compensating for browser limitations that no longer exist.

That doesn't mean the native feature is always sufficient. It means **the native feature should be the starting point against which additional complexity has to justify itself.**

## Complex widgets are different

There are plenty of UI patterns where HTML does not give us enough. A good combobox is genuinely difficult. So is a date picker with calendar context, a data grid, a tree view or a sophisticated menu system. These involve keyboard models, focus management, selection state, positioning, asynchronous data, accessibility semantics and many edge cases.

That's exactly where I want a component to earn its keep.

I ran into this distinction while building my own [combobox](https://github.com/lekoala/combobox). It supports searchable selects, multiple values, tags, remote suggestions, keyboard interaction and custom rendering. There is clearly enough missing behavior there to justify a component.

But the component deliberately doesn't create a new form model.

A normal `<select>` or `<input>` remains the source of truth. The searchable input around a select is an interaction surface, not the submitted control. A selected remote result eventually becomes a native `<option>`. `required`, `disabled`, `form.reset()`, server-rendered values and native `input` and `change` events keep working because the browser still owns the underlying control.

For example:

```html
<combo-box search="fuzzy">
  <select name="country">
    <option value="">Choose…</option>
    <option value="be">Belgium</option>
    <option value="fr">France</option>
  </select>
</combo-box>
```

The custom element is useful because it adds behavior HTML doesn't provide. But if the JavaScript enhancement disappears, the `<select>` underneath is still a perfectly understandable form control. On browsers without the APIs required by the enhanced picker, the library deliberately falls back to that native control rather than carrying an entire second JavaScript implementation.

That difference matters to me.

## Sometimes the native control really is enough

My [date picker](https://github.com/lekoala/date-picker) follows the same principle, but it also illustrates the opposite case.

The documentation explicitly says that a normal date field remains the right answer for a simple editing form. There is no reason to insert a calendar component merely because the value happens to be a date.

The component becomes useful when the date has context: disabled days, availability, annotations, linked start/end dates, remote state or a mini-calendar navigating another interface. At that point the abstraction is buying something significant.

Even then, I try to leave things native when possible. A date can be accompanied by a regular `input[type="time"]`; the picker doesn't invent a custom time picker simply because it could. The application can combine the civil date and time later if it needs a datetime.

I like that boundary because it prevents "being a component library" from becoming an excuse to own every pixel and every interaction.

## Components are an implementation technique

I think some of the problem comes from the way modern frontend development is framed. We start with components, so eventually everything becomes one:

`Button`.

`Checkbox`.

`Label`.

`Control`.

`Indicator`.

`Root`.

At some point HTML stops being seen as an API and becomes a rendering target underneath a component tree.

But HTML is not just an output format. It is a semantic interface implemented by every browser. It has state, events, accessibility behavior, keyboard interaction, form participation and relationships between elements. Modern HTML also includes increasingly capable interactive primitives such as dialogs and popovers.

When we ignore that layer, we don't remove complexity. We usually move it somewhere else, and then maintain it ourselves.

The browser, meanwhile, doesn't need to be installed from npm, bundled, hydrated or adapted to four different frameworks.

## This is also where Actual CSS ended up

This thinking is increasingly the conceptual direction behind [Actual CSS](https://github.com/lekoala/actual-css).

Actual started from a fairly simple idea: style semantic HTML with a coherent CSS vocabulary, then progressively enhance the places where CSS and HTML alone aren't sufficient. Presentation and behavior are deliberately separate layers.

A button looks like this:

```html
<button class="btn primary">Save</button>
```

It doesn't need an `ActualButton`, a `ButtonRoot`, a `ButtonLabel` or a JavaScript runtime to be a button. The HTML element already defines what it is; Actual CSS provides its visual language.

For interactive components the same rule applies. Dialogs build on `<dialog>`. Interactive surfaces use the native Popover API for their transport. JavaScript modules enhance those primitives with the additional behavior Actual actually needs rather than pretending the platform doesn't exist.

And the runtime itself is optional and modular. CSS remains CSS; behavior is attached as an enhancement.

This doesn't mean Actual CSS is "HTML only". Tooltips still need positioning logic (the browser gives us the popover and only needs a little help, which is why I built [Floating](https://github.com/lekoala/floating)). Context menus need behavior. Tabs need coordination. A sophisticated combobox deserves a dedicated library. The point is simply that **the amount of abstraction should roughly correspond to the amount of missing platform behavior.**

A checkbox and a remote searchable multi-select should not automatically start from the same architectural weight just because both happen to appear in a design system.

## Native first, enhance when necessary

The rule I've ended up following is fairly simple: start with semantic HTML, style it with CSS, use native browser behavior where it exists, and add JavaScript where behavior is actually missing. Create a component when encapsulation provides a meaningful benefit rather than because components are the default unit of thought.

This is not about chasing an imaginary zero-JavaScript ideal. I write plenty of JavaScript, and complex interfaces need it. My combobox contains substantially more machinery than an `<input>`, because a combobox substantially does more than an `<input>`.

It is about putting the abstraction at the right level.

There is an important difference between a library saying **"here is some HTML, and I will enhance it"** and saying **"here is my component model, which will eventually produce some HTML."**

Both approaches have their place, and there is nothing inherently wrong with the second one. But modern frontend development has drifted very far in that direction, sometimes even for things the browser has understood perfectly well for decades.

Personally, that makes me slightly uncomfortable. I would rather learn what the platform can actually do than learn another abstraction that hides it from me. HTML, CSS and browser APIs are already a system worth understanding, and that knowledge transfers between frameworks, libraries and projects in a way that `controlFirst`, `Checkbox.Root` or the next component API may not.

That doesn't mean avoiding abstractions. It means I want them to start where the platform stops. I want a combobox library to teach me about the difficult parts of building a combobox, not redefine what a checkbox is.

Maybe we don't need to reinvent every primitive before we can start building the interesting parts.

Sometimes the best component is still the one the browser already shipped.
