Submitting Images in Forms

Allowing images to be uploaded in forms.

A common use case is a form field that allows an image to be uploaded. For instance, an application may let users customize their avatars. This tutorial shows how this can be implemented for forms, using the vaadin-upload component for local file selection (by file browser or dragging), but postponing the actual server upload until the form is submitted.

The tutorial application allows a contact card to be edited. It assumes a server-side bean where the image is stored as a Base64-encoded string in the field avatarBase64:

package com.vaadin.demo.fusion.forms;

 * Contact card with base64-encoded image
public class Contact {
  // other contact fields: name, address, phone, ...

  private String avatarBase64;

  public String getAvatarBase64() {
      return avatarBase64;

  public void setAvatarBase64(String avatarBase64) {
      this.avatarBase64 = avatarBase64;

It also assumes that the server exposes an endpoint for saving updated Contact instances:

package com.vaadin.demo.fusion.forms;

import dev.hilla.Endpoint;

public class ContactEndpoint {
  // other endpoint methods: read, delete, ...

  public void saveContact(Contact contact) {
    // persistently store the contact

The following form binds the avatarBase64 field of the instance to a vaadin-upload component:

import ContactModel from 'Frontend/generated/com/vaadin/demo/fusion/forms/ContactModel';
import { ContactEndpoint } from 'Frontend/generated/endpoints';
import { useForm } from '@hilla/react-form';
import { useEffect } from "react";
import { Upload } from "@hilla/react-components/Upload.js";
import { UploadBeforeEvent } from "@vaadin/upload";
import { readAsDataURL } from "promise-file-reader";
import { Button } from "@hilla/react-components/Button.js";

export default function ContactForm() {

    const form = useForm(ContactModel, {
      onSubmit: async (contact) => {
        await ContactEndpoint.saveContact(contact);

    useEffect(() => {
    }, [])

    return (
        <img src={form.value?.avatarBase64} alt="contact's avatar"></img>
        <Upload capture="camera" accept="image/*" max-files="1"
          onUploadBefore={async (e: UploadBeforeEvent) => {
            const file = e.detail.file;
            if (form.value) {
              form.value.avatarBase64 = await readAsDataURL(file);
        <Button onClick={form.submit}>Save</Button>

In the above code, the custom onUploadBefore listener prevents Upload from uploading the received file to the server. Instead, it reads the file into a Base64-encoded string and updates the form field avatarBase64 via the Binder. The small promise-file-reader library wrapping FileReader inside a promise is used here to handle the result synchronously.

The Contact instance is first submitted to the saveContact() endpoint. This happens through the binder.submit that is called upon clicking the Save button which executes the provided logic for onSubmit while obtaining the binder instance. Only then does the server-side endpoint implementation receive the image string. The server can then choose to recode the image for more efficient storage, if necessary.

The advantage of using the string type is simplicity; you can use the built-in serialization mechanism of Hilla’s form binder and endpoints. This approach isn’t suitable for larger files. A streamed upload may be more appropriate.