Styling
The chat application developed in this tutorial is now fully functional. However, it still looks a bit rough. In this part of the tutorial, you’ll tweak the look and feel of the application, add some styling.
Styling in Flow
You may not realize it, but so far in this tutorial, you’ve been using a theme called Lumo. This is the default theme in Vaadin applications. It provides several utility CSS classes that can be added to your components and HTML elements to style them, easily. In Java, these utility classes are available in the LumoUtility
class (from the com.vaadin.flow.theme.lumo
package). Each utility class applies a particular style to the element, such as background color, borders, fonts, sizing, and spacing.
However, some cases can’t be handled with utility classes alone. One such case would be when you need to use a different selector than just a classname. Another case would be a complex layout where semantic CSS makes more sense than using several utility classes.
In both of these cases, you have to create a new theme, either to enable the Lumo utility classes or to hold your custom CSS. Applications generated with start.vaadin.com already have this configured. In this tutorial, though, you’ll learn how to do it yourself.
You can find more information about styling in the Styling page of the Vaadin documentation.
Create a Theme
To start, make a new directory: src/main/frontend/themes/chat-theme
. Inside this directory, create an empty file styles.css
. You’ll add custom styles and @import
lines to this file later in this tutorial. Vaadin will automatically import the Lumo theme and apply your styles on top of the Lumo styles, allowing you to override them.
Next, you should create a theme configuration file called, theme.json
in the same directory as styles.css
. You’ll use this file to configure various theme-related features. For now, use it to enable the Lumo utility classes. Do this by copy-pasting the following code into the file:
{
"lumoImports" : [ "typography", "color", "sizing", "spacing", "utility" ]
}
If you left out this file, the typography
, color
, sizing
, and spacing
modules would have been loaded by default.
Finally, you need to configure your application to use the new theme. You do this by adding the @Theme
annotation to your application shell class or in this case, the Application
class. In your IDE, locate the com.example.application
package and open the Application
class. Then add the @Theme
annotation (from the com.vaadin.flow.theme
package) like this:
@SpringBootApplication
@Push
// tag::snippet[]
@Theme("chat-theme") // (1)
// end::snippet[]
public class Application implements AppShellConfigurator {
...
}
-
The name of your new theme is
chat-theme
and it matches the name of the directory insrc/main/frontend/themes
.
The next time your application starts, Vaadin will automatically load your new theme.
Highlight User’s Messages
With your new theme in place, it is time to go back to the channel view. In your IDE, locate the com.example.application.views.channel
package and open the ChannelView
class.
So far, all of the messages in ChannelView
have been displayed in the same way regardless of who wrote them, as seen in this screenshot:
To change this so that all messages sent by the current user look different, you first need to get a hold of the current user’s username. You can retrieve this from the AuthenticationContext
class (from the com.vaadin.flow.spring.security
package).
Inside the ChannelView
, declare a new String
field that will contain the current user’s username. Next, inject an instance of AuthenticationContext
into the constructor and use it to retrieve the principal name, which in this case is the same as the current user’s username. Finally, store this in the field. In code, it looks like this:
// tag::snippet[]
private final String currentUserName; // (1)
// end::snippet[]
...
public ChannelView(ChatService chatService,
// tag::snippet[]
AuthenticationContext authenticationContext) { // (2)
this.currentUserName = authenticationContext.getPrincipalName().orElseThrow(); // (3)
// end::snippet[]
...
}
-
This is the field that will contain the current user’s username. It is marked as
final
since it will never change onceChannelView
has been created. -
This injects the
AuthenticationContext
as a constructor parameter. -
This retrieves the principal name and stores it in the field. If there is no principal name, it is okay to throw an exception as it should not be possible to reach the channel view without logging in in the first place.
Now when you know the identity of the current user, you can put it to good use. To distinguish messages from the current user, use a Lumo utility CSS class to add a darker background to all messages sent by that person. Also, add a small margin and a round border around them.
Still in the ChannelView
class, look up the createMessageListItem()
method and change it to look like this:
private MessageListItem createMessageListItem(Message message) {
var item = new MessageListItem(
message.message(),
message.timestamp(),
message.author()
);
// tag::snippet[]
item.addClassNames(LumoUtility.Margin.SMALL, LumoUtility.BorderRadius.MEDIUM); // (1)
if (message.author().equals(currentUserName)) {
item.addClassNames(LumoUtility.Background.CONTRAST_5); // (2)
}
// end::snippet[]
return item;
}
-
Add a small margin and a medium border radius to the
MessageListItem
(just to make it look nicer). -
Add a darker background to all message items that have been written by the current user.
If you restart the application, open a channel and send some messages, the view will look like this:
The message list is now nicer looking, but you can make it even better!
Color Avatars
Using avatars in the message list can make it easier to distinguish messages from different authors. Since the avatars are all gray and only contain the initial letter of the username, they haven’t been very useful. The best solution would be to actually show pictures of the users. However, since that information isn’t available, give the avatars different colors.
MessageListItem
has a property called, userColorIndex
. It can take a value between 0 and 6. Each value corresponds to a different color of the user’s avatar. Set a color index based on the hashCode()
of the message author. You can use a modulo operation to turn the hash into an integer between 0 and 6:
private MessageListItem createMessageListItem(Message message) {
var item = new MessageListItem(
message.message(),
message.timestamp(),
message.author()
);
// tag::snippet[]
item.setUserColorIndex(Math.abs(message.author().hashCode() % 7)); // (1)
// end::snippet[]
item.addClassNames(LumoUtility.Margin.SMALL, LumoUtility.BorderRadius.MEDIUM);
if (message.author().equals(currentUserName)) {
item.addClassNames(LumoUtility.Background.CONTRAST_5);
}
return item;
}
-
The hash code can be negative, so you have to use
Math.abs()
to get the absolute value after applying the modulo operation.
If you restart the application, open a channel and send some messages, the view will look like this:
Notice how each user’s avatar, their initials are in a different color circle to distinguish them from each other. This will be particularly useful for clarity when there are a few users chatting and they’ve posted several messages each. The colors also brighten the overall impression of the view.
Tweak Message List
If you look at the channel view, it has some extra whitespace around both the message list and the message input. This looks a bit strange and should be changed.
By default, the MessageInput
component has a medium padding. To remove this default, you should add the following lines to the styles.css
file (in the src/main/frontend/themes/chat-theme
directory):
vaadin-message-input {
padding: 0; /* <1> */
overflow: visible; /* <2> */
}
-
This removes the padding from the
MessageInput
component. -
When focused, the text field inside the
MessageInput
component has a blue border called a focus ring. When the padding is removed, the focus ring does not entirely fit inside the component anymore and is clipped. By changing theoverflow
property tovisible
, the focus ring becomes fully visible again.
Next, you should add a border to the message list. The easiest way to do this is to use a Lumo utility class. Open the ChannelView
class, lookup the line in the constructor that creates a new instance of MessageList
and add the following line:
...
messageList = new MessageList();
// tag::snippet[]
messageList.addClassNames(LumoUtility.Border.ALL); // (1)
// end::snippet[]
messageList.setSizeFull();
add(messageList);
...
-
This adds a thin border to all sides of the message list component.
If you restart the application, open a channel and send some messages, the view will look like this:
The channel view is starting to look good. Now you’re going to turn your attention to the lobby view.
Expand Channel Information
At this point, the lobby shows only a list of channels. However, if you look at the Channel
objects returned by ChatService
, you can see that the last message posted to the channel is also provided, including its author, timestamp and the message text. To show all channel information in a tidy way, construct the following custom layout:
The graphic outlines visually how the layout should look: The channel
div contains the channel’s avatar and an inner div, called content
. The content
div contains another div, called name
, and a truncated version of the last message posted to the channel, if any. And the name
div contains a link to the channel and the timestamp of the last message posted to the channel, if any.
Making a layout like this in HTML is quite easy, but Flow also makes it possible to do it completely in Java. You could even style it using Lumo utility classes, but that would clutter the code. Therefore, in this tutorial, you’ll build the layout in Java, but do the styling in CSS.
In your IDE, open the LobbyView
class and locate the createChannelComponent()
method. Then change it like this:
private Component createChannelComponent(Channel channel) {
// tag::snippet[]
var channelComponent = new Div(); // (1)
channelComponent.addClassNames("channel");
var avatar = new Avatar(channel.name());
avatar.setColorIndex(Math.abs(channel.id().hashCode() % 7)); // (2)
channelComponent.add(avatar);
var contentDiv = new Div();
contentDiv.addClassNames("content");
channelComponent.add(contentDiv); // (3)
var channelName = new Div();
channelName.addClassNames("name");
contentDiv.add(channelName);
var channelLink = new RouterLink(channel.name(), ChannelView.class, channel.id()); // (4)
channelName.add(channelLink);
if (channel.lastMessage() != null) {
var lastMessageTimestamp = new Span(formatInstant(channel.lastMessage().timestamp(), getLocale())); // (5)
lastMessageTimestamp.addClassNames("last-message-timestamp");
channelName.add(lastMessageTimestamp);
}
var lastMessage = new Span();
lastMessage.addClassNames("last-message");
contentDiv.add(lastMessage);
if (channel.lastMessage() != null) {
var author = new Span(channel.lastMessage().author());
author.addClassNames("author");
lastMessage.add(author, new Text(": " + truncateMessage(channel.lastMessage().message()))); // (6)
} else {
lastMessage.setText("No messages yet");
}
return channelComponent;
// end::snippet[]
}
-
This is how you create new
<div>
elements in Java. -
This is the same trick you used in the channel view to color the avatars.
-
This is how you add a
<div>
into another<div>
. It is important to have good names for your variables. Otherwise, you can easily get confused and accidentally add components to the wrong element. -
Originally, the method only returned this
RouterLink
. Now it is embedded inside a more complex layout. -
The
formatInstant()
method doesn’t exist yet. You’ll add it shortly. -
The
truncateMessage()
method also doesn’t exist yet. You’ll add it shortly.
If you try to compile the code now, it won’t work. This is because the formatInstant()
and truncateMessage()
methods are missing. You’re going to add them next.
Add the formatInstant()
method to the LobbyView
class first:
private String formatInstant(Instant instant, Locale locale) {
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(locale)
.format(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
}
Since this is the only place in the application that needs to format Instant
objects, you can keep the method directly in the LobbyView
class. In applications that require instant formatting in multiple places, you’d put this method inside a class of its own.
Next, add the truncateMessage()
method directly after the formatInstant()
method:
private String truncateMessage(String msg) {
return msg.length() > 50 ? msg.substring(0, 50) + "..." : msg;
}
Again, in applications that require string truncation in multiple places, you’d put this method inside a class of its own.
With the Java code in place, it’s time for the CSS styles. Create a new file called channel-list.css
in the src/main/frontend/themes/chat-theme
directory. Copy the following styles into it:
.channel-list .channel {
display: flex;
gap: var(--lumo-space-m);
padding: var(--lumo-space-m);
border-radius: var(--lumo-border-radius-m);
}
.channel-list .channel .content {
display: flex;
flex-direction: column;
flex: auto;
line-height: var(--lumo-line-height-xs);
gap: var(--lumo-space-xs);
}
.channel-list .channel .name {
display: flex;
align-items: baseline;
justify-content: start;
gap: var(--lumo-space-s);
}
.channel-list .channel .name a {
font-size: var(--lumo-font-size-m);
font-weight: bold;
color: var(--lumo-body-text-color);
}
.channel-list .channel .name .last-message-timestamp {
font-size: var(--lumo-font-size-s);
color: var(--lumo-secondary-text-color);
}
.channel-list .channel .last-message {
font-size: var(--lumo-font-size-s);
color: var(--lumo-secondary-text-color);
}
.channel-list .channel .last-message .author {
font-weight: bold;
}
.channel-list .channel:hover {
background-color: var(--lumo-contrast-5pct);
}
Next, import the the CSS file into the theme by adding this line to the very top of styles.css
:
@import "channel-list.css";
Tweak Channel List
Just as you tweaked the message list, make a couple of small additions to improve the channel list to be consistent with the rest of the application. Right now, the lobby view looks like this:
It is definitely usable, but that scrollbar on the right-hand side looks like it is in the wrong place. One way of fixing this is to add a border, and some padding between the border and the channels. The easiest way to do this is by using Lumo utility classes.
In your IDE, open the LobbyView
class and locate the line that adds the CSS class name channel-list
to the channels
object. Then add these two additional CSS classes:
...
channels = new VirtualList<>();
channels.addClassNames(
// tag::snippet[]
LumoUtility.Border.ALL, // (1)
LumoUtility.Padding.SMALL, // (2)
// end::snippet[]
"channel-list"
);
...
-
This adds a thin border to all sides of the channels list.
-
This adds a small padding between the channels and the border.
The lobby view should look like this:
The lobby view is looking much more professional, like a real-world application.