AccordionSummary.java
package com.vaadin.demo.component.accordion;
import com.vaadin.flow.component.accordion.Accordion;
import com.vaadin.flow.component.accordion.AccordionPanel;
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.formlayout.FormLayout;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
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.data.binder.Binder;
import com.vaadin.flow.router.Route;
import com.vaadin.demo.domain.Address;
import com.vaadin.demo.domain.Card;
import com.vaadin.demo.domain.Country;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
@Route("accordion-summary")
public class AccordionSummary extends Div {
    private static final String PAYMENT = "Payment";
    private static final String BILLING_ADDRESS = "Billing address";
    private static final String CUSTOMER_DETAILS = "Customer details";
    public AccordionSummary() {
        Accordion accordion = new Accordion();
        Binder<Person> personBinder = new Binder<>(Person.class);
        personBinder.setBean(new Person());
        Binder<Card> cardBinder = new Binder<>(Card.class);
        cardBinder.setBean(new Card());
        FormLayout customerDetailsFormLayout = createFormLayout();
        // tag::snippet[]
        AccordionPanel customDetailsPanel = accordion.add(CUSTOMER_DETAILS,
                customerDetailsFormLayout);
        // end::snippet[]
        FormLayout billingAddressFormLayout = createFormLayout();
        AccordionPanel billingAddressPanel = accordion.add(BILLING_ADDRESS,
                billingAddressFormLayout);
        FormLayout paymentFormLayout = createFormLayout();
        AccordionPanel paymentPanel = accordion.add(PAYMENT, paymentFormLayout);
        // Customer details fields
        TextField firstName = new TextField("First name");
        personBinder.forField(firstName).bind("firstName");
        TextField lastName = new TextField("Last name");
        personBinder.forField(lastName).bind("lastName");
        EmailField email = new EmailField("Email address");
        personBinder.forField(email).bind("email");
        TextField phone = new TextField("Phone number");
        personBinder.forField(phone).bind(person -> {
            if (person.getAddress() != null) {
                return person.getAddress().getPhone();
            }
            return "";
        }, (person, value) -> {
            if (person.getAddress() == null) {
                person.setAddress(new Address());
            }
            person.getAddress().setPhone(value);
        });
        customerDetailsFormLayout.add(firstName, lastName);
        customerDetailsFormLayout.add(email, 2);
        customerDetailsFormLayout.add(phone, 2);
        // tag::snippet[]
        customDetailsPanel.addOpenedChangeListener(e -> {
            if (e.isOpened()) {
                customDetailsPanel.setSummaryText(CUSTOMER_DETAILS);
            } else if (personBinder.getBean() != null) {
                Person personValues = personBinder.getBean();
                customDetailsPanel.setSummary(createSummary(CUSTOMER_DETAILS,
                        personValues.getFirstName() + " "
                                + personValues.getLastName(),
                        personValues.getEmail(),
                        personValues.getAddress() != null
                                ? personValues.getAddress().getPhone()
                                : ""));
            }
        });
        // end::snippet[]
        Button customDetailsButton = new Button("Continue",
                (e) -> billingAddressPanel.setOpened(true));
        customDetailsButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        customDetailsPanel.addContent(customDetailsButton);
        // Billing address fields
        TextField address = new TextField("Address");
        personBinder.forField(address).bind(person -> {
            if (person.getAddress() != null) {
                return person.getAddress().getStreet();
            }
            return "";
        }, (person, value) -> {
            if (person.getAddress() == null) {
                person.setAddress(new Address());
            }
            person.getAddress().setStreet(value);
        });
        TextField zipCode = new TextField("ZIP code");
        personBinder.forField(zipCode).bind(person -> {
            if (person.getAddress() != null) {
                return person.getAddress().getZip();
            }
            return "";
        }, (person, value) -> {
            if (person.getAddress() == null) {
                person.setAddress(new Address());
            }
            person.getAddress().setZip(value);
        });
        TextField city = new TextField("City");
        personBinder.forField(city).bind(person -> {
            if (person.getAddress() != null) {
                return person.getAddress().getCity();
            }
            return "";
        }, (person, value) -> {
            if (person.getAddress() == null) {
                person.setAddress(new Address());
            }
            person.getAddress().setCity(value);
        });
        ComboBox<Country> countries = new ComboBox<>("Country");
        countries.setItems(DataService.getCountries());
        countries.setItemLabelGenerator(Country::getName);
        personBinder.forField(countries).bind(person -> {
            if (person.getAddress() != null) {
                Country country = new Country();
                country.setName(person.getAddress().getCountry());
                return country;
            }
            return null;
        }, (person, value) -> {
            if (person.getAddress() == null) {
                person.setAddress(new Address());
            }
            person.getAddress().setCountry(value.getName());
        });
        billingAddressFormLayout.add(address, 2);
        billingAddressFormLayout.add(zipCode, city, countries);
        billingAddressPanel.addOpenedChangeListener(e -> {
            if (e.isOpened()) {
                billingAddressPanel.setSummaryText(BILLING_ADDRESS);
            } else if (personBinder.getBean().getAddress() != null) {
                Address addressValues = personBinder.getBean().getAddress();
                billingAddressPanel.setSummary(createSummary(BILLING_ADDRESS,
                        addressValues.getStreet(),
                        addressValues.getZip() + " " + addressValues.getCity(),
                        addressValues.getCountry()));
            }
        });
        Button billingAddressButton = new Button("Continue",
                (e) -> paymentPanel.setOpened(true));
        billingAddressButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        billingAddressPanel.addContent(billingAddressButton);
        // Payment fields
        TextField accountNumber = new TextField("Card number");
        cardBinder.forField(accountNumber).bind("accountNumber");
        TextField expiryDate = new TextField("Expiry date");
        cardBinder.forField(expiryDate).bind("expiryDate");
        TextField cvv = new TextField("CVV");
        cardBinder.forField(cvv).bind("cvv");
        paymentFormLayout.add(accountNumber, 2);
        paymentFormLayout.add(expiryDate, cvv);
        paymentPanel.addOpenedChangeListener(e -> {
            if (e.isOpened()) {
                paymentPanel.setSummaryText(PAYMENT);
            } else if (cardBinder.getBean() != null) {
                Card cardValues = cardBinder.getBean();
                paymentPanel.setSummary(
                        createSummary(PAYMENT, cardValues.getAccountNumber(),
                                cardValues.getExpiryDate()));
            }
        });
        Button paymentButton = new Button("Finish",
                (e) -> paymentPanel.setOpened(false));
        paymentButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        paymentPanel.addContent(paymentButton);
        add(accordion);
    }
    private FormLayout createFormLayout() {
        FormLayout billingAddressFormLayout = new FormLayout();
        billingAddressFormLayout.setResponsiveSteps(
                new FormLayout.ResponsiveStep("0", 1),
                new FormLayout.ResponsiveStep("20em", 2));
        return billingAddressFormLayout;
    }
    private VerticalLayout createSummary(String title, String... details) {
        VerticalLayout layout = new VerticalLayout();
        layout.setSpacing(false);
        layout.setPadding(false);
        layout.add(title);
        if (details.length > 0) {
            VerticalLayout detailsLayout = new VerticalLayout();
            detailsLayout.setSpacing(false);
            detailsLayout.setPadding(false);
            detailsLayout.getStyle().set("font-size",
                    "var(--lumo-font-size-s)");
            for (String detail : details) {
                if (detail != null && !detail.isEmpty()) {
                    detailsLayout.add(new Span(detail));
                }
            }
            layout.add(detailsLayout);
        }
        return layout;
    }
}
accordion-summary.tsx
import React, { useEffect } from 'react';
import { useForm } from '@vaadin/hilla-react-form';
import { useSignal } from '@vaadin/hilla-react-signals';
import {
  Accordion,
  AccordionHeading,
  type AccordionOpenedChangedEvent,
  AccordionPanel,
  Button,
  ComboBox,
  EmailField,
  FormLayout,
  type FormLayoutResponsiveStep,
  HorizontalLayout,
  TextField,
  VerticalLayout,
} from '@vaadin/react-components';
import { getCountries } from 'Frontend/demo/domain/DataService';
import CardModel from 'Frontend/generated/com/vaadin/demo/domain/CardModel';
import type Country from 'Frontend/generated/com/vaadin/demo/domain/Country';
import PersonModel from 'Frontend/generated/com/vaadin/demo/domain/PersonModel';
const responsiveSteps: FormLayoutResponsiveStep[] = [
  { minWidth: 0, columns: 1 },
  { minWidth: '20em', columns: 2 },
];
function Example() {
  const countries = useSignal<Country[]>([]);
  const openedPanelIndex = useSignal<number | null>(0);
  const person = useForm(PersonModel);
  const card = useForm(CardModel);
  const handleAccordionPanelOpen = (event: AccordionOpenedChangedEvent) => {
    openedPanelIndex.value = event.detail.value;
  };
  useEffect(() => {
    getCountries().then((items) => {
      countries.value = items;
    });
  }, []);
  // tag::snippet[]
  return (
    <Accordion opened={openedPanelIndex.value} onOpenedChanged={handleAccordionPanelOpen}>
      <AccordionPanel>
        <AccordionHeading slot="summary">
          <HorizontalLayout style={{ width: '100%', alignItems: 'center' }}>
            Customer details
            <VerticalLayout
              hidden={openedPanelIndex.value === 0}
              style={{ fontSize: 'var(--lumo-font-size-s)', marginLeft: 'auto' }}
            >
              <span>
                {person.value.firstName} {person.value.lastName}
              </span>
              <span>{person.value.email}</span>
              <span>{person.value.address?.phone}</span>
            </VerticalLayout>
          </HorizontalLayout>
        </AccordionHeading>
        <FormLayout responsiveSteps={responsiveSteps}>
          <TextField label="First name" {...person.field(person.model.firstName)} />
          <TextField label="Last name" {...person.field(person.model.lastName)} />
          <EmailField
            label="Email address"
            data-colspan="2"
            {...person.field(person.model.email)}
          />
          <TextField
            label="Phone number"
            data-colspan="2"
            {...person.field(person.model.address.phone)}
          />
        </FormLayout>
        <Button
          theme="primary"
          onClick={() => {
            openedPanelIndex.value = 1;
          }}
        >
          Continue
        </Button>
      </AccordionPanel>
      <AccordionPanel>
        <AccordionHeading slot="summary">
          <HorizontalLayout style={{ width: '100%', alignItems: 'center' }}>
            Billing address
            <VerticalLayout
              hidden={openedPanelIndex.value === 1}
              style={{ fontSize: 'var(--lumo-font-size-s)', marginLeft: 'auto' }}
            >
              <span>{person.value.address?.street}</span>
              <span>
                {person.value.address?.zip} {person.value.address?.city}
              </span>
              <span>{person.value.address?.country}</span>
            </VerticalLayout>
          </HorizontalLayout>
        </AccordionHeading>
        <FormLayout responsiveSteps={responsiveSteps}>
          <TextField
            label="Address"
            data-colspan="2"
            {...person.field(person.model.address.street)}
          />
          <TextField label="ZIP code" {...person.field(person.model.address.zip)} />
          <TextField label="City" {...person.field(person.model.address.city)} />
          <ComboBox
            label="Country"
            itemLabelPath="name"
            itemValuePath="name"
            items={countries.value}
            {...person.field(person.model.address.country)}
          />
        </FormLayout>
        <Button
          theme="primary"
          onClick={() => {
            openedPanelIndex.value = 2;
          }}
        >
          Continue
        </Button>
      </AccordionPanel>
      <AccordionPanel>
        <AccordionHeading slot="summary">
          <HorizontalLayout style={{ width: '100%', alignItems: 'center' }}>
            Payment
            <VerticalLayout
              hidden={openedPanelIndex.value === 2}
              style={{ fontSize: 'var(--lumo-font-size-s)', marginLeft: 'auto' }}
            >
              <span>{card.value.accountNumber}</span>
              <span>{card.value.expiryDate}</span>
            </VerticalLayout>
          </HorizontalLayout>
        </AccordionHeading>
        <FormLayout responsiveSteps={responsiveSteps}>
          <TextField
            label="Card number"
            data-colspan="2"
            {...card.field(card.model.accountNumber)}
          />
          <TextField label="Expiry date" {...card.field(card.model.expiryDate)} />
          <TextField label="CVV" {...card.field(card.model.cvv)} />
        </FormLayout>
        <Button
          theme="primary"
          onClick={() => {
            openedPanelIndex.value = -1;
          }}
        >
          Finish
        </Button>
      </AccordionPanel>
    </Accordion>
  );
  // end::snippet[]
}
accordion-summary.ts
import '@vaadin/accordion';
import '@vaadin/button';
import '@vaadin/combo-box';
import '@vaadin/email-field';
import '@vaadin/form-layout';
import '@vaadin/horizontal-layout';
import '@vaadin/text-field';
import '@vaadin/vertical-layout';
import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import type { AccordionOpenedChangedEvent } from '@vaadin/accordion';
import type { FormLayoutResponsiveStep } from '@vaadin/form-layout';
import { Binder, field } from '@vaadin/hilla-lit-form';
import { getCountries } from 'Frontend/demo/domain/DataService';
import CardModel from 'Frontend/generated/com/vaadin/demo/domain/CardModel';
import type Country from 'Frontend/generated/com/vaadin/demo/domain/Country';
import PersonModel from 'Frontend/generated/com/vaadin/demo/domain/PersonModel';
import { applyTheme } from 'Frontend/generated/theme';
const responsiveSteps: FormLayoutResponsiveStep[] = [
  { minWidth: 0, columns: 1 },
  { minWidth: '20em', columns: 2 },
];
@customElement('accordion-summary')
export class Example extends LitElement {
  @state()
  private countries: Country[] = [];
  private readonly personBinder = new Binder(this, PersonModel);
  private readonly cardBinder = new Binder(this, CardModel);
  @state()
  private openedPanelIndex: number | null = 0;
  protected override async firstUpdated() {
    this.countries = await getCountries();
  }
  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`
      <!-- tag::snippet[] -->
      <vaadin-accordion
        .opened="${this.openedPanelIndex}"
        @opened-changed="${(event: AccordionOpenedChangedEvent) => {
          this.openedPanelIndex = event.detail.value;
        }}"
      >
        <vaadin-accordion-panel>
          <vaadin-accordion-heading slot="summary">
            <vaadin-horizontal-layout style="width: 100%; align-items: center">
              Customer details
              <vaadin-vertical-layout
                .hidden="${this.openedPanelIndex === 0}"
                style="font-size: var(--lumo-font-size-s); margin-left: auto"
              >
                <span>
                  ${this.personBinder.value.firstName} ${this.personBinder.value.lastName}
                </span>
                <span>${this.personBinder.value.email}</span>
                <span>${this.personBinder.value.address?.phone}</span>
              </vaadin-vertical-layout>
            </vaadin-horizontal-layout>
          </vaadin-accordion-heading>
          <!-- end::snippet[] -->
          <vaadin-form-layout .responsiveSteps="${responsiveSteps}">
            <vaadin-text-field
              label="First name"
              ${field(this.personBinder.model.firstName)}
            ></vaadin-text-field>
            <vaadin-text-field
              label="Last name"
              ${field(this.personBinder.model.lastName)}
            ></vaadin-text-field>
            <vaadin-email-field
              label="Email address"
              ${field(this.personBinder.model.email)}
              colspan="2"
            ></vaadin-email-field>
            <vaadin-text-field
              label="Phone number"
              ${field(this.personBinder.model.address.phone)}
              colspan="2"
            ></vaadin-text-field>
          </vaadin-form-layout>
          <vaadin-button
            theme="primary"
            @click="${() => {
              this.openedPanelIndex = 1;
            }}"
          >
            Continue
          </vaadin-button>
        </vaadin-accordion-panel>
        <vaadin-accordion-panel>
          <vaadin-accordion-heading slot="summary">
            <vaadin-horizontal-layout style="width: 100%; align-items: center">
              Billing address
              <vaadin-vertical-layout
                .hidden="${this.openedPanelIndex === 1}"
                style="font-size: var(--lumo-font-size-s); margin-left: auto"
              >
                <span>${this.personBinder.value.address?.street}</span>
                <span>
                  ${this.personBinder.value.address?.zip} ${this.personBinder.value.address?.city}
                </span>
                <span> ${this.personBinder.value.address?.country} </span>
              </vaadin-vertical-layout>
            </vaadin-horizontal-layout>
          </vaadin-accordion-heading>
          <vaadin-form-layout .responsiveSteps="${responsiveSteps}">
            <vaadin-text-field
              label="Address"
              ${field(this.personBinder.model.address.street)}
              colspan="2"
            ></vaadin-text-field>
            <vaadin-text-field
              label="ZIP code"
              ${field(this.personBinder.model.address.zip)}
            ></vaadin-text-field>
            <vaadin-text-field
              label="City"
              ${field(this.personBinder.model.address.city)}
            ></vaadin-text-field>
            <vaadin-combo-box
              label="Country"
              ${field(this.personBinder.model.address.country)}
              item-label-path="name"
              item-value-path="name"
              .items="${this.countries}"
            ></vaadin-combo-box>
          </vaadin-form-layout>
          <vaadin-button
            theme="primary"
            @click="${() => {
              this.openedPanelIndex = 2;
            }}"
          >
            Continue
          </vaadin-button>
        </vaadin-accordion-panel>
        <vaadin-accordion-panel>
          <vaadin-accordion-heading slot="summary">
            <vaadin-horizontal-layout style="width: 100%; align-items: center">
              Payment
              <vaadin-vertical-layout
                .hidden="${this.openedPanelIndex === 2}"
                style="font-size: var(--lumo-font-size-s); margin-left: auto"
              >
                <span>${this.cardBinder.value.accountNumber}</span>
                <span>${this.cardBinder.value.expiryDate}</span>
              </vaadin-vertical-layout>
            </vaadin-horizontal-layout>
          </vaadin-accordion-heading>
          <vaadin-form-layout .responsiveSteps="${responsiveSteps}">
            <vaadin-text-field
              label="Card number"
              ${field(this.cardBinder.model.accountNumber)}
              colspan="2"
            ></vaadin-text-field>
            <vaadin-text-field
              label="Expiry date"
              ${field(this.cardBinder.model.expiryDate)}
            ></vaadin-text-field>
            <vaadin-text-field label="CVV" ${field(this.cardBinder.model.cvv)}></vaadin-text-field>
          </vaadin-form-layout>
          <vaadin-button
            theme="primary"
            @click="${() => {
              this.openedPanelIndex = -1;
            }}"
          >
            Finish
          </vaadin-button>
          <!-- tag::snippet[] -->
        </vaadin-accordion-panel>
      </vaadin-accordion>
      <!-- end::snippet[] -->
    `;
  }
}