The Button component allows users to perform actions. It comes in several different style variants and supports icons as well as text labels.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-basic") public class ButtonBasic extends Div { private int counter = 0; public ButtonBasic() { // tag::snippet[] Button button = new Button("Button"); Paragraph info = new Paragraph(infoText()); button.addClickListener(clickEvent -> { counter += 1; info.setText(infoText()); }); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(button, info); horizontalLayout.setAlignItems(FlexComponent.Alignment.BASELINE); add(horizontalLayout); } private String infoText() { return String.format("Clicked %d times", counter); } }
button-basic.tsximport { useSignal } from '@vaadin/hilla-react-signals'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { const counter = useSignal(0); return ( // tag::snippet[] <HorizontalLayout theme="spacing" style={{ alignItems: 'baseline' }}> <Button onClick={() => counter.value++}>Button</Button> <p>Clicked {counter} times</p> </HorizontalLayout> // end::snippet[] ); }
button-basic.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-basic') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private counter = 0; protected override render() { return html` <!-- tag::snippet[] --> <vaadin-horizontal-layout theme="spacing" style="align-items: baseline"> <vaadin-button @click="${() => this.counter++}">Button</vaadin-button> <p>Clicked ${this.counter} times</p> </vaadin-horizontal-layout> <!-- end::snippet[] --> `; } }
StylesThe following variants can be used to distinguish between actions of different importance in the UI:
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-styles") public class ButtonStyles extends Div { public ButtonStyles() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); Button secondaryButton = new Button("Secondary"); Button tertiaryButton = new Button("Tertiary"); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-styles.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary">Primary</Button> <Button theme="secondary">Secondary</Button> <Button theme="tertiary">Tertiary</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-styles.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-styles') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary">Primary</vaadin-button> <vaadin-button theme="secondary">Secondary</vaadin-button> <vaadin-button theme="tertiary">Tertiary</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Variant Usage RecommendationPrimary
This is the most important action in a view or in part of one. It’s the main closure or continuation action (e.g., Save) in a form or dialog. Avoid presenting the user with more than one at any time.
Secondary
This is the default style recommended for most actions. It can be the alternate or negative closure actions (e.g., Cancel) in a form or dialog.
Tertiary
These are lower-importance actions — especially in parts of the UI with less space, such as cards, or repeated actions for items in lists, tables, etc. Caution: this can be mistaken for non-interactive text.
Danger & Error VariantsThis is a style for distinguishing actions related to dangers, warnings, or errors. Dangerous actions would be those that lose or destroy data.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-error") public class ButtonError extends Div { public ButtonError() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); Button secondaryButton = new Button("Secondary"); secondaryButton.addThemeVariants(ButtonVariant.LUMO_ERROR); Button tertiaryButton = new Button("Tertiary"); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-error.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary error">Primary</Button> <Button theme="secondary error">Secondary</Button> <Button theme="tertiary error">Tertiary</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-error.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-error') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary error">Primary</vaadin-button> <vaadin-button theme="secondary error">Secondary</vaadin-button> <vaadin-button theme="tertiary error">Tertiary</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Primary danger buttons should only be used when a dangerous action is the most likely action. An example of this would be the affirmative Delete action in a deletion confirmation dialog. Secondary and Tertiary variants can be used for actions related to current errors, such as resolving them or viewing their details.
Warning VariantThis is a style for distinguishing actions related to warnings: for example, in dialogs that are intended to warn the user, or to provide information that requires extra attention.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-warning") public class ButtonWarning extends Div { public ButtonWarning() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_WARNING); Button secondaryButton = new Button("Secondary"); secondaryButton.addThemeVariants(ButtonVariant.LUMO_WARNING); Button tertiaryButton = new Button("Tertiary"); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_WARNING); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-warning.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary warning">Primary</Button> <Button theme="secondary warning">Secondary</Button> <Button theme="tertiary warning">Tertiary</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-warning.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-warning') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary warning">Primary</vaadin-button> <vaadin-button theme="secondary warning">Secondary</vaadin-button> <vaadin-button theme="tertiary warning">Tertiary</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Size VariantsThe following size variants are available for Button instances whose size needs to be different from the default:
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-sizes") public class ButtonSizes extends Div { public ButtonSizes() { // tag::snippet[] Button largeButton = new Button("Large"); largeButton.addThemeVariants(ButtonVariant.LUMO_LARGE); Button normalButton = new Button("Normal"); Button smallButton = new Button("Small"); smallButton.addThemeVariants(ButtonVariant.LUMO_SMALL); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(largeButton, normalButton, smallButton); add(horizontalLayout); } }
button-sizes.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="large">Large</Button> <Button theme="normal">Normal</Button> <Button theme="small">Small</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-sizes.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-sizes') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="large">Large</vaadin-button> <vaadin-button theme="normal">Normal</vaadin-button> <vaadin-button theme="small">Small</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Variant Usage RecommendationLarge
For important call-to-action buttons — where more emphasis is needed.
Normal
Default size.
Small
Compact option for cramped parts of the UI — where a Tertiary variant isn’t deemed appropriate.
Tip
Customize Default Button Size
Size variants should only be used in special cases. See Size and Space for details on how to change the default button size. Miscellaneous Style VariantsThe Tertiary Inline variant omits all white space around the label. This can be useful for embedding a Button as part of text content or another component. It shouldn’t be confused with a link.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.router.Route; @Route("button-tertiary-inline") public class ButtonTertiaryInline extends Div { public ButtonTertiaryInline() { // tag::snippet[] Button tertiaryInlineButton = new Button("Tertiary inline"); tertiaryInlineButton .addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); // end::snippet[] add(tertiaryInlineButton); } }
button-tertiary-inline.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; function Example() { return ( // tag::snippet[] <Button theme="tertiary-inline">Tertiary inline</Button> // end::snippet[] ); }
button-tertiary-inline.tsimport '@vaadin/button'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-tertiary-inline') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-button theme="tertiary-inline">Tertiary inline</vaadin-button> <!-- end::snippet[] --> `; } }
The Success and Contrast variants should provide additional color options for buttons.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-success") public class ButtonSuccess extends Div { public ButtonSuccess() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); Button secondaryButton = new Button("Secondary"); secondaryButton.addThemeVariants(ButtonVariant.LUMO_SUCCESS); Button tertiaryButton = new Button("Tertiary"); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-success.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary success">Primary</Button> <Button theme="secondary success">Secondary</Button> <Button theme="tertiary success">Tertiary</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-success.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-success') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary success">Primary</vaadin-button> <vaadin-button theme="secondary success">Secondary</vaadin-button> <vaadin-button theme="tertiary success">Tertiary</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-contrast") public class ButtonContrast extends Div { public ButtonContrast() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_CONTRAST); Button secondaryButton = new Button("Secondary"); secondaryButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); Button tertiaryButton = new Button("Tertiary (avoid)"); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_CONTRAST); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-contrast.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary contrast">Primary</Button> <Button theme="secondary contrast">Secondary</Button> <Button theme="tertiary contrast">Tertiary (avoid)</Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-contrast.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-contrast') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary contrast">Primary</vaadin-button> <vaadin-button theme="secondary contrast">Secondary</vaadin-button> <vaadin-button theme="tertiary contrast">Tertiary (avoid)</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
The Tertiary + Contrast combination should be avoided because of similarity to non-interactive text elements.
Tip
Customize Default Button Colors
The standard Button colors can be adjusted using the Lumo color properties. Therefore, these variants shouldn’t be used to replace standard buttons only to achieve a different color. Buttons with IconsButtons can have icons instead of text, or they can have icons along with text.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-icons") public class ButtonIcons extends Div { public ButtonIcons() { // tag::snippet[] // Icon button using an aria-label to provide a textual alternative // to screen readers Button plusButton = new Button(new Icon(VaadinIcon.PLUS)); plusButton.addThemeVariants(ButtonVariant.LUMO_ICON); plusButton.setAriaLabel("Add item"); // Icon button using a tooltip to provide textual description // of the action that it triggers Button closeButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); closeButton.addThemeVariants(ButtonVariant.LUMO_ICON); closeButton.setAriaLabel("Close"); closeButton.setTooltipText("Close the dialog"); Button arrowLeftButton = new Button("Left", new Icon(VaadinIcon.ARROW_LEFT)); Button arrowRightButton = new Button("Right", new Icon(VaadinIcon.ARROW_RIGHT)); arrowRightButton.setIconAfterText(true); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(plusButton, closeButton, arrowLeftButton, arrowRightButton); add(horizontalLayout); } }
button-icons.tsximport '@vaadin/icons'; import React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { Icon } from '@vaadin/react-components/Icon.js'; import { Tooltip } from '@vaadin/react-components/Tooltip.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} {/* Icon button using an aria-label to provide a textual alternative to screen readers */} <Button theme="icon" aria-label="Add item"> <Icon icon="vaadin:plus" /> </Button> {/* Icon button using a tooltip to provide a textual description of the action that it triggers */} <Button theme="icon" aria-label="Close"> <Icon icon="vaadin:close-small" /> <Tooltip slot="tooltip" text="Close the dialog" /> </Button> <Button> <Icon icon="vaadin:arrow-left" slot={'prefix'} /> Left </Button> <Button> Right <Icon icon="vaadin:arrow-right" slot={'suffix'} /> </Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-icons.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import '@vaadin/icon'; import '@vaadin/icons'; import '@vaadin/tooltip'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-icons') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <!-- Icon button using an aria-label to provide a textual alternative to screen readers --> <vaadin-button theme="icon" aria-label="Add item"> <vaadin-icon icon="vaadin:plus"></vaadin-icon> </vaadin-button> <!-- Icon button using a tooltip to provide a textual description of the action that it triggers --> <vaadin-button theme="icon" aria-label="Close"> <vaadin-icon icon="vaadin:close-small"></vaadin-icon> <vaadin-tooltip slot="tooltip" text="Close the dialog"></vaadin-tooltip> </vaadin-button> <vaadin-button> <vaadin-icon icon="vaadin:arrow-left" slot="prefix"></vaadin-icon> Left </vaadin-button> <vaadin-button> Right <vaadin-icon icon="vaadin:arrow-right" slot="suffix"></vaadin-icon> </vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Use icons sparingly. Most actions are difficult to represent reliably with icons. The benefit of icons plus text should be weighed against the visual clutter they create.
Icon-only buttons should be used primarily for common recurring actions with highly standardized, universally understood icons (e.g., a cross for close), and for actions that are repeated, such as in lists and tables. They should also include a textual alternative for screen readers using the aria-label
attribute (see the first two buttons in the previous example).
Additionally, tooltips can be added to provide a description of the action that the button triggers (see the Close button in the previous example).
Note
Icon-Only Button Style Variant
Use theicon
/ LUMO_ICON
theme variant on icon-only buttons to reduce the white space on either side of the icon. The Flow Button
component automatically applies the icon
variant if the icon is the only child of the component. Buttons with Images
Images on buttons can be used like icons. See the icon usage recommendations for more information.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Image; import com.vaadin.flow.router.Route; import com.vaadin.flow.server.streams.DownloadHandler; @Route("button-images") public class ButtonImages extends Div { public ButtonImages() { DownloadHandler src = DownloadHandler.forClassResource( getClass(), "/images/vaadin-logo-dark.png", "vaadin-logo-dark.png"); // tag::snippet[] Image img = new Image(src, "Vaadin logo"); img.setWidth("100px"); Button imgButton = new Button(img); imgButton.addThemeVariants(ButtonVariant.LUMO_ICON); // end::snippet[] add(imgButton); } }
button-images.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import img from '../../../../../src/main/resources/images/vaadin-logo-dark.png?url'; function Example() { return ( // tag::snippet[] <Button theme="icon"> <img src={img} width="100" alt="Vaadin logo" /> </Button> // end::snippet[] ); }
button-images.tsimport '@vaadin/button'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; import img from '../../../../src/main/resources/images/vaadin-logo-dark.png?url'; @customElement('button-images') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-button theme="icon"> <img src="${img}" width="100" alt="Vaadin logo" /> </vaadin-button> <!-- end::snippet[] --> `; } }
DisabledButtons representing actions that aren’t currently available to the user should be either hidden or disabled. A disabled button is rendered as "dimmed".
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-disabled") public class ButtonDisabled extends Div { public ButtonDisabled() { // tag::snippet[] Button primaryButton = new Button("Primary"); primaryButton.setEnabled(false); primaryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); Button secondaryButton = new Button("Secondary"); secondaryButton.setEnabled(false); Button tertiaryButton = new Button("Tertiary"); tertiaryButton.setEnabled(false); tertiaryButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); // end::snippet[] HorizontalLayout horizontalLayout = new HorizontalLayout(primaryButton, secondaryButton, tertiaryButton); add(horizontalLayout); } }
button-disabled.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; function Example() { return ( <HorizontalLayout theme="spacing"> {/* tag::snippet[] */} <Button theme="primary" disabled> Primary </Button> <Button theme="secondary" disabled> Secondary </Button> <Button theme="tertiary" disabled> Tertiary </Button> {/* end::snippet[] */} </HorizontalLayout> ); }
button-disabled.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-disabled') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <vaadin-horizontal-layout theme="spacing"> <!-- tag::snippet[] --> <vaadin-button theme="primary" disabled>Primary</vaadin-button> <vaadin-button theme="secondary" disabled>Secondary</vaadin-button> <vaadin-button theme="tertiary" disabled>Tertiary</vaadin-button> <!-- end::snippet[] --> </vaadin-horizontal-layout> `; } }
Focus & HoverBy default, disabled buttons are not focusable and cannot react to hover events. This can cause accessibility issues by making them entirely invisible to assistive technologies, and prevents the use of Tooltips to explain why the action is not available. This can be addressed by enabling the feature flag accessibleDisabledButtons
, which allows you to focus and hover on disabled buttons, while preventing them from being triggered:
Flow
# Add this line to src/main/resources/vaadin-featureflags.properties
com.vaadin.experimental.accessibleDisabledButtons=true
Lit & React
// Set before any button is added to the DOM
window.Vaadin.featureFlags.accessibleDisabledButtons = true;
Alternatives to Disabling
The most obvious alternative to disabling a button is to hide it. This reduces UI clutter, but can cause confusion since the user won’t know where to watch for the button once the action it represents becomes available. There’s also a risk of undesired layout shifts in the UI when the button appears.
Another option is to keep the button visible and enabled, but show an error, e.g. using a Notification, when it’s clicked. This option is best combined with some custom styling of the button to give the user a hint that the action is unavailable.
Prevent Multiple ClicksButtons can be configured to be disabled when clicked. This can be useful especially for actions that take a bit longer to perform. Not only does this avoid the need for special handling of additional clicks while the action is in progress, but it also communicates to the user that the action was received successfully and is being processed.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.router.Route; @Route("button-disable-long-action") public class ButtonDisableLongAction extends Div { public ButtonDisableLongAction() { // tag::snippet[] Button button = new Button("Perform Action"); FakeProgressBar progressBar = new FakeProgressBar(); button.setDisableOnClick(true); button.addClickListener(event -> progressBar.simulateProgress()); progressBar.addProgressEndListener(event -> { button.setEnabled(true); }); // end::snippet[] button.getStyle().set("flex", "none"); HorizontalLayout horizontalLayout = new HorizontalLayout(button, progressBar); horizontalLayout.setAlignItems(FlexComponent.Alignment.CENTER); add(horizontalLayout); } }
button-disable-long-action.tsximport React, { useEffect } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { Button } from '@vaadin/react-components/Button.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { ProgressBar } from '@vaadin/react-components/ProgressBar.js'; function Example() { // tag::snippet[] const progress = useSignal(-1); useEffect(() => { if (progress.value >= 1) { progress.value = -1; } else if (progress.value >= 0) { setTimeout(() => { progress.value += 0.005; }, 25); } }, [progress.value]); return ( <HorizontalLayout theme="spacing" style={{ alignItems: 'center' }}> <Button disabled={progress.value >= 0} onClick={() => { progress.value = 0; }} > Perform Action </Button> <ProgressBar value={progress.value} /> </HorizontalLayout> ); // end::snippet[] }
button-disable-long-action.tsimport '@vaadin/button'; import '@vaadin/horizontal-layout'; import '@vaadin/progress-bar'; import './fake-progress-bar'; import { html, LitElement } from 'lit'; import { customElement, query, state } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; import type { FakeProgressBar } from './fake-progress-bar'; @customElement('button-disable-long-action') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private isDisabled = false; @query('fake-progress-bar') private fakeProgressBar!: FakeProgressBar; protected override render() { return html` <!-- tag::snippet[] --> <vaadin-horizontal-layout theme="spacing" style="align-items: center;"> <vaadin-button ?disabled=${this.isDisabled} @click=${() => { this.isDisabled = true; this.fakeProgressBar.simulateProgress(); }} style="flex: none;" >Perform Action</vaadin-button > <fake-progress-bar @progress-end=${() => { this.isDisabled = false; }} ></fake-progress-bar> </vaadin-horizontal-layout> <!-- end::snippet[] --> `; } }
FocusAs with other components, the focus ring is only rendered when the button is focused by keyboard or programmatically.
Open in aimport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; function Example() { return ( // tag::snippet[] <Button focus-ring>Keyboard focus</Button> // end::snippet[] ); }
button-focus.tsimport '@vaadin/button'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-focus') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-button focus-ring>Keyboard focus</vaadin-button> <!-- end::snippet[] --> `; } }
Auto FocusButtons can receive keyboard focus automatically when the UI in which they appear is rendered.
Source code JavaButton button = new Button("Button");
button.setAutofocus(true);
Java HTML
<Button autofocus>Button</Button>
HTML HTML
<vaadin-button autofocus>Button</vaadin-button>
HTML Keyboard Usage
A focused button can be triggered with Enter or Space.
Best PracticesBelow are some best practice recommendations related to buttons and their labels.
Button LabelsA label should describe the action, preferably using active verbs, such as "View Details" rather than "Details". To avoid ambiguity, also specify the object of the verb, such as "Save Changes" instead of "Save". They also should be brief, ideally less than three words or twenty-five characters.
Button groups representing options, such as the buttons of a Confirm Dialog, should state what each option represents (e.g., "Save Changes"). Don’t label a button "Yes" since that requires the user to read the question being asked. It’ll increase the risk of selecting the wrong option.
Use ellipsis (i.e., …) when an action is not immediate, but requires more steps to complete. This is useful, for example, for destructive actions like "Delete…" when a Confirm Dialog is used to confirm the action before it’s executed.
ARIA LabelsThe aria-label
attribute can be used to provide a separate label for accessibility technologies (AT), such as screen readers. This is important, for example, for icon-only buttons that lack a visible label.
A button with a regular, visible label can also benefit from a separate aria-label
to provide more context that may otherwise be difficult for an AT user to perceive. In the example here, each button’s aria-label
specifies which email address is removed:
package com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.router.Route; @Route("button-labels") public class ButtonLabels extends VerticalLayout { public ButtonLabels() { EmailField emailField = new EmailField("Primary email address"); emailField.setValue("foo@example.com"); EmailField secondaryEmailField = new EmailField( "Secondary email address"); secondaryEmailField.setValue("bar@example.com"); // tag::snippet[] Button clearPrimaryEmail = new Button("Remove", event -> { emailField.setValue(""); }); clearPrimaryEmail.setAriaLabel("Remove primary email address"); Button clearSecondaryEmail = new Button("Remove", event -> { secondaryEmailField.setValue(""); }); clearSecondaryEmail.setAriaLabel("Remove secondary email address"); // end::snippet[] HorizontalLayout horizontalLayout1 = new HorizontalLayout(emailField, clearPrimaryEmail); HorizontalLayout horizontalLayout2 = new HorizontalLayout( secondaryEmailField, clearSecondaryEmail); horizontalLayout1.setAlignItems(FlexComponent.Alignment.BASELINE); horizontalLayout2.setAlignItems(FlexComponent.Alignment.BASELINE); horizontalLayout2.getStyle().set("margin-top", "0"); setPadding(false); add(horizontalLayout1, horizontalLayout2); } }
button-labels.tsximport { useSignal } from '@vaadin/hilla-react-signals'; import { Button } from '@vaadin/react-components/Button.js'; import { EmailField } from '@vaadin/react-components/EmailField.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; function Example() { const primaryEmail = useSignal('foo@example.com'); const secondaryEmail = useSignal('bar@example.com'); return ( <> {/* tag::snippet[] */} <VerticalLayout> <HorizontalLayout theme="spacing" style={{ alignItems: 'baseline' }}> <EmailField label="Primary email address" value={primaryEmail.value} onValueChanged={(event) => { primaryEmail.value = event.detail.value; }} /> <Button onClick={() => { primaryEmail.value = ''; }} > Remove </Button> </HorizontalLayout> <HorizontalLayout theme="spacing" style={{ alignItems: 'baseline' }}> <EmailField label="Secondary email address" value={secondaryEmail.value} onValueChanged={(event) => { secondaryEmail.value = event.detail.value; }} /> <Button onClick={() => { secondaryEmail.value = ''; }} > Remove </Button> </HorizontalLayout> </VerticalLayout> {/* end::snippet[] */} </> ); }
button-labels.tsimport '@vaadin/button'; import '@vaadin/email-field'; import '@vaadin/horizontal-layout'; import '@vaadin/vertical-layout'; import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { EmailFieldValueChangedEvent } from '@vaadin/email-field'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-labels') export class Example extends LitElement { static override styles = css` vaadin-horizontal-layout { align-items: baseline; } `; protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private primaryEmail = 'foo@example.com'; @state() private secondaryEmail = 'bar@example.com'; protected override render() { return html` <!-- tag::snippet[] --> <vaadin-vertical-layout> <vaadin-horizontal-layout theme="spacing"> <vaadin-email-field id="primary-email" label="Primary email address" .value="${this.primaryEmail}" @value-changed="${(event: EmailFieldValueChangedEvent) => { this.primaryEmail = event.detail.value; }}" ></vaadin-email-field> <vaadin-button arial-label="Remove primary email address" @click="${() => { this.primaryEmail = ''; }}" > Remove </vaadin-button> </vaadin-horizontal-layout> <vaadin-horizontal-layout theme="spacing"> <vaadin-email-field id="secondary-email" label="Secondary email address" .value="${this.secondaryEmail}" @value-changed="${(event: EmailFieldValueChangedEvent) => { this.secondaryEmail = event.detail.value; }}" ></vaadin-email-field> <vaadin-button arial-label="Remove secondary email address" @click="${() => { this.secondaryEmail = ''; }}" > Remove </vaadin-button> </vaadin-horizontal-layout> </vaadin-vertical-layout> <!-- end::snippet[] --> `; } }
Buttons in FormsButtons in forms should be placed below the form with which they’re associated. They should be aligned left, with the primary action first, followed by other actions, in order of importance.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.formlayout.FormLayout.ResponsiveStep; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; @Route("button-form") public class ButtonForm extends VerticalLayout { public ButtonForm() { // tag::snippet[] TextField firstNameField = new TextField("First name", "John", ""); TextField lastNameField = new TextField("Last name", "Smith", ""); EmailField emailField = new EmailField("Email address"); emailField.setValue("john.smith@example.com"); FormLayout formLayout = new FormLayout(firstNameField, lastNameField, emailField); formLayout.setResponsiveSteps(new ResponsiveStep("0", 2)); formLayout.setColspan(emailField, 2); Button createAccount = new Button("Create account"); createAccount.addThemeVariants(ButtonVariant.LUMO_PRIMARY); Button cancel = new Button("Cancel"); HorizontalLayout buttonLayout = new HorizontalLayout(createAccount, cancel); // end::snippet[] setPadding(false); add(formLayout, buttonLayout); } }
button-form.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { EmailField } from '@vaadin/react-components/EmailField.js'; import { FormLayout } from '@vaadin/react-components/FormLayout.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { TextField } from '@vaadin/react-components/TextField.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; function Example() { return ( // tag::snippet[] <VerticalLayout theme="spacing"> <FormLayout responsiveSteps={[{ columns: 2 }]}> <TextField label="First name" value="John" /> <TextField label="Last name" value="Smith" /> <EmailField label="Email address" value="john.smith@example.com" data-colspan="2" /> </FormLayout> <HorizontalLayout theme="spacing"> <Button theme="primary">Create account</Button> <Button theme="secondary">Cancel</Button> </HorizontalLayout> </VerticalLayout> // end::snippet[] ); }
button-form.tsimport '@vaadin/button'; import '@vaadin/email-field'; import '@vaadin/form-layout'; import '@vaadin/horizontal-layout'; import '@vaadin/text-field'; import '@vaadin/vertical-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-form') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-vertical-layout theme="spacing"> <vaadin-form-layout .responsiveSteps="${[{ columns: 2 }]}"> <vaadin-text-field label="First name" value="John"></vaadin-text-field> <vaadin-text-field label="Last name" value="Smith"></vaadin-text-field> <vaadin-email-field label="Email address" value="john.smith@example.com" colspan="2" ></vaadin-email-field> </vaadin-form-layout> <vaadin-horizontal-layout theme="spacing"> <vaadin-button theme="primary">Create account</vaadin-button> <vaadin-button theme="secondary">Cancel</vaadin-button> </vaadin-horizontal-layout> </vaadin-vertical-layout> <!-- end::snippet[] --> `; } }
Buttons in DialogsButtons in dialogs should be placed at the bottom of the dialog and aligned right. Primary action should be last, preceded by other actions. Dangerous actions should be aligned left, to avoid accidental clicks, especially if no confirmation step is included.
Open in apackage com.vaadin.demo.component.button; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; @Route("button-dialog") public class ButtonDialog extends VerticalLayout { public ButtonDialog() { // tag::snippet[] TextField firstNameField = new TextField("First name", "John", ""); TextField lastNameField = new TextField("Last name", "Smith", ""); EmailField emailField = new EmailField("Email address"); emailField.setValue("john.smith@example.com"); FormLayout formLayout = new FormLayout(firstNameField, lastNameField, emailField); formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); formLayout.setColspan(emailField, 2); Button delete = new Button("Delete"); delete.addThemeVariants(ButtonVariant.LUMO_ERROR); delete.getStyle().set("margin-inline-end", "auto"); Button cancel = new Button("Cancel"); Button createAccount = new Button("Create account"); createAccount.addThemeVariants(ButtonVariant.LUMO_PRIMARY); HorizontalLayout buttonLayout = new HorizontalLayout(delete, cancel, createAccount); buttonLayout.getStyle().set("flex-wrap", "wrap"); buttonLayout.setJustifyContentMode(JustifyContentMode.END); // end::snippet[] setPadding(false); setAlignItems(Alignment.STRETCH); add(formLayout, buttonLayout); } }
button-dialog.tsximport React from 'react'; import { Button } from '@vaadin/react-components/Button.js'; import { EmailField } from '@vaadin/react-components/EmailField.js'; import { FormLayout } from '@vaadin/react-components/FormLayout.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { TextField } from '@vaadin/react-components/TextField.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; function Example() { return ( // tag::snippet[] <VerticalLayout theme="spacing" style={{ alignItems: 'stretch' }}> <FormLayout responsiveSteps={[{ columns: 2 }]}> <TextField label="First name" value="John" /> <TextField label="Last name" value="Smith" /> <EmailField label="Email address" value="john.smith@example.com" data-colspan="2" /> </FormLayout> <HorizontalLayout theme="spacing" style={{ flexWrap: 'wrap', justifyContent: 'flex-end' }}> <Button theme="secondary error" style={{ marginInlineEnd: 'auto' }}> Delete </Button> <Button theme="secondary">Cancel</Button> <Button theme="primary">Create account</Button> </HorizontalLayout> </VerticalLayout> // end::snippet[] ); }
button-dialog.tsimport '@vaadin/button'; import '@vaadin/email-field'; import '@vaadin/form-layout'; import '@vaadin/horizontal-layout'; import '@vaadin/text-field'; import '@vaadin/vertical-layout'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-dialog') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-vertical-layout theme="spacing" style="align-items: stretch;"> <vaadin-form-layout .responsiveSteps="${[{ columns: 2 }]}"> <vaadin-text-field label="First name" value="John"></vaadin-text-field> <vaadin-text-field label="Last name" value="Smith"></vaadin-text-field> <vaadin-email-field label="Email address" value="john.smith@example.com" colspan="2" ></vaadin-email-field> </vaadin-form-layout> <vaadin-horizontal-layout theme="spacing" style="flex-wrap: wrap; justify-content: flex-end;" > <vaadin-button theme="secondary error" style="margin-inline-end: auto;"> Delete </vaadin-button> <vaadin-button theme="secondary">Cancel</vaadin-button> <vaadin-button theme="primary">Create account</vaadin-button> </vaadin-horizontal-layout> </vaadin-vertical-layout> <!-- end::snippet[] --> `; } }
Global vs. Selection-Specific ActionsIn lists of selectable items — such as in a Grid — that provide actions applicable to the selected item, buttons for selection-specific actions should be placed apart from global actions that aren’t selection-specific. They should be located below the list of selectable items.
In the example below, the global Add User action is separated from the selection-specific actions below the Grid:
Open in apackage com.vaadin.demo.component.button; import com.vaadin.demo.domain.DataService; import com.vaadin.demo.domain.Person; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.Route; import java.util.List; @Route("button-grid") public class ButtonGrid extends VerticalLayout { public ButtonGrid() { // tag::snippet[] H2 users = new H2("Users"); users.getStyle().set("margin", "0 auto 0 0"); Button addUser = new Button("Add user"); HorizontalLayout header = new HorizontalLayout(users, addUser); header.setAlignItems(Alignment.CENTER); header.getThemeList().clear(); Button editProfile = new Button("Edit profile"); editProfile.setEnabled(false); Button managePermissions = new Button("Manage permissions"); managePermissions.setEnabled(false); Button resetPassword = new Button("Reset password"); resetPassword.setEnabled(false); Button delete = new Button("Delete"); delete.setEnabled(false); delete.addThemeVariants(ButtonVariant.LUMO_ERROR); delete.getStyle().set("margin-inline-start", "auto"); Grid<Person> grid = new Grid<>(Person.class, false); grid.setSelectionMode(Grid.SelectionMode.MULTI); grid.addColumn(Person::getFirstName).setHeader("First name"); grid.addColumn(Person::getLastName).setHeader("Last name"); grid.addColumn(Person::getEmail).setHeader("Email"); grid.addSelectionListener(selection -> { int size = selection.getAllSelectedItems().size(); boolean isSingleSelection = size == 1; editProfile.setEnabled(isSingleSelection); managePermissions.setEnabled(isSingleSelection); resetPassword.setEnabled(isSingleSelection); delete.setEnabled(size != 0); }); HorizontalLayout footer = new HorizontalLayout(editProfile, managePermissions, resetPassword, delete); footer.getStyle().set("flex-wrap", "wrap"); // end::snippet[] List<Person> people = DataService.getPeople(); grid.setItems(people); setPadding(false); setAlignItems(Alignment.STRETCH); add(header, grid, footer); } }
button-grid.tsximport React, { useEffect } from 'react'; import { useSignal } from '@vaadin/hilla-react-signals'; import { Grid } from '@vaadin/react-components/Grid.js'; import { GridColumn } from '@vaadin/react-components/GridColumn.js'; import { GridSelectionColumn } from '@vaadin/react-components/GridSelectionColumn.js'; import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js'; import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; function Example() { // tag::snippet[] const items = useSignal<Person[]>([]); const selectedItems = useSignal<Person[]>([]); useEffect(() => { getPeople().then(({ people }) => { items.value = people; }); }, []); return ( <VerticalLayout theme="spacing" style={{ alignItems: 'stretch' }}> <HorizontalLayout theme="spacing" style={{ alignItems: 'center' }}> <h2 style={{ margin: '0 auto 0 0' }}>Users </h2> <Button>Add user</Button> </HorizontalLayout> <Grid items={items.value} selectedItems={selectedItems.value} onSelectedItemsChanged={({ detail: { value } }) => { selectedItems.value = value; }} > <GridSelectionColumn /> <GridColumn path="firstName" /> <GridColumn path="lastName" /> <GridColumn path="email" /> </Grid> <HorizontalLayout theme="spacing"> <Button disabled={selectedItems.value.length !== 1}>Edit profile</Button> <Button disabled={selectedItems.value.length !== 1}>Manage permissions</Button> <Button disabled={selectedItems.value.length !== 1}>Reset password</Button> <Button theme="error" disabled={selectedItems.value.length === 0} style={{ marginInlineStart: 'auto' }} > Delete </Button> </HorizontalLayout> </VerticalLayout> ); // end::snippet[] }
button-grid.tsimport '@vaadin/button'; import '@vaadin/grid'; import '@vaadin/grid/vaadin-grid-selection-column'; import '@vaadin/horizontal-layout'; import '@vaadin/vertical-layout'; import { html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { GridSelectedItemsChangedEvent } from '@vaadin/grid'; import type { GridSelectionColumnSelectAllChangedEvent } from '@vaadin/grid/vaadin-grid-selection-column'; import { getPeople } from 'Frontend/demo/domain/DataService'; import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person'; import { applyTheme } from 'Frontend/generated/theme'; @customElement('button-grid') export class Example extends LitElement { protected override createRenderRoot() { const root = super.createRenderRoot(); // Apply custom theme (only supported if your app uses one) applyTheme(root); return root; } @state() private items: Person[] = []; @state() private selectedItems: Person[] = []; protected override async firstUpdated() { const { people } = await getPeople(); this.items = people; } protected override render() { return html` <!-- tag::snippet[] --> <vaadin-vertical-layout theme="spacing" style="align-items: stretch;"> <vaadin-horizontal-layout style="align-items: center;"> <h2 style="margin: 0 auto 0 0;">Users</h2> <vaadin-button>Add user</vaadin-button> </vaadin-horizontal-layout> <vaadin-grid .items="${this.items}" @selected-items-changed="${(event: GridSelectedItemsChangedEvent<Person>) => { this.selectedItems = event.target ? [...event.detail.value] : this.selectedItems; }}" > <vaadin-grid-selection-column auto-select @select-all-changed="${(event: GridSelectionColumnSelectAllChangedEvent) => { this.selectedItems = event.detail.value ? this.items : this.selectedItems; }}" ></vaadin-grid-selection-column> <vaadin-grid-column path="firstName"></vaadin-grid-column> <vaadin-grid-column path="lastName"></vaadin-grid-column> <vaadin-grid-column path="email"></vaadin-grid-column> </vaadin-grid> <vaadin-horizontal-layout theme="spacing" style="flex-wrap: wrap;"> <vaadin-button ?disabled="${this.selectedItems.length !== 1}">Edit profile</vaadin-button> <vaadin-button ?disabled="${this.selectedItems.length !== 1}"> Manage permissions </vaadin-button> <vaadin-button ?disabled="${this.selectedItems.length !== 1}"> Reset password </vaadin-button> <vaadin-button theme="error" ?disabled="${this.selectedItems.length === 0}" style="margin-inline-start: auto;" > Delete </vaadin-button> </vaadin-horizontal-layout> </vaadin-vertical-layout> <!-- end::snippet[] --> `; } }
Component Usage RecommendationAnchor
Use anchor elements instead of buttons for navigation links.
Overlay menus with items that trigger actions. This can also be used for single "menu buttons" and "button groups" without overlay menus.
8E1BE28B-D5F0-490C-A8FA-82975D9A3B43
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4