Vaadin Tutorial: From Spring Boot to Beautiful UI Fast

 

Transcript:

In this video I will show you how to build a beautiful UI for your Springwood app in pure Java using Vaadin. Vaadin is a Java web framework that lets you build user interfaces completely in Java. It has a huge set of components. Greets, forms, dialogues, layouts, menus, various ready interaction components such as avatars, popovers, confirmation dialogue, who you consent, and so much more. The components are ready out of the box, but they are also easily customizable. Plus, you can design UIs in Figma using Vaadin Figma libraries. The security support is also built in. So out of the box you get server site UI state CSRF protection JSR-303 validation and great integration with spring security.

So I will show you how to integrate body into your application. Build a login page main layout with a grid a dialogue view with details and an edit form which is secured by roles. I will use my new hero watch project for that. It couples Spring Boot, Spring Security and MongoDB. The MongoDB instance is brought up via Docker Compose. The whole code is available on GitHub. The link is in the description, so you can pull the project right now and follow along.

Okay, let's start. If you add the vin dependencies manually, you will need the vin core dependency, springwood starter vin dependency and also profile for building the UI for production. We will have a main layout and login page. First, let's create a login view class. It will extend main which is a Vaadin component that renders as an HTML main element. It also implements before enter observer. So the view can intercept navigation before the routter enters it. Let's start with some useful class level annotations. The root annotation registers the view at the login route. The auto layout set to false tells for not to wrap this view in your app's normal layout. The page title annotation says the browser tab title to login when this view is active. The anonymous allowed annotation allows unauthenticated users to access this route. Okay, let's create a login path constant used for the forums action. Let's add the authentication context from Vaadin Spring Security. It gives you helper methods like is authenticated. Let's also add the login form which is the Vaadin ready made form component. The out-of-the-box login form consists of two input fields, username and password, and two buttons, login and forgot password. And all of that is available at your fingertips with only creating the login form object. Let's make use of spring dependency injection in the login view constructor. Here we also instantiate the login form. Set the form's post target with a set action method. On submit, the browser posts credentials to login. Let's also hide the built-in forgot password button for simplicity sake. The set size full method makes the root main fill the available space. Content div element creates a wrapper div and places the login form inside it. It is useful for styling and centering the form. The add method adds the div with the login form inside to the main component so it appears on the stage. Also, let's create before enter method that uh accepts the before enter event. It fires right before navigation enters this view. If the user is already logged in, we will not show the login page and forward them to the root root. Then we check the query string for error. Spring security appends this when login attempt fails. The login set error method uh flips the component into its error state and shows the invalid username or password message. And here it is our login page with a form. We have a username field, a password field and also you can make the password visible. This button is available out of the box with Vaadin.

Okay, let's now create the main layout for our application. Create the main layout class. It should extend the app layout that gives you a responsive shell with a top navigation bar and left side drawer out of the box. Let's add some annotations. The layout annotation marks this class as a rooting layout. So views can be shown inside it. It means that other views annotated with rooted will use this one as their parent layout. In the class constructor, the shell's UI, which is a header and a drawer, will be built when the layout is created. Let's add the drawer toggle. It is a button that opens and closes the left drawer with a vertical menu. H1 creates a big header text component that says new watch. Let's add some inline styles to the logo to make it nicer. Now the add to navbar method adds the button and the logo into the top navigation bar area of app layout. The root link objects create clickable navigation links labeled civilians and implant locks. They route to the view classes civilian view and implant lock view. We won't look into the implant lock view in this demo. This is only to show you that you can add as many links to other views as you want and they will always be available in the side drawer. Now it's time to create a civilian view that displays a grid with civilians from the database. In this section, you will learn how to create a grid to display the data, how to add filters to filter items in the grid, how to create a dialogue window that displays detailed information about each civilian in the grid, and how to add an edit form to this window that can be visible to admins only.

Okay, let's start with the grid. We will create a foundation now and then add some UI elements as we move along. Let's create a civilian view class that extends a vertical layout. It is a ving view that lays children top to bottom. Add the root annotation that registers this view at the root path and says uh render me inside main layout. So this way we get the nav bar and drawer frame. The permit all annotation means that anyone who has logged in can open this view. Inject the civilian service and add the grid for civilian class. Pulse means not to autogenerate columns because we will define them manually. Add also the callback data provider. We will build it later and use it to populate the grid with data. In the constructor, we build the data provider, set up the grid and add the Lumo utility classes which is a Vance utility for CSS. So we adjust the sizing, we flex the columns, uh we adjust the spacing and say that uh children can wrap elements. Put the grid into view with the add method. Now do the actual grid configuration. First let's bind lazy data provider with a set items method. It accepts callback data provider. Vin will ask for slices of data as we scroll over page. We will build a provider in a separate method in a couple of minutes. Then add two columns national ID and legal name with the add column method. The values will be read via methods references. With the add theme variance method, we enable theme variant wrap cell content. This way long cell text will wrap instead of overflowing. Set size full makes the view fill the available space. Okay. Now we need to build the callback provider. For simplicity sake, uh we load all civilians from the database and uh enable lazy loading at the view level. This way uh we are loading the items that are currently displayed by setting the offset and limit. This uh prevents the front end from loading all available data at once and hanging as a result. But of course in real life it is recommended to use a lazy loading at the repository level for big data sets. The Vadian call back provider uses two functional interfaces, fetch call back and count call back. They allow us to retrieve a stream of data using a query and to count the total number of items. For the fetch call back, we call the service query method that returns the collection of civilians. Then we stream it, skip the offset, take the limit and return a stream of civilians. For the count call back, we return a total number of rows so the grit can pagionate. After you log into the app, you will see a grid with all the civilians from the database and the left side drawer.

Okay, so right now we display all civilians. But in most cases, we need some sort of filtering. Well, let's add the filters. First, let's add some UI elements. We will have three integer fields for filtering by lot numbers and uh also a text field to search for civilian by national ID. Let's also add a button that will clear all filter fields when clicked. We will configure the filters in a separate method just like uh the grid. So in the constructor, we only call this method. In the constructor, we also create a new horizontal layout and add the filters and the button there so that they are displayed in the horizontal row. The set default vertical component alignment method aligns the child components so their ends line up. This way we align them with the grid header. We also add the filters to the add method to display them over the grid. Let's move on to configuring the filters. Has value creates one generic listener that calls the refresh method whenever any filter value changes. The while generics let one variable hold listeners for different field types. And now let's subscribe that listener to all four inputs. As a result, changing any filter triggers the refresh method, which we will define as well. Let's also add a click listener to the clear button that will trigger clearing all filter fields. The refresh method is super simple. We just call the inbuilt refresh all method of the data provider to update the provider when filters are applied or cleared. We remember that the build provider method uses the service query method to retrieve the data. So, we need to improve it. a bit to add filtered search so that it decides which backend method it should call based on the current filter values. In our logic only one filter can be applied at a time. So we check whether a filter field has value fetch civilians and return. If you want to implement combined filtering, we need to change a service query to compute a single predicate and query with it. Now our main view has filters. You can search for civilian by national ID or select all civilians with a lot number greater than, lesser than or equal to the specified number.

Our grid displays only the national ID and the legal name of a civilian. Let's add a dialogue window that will pop up when we click on the civilian to display more detailed information about this person. For that we need to add a listener to the grid uh via a single select method. It switches the grid selection model to a single select and listens for selection changes. And here we will call the open civilian dialogue method when some civilian in a grid is selected. In this method, we create the variance dialogue. We set the dialog's header title to the civilian's legal name and uh we also give the dialogue a fixed width. So a nice spacious card is displayed. Now let's create a tab labeled details. We will add one more tab later. Then we wrap the tab or as many tabs as we create in a tabs component which provides a clickable tab strip using the component class. We need to build the actual content for the details tab. For that we use a separate build details panel method which we will define later. In a map that accepts tab in the component we map each tab to its corresponding content component. Even with one tab, this sets up the pattern for multiple tabs so you can conveniently add them later. The div pages creates container that will hold the tab pages. We also set the CSS position hide all pages initially uh with set visible false and then we show the details page by default with set visible true set uh to details panel. Add selected change listener reacts when the user switches tabs. So we hide all pages initially and then we show the page associated with the currently selected tab. The add method adds the top bar and the pages container to the dialogue body. The get footer add new button method adds a close button to the dialogue footer. Clicking it closes the dialogue. The open method opens the dialogue.

Finally, let's populate the details panel. In the build details panel, create a form layout that provides a form style layout for uh label and value pairs related to the civilian. Then we just add all required readonly items to this form. Setting a label and retrieving a relevant value from the civilian object. The dialog window should also display information about implants of a civilian. So let's create a grid for implants and populate each column with values from the implant object. Each column auto sizes and flex grows to take space. After that, we fix the grid height to 200 pixels so we get a small scrollable area instead of a giant table. We also enable text wrapping in cells to avoid the overflow cut offs. Let's create a vertical layout to stack the form and the grid vertically. And then we just remove extra padding spacing and make the content fill available space within the dialog content area. Now, when you click on the civilian in the grid, a window pops up that displays the detailed information about the civilian and their implants. Finally, let's add another tab to our dialogue window that enables the users with the role admin edit the civilians. First, let's create a has a role method that accepts the required role and uses security context holder to verify whether the user has this role. Now add a new tab called edit tab to the open civilian dialogue method. Use the has ro method to check whether the user is admin. If yes, add the new tab to the tabs. Create the edit panel component. Add it to the map together with the edit tab and to the diff as well. We will build this component in a separate method and build edit form. Here we create a text field and two check boxes for civilian data we can edit. Then we create a val binder for the civilian bin type. The binder handles syncing UI fields with bin properties and validation. Then using the bind method of the binder, we surprise bind the text fields and checkboxes to the relevant civilian values. The read bin method reads values from the civilian bin into the bound fields and sets initial UI state. Let's also add the save button with a click listener. Here we call the write bin if valid method of the binder. It tries to write the current field values into the civilian bean but uh only if all bound fields are valid. This method returns true on success. If true we call the civilian service to persist the updated civilian. Then we call our refresh method to update the greet and close the dial window. Let's also add the cancel button to close the dialogue window without persisting any changes. And finally, we return a vertical stack containing the input fields and the two buttons. The caller will add this to the dialogue. If you log in as a user to the application, you won't see this edit tab. But if you log in as the admin, you will see the edit tab in the tile window. You can change the values and the updated data will be immediately displayed in the grid. The UI already looks quite nice. If you don't need custom design, you can use it as is. You can also use the light or dark theme provided by Vin out of the box. But in this video, I will also show you quickly how to add the custom styles. Go to the main class of our application and add the theme annotation here. point to the directory with our custom theme. In our case, it's called cyber. Also, you can enable the dark theme by default here. Vin by default looks for styles under main front end themes directory. So there you create the cyber directory and vin picks up all the CSS and JSON files from this directory. This demo uses two CSS files, one for the login page and one for the main view. I won't describe the uh CSS styling in detail here. You can find more information in the official documentation. So when you add the custom themes, you can run the application. And voila, we now have a beautiful cyberpunk themed UI. We have only touched the tip of the iceberg of what you can do with Vin. I encourage you to try out this framework and drop me a comment if you want more ving centered videos. And of course, like, subscribe, and until next time.

Summary

This video shows how to build a complete UI for a Spring Boot application using Vaadin, a Java web framework that provides rich, customizable UI components entirely in Java. The guide walks through creating a login page, a main layout with navigation, and a data grid that supports filtering, lazy loading, and detailed dialogs. It also demonstrates how to build a multi-tab dialog with read-only details and an admin-only edit form using Vaadin Binder for validation and data binding. The video explains how to integrate Spring Security, handle database data from MongoDB, and build responsive layouts. Finally, it shows how to apply a custom cyberpunk theme and encourages exploring Vaadin’s broader capabilities.

About Catherine

Java developer passionate about Spring Boot. Writer. Developer Advocate at BellSoft

Social Media

Videos
card image
Jan 20, 2026
JDBC vs ORM vs jOOQ: Choose the Right Java Database Tool

Still unsure what is the difference between JPA, Hibernate, JDBC, or jOOQ and when to use which? This video clarifies the entire Java database access stack with real, production-oriented examples. We start at the foundation, which is JDBC, a low-level API every other tool eventually relies on for database communication. Then, we go through the ORM concept, JPA as a specification of ORM, Hibernate as the implementation and extension of JPA, and Blaze Persistence as a powerful upgrade to JPA Criteria API. From there, we take a different path with jOOQ: a database-first, SQL-centric approach that provides type-safe queries and catches many SQL errors at compile time instead of runtime. You’ll see when raw JDBC makes sense for small, focused services, when Hibernate fits CRUD-heavy domains, and when jOOQ excels at complex reporting and analytics. We discuss real performance pitfalls such as N+1 queries and lazy loading, and show practical combination strategies like “JPA for CRUD, jOOQ for reports.” The goal is to equip you with clarity so that you can make informed architectural decisions based on domain complexity, query patterns, and long-term maintainability.

Videos
card image
Jan 13, 2026
Hibernate: Ditch or Double Down? When ORM Isn't Enough

Every Java team debates Hibernate at some point: productivity champion or performance liability? Both are right. This video shows you when to rely on Hibernate's ORM magic and when to drop down to SQL. We walk through production scenarios: domain models with many-to-many relations where Hibernate excels, analytical reports with window functions where JDBC dominates, and hybrid architectures that use both in the same Spring Boot codebase. You'll see real code examples: the N+1 query trap that kills performance, complex window functions and anti-joins that Hibernate can't handle, equals/hashCode pitfalls with lazy loading, and practical two-level caching strategies. We also explore how Hibernate works under the hood—translating HQL to database-specific SQL dialects, managing sessions and transactions through JDBC, implementing JPA specifications. The strategic insight: modern applications need both ORM convenience for transactional business logic and SQL precision for data-intensive analytics. Use Hibernate for CRUD and relationship management. Use SQL where ORM abstractions leak or performance demands direct control.

Further watching

Videos
card image
Feb 6, 2026
Backend Developer Roadmap 2026: What You Need to Know

Backend complexity keeps growing, and frameworks can't keep up. In 2026, knowing React or Django isn't enough. You need fundamentals that hold up when systems break, traffic spikes, or your architecture gets rewritten for the third time.I've been building production systems for 15 years. This roadmap covers three areas that separate people who know frameworks from people who can actually architect backend systems: data, architecture, and infrastructure. This is about how to think, not what tools to install.

Videos
card image
Jan 29, 2026
JDBC Connection Pools in Microservices. Why They Break Down (and What to Do Instead)

In this livestream, Catherine is joined by Rogerio Robetti, the founder of Open J Proxy, to discuss why traditional JDBC connection pools break down when teams migrate to microservices, and what is a more efficient and reliable approach to organizing database access with microservice architecture.

Videos
card image
Jan 27, 2026
Sizing JDBC Connection Pools for Real Production Load

Many production outages start with connection pool exhaustion. Your app waits seconds for connections while queries take milliseconds; yet, most teams run default settings that collapse under load. This video shows how to configure connection pools that survive real production traffic: sizing based on database limits and thread counts, setting timeouts that prevent cascading failures, and implementing an open source database proxy Open J Proxy for centralized connection management with virtual connection handles, client-side load balancing, and slow query segregation. For senior Java developers, DevOps engineers, and architects who need database performance that holds under pressure.