DEV Community

Cover image for Stop Using Random Buttons: Use Button Groups for Clean UI
Vaibhav Gupta
Vaibhav Gupta

Posted on

Stop Using Random Buttons: Use Button Groups for Clean UI

Most UI issues in dashboards come from small inconsistencies. Buttons work fine alone, but when grouped for real actions, spacing, borders, and states start breaking.

Instead of fixing this repeatedly, shadcn button groups give you a structured way to manage related actions. You define layout and behavior once, then reuse it across tables, filters, and navigation.

This list is based on real usage in SaaS dashboards and admin panels. Each pattern solves a specific problem that shows up in production.


What is a button group component

A button group is a wrapper that combines multiple buttons into one logical unit. It manages spacing, borders, and interaction states without writing custom styles every time.

You use it when actions are related, like edit, delete, or switching views. This keeps UI consistent and avoids random placement issues.

In React and Next.js setups, button groups also help standardize hover, focus, and disabled states across the app.


Developer checklist before using button groups

  • Group only related actions together
  • Keep button size and spacing consistent
  • Always define active and disabled states
  • Use separators for different action intents
  • Test keyboard navigation and focus states
  • Avoid overcrowding a single group
  • Reuse the same pattern across screens

Installation approach

All shadcn components are open source and built on Radix and Base UI primitives. You can install them using pnpm, npm, yarn, or bun.

Example with pnpm:

pnpm dlx shadcn@latest add @shadcn-space/button-group-01

Enter fullscreen mode Exit fullscreen mode

After installation, components follow a copy-and-paste approach. No complex setup required.


Tech stack used

  • React
  • Next.js
  • Tailwind CSS
  • Framer Motion

Best Shadcn button group components

These are 6 patterns used in real dashboards. Each one handles a different interaction case.


Basic button group

Groups related actions like edit, copy, and delete into a single aligned block where spacing and borders stay consistent across buttons without extra styling work, and helps avoid scattered UI in tables or cards. It keeps interaction predictable by maintaining uniform button size and behavior across actions, so users can quickly perform operations without confusion. This pattern reduces repeated Tailwind classes and keeps your component structure clean when handling multiple actions together.

Code

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { Edit2, Copy, Trash2 } from "lucide-react";

export default function BasicButtonGroupDemo() {
  return (
    <>
      <ButtonGroup>
        <Button variant="outline" size="icon" className="cursor-pointer">
          <Edit2 className="size-4" />
        </Button>
        <Button variant="outline" size="icon" className="cursor-pointer">
          <Copy className="size-4" />
        </Button>
        <Button variant="outline" size="icon" className="cursor-pointer">
          <Trash2 className="size-4 text-destructive" />
        </Button>
      </ButtonGroup>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Key features

  • Shared border and spacing logic across all buttons
  • Supports icons and text together in one group
  • Reduces repeated Tailwind utility usage
  • Maintains consistent hover and active states
  • Easy to reuse across table rows and cards

Best for: CRUD actions in tables and list views


Vertical button group

Stacks buttons vertically, which helps when horizontal space is limited and ensures layout remains structured without breaking alignment in sidebars or compact UI sections. It maintains equal width and spacing for each button, so even with different labels or icons, the UI stays balanced and readable. This pattern is useful when switching between views or layouts where vertical grouping improves usability.

Code

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { LayoutGrid, List, Table } from "lucide-react";

export default function VerticalButtonGroupDemo() {
  return (
    <>
      <ButtonGroup orientation="vertical">
        <Button variant="outline" className="justify-start gap-2 cursor-pointer">
          <LayoutGrid className="size-4" />
          Grid View
        </Button>
        <Button variant="outline" className="justify-start gap-2 cursor-pointer">
          <List className="size-4" />
          List View
        </Button>
        <Button variant="outline" className="justify-start gap-2 cursor-pointer">
          <Table className="size-4" />
          Table View
        </Button>
      </ButtonGroup>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Key features

  • Supports vertical stacking without layout shift
  • Maintains equal width across all buttons
  • Works well in sidebars and narrow layouts
  • Keeps spacing consistent between items
  • Handles active state cleanly in stacked form

Best for: View switchers and sidebar controls


Pagination button group

Manages previous and next navigation with controlled state handling, which helps maintain predictable page transitions and avoids invalid navigation scenarios in data-driven interfaces. It disables actions when limits are reached, so users cannot go beyond available pages, which reduces frontend errors. This keeps the pagination UI clean and directly connected with API-driven data updates.

Code

"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import { ChevronLeft, ChevronRight } from "lucide-react";

export default function PaginationButtonGroupDemo() {
  const [page, setPage] = useState(1);
  const totalPages = 10;

  const handlePrevious = () => {
    setPage((prev) => Math.max(1, prev - 1));
  };

  const handleNext = () => {
    setPage((prev) => Math.min(totalPages, prev + 1));
  };

  return (
    <>
      <ButtonGroup>
        <Button 
          variant="outline" 
          size="icon" 
          onClick={handlePrevious}
          disabled={page === 1}
          className="cursor-pointer"
        >
          <ChevronLeft className="size-4" />
        </Button>
        <ButtonGroupText className="px-4 min-w-32 justify-center">
          Page {page} of {totalPages}
        </ButtonGroupText>
        <Button 
          variant="outline" 
          size="icon" 
          onClick={handleNext}
          disabled={page === totalPages}
          className="cursor-pointer"
        >
          <ChevronRight className="size-4" />
        </Button>
      </ButtonGroup>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

Key features

  • Built-in support for previous and next actions
  • Prevents invalid navigation with disabled states
  • Easy integration with API pagination logic
  • Keeps layout compact and readable
  • Supports dynamic page updates with state

Best for: Tables, listings, and data-heavy screens


Button group with separator

Adds a visual separator between actions, which helps distinguish between different intent actions like safe and destructive operations without breaking the group structure. It improves clarity by logically dividing buttons while still keeping them inside one unified layout. This makes it easier for users to understand action priority and reduces accidental clicks.

Code

import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
import { Save, X, MoreHorizontal } from "lucide-react";

export default function ButtonGroupSeparatorDemo() {
  return (
    <div className="flex flex-col items-center gap-4">
      <ButtonGroup>
        <Button className="gap-2 cursor-pointer">
          <Save className="size-4" />
          Save Changes
        </Button>
        <ButtonGroupSeparator />
        <Button variant="outline" size="icon" className="cursor-pointer">
          <MoreHorizontal className="size-4" />
        </Button>
      </ButtonGroup>

      <ButtonGroup>
        <Button variant="outline" className="gap-2 cursor-pointer">
          <X className="size-4" />
          Discard
        </Button>
        <ButtonGroupSeparator />
        <Button variant="default" className="gap-2 cursor-pointer">
          <Save className="size-4" />
          Publish
        </Button>
      </ButtonGroup>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Key features

  • Adds clear separation between grouped actions
  • Helps distinguish between destructive and safe actions
  • Maintains alignment within the same group
  • No extra layout wrappers required
  • Improves readability in dense UI sections

Best for: Form actions like save, discard, publish


Currency selector button group

Combines button interaction with a select input, which allows dynamic value switching while keeping UI compact and aligned in one component structure. It updates the displayed value based on selection so users can instantly see the impact of their choice without extra steps. This pattern is useful when dealing with pricing or currency-based data.

Code

"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

const currencies = [
  { code: "USD", symbol: "$" },
  { code: "EUR", symbol: "" },
  { code: "GBP", symbol: "£" },
  { code: "JPY", symbol: "¥" },
  { code: "INR", symbol: "" },
];

export default function ButtonGroupCurrencyDemo() {
  const [currency, setCurrency] = useState(currencies[0]);

  return (
    <div className="flex flex-col items-center gap-4">
      <ButtonGroup className="shadow-lg">
        <Button 
          className="bg-background hover:bg-muted font-bold h-9 w-10 p-0 text-base border border-border text-foreground cursor-pointer"
        >
          {currency.symbol}
        </Button>
        <ButtonGroupText className="bg-background border-x-0 font-mono font-medium min-w-24 justify-center text-lg h-9">
          1,499.00
        </ButtonGroupText>
        <Select 
          value={currency.code} 
          onValueChange={(value) => {
            const selected = currencies.find(c => c.code === value);
            if (selected) setCurrency(selected);
          }}
        >
          <SelectTrigger className="rounded-l-none h-9 min-w-20 font-bold cursor-pointer">
            <SelectValue />
          </SelectTrigger>
          <SelectContent align="end">
            {currencies.map((c) => (
              <SelectItem key={c.code} value={c.code}>
                {c.code} ({c.symbol})
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </ButtonGroup>

      <div className="text-xs text-muted-foreground font-medium">
        Interactive Currency Selection
      </div>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Key features

  • Syncs button label with selected value
  • Integrates select dropdown with button UI
  • Handles formatted values like pricing
  • Keeps UI compact for multiple interactions
  • Supports dynamic state updates

Best for: Pricing UI and billing dashboards


Vercel style button group

Follows patterns used in modern developer tools where text and icon buttons are combined in a compact layout that keeps interactions fast and accessible. It handles hover and focus states cleanly, which helps maintain a consistent feel across toolbars and action panels. This pattern is useful when multiple quick actions need to be grouped without clutter.

Code

import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { ListFilter, BookOpen } from "lucide-react";

export default function VercelButtonGroupDemo() {
  return (
    <>
      <ButtonGroup>
        <Button 
          variant="outline" 
          className="h-9 px-3 gap-2 bg-background border-border hover:bg-accent hover:text-accent-foreground rounded-md shadow-sm transition-all cursor-pointer"
        >
          <ListFilter className="size-4 shrink-0" />
          <span className="text-sm font-medium">Deployments</span>
        </Button>
        <Button 
          variant="outline" 
          size="icon"
          className="h-9 w-9 bg-background border-border hover:bg-accent hover:text-accent-foreground rounded-md shadow-sm transition-all cursor-pointer"
        >
          <BookOpen className="size-4" />
        </Button>
      </ButtonGroup>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

Key features

  • Combines icon and text buttons efficiently
  • Maintains compact layout for toolbars
  • Handles hover and focus states consistently
  • Reduces visual clutter in action areas
  • Aligns with modern dev tool UI patterns

Best for: Toolbars, deployment actions, filters


FAQs


1. How do I manage the active state in a button group in React?

Use a state variable and update it on click, then apply conditional styles based on the selected value to keep UI and state in sync.

2. When should I use a button group instead of a dropdown?

Use it when options are limited and need quick access, while dropdowns work better for larger or searchable lists.

3. How do I keep button groups accessible?

Use proper roles, focus states, and keyboard navigation while relying on Radix and Base UI primitives for built-in accessibility support.


Final Thoughts

Button groups solve a common issue in dashboards where multiple actions need to work together without breaking layout or interaction.

Using them reduces repeated styling and keeps UI consistent as your application grows.

Start with basic patterns and extend based on your use case to maintain clean and predictable interfaces.

Top comments (0)