Compose components
Preserve native semantics
Choose links, buttons, labels, inputs, and headings by behavior first, then apply components and visual styles without erasing meaning.
A polished component can still be inaccessible if the underlying element is wrong. Navigation is a link. An in-place action is a button. Pick behavior first, then apply shadcn styling.
Use the composition mechanism documented for your selected component base when another element must receive the component styles. Base UI, Radix, and React Aria may use different APIs. Verify the rendered DOM rather than assuming a prop named in an older example still applies.
Styling an anchor like a button does not turn it into a button, and that is good. It should still support copying the address, opening in a new tab, and modified clicks. Conversely, a button must not gain a fake href to perform an in-place mutation. Never nest one interactive element inside another to combine styles.
Composition can also change which element receives refs, event handlers, disabled state, and accessibility attributes. The source DOM is the evidence. Check the element name, role, accessible name, focus order, and keyboard behavior after composition, not only the React props before it.
Style a real Next.js Link as a button-like call to action using the documented API for your base. On a Radix-based Button you might use asChild:
import Link from 'next/link'
import { Button } from '@/components/ui/button'
<Button asChild>
<Link href='/projects/new'>New project</Link>
</Button>
Inspect the final element in DevTools. You should see a single <a> with button styling, not a button wrapping a link. Open it with a modified click (Cmd+click on macOS). Compare it with an in-place button activated by Enter and Space. There should be no nested interactive nodes.
Try this on your own project: style one navigation action as a link and one mutation as a button, then keyboard-test both.
Lesson completed