UI Components Easy
Button Group
Group of buttons joined together in a segmented control style with shared borders, active state toggling, and rounded end caps.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #0a0a0a;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 520px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.demo-section {
margin-bottom: 1.75rem;
}
.demo-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.625rem;
}
/* ── Button Group ── */
.btn-group {
display: inline-flex;
border-radius: 0.625rem;
overflow: hidden;
border: 1px solid #2a2a2a;
background: #141414;
}
.btn-group-item {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: #94a3b8;
font-size: 0.8125rem;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background 0.15s, color 0.15s;
position: relative;
white-space: nowrap;
}
.btn-group-item + .btn-group-item {
border-left: 1px solid #2a2a2a;
}
.btn-group-item:hover {
background: rgba(255, 255, 255, 0.04);
color: #f2f6ff;
}
.btn-group-item.is-active {
background: rgba(56, 189, 248, 0.12);
color: #38bdf8;
font-weight: 600;
}
.btn-group-item:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: -2px;
z-index: 1;
}
/* ── Small variant ── */
.btn-group--sm .btn-group-item {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
/* ── Outlined variant ── */
.btn-group--outline {
background: transparent;
border: 1px solid #2a2a2a;
}
.btn-group--outline .btn-group-item.is-active {
background: #38bdf8;
color: #0a0a0a;
font-weight: 700;
}
.btn-group--outline .btn-group-item + .btn-group-item {
border-left: 1px solid #2a2a2a;
}
.btn-group--outline .btn-group-item.is-active + .btn-group-item,
.btn-group--outline .btn-group-item + .btn-group-item.is-active {
border-left-color: transparent;
}(function () {
document.querySelectorAll("[data-btn-group]").forEach(function (group) {
var buttons = group.querySelectorAll(".btn-group-item");
buttons.forEach(function (btn) {
btn.addEventListener("click", function () {
buttons.forEach(function (b) {
b.classList.remove("is-active");
});
btn.classList.add("is-active");
});
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Button Group</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Button Group</h1>
<p class="demo-sub">Segmented control style buttons with shared borders.</p>
<div class="demo-section">
<span class="demo-label">View Mode</span>
<div class="btn-group" data-btn-group>
<button class="btn-group-item is-active" data-value="grid">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="9" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="9" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>
Grid
</button>
<button class="btn-group-item" data-value="list">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M1 3h14M1 8h14M1 13h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
List
</button>
<button class="btn-group-item" data-value="board">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="1" width="4" height="14" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="6" y="1" width="4" height="10" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="11" y="1" width="4" height="12" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>
Board
</button>
</div>
</div>
<div class="demo-section">
<span class="demo-label">Alignment</span>
<div class="btn-group" data-btn-group>
<button class="btn-group-item is-active" data-value="left">Left</button>
<button class="btn-group-item" data-value="center">Center</button>
<button class="btn-group-item" data-value="right">Right</button>
<button class="btn-group-item" data-value="justify">Justify</button>
</div>
</div>
<div class="demo-section">
<span class="demo-label">Size Variants</span>
<div class="btn-group btn-group--sm" data-btn-group>
<button class="btn-group-item is-active" data-value="s">Small</button>
<button class="btn-group-item" data-value="m">Medium</button>
<button class="btn-group-item" data-value="l">Large</button>
</div>
</div>
<div class="demo-section">
<span class="demo-label">Outlined Variant</span>
<div class="btn-group btn-group--outline" data-btn-group>
<button class="btn-group-item is-active" data-value="daily">Daily</button>
<button class="btn-group-item" data-value="weekly">Weekly</button>
<button class="btn-group-item" data-value="monthly">Monthly</button>
<button class="btn-group-item" data-value="yearly">Yearly</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
interface ButtonGroupItem {
value: string;
label: string;
icon?: React.ReactNode;
}
interface ButtonGroupProps {
items: ButtonGroupItem[];
value?: string;
onChange?: (value: string) => void;
size?: "sm" | "md";
variant?: "default" | "outline";
}
export function ButtonGroup({
items,
value,
onChange,
size = "md",
variant = "default",
}: ButtonGroupProps) {
const [internal, setInternal] = useState(value ?? items[0]?.value ?? "");
const active = value ?? internal;
const handleClick = (v: string) => {
setInternal(v);
onChange?.(v);
};
const padY = size === "sm" ? "0.375rem" : "0.5rem";
const padX = size === "sm" ? "0.75rem" : "1rem";
const fontSize = size === "sm" ? "0.75rem" : "0.8125rem";
return (
<div
style={{
display: "inline-flex",
borderRadius: "0.625rem",
overflow: "hidden",
border: "1px solid #2a2a2a",
background: variant === "outline" ? "transparent" : "#141414",
}}
>
{items.map((item, i) => {
const isActive = active === item.value;
let bg = "transparent";
let color = "#94a3b8";
let fontWeight: number = 500;
if (isActive) {
if (variant === "outline") {
bg = "#38bdf8";
color = "#0a0a0a";
fontWeight = 700;
} else {
bg = "rgba(56,189,248,0.12)";
color = "#38bdf8";
fontWeight = 600;
}
}
return (
<button
key={item.value}
type="button"
onClick={() => handleClick(item.value)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.375rem",
padding: `${padY} ${padX}`,
border: "none",
borderLeft: i > 0 ? "1px solid #2a2a2a" : "none",
background: bg,
color,
fontSize,
fontWeight,
fontFamily: "inherit",
cursor: "pointer",
whiteSpace: "nowrap",
transition: "background 0.15s, color 0.15s",
}}
>
{item.icon}
{item.label}
</button>
);
})}
</div>
);
}
/* Demo */
export default function ButtonGroupDemo() {
const [view, setView] = useState("grid");
const [align, setAlign] = useState("left");
const [period, setPeriod] = useState("daily");
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0a0a0a",
fontFamily: "Inter, system-ui, sans-serif",
color: "#f2f6ff",
padding: "2rem",
}}
>
<div style={{ width: "100%", maxWidth: 520 }}>
<h1 style={{ fontSize: "1.5rem", fontWeight: 800, marginBottom: "0.375rem" }}>
Button Group
</h1>
<p style={{ color: "#475569", fontSize: "0.875rem", marginBottom: "2rem" }}>
Segmented control style buttons with shared borders.
</p>
<Section label="View Mode">
<ButtonGroup
items={[
{ value: "grid", label: "Grid" },
{ value: "list", label: "List" },
{ value: "board", label: "Board" },
]}
value={view}
onChange={setView}
/>
<p style={{ marginTop: "0.5rem", fontSize: "0.8125rem", color: "#94a3b8" }}>
Selected: <strong style={{ color: "#38bdf8" }}>{view}</strong>
</p>
</Section>
<Section label="Alignment">
<ButtonGroup
items={[
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
{ value: "right", label: "Right" },
{ value: "justify", label: "Justify" },
]}
value={align}
onChange={setAlign}
/>
</Section>
<Section label="Outlined Variant">
<ButtonGroup
variant="outline"
items={[
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
{ value: "yearly", label: "Yearly" },
]}
value={period}
onChange={setPeriod}
/>
</Section>
<Section label="Small Size">
<ButtonGroup
size="sm"
items={[
{ value: "s", label: "Small" },
{ value: "m", label: "Medium" },
{ value: "l", label: "Large" },
]}
/>
</Section>
</div>
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ marginBottom: "1.75rem" }}>
<span
style={{
display: "block",
fontSize: "0.75rem",
fontWeight: 600,
color: "#94a3b8",
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: "0.625rem",
}}
>
{label}
</span>
{children}
</div>
);
}<script setup>
import { ref } from "vue";
const view = ref("grid");
const align = ref("left");
const period = ref("daily");
const smallVal = ref("s");
const viewItems = [
{ value: "grid", label: "Grid" },
{ value: "list", label: "List" },
{ value: "board", label: "Board" },
];
const alignItems = [
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
{ value: "right", label: "Right" },
{ value: "justify", label: "Justify" },
];
const periodItems = [
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
{ value: "yearly", label: "Yearly" },
];
const sizeItems = [
{ value: "s", label: "Small" },
{ value: "m", label: "Medium" },
{ value: "l", label: "Large" },
];
function getStyle(item, active, i, variant = "default", size = "md") {
const isActive = active === item.value;
let bg = "transparent";
let color = "#94a3b8";
let fontWeight = 500;
if (isActive) {
if (variant === "outline") {
bg = "#38bdf8";
color = "#0a0a0a";
fontWeight = 700;
} else {
bg = "rgba(56,189,248,0.12)";
color = "#38bdf8";
fontWeight = 600;
}
}
const padY = size === "sm" ? "0.375rem" : "0.5rem";
const padX = size === "sm" ? "0.75rem" : "1rem";
const fontSize = size === "sm" ? "0.75rem" : "0.8125rem";
return {
display: "inline-flex",
alignItems: "center",
gap: "0.375rem",
padding: `${padY} ${padX}`,
border: "none",
borderLeft: i > 0 ? "1px solid #2a2a2a" : "none",
background: bg,
color,
fontSize,
fontWeight: String(fontWeight),
fontFamily: "inherit",
cursor: "pointer",
whiteSpace: "nowrap",
transition: "background 0.15s, color 0.15s",
};
}
</script>
<template>
<div class="demo">
<div class="container">
<h1>Button Group</h1>
<p class="subtitle">Segmented control style buttons with shared borders.</p>
<div class="section">
<span class="section-label">View Mode</span>
<div class="group">
<button v-for="(item, i) in viewItems" :key="item.value" type="button"
:style="getStyle(item, view, i)" @click="view = item.value">
{{ item.label }}
</button>
</div>
<p class="selected-text">Selected: <strong>{{ view }}</strong></p>
</div>
<div class="section">
<span class="section-label">Alignment</span>
<div class="group">
<button v-for="(item, i) in alignItems" :key="item.value" type="button"
:style="getStyle(item, align, i)" @click="align = item.value">
{{ item.label }}
</button>
</div>
</div>
<div class="section">
<span class="section-label">Outlined Variant</span>
<div class="group outline">
<button v-for="(item, i) in periodItems" :key="item.value" type="button"
:style="getStyle(item, period, i, 'outline')" @click="period = item.value">
{{ item.label }}
</button>
</div>
</div>
<div class="section">
<span class="section-label">Small Size</span>
<div class="group">
<button v-for="(item, i) in sizeItems" :key="item.value" type="button"
:style="getStyle(item, smallVal, i, 'default', 'sm')" @click="smallVal = item.value">
{{ item.label }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.container { width: 100%; max-width: 520px; }
h1 { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.375rem; }
.subtitle { color: #475569; font-size: 0.875rem; margin-bottom: 2rem; }
.section { margin-bottom: 1.75rem; }
.section-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.625rem;
}
.group {
display: inline-flex;
border-radius: 0.625rem;
overflow: hidden;
border: 1px solid #2a2a2a;
background: #141414;
}
.group.outline { background: transparent; }
.selected-text {
margin-top: 0.5rem;
font-size: 0.8125rem;
color: #94a3b8;
}
.selected-text strong { color: #38bdf8; }
</style><script>
let view = "grid";
let align = "left";
let period = "daily";
let smallVal = "s";
const viewItems = [
{ value: "grid", label: "Grid" },
{ value: "list", label: "List" },
{ value: "board", label: "Board" },
];
const alignItems = [
{ value: "left", label: "Left" },
{ value: "center", label: "Center" },
{ value: "right", label: "Right" },
{ value: "justify", label: "Justify" },
];
const periodItems = [
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
{ value: "yearly", label: "Yearly" },
];
const sizeItems = [
{ value: "s", label: "Small" },
{ value: "m", label: "Medium" },
{ value: "l", label: "Large" },
];
function getStyle(item, active, i, variant = "default", size = "md") {
const isActive = active === item.value;
let bg = "transparent";
let color = "#94a3b8";
let fontWeight = 500;
if (isActive) {
if (variant === "outline") {
bg = "#38bdf8";
color = "#0a0a0a";
fontWeight = 700;
} else {
bg = "rgba(56,189,248,0.12)";
color = "#38bdf8";
fontWeight = 600;
}
}
const padY = size === "sm" ? "0.375rem" : "0.5rem";
const padX = size === "sm" ? "0.75rem" : "1rem";
const fontSize = size === "sm" ? "0.75rem" : "0.8125rem";
return `display:inline-flex;align-items:center;gap:0.375rem;padding:${padY} ${padX};border:none;border-left:${i > 0 ? "1px solid #2a2a2a" : "none"};background:${bg};color:${color};font-size:${fontSize};font-weight:${fontWeight};font-family:inherit;cursor:pointer;white-space:nowrap;transition:background 0.15s,color 0.15s;`;
}
</script>
<style>
.demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.container { width: 100%; max-width: 520px; }
h1 { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.375rem; }
.subtitle { color: #475569; font-size: 0.875rem; margin-bottom: 2rem; }
.section { margin-bottom: 1.75rem; }
.section-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.625rem;
}
.group {
display: inline-flex;
border-radius: 0.625rem;
overflow: hidden;
border: 1px solid #2a2a2a;
background: #141414;
}
.group.outline { background: transparent; }
.selected-text {
margin-top: 0.5rem;
font-size: 0.8125rem;
color: #94a3b8;
}
.selected-text strong { color: #38bdf8; }
</style>
<div class="demo">
<div class="container">
<h1>Button Group</h1>
<p class="subtitle">Segmented control style buttons with shared borders.</p>
<div class="section">
<span class="section-label">View Mode</span>
<div class="group">
{#each viewItems as item, i}
<button type="button" style={getStyle(item, view, i)} on:click={() => view = item.value}>
{item.label}
</button>
{/each}
</div>
<p class="selected-text">Selected: <strong>{view}</strong></p>
</div>
<div class="section">
<span class="section-label">Alignment</span>
<div class="group">
{#each alignItems as item, i}
<button type="button" style={getStyle(item, align, i)} on:click={() => align = item.value}>
{item.label}
</button>
{/each}
</div>
</div>
<div class="section">
<span class="section-label">Outlined Variant</span>
<div class="group outline">
{#each periodItems as item, i}
<button type="button" style={getStyle(item, period, i, 'outline')} on:click={() => period = item.value}>
{item.label}
</button>
{/each}
</div>
</div>
<div class="section">
<span class="section-label">Small Size</span>
<div class="group">
{#each sizeItems as item, i}
<button type="button" style={getStyle(item, smallVal, i, 'default', 'sm')} on:click={() => smallVal = item.value}>
{item.label}
</button>
{/each}
</div>
</div>
</div>
</div>Button Group
Joined buttons in a segmented control layout with shared borders, active state, and rounded only on the outer edges.
Features
- Shared borders between adjacent buttons
- Active state toggle on click
- Multiple size variants
- Rounded corners only on first and last buttons
- Dark theme styling