Scroller
- Usage
- Styling
Scroller is a container component that enables scrollable areas within the UI.
new tab
Source code
ScrollerBasic.java
package com.vaadin.demo.component.scroller;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import java.time.LocalDate;
@Route("scroller-basic")
public class ScrollerBasic extends VerticalLayout {
public static final String PERSONAL_TITLE_ID = "personal-title";
public static final String EMPLOYMENT_TITLE_ID = "employment-title";
public ScrollerBasic() {
addClassNames(LumoUtility.Border.ALL, LumoUtility.BorderColor.CONTRAST_20);
setHeight(400, Unit.PIXELS);
setMaxWidth(100, Unit.PERCENTAGE);
setPadding(false);
setSpacing(false);
setWidth(360, Unit.PIXELS);
// Header
Header header = new Header();
header.addClassNames(LumoUtility.AlignItems.CENTER, LumoUtility.Border.BOTTOM, LumoUtility.Display.FLEX,
LumoUtility.Gap.MEDIUM, LumoUtility.Padding.MEDIUM);
H2 editEmployee = new H2("Edit employee");
editEmployee.addClassNames(LumoUtility.FontSize.XLARGE);
Icon arrowLeft = VaadinIcon.ARROW_LEFT.create();
arrowLeft.addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.IconSize.MEDIUM, LumoUtility.Padding.XSMALL);
arrowLeft.getElement().setAttribute("aria-hidden", "true");
Anchor goBack = new Anchor("#", arrowLeft);
goBack.setAriaLabel("Go back");
header.add(goBack, editEmployee);
add(header);
// Personal information
H3 personalTitle = new H3("Personal information");
personalTitle.addClassNames(LumoUtility.FontSize.LARGE);
personalTitle.setId(PERSONAL_TITLE_ID);
TextField firstName = new TextField("First name");
firstName.setWidthFull();
TextField lastName = new TextField("Last name");
lastName.setWidthFull();
DatePicker birthDate = new DatePicker("Birthdate");
birthDate.setInitialPosition(LocalDate.of(1990, 1, 1));
birthDate.setWidthFull();
Section personalInformation = new Section(personalTitle, firstName,
lastName, birthDate);
personalInformation.getElement().setAttribute("aria-labelledby",
PERSONAL_TITLE_ID);
// Employment information
H3 employmentTitle = new H3("Employment information");
employmentTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.Top.LARGE);
employmentTitle.setId(EMPLOYMENT_TITLE_ID);
TextField position = new TextField("Position");
position.setWidthFull();
TextArea additionalInformation = new TextArea("Additional Information");
additionalInformation.setWidthFull();
Section employmentInformation = new Section(employmentTitle, position,
additionalInformation);
employmentInformation.getElement().setAttribute("aria-labelledby",
EMPLOYMENT_TITLE_ID);
Scroller scroller = new Scroller(new Div(personalInformation, employmentInformation));
scroller.addClassNames(LumoUtility.Border.BOTTOM, LumoUtility.Padding.MEDIUM);
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
add(scroller);
// Footer
Button save = new Button("Save");
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button reset = new Button("Reset");
reset.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Footer footer = new Footer(save, reset);
footer.addClassNames(LumoUtility.Display.FLEX, LumoUtility.Gap.SMALL, LumoUtility.Padding.Horizontal.MEDIUM,
LumoUtility.Padding.Vertical.SMALL);
add(footer);
}
}
scroller-basic.tsx
import '@vaadin/icons';
import React from 'react';
import { Button } from '@vaadin/react-components/Button.js';
import { DatePicker } from '@vaadin/react-components/DatePicker.js';
import { Icon } from '@vaadin/react-components/Icon.js';
import { Scroller } from '@vaadin/react-components/Scroller.js';
import { TextArea } from '@vaadin/react-components/TextArea.js';
import { TextField } from '@vaadin/react-components/TextField.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
function Example() {
return (
<VerticalLayout className="border border-contrast-20 items-stretch max-w-full" id="container">
<header className="flex gap-m items-center border-b p-m">
<a href="#" aria-label="Go back">
<Icon className="box-border icon-m p-xs" icon="vaadin:arrow-left" aria-hidden="true" />
</a>
<h2 className="text-xl">Edit employee</h2>
</header>
<Scroller className="border-b p-m" scrollDirection="vertical">
<section aria-labelledby="personal-title">
<h3 className="text-l" id="personal-title">Personal information</h3>
<TextField className="w-full" label="First name" />
<TextField className="w-full" label="Last name" />
<DatePicker initialPosition="1990-01-01" label="Birthdate" className="w-full" />
</section>
<section aria-labelledby="employment-title">
<h3 className="mt-l text-l" id="employment-title">Employment information</h3>
<TextField className="w-full" label="Position" />
<TextArea className="w-full" label="Additional information" />
</section>
</Scroller>
<footer className="flex gap-s px-m py-s">
<Button theme="primary">Save</Button>
<Button theme="tertiary">Reset</Button>
</footer>
</VerticalLayout>
);
}
scroller-basic.ts
import '@vaadin/button';
import '@vaadin/date-picker';
import '@vaadin/icon';
import '@vaadin/icons';
import '@vaadin/scroller';
import '@vaadin/text-area';
import '@vaadin/text-field';
import '@vaadin/vertical-layout';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('scroller-basic')
export class Example extends LitElement {
static override styles = css`
#container {
height: 400px;
width: 360px;
}
`;
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-vertical-layout class="border border-contrast-20 items-stretch max-w-full" id="container">
<header class="flex gap-m items-center border-b p-m">
<a href="#" aria-label="Go back">
<vaadin-icon
class="box-border icon-m p-xs"
icon="vaadin:arrow-left"
aria-hidden="true"
></vaadin-icon>
</a>
<h2 class="text-xl">Edit employee</h2>
</header>
<vaadin-scroller class="border-b p-m" scroll-direction="vertical">
<section aria-labelledby="personal-title">
<h3 class="text-l" id="personal-title">Personal information</h3>
<vaadin-text-field class="w-full" label="First name"></vaadin-text-field>
<vaadin-text-field class="w-full" label="Last name"></vaadin-text-field>
<vaadin-date-picker
class="w-full"
initial-position="1990-01-01"
label="Birthdate"
></vaadin-date-picker>
</section>
<section aria-labelledby="employment-title">
<h3 class="mt-l text-l" id="employment-title">Employment information</h3>
<vaadin-text-field class="w-full" label="Position"></vaadin-text-field>
<vaadin-text-area class="w-full" label="Additional information"></vaadin-text-area>
</section>
</vaadin-scroller>
<footer class="flex gap-s px-m py-s">
<vaadin-button theme="primary">Save</vaadin-button>
<vaadin-button theme="tertiary">Reset</vaadin-button>
</footer>
</vaadin-vertical-layout>
`;
}
}
Scroll Direction
Scroller supports four scroll directions: vertical, horizontal, both, and none. The default is both.
Vertical
When vertical scrolling is enabled, users can scroll down if the content exceeds the container’s height. Horizontal overflow, however, is clipped and inaccessible—so the content’s width should be set to 100%.
Horizontal
When horizontal scrolling is enabled, users can scroll sideways if the content exceeds the container’s width. However, vertical overflow is clipped and inaccessible—so the content’s height should be set to 100%.
Note
|
Use horizontal scrolling with caution, as it’s less common and can be harder for users to notice and interact with—especially on non-mobile devices. |
Desktop
Aside from grids, horizontal scrolling is uncommon in desktop or business applications, as it can be unintuitive and cumbersome.
To improve usability, consider using buttons to make horizontal scrolling more noticeable and accessible. For horizontally scrollable lists, it’s good practice to indicate the total number of items and highlight which ones are currently in view.
Mobile
Horizontal scrolling or swiping is more common on mobile, often used for navigation. It can also help conserve vertical space—for example, when displaying less important content such as shortcuts or images.
new tab
Source code
ScrollerMobile.java
package com.vaadin.demo.component.scroller;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Section;
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.component.orderedlayout.Scroller;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
@Route("scroller-mobile")
public class ScrollerMobile extends Section {
public ScrollerMobile() {
addClassNames(LumoUtility.Border.ALL, LumoUtility.BorderColor.CONTRAST_20);
setMaxWidth(100, Unit.PERCENTAGE);
setWidth(360, Unit.PIXELS);
// Header
H2 createNewTitle = new H2("Create new...");
createNewTitle.addClassNames(LumoUtility.FontSize.XLARGE, LumoUtility.Padding.Top.MEDIUM,
LumoUtility.Padding.Horizontal.MEDIUM);
add(createNewTitle);
Scroller scroller = new Scroller();
scroller.setScrollDirection(Scroller.ScrollDirection.HORIZONTAL);
Button auditBtn = new Button("Audit");
auditBtn.setIcon(new Icon(VaadinIcon.CLIPBOARD_CHECK));
auditBtn.setHeight("100px");
Button reportBtn = new Button("Report");
reportBtn.setIcon(new Icon(VaadinIcon.BOOK_DOLLAR));
reportBtn.setHeight("100px");
Button dashboardBtn = new Button("Dashboard");
dashboardBtn.setIcon(new Icon(VaadinIcon.LINE_CHART));
dashboardBtn.setHeight("100px");
Button invoiceBtn = new Button("Invoice");
invoiceBtn.setIcon(new Icon(VaadinIcon.INVOICE));
invoiceBtn.setHeight("100px");
HorizontalLayout buttons = new HorizontalLayout(auditBtn, reportBtn,
dashboardBtn, invoiceBtn);
buttons.addClassName(LumoUtility.Display.INLINE_FLEX);
buttons.setPadding(true);
scroller.setContent(buttons);
add(scroller);
}
}
scroller-mobile.tsx
import '@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 { Scroller } from '@vaadin/react-components/Scroller.js';
const sectionStyle = {
width: '360px',
};
function Example() {
return (
<section className="border border-contrast-20 max-w-full" id="container" style={sectionStyle}>
<h2 className="pt-m px-m text-xl">Create new...</h2>
<Scroller scroll-direction="horizontal">
<HorizontalLayout className="inline-flex" theme="padding spacing">
<Button style={{ height: '100px' }}>
<Icon icon="vaadin:clipboard-check" slot="prefix" />
Audit
</Button>
<Button style={{ height: '100px' }}>
<Icon icon="vaadin:book-dollar" slot="prefix" />
Report
</Button>
<Button style={{ height: '100px' }}>
<Icon icon="vaadin:line-chart" slot="prefix" />
Dashboard
</Button>
<Button style={{ height: '100px' }}>
<Icon icon="vaadin:invoice" slot="prefix" />
Invoice
</Button>
</HorizontalLayout>
</Scroller>
</section>
);
}
scroller-mobile.ts
import '@vaadin/button';
import '@vaadin/horizontal-layout';
import '@vaadin/icon';
import '@vaadin/icons';
import '@vaadin/scroller';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('scroller-mobile')
export class Example extends LitElement {
static override styles = css`
section {
width: 360px;
}
`;
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`
<section class="border border-contrast-20 max-w-full" id="container">
<h2 class="pt-m px-m text-xl">Create new...</h2>
<vaadin-scroller scroll-direction="horizontal">
<vaadin-horizontal-layout class="inline-flex" theme="padding spacing">
<vaadin-button style="height: 100px;">
<vaadin-icon icon="vaadin:clipboard-check" slot="prefix"></vaadin-icon>
Audit
</vaadin-button>
<vaadin-button style="height: 100px;">
<vaadin-icon icon="vaadin:book-dollar" slot="prefix"></vaadin-icon>
Report
</vaadin-button>
<vaadin-button style="height: 100px;">
<vaadin-icon icon="vaadin:line-chart" slot="prefix"></vaadin-icon>
Dashboard
</vaadin-button>
<vaadin-button style="height: 100px;">
<vaadin-icon icon="vaadin:invoice" slot="prefix"></vaadin-icon>
Invoice
</vaadin-button>
</vaadin-horizontal-layout>
</vaadin-scroller>
</section>
`;
}
}
Both
When the scroll direction is set to Both (the default), users can scroll both vertically and horizontally if the content overflows in either direction.
This option is ideal for allowing users to pan across large elements, such as images. It can also serve as a fallback for responsive layouts that may overflow in certain situations.
new tab
Source code
ScrollerBoth.java
package com.vaadin.demo.component.scroller;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
@Route("scroller-both")
public class ScrollerBoth extends Div {
public ScrollerBoth() {
Scroller scroller = new Scroller();
scroller.setWidthFull();
scroller.setHeight("300px");
StreamResource imageResource = new StreamResource("reindeer+.jpg",
() -> getClass().getResourceAsStream("/images/reindeer.jpg"));
Image img = new Image(imageResource,
"A reindeer walking on a snowy lake shore at dusk");
scroller.setContent(img);
add(scroller);
}
}
scroller-both.tsx
import React from 'react';
import { Scroller } from '@vaadin/react-components/Scroller.js';
import img from '../../../../../src/main/resources/images/reindeer.jpg?url';
function Example() {
return (
<Scroller style={{ height: '300px', width: '100%' }}>
<img src={img} alt="A reindeer walking on a snowy lake shore at dusk" />
</Scroller>
);
}
scroller-both.ts
import '@vaadin/scroller';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { applyTheme } from 'Frontend/generated/theme';
import img from '../../../../src/main/resources/images/reindeer.jpg?url';
@customElement('scroller-both')
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-scroller class="w-full" style="height: 300px">
<img src="${img}" alt="A reindeer walking on a snowy lake shore at dusk" />
</vaadin-scroller>
`;
}
}
Theme Variants
Scroller has one theme variant: overflow-indicators
.
This variant adds borders to indicate when content overflows the scroll container. For example, if more content is available by scrolling down, a bottom border appears. If content overflows at the top, a top border is shown, and so on for other directions.
You shouldn’t add padding to the scroller when using this variant, as it prevents the borders from appearing in the correct positions.
new tab
Source code
ScrollerOverflowIndicators.java
package com.vaadin.demo.component.scroller;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Footer;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.ScrollerVariant;
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;
import com.vaadin.flow.theme.lumo.LumoUtility;
@Route("scroller-overflow-indicators")
public class ScrollerOverflowIndicators extends VerticalLayout {
public ScrollerOverflowIndicators() {
addClassNames(LumoUtility.Border.ALL, LumoUtility.BorderColor.CONTRAST_20);
setAlignItems(Alignment.STRETCH);
setHeight(400, Unit.PIXELS);
setMaxWidth(100, Unit.PERCENTAGE);
setPadding(false);
setSpacing(false);
setWidth(360, Unit.PIXELS);
// Header
H2 createYourAccount = new H2("Create your account");
createYourAccount.addClassNames(LumoUtility.FontSize.XLARGE, LumoUtility.Padding.Horizontal.MEDIUM,
LumoUtility.Padding.Vertical.MEDIUM);
add(createYourAccount);
TextField firstName = new TextField("First name");
TextField lastName = new TextField("Last name");
EmailField email = new EmailField("Email");
TextField phoneNumber = new TextField("Phone number");
TextField address = new TextField("Address");
TextField city = new TextField("City");
ComboBox<String> state = new ComboBox<>("State");
TextField zipCode = new TextField("Zip code");
ComboBox<String> country = new ComboBox<>("Country");
Div div = new Div(firstName, lastName, email, phoneNumber, address, city, state, zipCode, country);
div.addClassNames(LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.Bottom.MEDIUM,
LumoUtility.Padding.Horizontal.MEDIUM);
Scroller scroller = new Scroller(div);
scroller.addThemeVariants(ScrollerVariant.LUMO_OVERFLOW_INDICATORS);
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
add(scroller);
// Footer
Button next = new Button("Next");
next.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button cancel = new Button("Cancel");
cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Footer footer = new Footer(next, cancel);
footer.addClassNames(LumoUtility.Display.FLEX, LumoUtility.Gap.SMALL, LumoUtility.Padding.Horizontal.MEDIUM,
LumoUtility.Padding.Vertical.SMALL);
add(footer);
}
}
scroller-overflow-indicators.tsx
import '@vaadin/icons';
import React from 'react';
import { Button } from '@vaadin/react-components/Button.js';
import { Checkbox } from '@vaadin/react-components/Checkbox.js';
import { Scroller } from '@vaadin/react-components/Scroller.js';
import { TextField } from '@vaadin/react-components/TextField.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
function Example() {
return (
<VerticalLayout className="border border-contrast-20 items-stretch max-w-full" id="container">
<h2 className="text-xl px-m py-m">Create your account</h2>
<Scroller scrollDirection="vertical" theme="overflow-indicators">
<div className="flex flex-col pb-m px-m">
<TextField className="w-full" label="First name"></TextField>
<TextField className="w-full" label="Last name"></TextField>
<TextField className="w-full" label="Email"></TextField>
<TextField className="w-full" label="Phone number"></TextField>
<TextField className="w-full" label="Address"></TextField>
<TextField className="w-full" label="City"></TextField>
<TextField className="w-full" label="State"></TextField>
<TextField className="w-full" label="Zip code"></TextField>
<TextField className="w-full" label="Country"></TextField>
<Checkbox label="Agree to terms and conditions" />
</div>
</Scroller>
<footer className="flex gap-s px-m py-s">
<Button theme="primary">Next</Button>
<Button theme="tertiary">Cancel</Button>
</footer>
</VerticalLayout>
);
}
scroller-overflow-indicators.ts
import '@vaadin/button';
import '@vaadin/checkbox';
import '@vaadin/email-field';
import '@vaadin/scroller';
import '@vaadin/text-field';
import '@vaadin/vertical-layout';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('scroller-overflow-indicators')
export class Example extends LitElement {
static override styles = css`
#container {
align-items: stretch;
border: 1px solid var(--lumo-contrast-20pct);
max-width: 100%;
height: 400px;
width: 360px;
}
`;
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-vertical-layout id="container">
<h2 class="text-xl px-m py-m">Create your account</h2>
<vaadin-scroller scroll-direction="vertical" theme="overflow-indicators">
<div class="flex flex-col pb-m px-m">
<vaadin-text-field label="First name"></vaadin-text-field>
<vaadin-text-field label="Last name"></vaadin-text-field>
<vaadin-email-field label="Email"></vaadin-email-field>
<vaadin-text-field label="Phone number"></vaadin-text-field>
<vaadin-text-field label="Address"></vaadin-text-field>
<vaadin-text-field label="City"></vaadin-text-field>
<vaadin-combo-box label="State"></vaadin-combo-box>
<vaadin-text-field label="Zip code"></vaadin-text-field>
<vaadin-combo-box label="Country"></vaadin-combo-box>
<vaadin-checkbox label="Agree to terms and conditions"></vaadin-checkbox>
</div>
</vaadin-scroller>
<footer class="flex gap-s px-m py-s">
<vaadin-button theme="primary">Next</vaadin-button>
<vaadin-button theme="tertiary">Cancel</vaadin-button>
</footer>
</vaadin-vertical-layout>
`;
}
}