Creating Elegant Clickable Table Rows in Next.js


The Challenge
We've all been there - you're building a data-heavy application with tables, and you want users to be able to click anywhere on a row to navigate to a detail page. But wait, what about those action buttons in the last column? How do you make the entire row clickable without breaking the functionality of those buttons?
๐ก Enter LinkBox and LinkOverlay
Let's explore a clean, reusable solution that solves this common UI challenge using two simple components:
The LinkBox Component
export const LinkBox: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
element?: ElementType;
}
> = ({ children, className, element: Element = "div", ...props }) => {
return (
<Element
{...props}
className={cn(
"relative",
"[&_a:not(.link-overlay)]:relative [&_a:not(.link-overlay)]:z-10",
"[&_button]:relative [&_button]:z-10",
className,
)}
>
{children}
</Element>
);
};
The LinkOverlay Component
export const LinkOverlay: React.FC<LinkOverlayProps> = ({
children,
href,
className,
target,
...props
}) => {
return (
<Link
href={href}
target={target}
rel={target === "_blank" ? "noopener noreferrer" : ""}
{...props}
className={cn(
"link-overlay static before:absolute before:inset-0 before:z-0",
className,
)}
>
{children}
</Link>
);
};
โจ How It Works
This solution uses some CSS magic to create a clickable overlay while preserving the functionality of nested interactive elements:
LinkBox
creates a positioning context and ensures any buttons or links inside have a higher z-indexLinkOverlay
uses a pseudo-element that stretches across the entire parent containerWhen a user clicks anywhere in the row, they navigate to the detail page
When they click on a button, the button's click event takes precedence
๐ Putting It All Together
Here's how you can implement clickable table rows:
<TableBody>
{items.map((item) => (
<LinkBox key={item.id} element={TableRow}>
<TableCell className="font-medium">
<LinkOverlay href={`/items/${item.id}`}>
{item.name}
</LinkOverlay>
</TableCell>
<TableCell>{item.category}</TableCell>
<TableCell>{item.status}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEditClick(item)}
>
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteClick(item)}
>
Delete
</Button>
</div>
</TableCell>
</LinkBox>
))}
</TableBody>
๐ The Magic in the Details
What makes this approach special:
Flexibility: The
LinkBox
can wrap any element, not just table rowsSimplicity: The API is straightforward and easy to implement
Accessibility: Maintains proper semantic HTML structure
Reusability: Can be used across your entire application
๐ ๏ธ Implementation Tips
Place the
LinkOverlay
in the most logical cell (usually the name or title)Make sure your buttons have proper event handlers
Use the
element
prop to specify what HTML element theLinkBox
should render as
๐ Conclusion
This pattern solves a common UI challenge in a clean, reusable way. No more hacky solutions or compromising on user experience! Your tables can now have fully clickable rows while maintaining the functionality of nested buttons.
Next time you're building a data table, give this approach a try - your users (and fellow developers) will thank you! ๐
Subscribe to my newsletter
Read articles from Srujan Gurram directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Srujan Gurram
Srujan Gurram
I am a Student and a Web Developer