From 549c9216b217350c35fab6b7b1cbd80cc975c88a Mon Sep 17 00:00:00 2001 From: treeder <75826+treeder@users.noreply.github.com> Date: Sun, 17 May 2026 03:01:16 +0000 Subject: [PATCH] feat(buttons): implement m3 button groups This patch introduces the `md-button-group` component following the Material 3 guidelines. It supports the default standard group (with an 8px gap) and the `connected` attribute to group buttons together with overlapping borders. Component exports are added to `all.js` and `common.js`, and documentation is included in `buttons/README.md`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- all.js | 2 + buttons/README.md | 24 +++++++++++ buttons/button-group.js | 94 +++++++++++++++++++++++++++++++++++++++++ common.js | 2 + 4 files changed, 122 insertions(+) create mode 100644 buttons/button-group.js diff --git a/all.js b/all.js index bc4175c..dc15f50 100644 --- a/all.js +++ b/all.js @@ -6,6 +6,7 @@ */ import './buttons/elevated-button.js' import './buttons/button.js' +import './buttons/button-group.js' import './buttons/filled-tonal-button.js' import './buttons/outlined-button.js' import './buttons/text-button.js' @@ -48,6 +49,7 @@ import './text/text-field.js' // LINT.IfChange(exports) // go/keep-sorted start export * from './buttons/button.js' +export * from './buttons/button-group.js' export * from './checkbox/checkbox.js' export * from './chips/chip.js' export * from './chips/chip-set.js' diff --git a/buttons/README.md b/buttons/README.md index d82ff78..57c0dee 100644 --- a/buttons/README.md +++ b/buttons/README.md @@ -32,6 +32,30 @@ Changes from previous version: ``` +## Button Groups + +[Material 3 Button Groups](https://m3.material.io/components/button-groups/overview) + +Standard Button Group: + +```html + + One + Two + Three + +``` + +Connected Button Group: + +```html + + One + Two + Three + +``` + ## Floating Action Button - FAB [Material 3 Floating Action Button](https://m3.material.io/components/floating-action-button/overview)] diff --git a/buttons/button-group.js b/buttons/button-group.js new file mode 100644 index 0000000..72b8dde --- /dev/null +++ b/buttons/button-group.js @@ -0,0 +1,94 @@ +import { html, LitElement, css } from 'lit' + +export class ButtonGroup extends LitElement { + static styles = css` + :host { + display: inline-flex; + flex-direction: row; + vertical-align: middle; + } + + /* Standard group adds gap between buttons */ + :host(:not([connected])) { + gap: 8px; /* M3 standard button group gap */ + } + + /* Target all buttons except the first one */ + :host([connected]) ::slotted(*:not(:first-child)) { + /* Make flat on the left side */ + --_container-shape-start-start: 0; + --_container-shape-end-start: 0; + + /* Target variant-specific variables explicitly */ + --md-button-container-shape-start-start: 0; + --md-button-container-shape-end-start: 0; + --md-filled-tonal-button-container-shape-start-start: 0; + --md-filled-tonal-button-container-shape-end-start: 0; + --md-elevated-button-container-shape-start-start: 0; + --md-elevated-button-container-shape-end-start: 0; + --md-outlined-button-container-shape-start-start: 0; + --md-outlined-button-container-shape-end-start: 0; + --md-icon-button-container-shape-start-start: 0; + --md-icon-button-container-shape-end-start: 0; + + /* Explicit border radius as fallback */ + border-start-start-radius: 0; + border-end-start-radius: 0; + + /* Overlap borders */ + margin-inline-start: -1px; + } + + /* Target all buttons except the last one */ + :host([connected]) ::slotted(*:not(:last-child)) { + /* Make flat on the right side */ + --_container-shape-start-end: 0; + --_container-shape-end-end: 0; + + /* Target variant-specific variables explicitly */ + --md-button-container-shape-start-end: 0; + --md-button-container-shape-end-end: 0; + --md-filled-tonal-button-container-shape-start-end: 0; + --md-filled-tonal-button-container-shape-end-end: 0; + --md-elevated-button-container-shape-start-end: 0; + --md-elevated-button-container-shape-end-end: 0; + --md-outlined-button-container-shape-start-end: 0; + --md-outlined-button-container-shape-end-end: 0; + --md-icon-button-container-shape-start-end: 0; + --md-icon-button-container-shape-end-end: 0; + + /* Explicit border radius as fallback */ + border-start-end-radius: 0; + border-end-end-radius: 0; + } + + /* Ensure the active/hovered/focused button is on top to show full border */ + :host([connected]) ::slotted(*:hover), + :host([connected]) ::slotted(*:focus-within), + :host([connected]) ::slotted(*:active) { + z-index: 1; + position: relative; + } + + /* Selected state needs to be above unselected for connected groups */ + :host([connected]) ::slotted([selected]) { + z-index: 1; + position: relative; + } + ` + + static properties = { + connected: { type: Boolean, reflect: true } + } + + constructor() { + super() + this.connected = false + } + + render() { + return html`` + } +} + +customElements.define('md-button-group', ButtonGroup) diff --git a/common.js b/common.js index d063462..57b0e1f 100644 --- a/common.js +++ b/common.js @@ -7,6 +7,7 @@ * for production. */ import './buttons/button.js' +import './buttons/button-group.js' import './checkbox/checkbox.js' import './chips/chip.js' import './chips/chip-set.js' @@ -27,6 +28,7 @@ import './tabs/tabs.js' import './text/text-field.js' export * from './buttons/button.js' +export * from './buttons/button-group.js' export * from './checkbox/checkbox.js' export * from './chips/chip.js' export * from './chips/chip-set.js'