diff --git a/docs/examples/plugins/flash_messages/__init__.py b/docs/examples/plugins/flash_messages/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/examples/plugins/flash_messages/jinja.py b/docs/examples/plugins/flash_messages/jinja.py
new file mode 100644
index 0000000000..6772697642
--- /dev/null
+++ b/docs/examples/plugins/flash_messages/jinja.py
@@ -0,0 +1,9 @@
+from litestar import Litestar
+from litestar.contrib.jinja import JinjaTemplateEngine
+from litestar.plugins.flash import FlashConfig, FlashPlugin
+from litestar.template.config import TemplateConfig
+
+template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
+flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
+
+app = Litestar(plugins=[flash_plugin])
diff --git a/docs/examples/plugins/flash_messages/mako.py b/docs/examples/plugins/flash_messages/mako.py
new file mode 100644
index 0000000000..a5ce038eab
--- /dev/null
+++ b/docs/examples/plugins/flash_messages/mako.py
@@ -0,0 +1,9 @@
+from litestar import Litestar
+from litestar.contrib.mako import MakoTemplateEngine
+from litestar.plugins.flash import FlashConfig, FlashPlugin
+from litestar.template.config import TemplateConfig
+
+template_config = TemplateConfig(engine=MakoTemplateEngine, directory="templates")
+flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
+
+app = Litestar(plugins=[flash_plugin])
diff --git a/docs/examples/plugins/flash_messages/minijinja.py b/docs/examples/plugins/flash_messages/minijinja.py
new file mode 100644
index 0000000000..0ea2ce0f8e
--- /dev/null
+++ b/docs/examples/plugins/flash_messages/minijinja.py
@@ -0,0 +1,9 @@
+from litestar import Litestar
+from litestar.contrib.minijinja import MiniJinjaTemplateEngine
+from litestar.plugins.flash import FlashConfig, FlashPlugin
+from litestar.template.config import TemplateConfig
+
+template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates")
+flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
+
+app = Litestar(plugins=[flash_plugin])
diff --git a/docs/examples/plugins/flash_messages/usage.py b/docs/examples/plugins/flash_messages/usage.py
new file mode 100644
index 0000000000..914919ea0f
--- /dev/null
+++ b/docs/examples/plugins/flash_messages/usage.py
@@ -0,0 +1,26 @@
+from litestar import Litestar, Request, get
+from litestar.contrib.jinja import JinjaTemplateEngine
+from litestar.plugins.flash import FlashConfig, FlashPlugin, flash
+from litestar.response import Template
+from litestar.template.config import TemplateConfig
+
+template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
+flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
+
+
+@get()
+async def index(request: Request) -> Template:
+ """Example of adding and displaying a flash message."""
+ flash(request, "Oh no! I've been flashed!", category="error")
+
+ return Template(
+ template_str="""
+
Flash Message Example
+ {% for message in get_flashes() %}
+ {{ message.message }} (Category:{{ message.category }})
+ {% endfor %}
+ """
+ )
+
+
+app = Litestar(plugins=[flash_plugin], route_handlers=[index], template_config=template_config)
diff --git a/docs/index.rst b/docs/index.rst
index be01d21e10..92953eec9b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,7 +3,7 @@ Litestar library documentation
Litestar is a powerful, flexible, highly performant, and opinionated ASGI framework.
-The Litestar framework supports :doc:`/usage/plugins`, ships
+The Litestar framework supports :doc:`/usage/plugins/index`, ships
with :doc:`dependency injection `, :doc:`security primitives `,
:doc:`OpenAPI schema generation `, `MessagePack `_,
:doc:`middlewares `, a great :doc:`CLI ` experience, and much more.
diff --git a/docs/reference/plugins/flash_messages.rst b/docs/reference/plugins/flash_messages.rst
new file mode 100644
index 0000000000..34d1b411d5
--- /dev/null
+++ b/docs/reference/plugins/flash_messages.rst
@@ -0,0 +1,7 @@
+=====
+flash
+=====
+
+
+.. automodule:: litestar.plugins.flash
+ :members:
diff --git a/docs/reference/plugins/index.rst b/docs/reference/plugins/index.rst
index 5de973df3c..128fdf0302 100644
--- a/docs/reference/plugins/index.rst
+++ b/docs/reference/plugins/index.rst
@@ -9,5 +9,6 @@ plugins
:maxdepth: 1
:hidden:
+ flash_messages
structlog
sqlalchemy
diff --git a/docs/usage/index.rst b/docs/usage/index.rst
index 632375c475..b19abc4ca4 100644
--- a/docs/usage/index.rst
+++ b/docs/usage/index.rst
@@ -22,7 +22,7 @@ Usage
metrics/index
middleware/index
openapi
- plugins
+ plugins/index
responses
security/index
static-files
diff --git a/docs/usage/plugins/flash_messages.rst b/docs/usage/plugins/flash_messages.rst
new file mode 100644
index 0000000000..8ff46b8db6
--- /dev/null
+++ b/docs/usage/plugins/flash_messages.rst
@@ -0,0 +1,73 @@
+==============
+Flash Messages
+==============
+
+.. versionadded:: 2.7.0
+
+Flash messages are a powerful tool for conveying information to the user,
+such as success notifications, warnings, or errors through one-time messages alongside a response due
+to some kind of user action.
+
+They are typically used to display a message on the next page load and are a great way
+to enhance user experience by providing immediate feedback on their actions from things like form submissions.
+
+Registering the plugin
+----------------------
+
+The FlashPlugin can be easily integrated with different templating engines.
+Below are examples of how to register the ``FlashPlugin`` with ``Jinja2``, ``Mako``, and ``MiniJinja`` templating engines.
+
+.. tab-set::
+
+ .. tab-item:: Jinja2
+ :sync: jinja
+
+ .. literalinclude:: /examples/plugins/flash_messages/jinja.py
+ :language: python
+ :caption: Registering the flash message plugin using the Jinja2 templating engine
+
+ .. tab-item:: Mako
+ :sync: mako
+
+ .. literalinclude:: /examples/plugins/flash_messages/mako.py
+ :language: python
+ :caption: Registering the flash message plugin using the Mako templating engine
+
+ .. tab-item:: MiniJinja
+ :sync: minijinja
+
+ .. literalinclude:: /examples/plugins/flash_messages/minijinja.py
+ :language: python
+ :caption: Registering the flash message plugin using the MiniJinja templating engine
+
+Using the plugin
+----------------
+
+After registering the FlashPlugin with your application, you can start using it to add and display
+flash messages within your application routes.
+
+Here is an example showing how to use the FlashPlugin with the Jinja2 templating engine to display flash messages.
+The same approach applies to Mako and MiniJinja engines as well.
+
+.. literalinclude:: /examples/plugins/flash_messages/usage.py
+ :language: python
+ :caption: Using the flash message plugin with Jinja2 templating engine to display flash messages
+
+Breakdown
++++++++++
+
+#. Here we import the requires classes and functions from the Litestar package and related plugins.
+#. We then create our ``TemplateConfig`` and ``FlashConfig`` instances, each setting up the configuration for
+ the template engine and flash messages, respectively.
+#. A single route handler named ``index`` is defined using the ``@get()`` decorator.
+
+ * Within this handler, the ``flash`` function is called to add a new flash message.
+ This message is stored in the request's context, making it accessible to the template engine for rendering in the response.
+ * The function returns a ``Template`` instance, where ``template_str``
+ (read more about :ref:`template strings `)
+ contains inline HTML and Jinja2 template code.
+ This template dynamically displays any flash messages by iterating over them with a Jinja2 for loop.
+ Each message is wrapped in a paragraph (````) tag, showing the message content and its category.
+
+#. Finally, a ``Litestar`` application instance is created, specifying the ``flash_plugin`` and ``index`` route handler in its configuration.
+ The application is also configured with the ``template_config``, which includes the ``Jinja2`` templating engine and the path to the templates directory.
diff --git a/docs/usage/plugins.rst b/docs/usage/plugins/index.rst
similarity index 97%
rename from docs/usage/plugins.rst
rename to docs/usage/plugins/index.rst
index 4911b8da23..ff0cdc1652 100644
--- a/docs/usage/plugins.rst
+++ b/docs/usage/plugins/index.rst
@@ -1,3 +1,4 @@
+=======
Plugins
=======
@@ -84,7 +85,7 @@ Example
The following example shows the actual implementation of the ``SerializationPluginProtocol`` for
`SQLAlchemy `_ models that is is provided in ``advanced_alchemy``.
-.. literalinclude:: ../../litestar/contrib/sqlalchemy/plugins/serialization.py
+.. literalinclude:: ../../../litestar/contrib/sqlalchemy/plugins/serialization.py
:language: python
:caption: ``SerializationPluginProtocol`` implementation example
@@ -123,3 +124,8 @@ signature (their :func:`__init__` method).
.. literalinclude:: /examples/plugins/di_plugin.py
:language: python
:caption: Dynamically generating signature information for a custom type
+
+.. toctree::
+ :titlesonly:
+
+ flash_messages
diff --git a/docs/usage/requests.rst b/docs/usage/requests.rst
index fdc65f2784..485748ac8c 100644
--- a/docs/usage/requests.rst
+++ b/docs/usage/requests.rst
@@ -17,7 +17,7 @@ The type of ``data`` an be any supported type, including
* :class:`TypedDicts `
* Pydantic models
* Arbitrary stdlib types
-* Typed supported via :doc:`plugins
`
+* Typed supported via :doc:`plugins `
.. literalinclude:: /examples/request_data/request_data_2.py
:language: python
diff --git a/litestar/contrib/minijinja.py b/litestar/contrib/minijinja.py
index 6007a18180..1fcd14bc10 100644
--- a/litestar/contrib/minijinja.py
+++ b/litestar/contrib/minijinja.py
@@ -159,7 +159,9 @@ def get_template(self, template_name: str) -> MiniJinjaTemplate:
return MiniJinjaTemplate(self.engine, template_name)
def register_template_callable(
- self, key: str, template_callable: TemplateCallableType[StateProtocol, P, T]
+ self,
+ key: str,
+ template_callable: TemplateCallableType[StateProtocol, P, T],
) -> None:
"""Register a callable on the template engine.
@@ -170,6 +172,12 @@ def register_template_callable(
Returns:
None
"""
+
+ def is_decorated(func: Callable) -> bool:
+ return hasattr(func, "__wrapped__") or func.__name__ not in globals()
+
+ if not is_decorated(template_callable):
+ template_callable = _transform_state(template_callable) # type: ignore[arg-type] # pragma: no cover
self.engine.add_global(key, pass_state(template_callable))
def render_string(self, template_string: str, context: Mapping[str, Any]) -> str:
diff --git a/litestar/plugins/flash.py b/litestar/plugins/flash.py
new file mode 100644
index 0000000000..6b61040120
--- /dev/null
+++ b/litestar/plugins/flash.py
@@ -0,0 +1,74 @@
+"""Plugin for creating and retrieving flash messages."""
+from dataclasses import dataclass
+from typing import Any, Mapping
+
+from litestar.config.app import AppConfig
+from litestar.connection import ASGIConnection
+from litestar.contrib.minijinja import MiniJinjaTemplateEngine
+from litestar.plugins import InitPluginProtocol
+from litestar.template import TemplateConfig
+from litestar.template.base import _get_request_from_context
+from litestar.utils.scope.state import ScopeState
+
+
+@dataclass
+class FlashConfig:
+ """Configuration for Flash messages."""
+
+ template_config: TemplateConfig
+
+
+class FlashPlugin(InitPluginProtocol):
+ """Flash messages Plugin."""
+
+ def __init__(self, config: FlashConfig):
+ """Initialize the plugin.
+
+ Args:
+ config: Configuration for flash messages, including the template engine instance.
+ """
+ self.config = config
+
+ def on_app_init(self, app_config: AppConfig) -> AppConfig:
+ """Register the message callable on the template engine instance.
+
+ Args:
+ app_config: The application configuration.
+
+ Returns:
+ The application configuration with the message callable registered.
+ """
+ if isinstance(self.config.template_config.engine_instance, MiniJinjaTemplateEngine):
+ from litestar.contrib.minijinja import _transform_state
+
+ self.config.template_config.engine_instance.register_template_callable(
+ "get_flashes", _transform_state(get_flashes)
+ )
+ else:
+ self.config.template_config.engine_instance.register_template_callable("get_flashes", get_flashes)
+ return app_config
+
+
+def flash(connection: ASGIConnection, message: str, category: str) -> None:
+ """Add a flash message to the request scope.
+
+ Args:
+ connection: The connection instance.
+ message: The message to flash.
+ category: The category of the message.
+ """
+ scope_state = ScopeState.from_scope(connection.scope)
+ scope_state.flash_messages.append({"message": message, "category": category})
+
+
+def get_flashes(context: Mapping[str, Any]) -> Any:
+ """Get flash messages from the request scope, if any.
+
+ Args:
+ context: The context dictionary.
+
+ Returns:
+ The flash messages, if any.
+ """
+ scope_state = ScopeState.from_scope(_get_request_from_context(context).scope)
+ return scope_state.flash_messages
diff --git a/litestar/utils/scope/state.py b/litestar/utils/scope/state.py
index bed43940e2..2799915a19 100644
--- a/litestar/utils/scope/state.py
+++ b/litestar/utils/scope/state.py
@@ -33,6 +33,7 @@ class ScopeState:
"csrf_token",
"dependency_cache",
"do_cache",
+ "flash_messages",
"form",
"headers",
"is_cached",
@@ -56,6 +57,7 @@ def __init__(self) -> None:
self.dependency_cache = Empty
self.do_cache = Empty
self.form = Empty
+ self.flash_messages = []
self.headers = Empty
self.is_cached = Empty
self.json = Empty
@@ -76,6 +78,7 @@ def __init__(self) -> None:
dependency_cache: dict[str, Any] | EmptyType
do_cache: bool | EmptyType
form: dict[str, str | list[str]] | EmptyType
+ flash_messages: list[dict[str, str]]
headers: Headers | EmptyType
is_cached: bool | EmptyType
json: Any | EmptyType
diff --git a/tests/unit/test_plugins/test_flash.py b/tests/unit/test_plugins/test_flash.py
new file mode 100644
index 0000000000..2a283b63fb
--- /dev/null
+++ b/tests/unit/test_plugins/test_flash.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from enum import Enum
+from pathlib import Path
+
+import pytest
+
+from litestar import Request, get
+from litestar.contrib.jinja import JinjaTemplateEngine
+from litestar.contrib.mako import MakoTemplateEngine
+from litestar.contrib.minijinja import MiniJinjaTemplateEngine
+from litestar.plugins.flash import FlashConfig, FlashPlugin, flash
+from litestar.response import Template
+from litestar.template import TemplateConfig, TemplateEngineProtocol
+from litestar.testing import create_test_client
+
+text_html_jinja = """{% for message in get_flashes() %}{{ message.message }}{% endfor %}"""
+text_html_mako = """<% messages = get_flashes() %>\\
+% for m in messages:
+${m['message']}\\
+% endfor
+"""
+
+
+class CustomCategory(str, Enum):
+ custom1 = "1"
+ custom2 = "2"
+ custom3 = "3"
+
+
+class FlashCategory(str, Enum):
+ info = "INFO"
+ error = "ERROR"
+ warning = "WARNING"
+ success = "SUCCESS"
+
+
+@pytest.mark.parametrize(
+ "engine, template_str",
+ (
+ (JinjaTemplateEngine, text_html_jinja),
+ (MakoTemplateEngine, text_html_mako),
+ (MiniJinjaTemplateEngine, text_html_jinja),
+ ),
+ ids=("jinja", "mako", "minijinja"),
+)
+@pytest.mark.parametrize(
+ "category_enum",
+ (CustomCategory, FlashCategory),
+ ids=("custom_category", "flash_category"),
+)
+def test_flash_plugin(
+ tmp_path: Path,
+ engine: type[TemplateEngineProtocol],
+ template_str: str,
+ category_enum: Enum,
+) -> None:
+ Path(tmp_path / "flash.html").write_text(template_str)
+ text_expected = "".join(
+ [f'message {category.value}' for category in category_enum] # type: ignore[attr-defined]
+ )
+
+ @get("/flash")
+ def flash_handler(request: Request) -> Template:
+ for category in category_enum: # type: ignore[attr-defined]
+ flash(request, f"message {category.value}", category=category.value)
+ return Template("flash.html")
+
+ template_config: TemplateConfig = TemplateConfig(
+ directory=Path(tmp_path),
+ engine=engine,
+ )
+ with create_test_client(
+ [flash_handler],
+ template_config=template_config,
+ plugins=[FlashPlugin(config=FlashConfig(template_config=template_config))],
+ ) as client:
+ r = client.get("/flash")
+ assert r.status_code == 200
+ assert r.text == text_expected