Vaadin is a powerful Java framework that enables developers to build modern web applications entirely in Java, without needing to write HTML, CSS, or JavaScript. It's particularly valuable for desktop-style applications that need to run in a browser.
What is Vaadin?
Vaadin provides a component-based architecture where you build web UIs using pure Java. The framework handles:
- Client-Server Communication: Automatic AJAX updates
- Component Rendering: Converts Java components to HTML/JavaScript
- State Management: Maintains UI state on the server
- Theming: Consistent styling across components
Key Features
- 100% Java: No frontend coding required
- Type-Safe: Compile-time error checking
- Rich Component Library: 50+ pre-built UI components
- Progressive Web App (PWA) Support: Offline capabilities
- Security: Built-in protection against common vulnerabilities
- Integration: Easy integration with Spring, CDI, and other Java frameworks
Getting Started
Dependencies (Maven)
<properties> <vaadin.version>24.3.0</vaadin.version> </properties> <dependencies> <!-- Vaadin Core --> <dependency> <groupId>com.vaadin</groupId> <artifactId>vaadin-core</artifactId> <version>${vaadin.version}</version> </dependency> <!-- Spring Boot Integration (optional) --> <dependency> <groupId>com.vaadin</groupId> <artifactId>vaadin-spring-boot-starter</artifactId> <version>${vaadin.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.vaadin</groupId> <artifactId>vaadin-maven-plugin</artifactId> <version>${vaadin.version}</version> <executions> <execution> <goals> <goal>prepare-frontend</goal> </goals> </execution> </executions> </plugin> </plugins> </build> Basic Vaadin Application
Example 1: Simple Vaadin Application
package com.example.vaadindemo; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; import com.vaadin.flow.server.PWA; @Route("") // Maps to root URL @PWA(name = "My Vaadin App", shortName = "MyApp") public class MainView extends VerticalLayout { public MainView() { // Create components TextField nameField = new TextField("Your name"); Button sayHelloButton = new Button("Say Hello", event -> sayHello(nameField.getValue())); // Add components to layout add(nameField, sayHelloButton); // Center the layout setAlignItems(Alignment.CENTER); setJustifyContentMode(JustifyContentMode.CENTER); setSizeFull(); } private void sayHello(String name) { if (name == null || name.trim().isEmpty()) { Notification.show("Please enter your name"); } else { Notification.show("Hello, " + name + "!"); } } } Example 2: Complete Desktop-Style Application
package com.example.vaadindemo; import com.vaadin.flow.component.*; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.tabs.Tab; import com.vaadin.flow.component.tabs.Tabs; import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.router.Route; import java.util.ArrayList; import java.util.List; @Route("desktop") public class DesktopApplication extends VerticalLayout { private final List<Customer> customers = new ArrayList<>(); private final Grid<Customer> customerGrid = new Grid<>(Customer.class); private final Binder<Customer> binder = new Binder<>(Customer.class); // Form fields private final TextField firstName = new TextField("First Name"); private final TextField lastName = new TextField("Last Name"); private final EmailField email = new EmailField("Email"); private final TextField phone = new TextField("Phone"); public DesktopApplication() { initializeUI(); setupGrid(); setupForm(); loadSampleData(); } private void initializeUI() { // Application header H1 header = new H1("Customer Management System"); header.getStyle().set("color", "var(--lumo-primary-text-color)"); // Create tabs for different sections Tab customerTab = new Tab("Customers", new Icon(VaadinIcon.USERS)); Tab reportsTab = new Tab("Reports", new Icon(VaadinIcon.CHART)); Tab settingsTab = new Tab("Settings", new Icon(VaadinIcon.COG)); Tabs tabs = new Tabs(customerTab, reportsTab, settingsTab); tabs.setWidthFull(); // Main content area VerticalLayout contentArea = new VerticalLayout(); contentArea.setSizeFull(); contentArea.setPadding(false); // Add everything to main layout add(header, tabs, contentArea); setSizeFull(); setPadding(false); // Tab selection handler tabs.addSelectedChangeListener(event -> { contentArea.removeAll(); if (event.getSelectedTab() == customerTab) { contentArea.add(createCustomerManagementLayout()); } else if (event.getSelectedTab() == reportsTab) { contentArea.add(createReportsLayout()); } else if (event.getSelectedTab() == settingsTab) { contentArea.add(createSettingsLayout()); } }); // Show customer tab by default contentArea.add(createCustomerManagementLayout()); } private Component createCustomerManagementLayout() { HorizontalLayout layout = new HorizontalLayout(); layout.setSizeFull(); layout.setSpacing(true); // Form panel VerticalLayout formLayout = new VerticalLayout(); formLayout.setWidth("400px"); formLayout.setPadding(true); formLayout.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); H1 formTitle = new H1("Customer Form"); Button saveButton = new Button("Save Customer", new Icon(VaadinIcon.CHECK), event -> saveCustomer()); Button clearButton = new Button("Clear", new Icon(VaadinIcon.TRASH), event -> clearForm()); formLayout.add(formTitle, firstName, lastName, email, phone, new HorizontalLayout(saveButton, clearButton)); // Grid panel VerticalLayout gridLayout = new VerticalLayout(); gridLayout.setSizeFull(); Button refreshButton = new Button("Refresh", new Icon(VaadinIcon.REFRESH), event -> refreshGrid()); gridLayout.add(new HorizontalLayout(refreshButton), customerGrid); layout.add(formLayout, gridLayout); return layout; } private Component createReportsLayout() { VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.add(new H1("Reports Dashboard"), new com.vaadin.flow.component.html.Paragraph("Sales reports will be displayed here"), new Button("Generate Report", new Icon(VaadinIcon.FILE_TEXT))); return layout; } private Component createSettingsLayout() { VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.add(new H1("Application Settings"), new com.vaadin.flow.component.html.Paragraph("Configure your application preferences"), new Button("Save Settings", new Icon(VaadinIcon.COG))); return layout; } private void setupGrid() { customerGrid.setColumns("firstName", "lastName", "email", "phone"); customerGrid.getColumnByKey("firstName").setHeader("First Name"); customerGrid.getColumnByKey("lastName").setHeader("Last Name"); customerGrid.getColumnByKey("email").setHeader("Email"); customerGrid.getColumnByKey("phone").setHeader("Phone"); customerGrid.asSingleSelect().addValueChangeListener(event -> { if (event.getValue() != null) { populateForm(event.getValue()); } else { clearForm(); } }); } private void setupForm() { binder.forField(firstName) .asRequired("First name is required") .bind(Customer::getFirstName, Customer::setFirstName); binder.forField(lastName) .asRequired("Last name is required") .bind(Customer::getLastName, Customer::setLastName); binder.forField(email) .asRequired("Email is required") .withValidator(this::isValidEmail, "Invalid email format") .bind(Customer::getEmail, Customer::setEmail); binder.forField(phone) .bind(Customer::getPhone, Customer::setPhone); } private boolean isValidEmail(String email) { return email != null && email.contains("@") && email.contains("."); } private void saveCustomer() { Customer customer = new Customer(); try { binder.writeBean(customer); customers.add(customer); refreshGrid(); clearForm(); Notification.show("Customer saved successfully!"); } catch (ValidationException e) { Notification.show("Please fix validation errors"); } } private void clearForm() { binder.readBean(null); customerGrid.asSingleSelect().clear(); } private void populateForm(Customer customer) { binder.readBean(customer); } private void refreshGrid() { customerGrid.setItems(customers); } private void loadSampleData() { customers.add(new Customer("John", "Doe", "[email protected]", "555-0101")); customers.add(new Customer("Jane", "Smith", "[email protected]", "555-0102")); customers.add(new Customer("Bob", "Johnson", "[email protected]", "555-0103")); refreshGrid(); } // Customer data model public static class Customer { private String firstName; private String lastName; private String email; private String phone; public Customer() {} public Customer(String firstName, String lastName, String email, String phone) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.phone = phone; } // Getters and setters public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } } } Advanced Vaadin Features
Example 3: Data Binding with Backend Service
package com.example.vaadindemo; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.provider.ListDataProvider; import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.Route; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @Route("data-binding") public class DataBindingView extends VerticalLayout { private final ProductService productService; private final Grid<Product> grid = new Grid<>(Product.class); private final TextField filterField = new TextField(); private ListDataProvider<Product> dataProvider; public DataBindingView(ProductService productService) { this.productService = productService; initializeUI(); setupGrid(); setupFilter(); } private void initializeUI() { setSizeFull(); setPadding(true); filterField.setPlaceholder("Filter by product name..."); filterField.setClearButtonVisible(true); filterField.setWidth("300px"); add(filterField, grid); grid.setSizeFull(); } private void setupGrid() { // Configure columns grid.removeAllColumns(); grid.addColumn(Product::getName).setHeader("Product Name").setSortable(true); grid.addColumn(Product::getCategory).setHeader("Category").setSortable(true); grid.addColumn(Product::getPrice).setHeader("Price") .setSortable(true) .setComparator((p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice())); grid.addColumn(Product::getStock).setHeader("In Stock") .setSortable(true); // Load data List<Product> products = productService.getAllProducts(); dataProvider = new ListDataProvider<>(products); grid.setDataProvider(dataProvider); } private void setupFilter() { filterField.setValueChangeMode(ValueChangeMode.LAZY); filterField.addValueChangeListener(e -> applyFilter(e.getValue())); } private void applyFilter(String filterText) { dataProvider.setFilter(product -> product.getName().toLowerCase().contains(filterText.toLowerCase()) || product.getCategory().toLowerCase().contains(filterText.toLowerCase()) ); } } @Service class ProductService { private final List<Product> products = new ArrayList<>(); public ProductService() { // Sample data products.add(new Product("Laptop", "Electronics", 999.99, 15)); products.add(new Product("Smartphone", "Electronics", 699.99, 25)); products.add(new Product("Desk Chair", "Furniture", 199.99, 10)); products.add(new Product("Coffee Mug", "Kitchen", 12.99, 100)); products.add(new Product("Notebook", "Office Supplies", 4.99, 50)); } public List<Product> getAllProducts() { return new ArrayList<>(products); } public List<Product> searchProducts(String query) { return products.stream() .filter(p -> p.getName().toLowerCase().contains(query.toLowerCase()) || p.getCategory().toLowerCase().contains(query.toLowerCase())) .collect(Collectors.toList()); } } class Product { private String name; private String category; private double price; private int stock; public Product(String name, String category, double price, int stock) { this.name = name; this.category = category; this.price = price; this.stock = stock; } // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public int getStock() { return stock; } public void setStock(int stock) { this.stock = stock; } } Example 4: Dialog and Notification System
package com.example.vaadindemo; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.notification.Notification; 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.TextArea; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.Route; @Route("dialogs") public class DialogView extends VerticalLayout { public DialogView() { setAlignItems(Alignment.CENTER); setJustifyContentMode(JustifyContentMode.CENTER); setSizeFull(); setSpacing(true); Button confirmationDialogButton = new Button("Show Confirmation Dialog", event -> showConfirmationDialog()); Button formDialogButton = new Button("Show Form Dialog", event -> showFormDialog()); Button customDialogButton = new Button("Show Custom Dialog", event -> showCustomDialog()); add(new H2("Dialog Examples"), confirmationDialogButton, formDialogButton, customDialogButton); } private void showConfirmationDialog() { Dialog dialog = new Dialog(); VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.setAlignItems(Alignment.CENTER); com.vaadin.flow.component.html.Paragraph message = new com.vaadin.flow.component.html.Paragraph( "Are you sure you want to delete this item?"); Button confirmButton = new Button("Delete", event -> { Notification.show("Item deleted!"); dialog.close(); }); confirmButton.getStyle().set("color", "red"); Button cancelButton = new Button("Cancel", event -> dialog.close()); layout.add(message, new HorizontalLayout(confirmButton, cancelButton)); dialog.add(layout); dialog.open(); } private void showFormDialog() { Dialog dialog = new Dialog(); dialog.setWidth("400px"); FormLayout formLayout = new FormLayout(); TextField nameField = new TextField("Name"); TextField emailField = new TextField("Email"); TextArea messageArea = new TextArea("Message"); messageArea.setHeight("100px"); formLayout.add(nameField, emailField, messageArea); formLayout.setResponsiveSteps( new FormLayout.ResponsiveStep("0", 1) ); Button submitButton = new Button("Submit", event -> { if (!nameField.getValue().isEmpty() && !emailField.getValue().isEmpty()) { Notification.show("Form submitted successfully!"); dialog.close(); } else { Notification.show("Please fill all required fields"); } }); Button cancelButton = new Button("Cancel", event -> dialog.close()); HorizontalLayout buttonLayout = new HorizontalLayout(submitButton, cancelButton); buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END); VerticalLayout mainLayout = new VerticalLayout( new H2("Contact Form"), formLayout, buttonLayout ); mainLayout.setPadding(true); dialog.add(mainLayout); dialog.open(); } private void showCustomDialog() { Dialog dialog = new Dialog(); dialog.setCloseOnEsc(false); dialog.setCloseOnOutsideClick(false); // Custom styling dialog.getStyle() .set("border-radius", "10px") .set("box-shadow", "0 4px 20px rgba(0,0,0,0.2)"); VerticalLayout content = new VerticalLayout(); content.setPadding(true); content.setAlignItems(Alignment.CENTER); content.setSpacing(true); com.vaadin.flow.component.icon.Icon icon = new com.vaadin.flow.component.icon.Icon("lumo", "checkmark"); icon.setColor("var(--lumo-success-color)"); icon.setSize("48px"); H2 title = new H2("Operation Successful"); com.vaadin.flow.component.html.Paragraph message = new com.vaadin.flow.component.html.Paragraph( "Your changes have been saved successfully."); Button okButton = new Button("OK", event -> dialog.close()); okButton.getStyle().set("margin-top", "20px"); content.add(icon, title, message, okButton); dialog.add(content); dialog.open(); } } Spring Boot Integration
Example 5: Spring Boot Vaadin Application
package com.example.vaadindemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @SpringBootApplication public class VaadinDemoApplication { public static void main(String[] args) { SpringApplication.run(VaadinDemoApplication.class, args); } } // Service layer @Service class TaskService { private final List<Task> tasks = new ArrayList<>(); private long nextId = 1; public List<Task> getAllTasks() { return new ArrayList<>(tasks); } public Task saveTask(Task task) { if (task.getId() == null) { task.setId(nextId++); tasks.add(task); } else { tasks.removeIf(t -> t.getId().equals(task.getId())); tasks.add(task); } return task; } public void deleteTask(Long id) { tasks.removeIf(t -> t.getId().equals(id)); } public void toggleTask(Long id) { tasks.stream() .filter(t -> t.getId().equals(id)) .findFirst() .ifPresent(task -> task.setCompleted(!task.isCompleted())); } } // Entity class class Task { private Long id; private String title; private String description; private boolean completed; public Task() {} public Task(String title, String description) { this.title = title; this.description = description; this.completed = false; } // Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public boolean isCompleted() { return completed; } public void setCompleted(boolean completed) { this.completed = completed; } } Best Practices
1. Use Service Classes
@Service public class CustomerService { @PersistenceContext private EntityManager entityManager; public List<Customer> findAll() { return entityManager.createQuery("SELECT c FROM Customer c", Customer.class) .getResultList(); } } 2. Implement Responsive Design
public class ResponsiveView extends VerticalLayout { public ResponsiveView() { setPadding(true); setSpacing(true); // Responsive configuration setResponsiveSteps( new ResponsiveStep("0", 1), // 1 column on mobile new ResponsiveStep("500px", 2), // 2 columns on tablet new ResponsiveStep("800px", 3) // 3 columns on desktop ); } } 3. Use CSS for Custom Styling
public class StyledView extends VerticalLayout { public StyledView() { addClassName("styled-view"); Button primaryButton = new Button("Primary Action"); primaryButton.addClassName("primary-action"); add(primaryButton); } } .styled-view { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .primary-action { background-color: var(--lumo-primary-color); color: white; border-radius: 25px; padding: 10px 20px; } Deployment Options
1. Traditional WAR Deployment
<!-- Maven WAR packaging --> <packaging>war</packaging> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>
2. Docker Deployment
FROM openjdk:11-jre-slim WORKDIR /app COPY target/my-vaadin-app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
3. Cloud Deployment
- AWS Elastic Beanstalk
- Google App Engine
- Azure App Service
- Heroku
Conclusion
Vaadin provides a powerful solution for building desktop-like applications that run in the browser:
Key Advantages:
- Pure Java Development: No frontend expertise required
- Type Safety: Compile-time error checking
- Rich Component Set: Pre-built, customizable components
- Security: Built-in CSRF and XSS protection
- Integration: Seamless Spring Boot integration
- Progressive Web Apps: Offline capabilities
Ideal Use Cases:
- Enterprise Applications: CRM, ERP, internal tools
- Data-Intensive Applications: Dashboards, reporting tools
- Business Applications: Forms, workflows, data entry
- Admin Panels: Content management, system configuration
Performance Considerations:
- Use
@Pushfor real-time updates - Implement lazy loading for large datasets
- Optimize database queries
- Use CDN for static resources
Vaadin enables Java developers to create modern, responsive web applications while leveraging their existing Java skills and ecosystem.
Next Steps: Explore Vaadin Fusion for hybrid Java/TypeScript development or Vaadin Flow for pure Java solutions. Consider Vaadin's commercial components for advanced charts, spreadsheets, and reporting capabilities.