You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2023/10/05 14:32:50 UTC

[myfaces-tobago] 01/02: feature: Toasts

This is an automated email from the ASF dual-hosted git repository.

hnoeth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit f41b2a827bd12579501b4ffdc99767ce2932bde0
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Fri Aug 18 16:40:57 2023 +0200

    feature: Toasts
    
    * tc:toasts implemented
    * add an example
    
    Issue: TOBAGO-2231
---
 .../apache/myfaces/tobago/application/Toast.java   |  30 ++
 .../myfaces/tobago/component/Attributes.java       |   2 +
 .../apache/myfaces/tobago/component/Facets.java    |   2 +
 .../myfaces/tobago/component/RendererTypes.java    |   2 +
 .../org/apache/myfaces/tobago/component/Tags.java  |   2 +
 .../org/apache/myfaces/tobago/context/Markup.java  |   2 +
 .../internal/component/AbstractUIToasts.java       |  72 +++++
 .../internal/renderkit/renderer/PageRenderer.java  |   6 +-
 .../renderkit/renderer/ToastsRenderer.java         | 334 +++++++++++++++++++++
 .../taglib/component/ToastsTagDeclaration.java     |  89 ++++++
 .../apache/myfaces/tobago/layout/Placement.java    |  36 +++
 .../tobago/renderkit/css/BootstrapClass.java       |   3 +
 .../myfaces/tobago/renderkit/css/TobagoClass.java  |   3 +
 .../tobago/renderkit/html/DataAttributes.java      |   2 +
 .../tobago/renderkit/html/HtmlElements.java        |   1 +
 .../tobago/context/TobagoResourceBundle.properties |   1 +
 .../context/TobagoResourceBundle_de.properties     |   1 +
 .../context/TobagoResourceBundle_es.properties     |   1 +
 .../tobago/example/demo/ToastsController.java      | 194 ++++++++++++
 .../src/main/webapp/content/065-toast/Toast.xhtml  | 110 +++++++
 tobago-theme/src/main/scss/_tobago.scss            |  52 +++-
 .../src/main/ts/tobago-all.ts                      |   1 +
 .../src/main/ts/tobago-css.ts                      |  15 +
 .../src/main/ts/tobago-placement.ts                |  30 ++
 .../src/main/ts/tobago-toasts.ts                   | 242 +++++++++++++++
 .../org/apache/myfaces/tobago/apt/component.stg    |   4 +
 26 files changed, 1223 insertions(+), 14 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java
new file mode 100644
index 0000000000..2d43b23ce9
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/application/Toast.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.application;
+
+public interface Toast {
+
+  /**
+   * A toast must have an ID.
+   *
+   * @return toast id
+   */
+  String getId();
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
index 2cb97e7f92..46c5280352 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
@@ -95,6 +95,7 @@ public enum Attributes {
   directLinkCount,
   disabled,
   display,
+  disposeDelay,
   dropZone,
   enctype,
   escape,
@@ -341,6 +342,7 @@ public enum Attributes {
   pagingTarget,
   password,
   placeholder,
+  placement,
   plain,
   popupClose,
   popupList,
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
index 16fa53b04d..6a37bb35b5 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Facets.java
@@ -35,6 +35,7 @@ public enum Facets {
   contextMenu,
   footer,
   label,
+  header,
   pagerPage,
   pagerPageDirect,
   pagerRow,
@@ -56,6 +57,7 @@ public enum Facets {
   public static final String CONTEXT_MENU = "contextMenu";
   public static final String FOOTER = "footer";
   public static final String LABEL = "label";
+  public static final String HEADER = "header";
   public static final String PAGER_PAGE = "pagerPage";
   public static final String PAGER_PAGE_DIRECT = "pagerPageDirect";
   public static final String PAGER_ROW = "pagerRow";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
index 8c549a3a48..7571f6a23f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
@@ -86,6 +86,7 @@ public enum RendererTypes {
   Textarea,
   Tab,
   TabGroup,
+  Toasts,
   ToolBar,
   Tree,
   /**
@@ -162,6 +163,7 @@ public enum RendererTypes {
   public static final String TEXTAREA = "Textarea";
   public static final String TAB = "Tab";
   public static final String TAB_GROUP = "TabGroup";
+  public static final String TOASTS = "Toasts";
   public static final String TOOL_BAR = "ToolBar";
   public static final String TREE = "Tree";
   /**
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
index f5c183d191..252a7e92fc 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Tags.java
@@ -94,6 +94,7 @@ public enum Tags {
   textarea,
   tab,
   tabGroup,
+  toasts,
   toolBar,
   tree,
   treeIcon,
@@ -169,6 +170,7 @@ public enum Tags {
   public static final String TEXTAREA = "textarea";
   public static final String TAB = "tab";
   public static final String TAB_GROUP = "tabGroup";
+  public static final String TOASTS = "toasts";
   public static final String TOOL_BAR = "toolBar";
   public static final String TREE = "tree";
   public static final String TREE_ICON = "treeIcon";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
index d77d146a33..69be720c2a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/context/Markup.java
@@ -81,6 +81,7 @@ public final class Markup implements Serializable, Iterable<String> {
   public static final Markup EXTRA_LARGE = valueOf("extraLarge");
   public static final Markup FATAL = valueOf("fatal");
   public static final Markup FOLDER = valueOf("folder");
+  public static final Markup HIDE_CLOSE_BUTTON = valueOf("hideCloseButton");
   public static final Markup HIDE_TOGGLE_ICON = valueOf("hideToggleIcon");
   public static final Markup HOVER = valueOf("hover");
   public static final Markup INFO = valueOf("info");
@@ -144,6 +145,7 @@ public final class Markup implements Serializable, Iterable<String> {
   public static final String STRING_EXTRA_LARGE = "extraLarge";
   public static final String STRING_FATAL = "fatal";
   public static final String STRING_FOLDER = "folder";
+  public static final String STRING_HIDE_CLOSE_BUTTON = "hideCloseButton";
   public static final String STRING_HIDE_TOGGLE_ICON = "hideToggleIcon";
   public static final String STRING_HOVER = "hover";
   public static final String STRING_INFO = "info";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java
new file mode 100644
index 0000000000..8754a06be1
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUIToasts.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.internal.component;
+
+import jakarta.faces.component.UIData;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.component.Visual;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.ToastsRenderer;
+import org.apache.myfaces.tobago.layout.Placement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractUIToasts extends UIData implements ClientBehaviorHolder, Visual {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String STATES = "states";
+
+  @Override
+  public Collection<Toast> getValue() {
+    final Object value = super.getValue();
+    if (value instanceof Collection) {
+      return (Collection<Toast>) value;
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+  public Map<String, ToastsRenderer.StateData> getStates() {
+    Object hideTime = getStateHelper().eval(STATES);
+    if (hideTime instanceof Map) {
+      return (Map<String, ToastsRenderer.StateData>) hideTime;
+    }
+    return new HashMap<>();
+  }
+
+  public void setStates(Map<String, ToastsRenderer.StateData> states) {
+    getStateHelper().put(STATES, states);
+  }
+
+  public abstract Placement getPlacement();
+
+  public abstract Integer getDisposeDelay();
+
+  private enum PropertyKeys {
+    showedIds, hideTimes, closedIds
+  }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
index fc8d83a06d..f0619d508c 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/PageRenderer.java
@@ -281,6 +281,11 @@ public class PageRenderer<T extends AbstractUIPage> extends RendererBase<T> {
     writer.writeClassAttribute(TobagoClass.PAGE__MENU_STORE);
     writer.endElement(HtmlElements.DIV);
 
+    // placeholder for toasts
+    writer.startElement(HtmlElements.DIV);
+    writer.writeClassAttribute(TobagoClass.PAGE__TOAST_STORE);
+    writer.endElement(HtmlElements.DIV);
+
     writer.startElement(HtmlElements.SPAN);
     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "faces-state-container");
     writer.flush();
@@ -435,6 +440,5 @@ public class PageRenderer<T extends AbstractUIPage> extends RendererBase<T> {
       }
       return false;
     }
-
   }
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java
new file mode 100644
index 0000000000..e7e0d58f18
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ToastsRenderer.java
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.internal.renderkit.renderer;
+
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.context.FacesContext;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.component.Facets;
+import org.apache.myfaces.tobago.context.Markup;
+import org.apache.myfaces.tobago.internal.component.AbstractUIReload;
+import org.apache.myfaces.tobago.internal.component.AbstractUIStyle;
+import org.apache.myfaces.tobago.internal.component.AbstractUIToasts;
+import org.apache.myfaces.tobago.internal.util.HtmlRendererUtils;
+import org.apache.myfaces.tobago.internal.util.RenderUtils;
+import org.apache.myfaces.tobago.layout.Placement;
+import org.apache.myfaces.tobago.renderkit.RendererBase;
+import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
+import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.Arias;
+import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
+import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
+import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
+import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
+import org.apache.myfaces.tobago.renderkit.html.HtmlRoleValues;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.apache.myfaces.tobago.util.ResourceUtils;
+import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.stream.Collectors;
+
+public class ToastsRenderer<T extends AbstractUIToasts> extends RendererBase<T> {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final String SUFFIX_STATES = "states";
+
+  @Override
+  public void decodeInternal(final FacesContext facesContext, final T component) {
+    super.decodeInternal(facesContext, component);
+
+    final String clientId = component.getClientId(facesContext);
+    final String statesInputId = clientId + ComponentUtils.SUB_SEPARATOR + SUFFIX_STATES;
+
+    final Map<String, String> requestParameterMap = facesContext.getExternalContext().getRequestParameterMap();
+    if (requestParameterMap.containsKey(statesInputId)) {
+      final String json = requestParameterMap.get(statesInputId);
+      final Map<String, StateData> states = decodeStates(json);
+      final Collection<Toast> toasts = component.getValue();
+      final List<String> toastIds = toasts.stream()
+          .map(toast -> clientId + ComponentUtils.SUB_SEPARATOR + toast.getId())
+          .collect(Collectors.toList());
+
+      final List<String> idsForRemoval = new ArrayList<>();
+      states.forEach((key, value) -> {
+        if (value.state.equals(StateEnum.closed)
+            || value.hideTime != null && value.hideTime.isBefore(LocalTime.now())
+            || !toastIds.contains(key)) {
+          idsForRemoval.add(key);
+        }
+      });
+
+      toasts.removeIf(toast -> idsForRemoval.contains(clientId + ComponentUtils.SUB_SEPARATOR + toast.getId()));
+      states.entrySet().removeIf(entry -> idsForRemoval.contains(entry.getKey()));
+
+      component.setStates(states);
+    }
+  }
+
+  private Map<String, StateData> decodeStates(final String input) {
+    if (input == null) {
+      return null;
+    }
+    String string = input.trim().replaceAll("&quot;", "\"");
+    final Map<String, StateData> result = new HashMap<>();
+    if (string.length() < 2 || string.charAt(0) != '{' || string.charAt(string.length() - 1) != '}') {
+      LOG.warn("Can't parse: no surrounding curly brackets: '{}'", string);
+    } else {
+      string = string.substring(1, string.length() - 1);
+
+      final StringTokenizer tokenizer = new StringTokenizer(string, "\",{}");
+
+      String id = null;
+      StateEnum state = null;
+      LocalTime hideTime = null;
+
+      while (tokenizer.hasMoreTokens()) {
+        final String token = tokenizer.nextToken().trim();
+
+        if (!token.equals(":") && !token.equals("state") && !token.equals("hideTime")) {
+          if (id == null) {
+            id = token;
+          } else if (state == null) {
+            state = StateEnum.valueOf(token);
+          } else {
+            if (!token.equals(":null")) {
+              hideTime = LocalTime.parse(token);
+            }
+
+            result.put(id, new StateData(state, hideTime));
+            id = null;
+            state = null;
+            hideTime = null;
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public void encodeBeginInternal(final FacesContext facesContext, final T component) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+    final String clientId = component.getClientId(facesContext);
+    final Placement placement = component.getPlacement();
+    final Integer disposeDelay = component.getDisposeDelay();
+    final Markup markup = component.getMarkup() != null ? component.getMarkup() : Markup.NULL;
+    final Map<String, StateData> states = component.getStates();
+
+    insideBegin(facesContext, HtmlElements.TOBAGO_TOASTS);
+    writer.startElement(HtmlElements.TOBAGO_TOASTS);
+    writer.writeIdAttribute(clientId);
+    HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
+
+    final AbstractUIReload reload = ComponentUtils.getReloadFacet(component);
+    if (reload != null) {
+      reload.encodeAll(facesContext);
+    }
+
+    final Collection<Toast> toasts = component.getValue();
+    final UIComponent header = ComponentUtils.getFacet(component, Facets.header);
+    final List<UIComponent> children = component.getChildren();
+
+    if (toasts.size() == component.getRowCount()) {
+      int rowIndex = component.getFirst();
+      for (Toast toast : toasts) {
+        component.setRowIndex(rowIndex);
+        if (!component.isRowAvailable()) {
+          break;
+        }
+
+        final String toastId = clientId + ComponentUtils.SUB_SEPARATOR + toast.getId();
+
+        if (!states.containsKey(toastId)) {
+          states.put(toastId, new StateData(StateEnum.created, null));
+        }
+        final StateData stateData = states.get(toastId);
+
+        writer.startElement(HtmlElements.DIV);
+        writer.writeIdAttribute(toastId);
+        writer.writeClassAttribute(
+            BootstrapClass.TOAST,
+            component.getCustomClass(),
+            markup.contains(Markup.HIDE_CLOSE_BUTTON) ? TobagoClass.HIDE_CLOSE_BUTTON : null);
+        writer.writeAttribute(HtmlAttributes.NAME, toastId, false);
+        writer.writeAttribute(HtmlAttributes.ROLE, HtmlRoleValues.ALERT.toString(), false);
+        writer.writeAttribute(Arias.LIVE, "assertive", false);
+        writer.writeAttribute(Arias.ATOMIC, true);
+        writer.writeAttribute(DataAttributes.PLACEMENT, placement.name(), false);
+
+        if (stateData.state.equals(StateEnum.created)) {
+          writer.writeAttribute(DataAttributes.DISPOSE_DELAY, disposeDelay);
+          if (disposeDelay > 0) {
+            LocalTime hideTime = LocalTime.now().plus(disposeDelay, ChronoUnit.MILLIS);
+            states.put(toastId, new StateData(StateEnum.created, hideTime));
+          }
+        } else if (stateData.state.equals(StateEnum.showed)) {
+          if (stateData.hideTime != null) {
+            int decreasedDisposeDelay = (int) LocalTime.now().until(stateData.hideTime, ChronoUnit.MILLIS);
+            writer.writeAttribute(DataAttributes.DISPOSE_DELAY, Math.max(decreasedDisposeDelay, 0));
+
+            if (decreasedDisposeDelay <= 0) {
+              states.put(toastId, new StateData(StateEnum.closed, null));
+            }
+          } else {
+            writer.writeAttribute(DataAttributes.DISPOSE_DELAY, disposeDelay);
+          }
+        }
+
+        if (header != null) {
+          writer.startElement(HtmlElements.DIV);
+          writer.writeClassAttribute(BootstrapClass.TOAST_HEADER);
+          insideBegin(facesContext, Facets.header);
+          for (final UIComponent child : RenderUtils.getFacetChildren(header)) {
+            child.encodeAll(facesContext);
+          }
+          encodeCloseButton(facesContext, writer);
+          insideEnd(facesContext, Facets.header);
+          writer.endElement(HtmlElements.DIV);
+        }
+
+        writer.startElement(HtmlElements.DIV);
+        writer.writeClassAttribute(BootstrapClass.TOAST_BODY);
+        if (header == null) {
+          encodeCloseButton(facesContext, writer);
+        }
+        for (UIComponent child : children) {
+          child.encodeAll(facesContext);
+        }
+        writer.endElement(HtmlElements.DIV);
+        writer.endElement(HtmlElements.DIV);
+
+        rowIndex++;
+      }
+    }
+
+    component.setRowIndex(-1);
+
+    final String statesInputId = clientId + ComponentUtils.SUB_SEPARATOR + SUFFIX_STATES;
+    writer.startElement(HtmlElements.INPUT);
+    writer.writeIdAttribute(statesInputId);
+    writer.writeNameAttribute(statesInputId);
+    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN);
+    writer.writeAttribute(HtmlAttributes.VALUE, encodeStates(states), false);
+    writer.endElement(HtmlElements.INPUT);
+  }
+
+  private void encodeCloseButton(final FacesContext facesContext, final TobagoResponseWriter writer)
+      throws IOException {
+    writer.startElement(HtmlElements.BUTTON);
+    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.BUTTON);
+    writer.writeClassAttribute(BootstrapClass.BTN_CLOSE, TobagoClass.CLOSE);
+    writer.writeAttribute(DataAttributes.BS_DISMISS, "toast", false);
+    writer.writeAttribute(Arias.LABEL, ResourceUtils.getString(facesContext, "toast.close"), false);
+    writer.endElement(HtmlElements.BUTTON);
+  }
+
+  private String encodeStates(final Map<String, StateData> states) {
+    if (states == null) {
+      return null;
+    }
+
+    final StringBuilder builder = new StringBuilder();
+    boolean firstEntry = true;
+
+    builder.append("{");
+    for (Map.Entry<String, StateData> entry : states.entrySet()) {
+      if (firstEntry) {
+        firstEntry = false;
+      } else {
+        builder.append(",");
+      }
+
+      builder.append("\"");
+      builder.append(entry.getKey());
+      builder.append("\":{\"state\":\"");
+      builder.append(entry.getValue().state);
+      builder.append("\",\"hideTime\":");
+
+      if (entry.getValue().hideTime != null) {
+        builder.append("\"");
+        builder.append(entry.getValue().hideTime);
+        builder.append("\"");
+      } else {
+        builder.append("null");
+      }
+      builder.append("}");
+    }
+    builder.append("}");
+
+    return builder.toString();
+  }
+
+  @Override
+  public void encodeEndInternal(final FacesContext facesContext, final T component) throws IOException {
+    final TobagoResponseWriter writer = getResponseWriter(facesContext);
+    writer.endElement(HtmlElements.TOBAGO_TOASTS);
+    insideEnd(facesContext, HtmlElements.TOBAGO_TOASTS);
+  }
+
+  @Override
+  public boolean getRendersChildren() {
+    return true;
+  }
+
+  @Override
+  public void encodeChildrenInternal(final FacesContext facesContext, final T component) throws IOException {
+    for (final UIComponent child : component.getChildren()) {
+      if (child instanceof AbstractUIStyle) {
+        child.encodeAll(facesContext);
+      }
+    }
+  }
+
+  public static class StateData {
+    private final StateEnum state;
+    private final LocalTime hideTime;
+
+    public StateData(StateEnum state, LocalTime hideTime) {
+      this.state = state;
+      this.hideTime = hideTime;
+    }
+
+    public StateEnum getState() {
+      return state;
+    }
+
+    public LocalTime getHideTime() {
+      return hideTime;
+    }
+  }
+
+  private enum StateEnum {
+    created, showed, closed
+  }
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java
new file mode 100644
index 0000000000..53d7e4efdb
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/ToastsTagDeclaration.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.internal.taglib.component;
+
+import jakarta.faces.component.UIData;
+import org.apache.myfaces.tobago.apt.annotation.Behavior;
+import org.apache.myfaces.tobago.apt.annotation.DynamicExpression;
+import org.apache.myfaces.tobago.apt.annotation.Facet;
+import org.apache.myfaces.tobago.apt.annotation.Preliminary;
+import org.apache.myfaces.tobago.apt.annotation.Tag;
+import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
+import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+import org.apache.myfaces.tobago.component.ClientBehaviors;
+import org.apache.myfaces.tobago.component.Facets;
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasIdBindingAndRendered;
+import org.apache.myfaces.tobago.internal.taglib.declaration.HasVar;
+import org.apache.myfaces.tobago.internal.taglib.declaration.IsVisual;
+import org.apache.myfaces.tobago.layout.Placement;
+
+/**
+ * Render toasts elements.
+ */
+@Preliminary
+@Tag(name = "toasts")
+@UIComponentTag(
+    uiComponent = "org.apache.myfaces.tobago.component.UIToasts",
+    uiComponentFacesClass = "jakarta.faces.component.UIData",
+    componentFamily = UIData.COMPONENT_FAMILY,
+    rendererType = RendererTypes.TOASTS,
+    facets = {
+        @Facet(name = Facets.RELOAD, description = "Contains an instance of UIReload",
+            allowedChildComponenents = "org.apache.myfaces.tobago.Reload"),
+        @Facet(name = Facets.HEADER, description = "Contains code to be placed in the header of the toast.")},
+    behaviors = {
+        @Behavior(name = ClientBehaviors.RELOAD, isDefault = true)
+    })
+public interface ToastsTagDeclaration extends HasIdBindingAndRendered, HasVar, IsVisual {
+
+  /**
+   * Must be a collection of #{@link org.apache.myfaces.tobago.application.Toast} objects.
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(
+      type = {"java.util.Collection<org.apache.myfaces.tobago.application.Toast>"},
+      expression = DynamicExpression.VALUE_EXPRESSION_REQUIRED)
+  void setValue(String value);
+
+  /**
+   * Defines the placement of the toasts. Default is "bottomRight".
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(
+      type = "org.apache.myfaces.tobago.layout.Placement",
+      allowedValues = {
+          Placement.TOP_LEFT, Placement.TOP_CENTER, Placement.TOP_RIGHT,
+          Placement.MIDDLE_LEFT, Placement.MIDDLE_CENTER, Placement.MIDDLE_RIGHT,
+          Placement.BOTTOM_LEFT, Placement.BOTTOM_CENTER, Placement.BOTTOM_RIGHT
+      },
+      defaultValue = Placement.BOTTOM_RIGHT,
+      defaultCode = "org.apache.myfaces.tobago.layout.Placement.bottomRight")
+  void setPlacement(String placement);
+
+  /**
+   * Delay in milliseconds before hiding the toast. Default is "5000".
+   * Set to "-1" to never automatically hide the toast.
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(type = "java.lang.Integer", defaultValue = "5000")
+  void setDisposeDelay(String disposeDelay);
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.java
new file mode 100644
index 0000000000..3c3e728fca
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/layout/Placement.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.layout;
+
+public enum Placement {
+  topLeft, topCenter, topRight,
+  middleLeft, middleCenter, middleRight,
+  bottomLeft, bottomCenter, bottomRight;
+
+  public static final String TOP_LEFT = "topLeft";
+  public static final String TOP_CENTER = "topCenter";
+  public static final String TOP_RIGHT = "topRight";
+  public static final String MIDDLE_LEFT = "middleLeft";
+  public static final String MIDDLE_CENTER = "middleCenter";
+  public static final String MIDDLE_RIGHT = "middleRight";
+  public static final String BOTTOM_LEFT = "bottomLeft";
+  public static final String BOTTOM_CENTER = "bottomCenter";
+  public static final String BOTTOM_RIGHT = "bottomRight";
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
index 66330f98e0..7b5c73e167 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/BootstrapClass.java
@@ -638,6 +638,9 @@ public enum BootstrapClass implements CssItem {
   TABLE_PRIMARY("table-primary"),
   TABLE_SM("table-sm"),
   TABLE_STRIPED("table-striped"),
+  TOAST("toast"),
+  TOAST_BODY("toast-body"),
+  TOAST_HEADER("toast-header"),
   TOOLTIP_ARROW("tooltip-arrow"),
   VISUALLY_HIDDEN("visually-hidden"),
   WARNING_FEEDBACK("warning-feedback"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
index cd446fcd02..f7d5e47497 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
@@ -48,6 +48,7 @@ public enum TobagoClass implements CssItem {
   CIRCLE__7("tobago-circle-7"),
   CIRCLE__8("tobago-circle-8"),
   CIRCLE__9("tobago-circle-9"),
+  CLOSE("tobago-close"),
   COLLAPSED("tobago-collapsed"),
   DELETED("tobago-deleted"),
   DESCENDING("tobago-descending"),
@@ -61,6 +62,7 @@ public enum TobagoClass implements CssItem {
   FILTER("tobago-filter"),
   HEADER("tobago-header"),
   HELP__CONTAINER("tobago-help-container"),
+  HIDE_CLOSE_BUTTON("tobago-hideCloseButton"),
   HIDE_TOGGLE_ICON("tobago-hideToggleIcon"),
   LABEL("tobago-label"),
   LABEL__CONTAINER("tobago-label-container"),
@@ -79,6 +81,7 @@ public enum TobagoClass implements CssItem {
   //  PAGE("tobago-page"),
   PAGE__MENU_STORE("tobago-page-menuStore"),
   PAGE__NOSCRIPT("tobago-page-noscript"),
+  PAGE__TOAST_STORE("tobago-page-toastStore"),
   PAGING("tobago-paging"),
   PANEL("tobago-panel"),
   POPOVER__BOX("tobago-popover-box"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index c5306f537b..b67691c8ed 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -60,6 +60,7 @@ public enum DataAttributes implements MarkupLanguageAttributes {
   DISABLED("data-tobago-disabled"),
 
   DISMISS("data-dismiss"),
+  DISPOSE_DELAY("data-tobago-dispose-delay"),
 
   /**
    * Custom form attribute. Used to show the virtual form of the component.
@@ -132,6 +133,7 @@ public enum DataAttributes implements MarkupLanguageAttributes {
    * Custom attribute to describe a pattern, e. g. for an date input field.
    */
   PATTERN("data-tobago-pattern"),
+  PLACEMENT("data-tobago-placement"),
 
   /**
    * Custom reload attribute. Used to reload a panel.
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
index 253a13f0f5..5a3f1a0fe5 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
@@ -187,6 +187,7 @@ public enum HtmlElements {
   TOBAGO_TAB("tobago-tab"),
   TOBAGO_TAB_GROUP("tobago-tab-group"),
   TOBAGO_TEXTAREA("tobago-textarea"),
+  TOBAGO_TOASTS("tobago-toasts"),
   TOBAGO_TREE("tobago-tree"),
   TOBAGO_TREE_LISTBOX("tobago-tree-listbox"),
   TOBAGO_TREE_NODE("tobago-tree-node"),
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
index a1cb457232..404c2b9dad 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle.properties
@@ -18,6 +18,7 @@ object.noframe=Your user agent does not support frames or is currently configure
 page.noscript=This site requires JavaScript, but JavaScript is currently not activated in your browser. \
   To enable JavaScript you may consult your browsers the documentation.
 alert.close=Close
+toast.close=Close
 bar.toggleNavigation=Toggle navigation
 sheet.ascending=Ascending
 sheet.descending=Descending
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
index 0d87cc47d6..060318dc72 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_de.properties
@@ -17,6 +17,7 @@ object.noframe=Ihr Browser stellt keine Frames dar. Den Inhalt k\u00F6nnen Sie h
 page.noscript=Diese Seite ben\u00F6tigt JavaScript, allerdings ist JavaScript in Ihrem Browser derzeit deaktiviert. \
   Um JavaScript zu aktivieren, lesen Sie ggf. die Anleitung Ihres Browsers.
 alert.close=Schlie�en
+toast.close=Schlie�en
 bar.toggleNavigation=Navigation umschalten
 sheet.ascending=aufsteigend sortiert
 sheet.descending=absteigend sortiert
diff --git a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
index 92a33c6465..c1506faad5 100644
--- a/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
+++ b/tobago-core/src/main/resources/org/apache/myfaces/tobago/context/TobagoResourceBundle_es.properties
@@ -18,6 +18,7 @@ object.noframe=Tu navegador no soporta frames o est\u00E1 configurado para no de
 page.noscript=Esta p\u00E1gina requiere JavaScript, pero JavaScript no se activa actualmente en su navegador. \
   Para permitir JavaScript usted puede consultar la documentaci\u00F3n del navegador.
 alert.close=Cerrar
+toast.close=Cerrar
 bar.toggleNavigation=Alternar navegaci\u00F3n
 sheet.ascending=Ascendente
 sheet.descending=Descendente
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java
new file mode 100644
index 0000000000..51057096a3
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/ToastsController.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo;
+
+import jakarta.enterprise.context.SessionScoped;
+import jakarta.faces.context.FacesContext;
+import jakarta.inject.Named;
+import org.apache.myfaces.tobago.application.Toast;
+import org.apache.myfaces.tobago.util.ResourceUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Named
+@SessionScoped
+public class ToastsController implements Serializable {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final List<ToastFacesMessage> toastFacesMessages = new ArrayList<>();
+  private final List<Progress> progressBars = new ArrayList<>();
+  private final List<CustomToast> customToasts = new ArrayList<>();
+  private String severity;
+  private ScheduledExecutorService scheduledExecutorService;
+  private String customToastMessage;
+
+  public List<ToastFacesMessage> getToastFacesMessages() {
+    return toastFacesMessages;
+  }
+
+  public void createToastFacesMessage() {
+    toastFacesMessages.add(new ToastFacesMessage(severity, "Message text"));
+  }
+
+  public String getSeverity() {
+    return severity;
+  }
+
+  public void setSeverity(String severity) {
+    this.severity = severity;
+  }
+
+  public List<Progress> getProgressBars() {
+    return progressBars;
+  }
+
+  public void createProgressBarToast() {
+    progressBars.add(new Progress(100));
+
+    if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
+      scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+
+      scheduledExecutorService.scheduleAtFixedRate(() -> {
+        for (int i = progressBars.size() - 1; i >= 0; i--) {
+          Progress progress = progressBars.get(i);
+          if (progress.getValue() >= progress.getMax()) {
+            progressBars.remove(i);
+          }
+        }
+
+        for (Progress progress : progressBars) {
+          progress.increase();
+        }
+
+        if (progressBars.isEmpty()) {
+          scheduledExecutorService.shutdown();
+        }
+      }, 100, 100, TimeUnit.MILLISECONDS);
+    }
+  }
+
+  public List<CustomToast> getCustomToasts() {
+    return customToasts;
+  }
+
+  public void createCustomToast() {
+    customToasts.add(new CustomToast(customToastMessage));
+  }
+
+  public String getCustomToastMessage() {
+    return customToastMessage;
+  }
+
+  public void setCustomToastMessage(String customToastMessage) {
+    this.customToastMessage = customToastMessage;
+  }
+
+  public static class ToastFacesMessage implements Toast {
+    private final String id = String.valueOf(UUID.randomUUID());
+    private final String severity;
+    private final String text;
+
+    public ToastFacesMessage(String severity, String text) {
+      this.severity = severity;
+      this.text = text;
+    }
+
+    @Override
+    public String getId() {
+      return id;
+    }
+
+    public String getSeverityColor() {
+      switch (severity) {
+        default:
+        case "info":
+          return "bg-info";
+        case "warn":
+          return "bg-warning";
+        case "error":
+        case "fatal":
+          return "bg-danger";
+      }
+    }
+
+    public String getSeverityText() {
+      return ResourceUtils.getString(FacesContext.getCurrentInstance(), "severity." + severity);
+    }
+
+    public String getText() {
+      return text;
+    }
+  }
+
+  public static class Progress implements Toast {
+    private final String id = String.valueOf(UUID.randomUUID());
+    private final int max;
+    private int value = 0;
+
+    public Progress(int max) {
+      this.max = max;
+    }
+
+    @Override
+    public String getId() {
+      return id;
+    }
+
+    public void increase() {
+      value++;
+    }
+
+    public int getMax() {
+      return max;
+    }
+
+    public int getValue() {
+      return value;
+    }
+  }
+
+  public static class CustomToast implements Toast {
+    private final String id = String.valueOf(UUID.randomUUID());
+    private final String text;
+
+    public CustomToast(String text) {
+      this.text = text;
+    }
+
+    @Override
+    public String getId() {
+      return id;
+    }
+
+    public String getText() {
+      return text;
+    }
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml
new file mode 100644
index 0000000000..18d3e618af
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/065-toast/Toast.xhtml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<ui:composition template="/main.xhtml"
+                xmlns="http://www.w3.org/1999/xhtml"
+                xmlns:tc="http://myfaces.apache.org/tobago/component"
+                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
+                xmlns:f="http://xmlns.jcp.org/jsf/core">
+  <p>Toasts represent a type of notification message, commonly associated with push notifications on mobile devices.
+    The <code>value</code> attribute of the <code
+        class="language-markup">&lt;tc:toasts/></code> tag contains a list of toasts
+    objects, each representing a single toast.
+    A toast object MUST implement the <code>org.apache.myfaces.tobago.application.Toast</code> interface.
+    If a toast has been closed, the corresponding object is removed from the list.
+    The content of the toasts is fully customizable.</p>
+  <p>Tag Library Documentation:
+    <tc:link label="&lt;tc:toasts/>" image="#{request.contextPath}/image/feather-leaf.png"
+             link="#{apiController.tobagoTld}/tc/toasts.html"/></p>
+
+  <tc:section label="ToastFacesMessage">
+    <p>FacesMessage but as a toast.</p>
+    <tc:selectOneChoice label="Severity" value="#{toastsController.severity}">
+      <tc:selectItem itemLabel="Information" itemValue="info"/>
+      <tc:selectItem itemLabel="Warning" itemValue="warn"/>
+      <tc:selectItem itemLabel="Error" itemValue="error"/>
+      <tc:selectItem itemLabel="Fatal" itemValue="fatal"/>
+      <f:ajax/>
+    </tc:selectOneChoice>
+    <tc:button label="ToastFacesMessage" action="#{toastsController.createToastFacesMessage()}"/>
+    <tc:button label="ToastFacesMessage (AJAX)" action="#{toastsController.createToastFacesMessage()}">
+      <f:ajax execute="toastFacesMessages" render="toastFacesMessages"/>
+    </tc:button>
+
+    <tc:toasts id="toastFacesMessages" value="#{toastsController.toastFacesMessages}" var="toastFacesMessage"
+               placement="bottomLeft">
+      <f:facet name="header">
+        <tc:panel>
+          <tc:style customClass="rounded me-2 #{toastFacesMessage.severityColor}"
+                    width="1.25rem" height="1.25rem"/>
+        </tc:panel>
+        <strong>
+          <tc:out value="#{toastFacesMessage.severityText}" plain="true"/>
+        </strong>
+      </f:facet>
+      <tc:out value="#{toastFacesMessage.text}" plain="true"/>
+    </tc:toasts>
+  </tc:section>
+
+  <tc:section label="Progress bar">
+    <p>Create a toast with a progress bar. The toast is removed after the progress is 100%.
+      The close button is hidden.</p>
+    <tc:button label="Progress bar toast" action="#{toastsController.createProgressBarToast()}"/>
+    <tc:button label="Progress bar toast (AJAX)" action="#{toastsController.createProgressBarToast()}">
+      <f:ajax execute="progressBarToasts" render="progressBarToasts"/>
+    </tc:button>
+
+    <tc:toasts id="progressBarToasts" value="#{toastsController.progressBars}" var="progress" disposeDelay="-1"
+               markup="hideCloseButton">
+      <f:facet name="reload">
+        <tc:reload frequency="100"/>
+      </f:facet>
+      <f:facet name="header">
+        <tc:out value="Progress" plain="true"/>
+      </f:facet>
+      <tc:progress value="#{progress.value}" max="#{progress.max}"/>
+    </tc:toasts>
+  </tc:section>
+
+  <tc:section label="Custom toast">
+    <p>Create a custom toast. The message can be written in the input field. The toast also contains an AJAX refresh
+      button for the following timestamp.</p>
+    <tc:in id="customToastMessage" label="Message" value="#{toastsController.customToastMessage}"/>
+    <tc:button label="Custom toast" action="#{toastsController.createCustomToast()}"/>
+    <tc:button label="Custom toast (AJAX)" action="#{toastsController.createCustomToast()}">
+      <f:ajax execute="customToastMessage customToasts" render="customToasts"/>
+    </tc:button>
+
+    <tc:out id="now" label="Timestamp" value="#{eventController.currentTimestamp}"/>
+
+    <tc:toasts id="customToasts" value="#{toastsController.customToasts}" var="customToast" disposeDelay="-1">
+      <tc:style customClass="customCssClass"/>
+      <f:facet name="header">
+        <tc:out value="Custom toast" plain="true"/>
+      </f:facet>
+      <tc:out value="Message: #{customToast.text}" plain="true"/>
+      <tc:panel>
+        <tc:style height="1rem"/>
+      </tc:panel>
+      <tc:button label="refresh timestamp">
+        <f:ajax render=":::now"/>
+      </tc:button>
+    </tc:toasts>
+  </tc:section>
+</ui:composition>
diff --git a/tobago-theme/src/main/scss/_tobago.scss b/tobago-theme/src/main/scss/_tobago.scss
index bce29254c1..b48b0ce710 100644
--- a/tobago-theme/src/main/scss/_tobago.scss
+++ b/tobago-theme/src/main/scss/_tobago.scss
@@ -1091,12 +1091,45 @@ tobago-overlay {
   }
 }
 
-/* page ----------------------------------------------------------- */
-
 tobago-page {
   display: block;
   padding-top: $tobago-page-padding-top;
   padding-bottom: 1rem;
+
+  .tobago-page-menuStore {
+    @include tobagoDropdownMenu();
+
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 0;
+    height: 0;
+    z-index: $zindex-popover;
+  }
+
+  .tobago-page-toastStore {
+    .toast {
+      &.tobago-hideCloseButton .tobago-close {
+        display: none;
+      }
+
+      .toast-header {
+        .tobago-close {
+          margin-left: auto;
+        }
+      }
+
+      .toast-body {
+        .tobago-close {
+          float: right;
+        }
+      }
+
+      .tobago-auto-spacing {
+        margin-bottom: 0;
+      }
+    }
+  }
 }
 
 .tobago-page-noscript {
@@ -1120,17 +1153,6 @@ tobago-page {
   }
 }
 
-.tobago-page-menuStore {
-  @include tobagoDropdownMenu();
-
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 0;
-  height: 0;
-  z-index: $zindex-popover;
-}
-
 /* panel ------------------------------------------------------------- */
 
 // todo: remove
@@ -1742,6 +1764,10 @@ tobago-textarea {
   display: block;
 }
 
+tobago-toasts {
+  display: none;
+}
+
 /* tree ---------------------------------------------------------------------- */
 
 tobago-tree {
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
index 2addc09525..f303c49048 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-all.ts
@@ -50,6 +50,7 @@ import "./tobago-stars";
 import "./tobago-suggest";
 import "./tobago-tab";
 import "./tobago-textarea";
+import "./tobago-toasts";
 import "./tobago-tree";
 import "./tobago-tree-listbox";
 import "./tobago-tree-node";
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
index e377eb5048..a62e727c8f 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-css.ts
@@ -21,6 +21,7 @@ export enum Css {
   ACTIVE = "active",
   AUTOCOMPLETE = "autocomplete",
   AUTOCOMPLETE_INPUT = "autocomplete-input",
+  BOTTOM_0 = "bottom-0",
   COLLAPSE = "collapse",
   COLLAPSING = "collapsing",
   D_NONE = "d-none",
@@ -29,13 +30,22 @@ export enum Css {
   DROPDOWN_ITEM = "dropdown-item",
   DROPDOWN_MENU = "dropdown-menu",
   DROPDOWN_MENU_END = "dropdown-menu-end",
+  END_0 = "end-0",
+  FADE = "fade",
   FIXED_BOTTOM = "fixed-bottom",
   INPUT_GROUP = "input-group",
+  P_3 = "p-3",
+  POSITION_FIXED = "position-fixed",
   POSITION_RELATIVE = "position-relative",
   SHOW = "show",
+  START_0 = "start-0",
+  START_50 = "start-50",
   TABLE_ACTIVE = "table-active",
   TABLE_INFO = "table-info",
   TABLE_PRIMARY = "table-primary",
+  TOAST_BODY = "toast-body",
+  TOAST_CONTAINER = "toast-container",
+  TOAST_HEADER = "toast-header",
   TOBAGO_COLLAPSED = "tobago-collapsed",
   TOBAGO_DISABLED = "tobago-disabled",
   TOBAGO_DROP_ZONE = "tobago-drop-zone",
@@ -50,5 +60,10 @@ export enum Css {
   TOBAGO_SELECTED = "tobago-selected",
   TOBAGO_SHOW = "tobago-show",
   TOBAGO_SPLITLAYOUT_HORIZONTAL = "tobago-splitLayout-horizontal",
+  TOP_0 = "top-0",
+  TOP_50 = "top-50",
+  TRANSLATE_MIDDLE = "translate-middle",
+  TRANSLATE_MIDDLE_X = "translate-middle-x",
+  TRANSLATE_MIDDLE_Y = "translate-middle-y",
   TRASH = "trash"
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts
new file mode 100644
index 0000000000..2e089491de
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-placement.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export enum Placement {
+  topLeft,
+  topCenter,
+  topRight,
+  middleLeft,
+  middleCenter,
+  middleRight,
+  bottomLeft,
+  bottomCenter,
+  bottomRight
+}
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts
new file mode 100644
index 0000000000..d042827238
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-toasts.ts
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Toast} from "bootstrap";
+import {Placement} from "./tobago-placement";
+import {Css} from "./tobago-css";
+
+enum StateEnum {
+  created = "created",
+  showed = "showed",
+  closed = "closed"
+}
+
+interface StateData {
+  state: StateEnum;
+  hideTime: string;
+}
+
+const BootstrapToastEvent = {
+  HIDE: "hide.bs.toast",
+  HIDDEN: "hidden.bs.toast",
+  SHOW: "show.bs.toast",
+  SHOWN: "shown.bs.toast"
+};
+
+class Toasts extends HTMLElement {
+
+  constructor() {
+    super();
+  }
+
+  get toastStore(): HTMLDivElement {
+    const root = document.getRootNode() as ShadowRoot | Document;
+    return root.querySelector<HTMLDivElement>(".tobago-page-toastStore");
+  }
+
+  get localToasts(): NodeListOf<HTMLDivElement> {
+    return this.querySelectorAll(".toast");
+  }
+
+  get storeToasts(): NodeListOf<HTMLDivElement> {
+    return this.toastStore.querySelectorAll(".toast[name^='" + this.id + "::']");
+  }
+
+  get statesInput(): HTMLInputElement {
+    return this.querySelector("input[name='" + this.id + "::states']");
+  }
+
+  get states(): Map<string, StateData> {
+    return new Map(Object.entries(JSON.parse(this.statesInput.value)));
+  }
+
+  set states(states: Map<string, StateData>) {
+    this.statesInput.value = JSON.stringify(Object.fromEntries(states));
+  }
+
+  connectedCallback(): void {
+    for (const storeToast of this.storeToasts) {
+      const localToast = this.querySelector<HTMLDivElement>(".toast[id='" + storeToast.getAttribute("name") + "']");
+      if (!localToast) {
+        this.removeToast(storeToast);
+      }
+    }
+
+    for (const localToast of this.localToasts) {
+      const storeToast = this.toastStore.querySelector<HTMLDivElement>(".toast[name='" + localToast.id + "']");
+      if (storeToast) {
+        this.updateToast(storeToast, localToast);
+      } else {
+        this.showToast(localToast);
+      }
+    }
+  }
+
+  private removeToast(storeToast: HTMLDivElement): void {
+    const toast = new Toast(storeToast, {autohide: false, delay: 0});
+    this.addHiddenEventListeners(storeToast);
+    toast.hide();
+  }
+
+  private updateToast(storeToast: HTMLDivElement, localToast: HTMLDivElement): void {
+    storeToast.replaceChildren(
+        localToast.querySelector("." + Css.TOAST_HEADER),
+        localToast.querySelector("." + Css.TOAST_BODY));
+  }
+
+  private showToast(localToast: HTMLDivElement): void {
+    const placement: Placement = Placement[localToast.dataset.tobagoPlacement];
+    const disposeDelay = Number(localToast.dataset.tobagoDisposeDelay);
+
+    localToast.removeAttribute("id");
+    this.getToastContainer(placement).insertAdjacentElement("beforeend", localToast);
+
+    this.addShowEventListeners(localToast);
+    this.addHideEventListeners(localToast);
+    this.addHiddenEventListeners(localToast);
+
+    const toast = new Toast(localToast, {autohide: disposeDelay > 0, delay: Math.max(disposeDelay, 0)});
+    const id = localToast.getAttribute("name");
+    const stateData = this.states.get(id);
+
+    if (stateData.state === StateEnum.created) {
+      toast.show();
+    } else if (stateData.state === StateEnum.showed) {
+      localToast.classList.add(Css.SHOW);
+      localToast.classList.add(Css.FADE); //set fade for hide animation; set fade after show!
+
+      if (disposeDelay >= 0) {
+        setTimeout(() => {
+          toast.hide();
+        }, disposeDelay);
+      }
+    }
+  }
+
+  private addShowEventListeners(toast: HTMLDivElement) {
+    const id = toast.getAttribute("name");
+    toast.addEventListener("show.bs.toast", () => {
+      const states = this.states;
+      states.get(id).state = StateEnum.showed;
+      this.states = states;
+      faces.ajax.request(this.id, null, {
+        "javax.faces.behavior.event": "toastShown",
+        execute: this.id
+      });
+    });
+
+  }
+
+  private addHideEventListeners(toast: HTMLDivElement) {
+    const id = toast.getAttribute("name");
+    toast.addEventListener("hide.bs.toast", () => {
+      const states = this.states;
+      states.get(id).state = StateEnum.closed;
+      this.states = states;
+      faces.ajax.request(this.id, null, {
+        "javax.faces.behavior.event": "toastHide",
+        execute: this.id
+      });
+    });
+  }
+
+  private addHiddenEventListeners(toast: HTMLDivElement) {
+    toast.addEventListener("hidden.bs.toast", () => {
+      toast.remove();
+    });
+  }
+
+  private getToastContainer(placement: Placement): HTMLDivElement {
+    const cssClasses = this.getToastContainerCssClass(placement);
+    const selector = cssClasses.join(".");
+    const element = this.toastStore.querySelector<HTMLDivElement>("." + selector);
+    if (element) {
+      return element;
+    } else {
+      const div = document.createElement("div");
+      for (const cssClass of cssClasses) {
+        div.classList.add(cssClass);
+      }
+      return this.toastStore.insertAdjacentElement("beforeend", div) as HTMLDivElement;
+    }
+  }
+
+  private getToastContainerCssClass(placement: Placement): Css[] {
+    const css: Css[] = [];
+    css.push(Css.TOAST_CONTAINER);
+    css.push(Css.POSITION_FIXED);
+    css.push(Css.P_3);
+
+    switch (placement) {
+      case Placement.topLeft:
+      case Placement.topCenter:
+      case Placement.topRight:
+        css.push(Css.TOP_0);
+        break;
+      case Placement.middleLeft:
+      case Placement.middleCenter:
+      case Placement.middleRight:
+        css.push(Css.TOP_50);
+        break;
+      case Placement.bottomLeft:
+      case Placement.bottomCenter:
+      case Placement.bottomRight:
+        css.push(Css.BOTTOM_0);
+        break;
+    }
+
+    switch (placement) {
+      case Placement.topLeft:
+      case Placement.middleLeft:
+      case Placement.bottomLeft:
+        css.push(Css.START_0);
+        break;
+      case Placement.topCenter:
+      case Placement.middleCenter:
+      case Placement.bottomCenter:
+        css.push(Css.START_50);
+        break;
+      case Placement.topRight:
+      case Placement.middleRight:
+      case Placement.bottomRight:
+        css.push(Css.END_0);
+        break;
+    }
+
+    switch (placement) {
+      case Placement.topCenter:
+      case Placement.bottomCenter:
+        css.push(Css.TRANSLATE_MIDDLE_X);
+        break;
+      case Placement.middleLeft:
+      case Placement.middleRight:
+        css.push(Css.TRANSLATE_MIDDLE_Y);
+        break;
+      case Placement.middleCenter:
+        css.push(Css.TRANSLATE_MIDDLE);
+        break;
+    }
+
+    return css;
+  }
+}
+
+document.addEventListener("tobago.init", function (event: Event): void {
+  if (window.customElements.get("tobago-toasts") == null) {
+    window.customElements.define("tobago-toasts", Toasts);
+  }
+});
diff --git a/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg b/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
index 3d1ee8b58f..9426af5b30 100644
--- a/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
+++ b/tobago-tool/tobago-tool-apt/src/main/resources/org/apache/myfaces/tobago/apt/component.stg
@@ -395,6 +395,10 @@ AlignItemsProperty(property) ::= <<
 <NormalProperty(property)>
 >>
 
+PlacementProperty(property) ::= <<
+<NormalProperty(property)>
+>>
+
 LocalDateProperty(property) ::= <<
 <NormalProperty(property)>
 >>