I was building a form with a file input. Nothing fancy—just a place for users to upload a document. I wanted the trigger to look like the other buttons on the page: the same subtle shadows, the same hover effects, the same spacing. I was using Catalyst, the component kit from Tailwind Labs, so I had a <Button> component with all those styles baked in.
But I couldn’t use it.
A file input needs a <label> as its clickable element—that’s how you style file inputs without fighting the browser’s native UI. But Catalyst’s <Button> component only renders as a <button> element or a <Link>. There’s no way to apply those styles to a <label>.
Some component libraries offer escape hatches—props like asChild or render that let you swap out the underlying element. But these props don’t just pass through styles; they pass through the component’s behavior too. That’s fine when you want both. But when you just need the look—when you need an element to appear clickable while retaining its own native semantics—components leave you stuck.
This isn’t a bug in Catalyst. It’s a structural limitation of components as an abstraction. Components are poor vehicles for purely visual styles. And once you see this, you start seeing it everywhere.
Three Things, One Name
Consider the word “button.” In frontend development, it actually refers to three genuinely distinct things:
- The
<button>element — native HTML semantics and behavior - The
Buttoncomponent — your library’s encapsulation of structure, behavior, and possibly styles - The
buttonvisual pattern — rounded corners, padding, solid background, hover states; what makes something look clickable
When you need to make a <label> look like a <button>, you can’t reach for an element or a component.
The same is true for text inputs. There’s the element, the component, and the visual pattern—the border, the focus ring, the placeholder styling—that makes something look like a place to type. You might need that pattern on a <textarea>, a <select>, or a custom autocomplete built on a different element entirely.
These visual patterns have a name in design theory: affordances—visual signals that communicate how an element can be interacted with. The term comes from Don Norman’s The Design of Everyday Things, where he described how the shape of a door handle tells you whether to push or pull. In interfaces, affordances are what make a button look pressable, an input look typeable, a link look clickable.
The standard frontend architecture today needs to add this as the fourth conceptual layer: