Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

Note that the versions prior to `0.6.0` are very unstable.

## Unreleased

### Changed

- Default toast component styles now support Tailwind 3 and 4, and docs are changed accordingly.

### Fixed

- Connection notices can be shown when Tailwind CSS 4 gives the `hidden` attribute precedence over display styles.

## [v0.10.2] (2026-09-04)

### Fixed
Expand Down
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Live Toast is a drop-in replacement for the flash system in Phoenix/LiveView.
- **⚙️ Highly configurable:** Looks good out of the box, but can be changed in pretty much any way you want.
- **🌍 Simple asset delivery:** `LiveToast` simply ships Tailwind classes and lets your project bundle them up. No CSS
drop-in required.
- **🧩 Tailwind compatibility:** The default toast styles work with Tailwind CSS 3.4 and 4.x.

## Installation

Expand Down Expand Up @@ -66,8 +67,24 @@ let liveSocket = new LiveSocket('/live', Socket, {
})
```

Then, add `'../deps/live_toast/lib/**/*.*ex'` to your list of paths Tailwind will look for class names, in your
`tailwind.config.js`:
LiveToast's classes need to be included in Tailwind's source detection. The configuration is different for Tailwind 4
and Tailwind 3.

### Tailwind 4

Add LiveToast as an explicit source in `assets/css/app.css`:

```css
/* Keep your existing Tailwind import and source declarations. */
@source "../../deps/live_toast/lib";
```

The source path is relative to the stylesheet. For an umbrella application, it will usually be
`"../../../../deps/live_toast/lib"`.

### Tailwind 3

Add `'../deps/live_toast/lib/**/*.*ex'` to the `content` paths in `tailwind.config.js`:

```javascript
// assets/tailwind.config.js
Expand All @@ -82,12 +99,17 @@ module.exports = {
}
```

Your particular file will look different but all you need to do is make sure the last line is there.
Your particular file will look different, but all you need to do is make sure the last line is there.

> **Note for Umbrella Apps:**
> If you're using an umbrella application, your paths above may look different. You'll probably have an extra folder in
> there, so the line you need to add would be more like `"../../../deps/live_toast/lib/**/*.*ex"`

LiveToast's default class functions intentionally use syntax supported by both Tailwind 3.4 and 4.x. If you provide a
custom `toast_class_fn` or `group_class_fn`, those classes are compiled by your application and must be compatible with
the Tailwind version it uses. Tailwind's [upgrade guide](https://tailwindcss.com/docs/upgrade-guide) covers changes to
border and ring defaults, important modifiers, arbitrary variants, and other utilities.

Finally, replace your `<.flash_group />` component with the new `<LiveToast.toast_group />`. It's most likely in your
`app.html.heex`:

Expand Down Expand Up @@ -424,14 +446,14 @@ defmodule MyModule do
def toast_class_fn(assigns) do
[
# base classes
"group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",
"group/toast z-[100] pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border border-gray-200 col-start-1 col-end-1 row-start-1 row-end-2",
# start hidden if javascript is enabled
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled)]:[[data-phx-main]_&]:opacity-100",
# used to hide the disconnected flashes
if(assigns[:rest][:hidden] == true, do: "hidden", else: "flex"),
# override styles per severity
assigns[:kind] == :info && "bg-white text-black",
assigns[:kind] == :error && "!text-red-700 !bg-red-100 border-red-200"
assigns[:kind] == :error && "!text-red-700 !bg-red-100 !border-red-200"
]
end
end
Expand Down Expand Up @@ -477,15 +499,15 @@ need to override [`toast_class_fn/1`](https://hexdocs.pm/live_toast/LiveToast.ht
def custom_toast_class_fn(assigns) do
[
# base classes
"bg-white group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",
"bg-white group/toast z-[100] pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border border-gray-200 col-start-1 col-end-1 row-start-1 row-end-2",
# start hidden if javascript is enabled
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled)]:[[data-phx-main]_&]:opacity-100",
# used to hide the disconnected flashes
if(assigns[:rest][:hidden] == true, do: "hidden", else: "flex"),
# override styles per severity
assigns[:kind] == :info && "text-black",
assigns[:kind] == :error && "!text-red-700 !bg-red-100 border-red-200",
assigns[:kind] == :warning && "!text-amber-700 !bg-amber-100 border-amber-200"
assigns[:kind] == :error && "!text-red-700 !bg-red-100 !border-red-200",
assigns[:kind] == :warning && "!text-amber-700 !bg-amber-100 !border-amber-200"
]
end
```
Expand Down
1 change: 1 addition & 0 deletions assets/js/live_toast/live_toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export function createLiveToastHook(duration = 6000, maxItems = 3) {
// (don't want to do this quite yet because 1.0 is pretty new)
// also repeat this on hide.

this.el.removeAttribute('hidden')
this.el.style.display = 'flex'
})

Expand Down
36 changes: 36 additions & 0 deletions assets/test/live_toast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,31 @@ function mountToast(duration: number | 'Infinity' = 1000, countdown = false) {
}
}

function mountConnectionToast() {
document.body.innerHTML = `
<div
id="client-error"
phx-hook="LiveToast"
data-corner="bottom_right"
data-delay="0"
data-duration="0"
hidden
></div>
`

const el = document.getElementById('client-error') as HTMLElement
const callbacks = createLiveToastHook()
const hook = {
el,
pushEvent: () => undefined,
pushEventTo: () => undefined
}

callbacks.mounted.call(hook as never)

return { el }
}

function mountToastGroup() {
document.body.insertAdjacentHTML(
'beforeend',
Expand Down Expand Up @@ -225,6 +250,17 @@ describe('LiveToast timed dismissal', () => {
expect(toast.pushes).toHaveLength(0)
})

test('removes the hidden attribute before showing a connection error', async () => {
const toast = mountConnectionToast()

toast.el.dispatchEvent(new Event('show-error'))
advance(0)
await settle()

expect(toast.el.hidden).toBe(false)
expect(toast.el.style.display).toBe('flex')
})

test('cancels dismissal when the hook is destroyed', () => {
const toast = mountToast()

Expand Down
14 changes: 11 additions & 3 deletions demo/assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../../assets";
@source "../../../lib";
@source "../../lib/demo_web";

@custom-variant phx-no-feedback (.phx-no-feedback&, .phx-no-feedback &);
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);

/* This file is for your main application CSS */
45 changes: 0 additions & 45 deletions demo/assets/tailwind.config.cjs

This file was deleted.

11 changes: 5 additions & 6 deletions demo/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@ config :logger, :console,
config :phoenix, :json_library, Jason

config :tailwind,
version: "3.4.0",
version: "4.3.3",
demo: [
args: ~w(
--config=tailwind.config.cjs
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
--input=assets/css/app.css
--output=priv/static/assets/app.css
),
cd: Path.expand("..", __DIR__)
]

import_config "#{config_env()}.exs"
2 changes: 1 addition & 1 deletion demo/lib/demo_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule DemoWeb.CoreComponents do
<button
type={@type}
class={[
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-xs hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2",
@class
]}
{@rest}
Expand Down
12 changes: 6 additions & 6 deletions demo/lib/demo_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ defmodule DemoWeb.Layouts do
def demo_toast_class_fn(assigns) do
[
# base classes
"bg-white group/toast z-100 pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border col-start-1 col-end-1 row-start-1 row-end-2",
"bg-white group/toast z-[100] pointer-events-auto relative w-full items-center justify-between origin-center overflow-hidden rounded-lg p-4 shadow-lg border border-gray-200 col-start-1 col-end-1 row-start-1 row-end-2",
# start hidden if javascript is enabled
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled){[data-phx-main]_&}]:opacity-100",
"[@media(scripting:enabled)]:opacity-0 [@media(scripting:enabled)]:[[data-phx-main]_&]:opacity-100",
# used to hide the disconnected flashes
if(assigns[:rest][:hidden] == true, do: "hidden", else: "flex"),
# override styles per severity
assigns[:kind] == :info && "text-black",
assigns[:kind] == :error && "!text-red-700 !bg-red-100 border-red-200",
assigns[:kind] == :warn && "!text-amber-700 !bg-amber-100 border-amber-200"
assigns[:kind] == :error && "!text-red-700 !bg-red-100 !border-red-200",
assigns[:kind] == :warn && "!text-amber-700 !bg-amber-100 !border-amber-200"
]
end

Expand All @@ -26,11 +26,11 @@ defmodule DemoWeb.Layouts do
# classes to set container positioning
assigns[:corner] == :bottom_left && "items-end bottom-0 left-0 flex-col-reverse sm:top-auto",
assigns[:corner] == :bottom_center &&
"items-end bottom-0 left-1/2 transform -translate-x-1/2 flex-col-reverse sm:top-auto",
"items-end bottom-0 left-1/2 -translate-x-1/2 flex-col-reverse sm:top-auto",
assigns[:corner] == :bottom_right && "items-end bottom-0 right-0 flex-col-reverse sm:top-auto",
assigns[:corner] == :top_left && "items-start top-0 left-0 flex-col sm:bottom-auto",
assigns[:corner] == :top_center &&
"items-start top-0 left-1/2 transform -translate-x-1/2 flex-col sm:bottom-auto",
"items-start top-0 left-1/2 -translate-x-1/2 flex-col sm:bottom-auto",
assigns[:corner] == :top_right && "items-start top-0 right-0 flex-col sm:bottom-auto"
]
end
Expand Down
6 changes: 3 additions & 3 deletions demo/lib/demo_web/controllers/page_html/home.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@
<div class="mb-12 space-y-4">
<.link
href={~p"/demo?flash[]=info"}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-xs hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
>
Info Flash
</.link>

<.link
href={~p"/demo?flash[]=info&flash[]=error"}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-xs hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
>
Info and Error Flash
</.link>

<.link
href={~p"/demo"}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border border-zinc-200/50 transition-colors bg-zinc-100 text-zinc-900 shadow-xs hover:bg-zinc-200 phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-4 py-2"
>
Reset
</.link>
Expand Down
2 changes: 1 addition & 1 deletion demo/lib/demo_web/live/home_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ defmodule DemoWeb.HomeLive do
<button
type="button"
aria-label="Menu"
class="-mr-2 rounded p-2 active:bg-gray-400/20 md:hidden"
class="-mr-2 rounded-sm p-2 active:bg-gray-400/20 md:hidden"
phx-click={
JS.toggle_class(
"max-md:block",
Expand Down
2 changes: 1 addition & 1 deletion demo/lib/demo_web/live/home_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

<.link
href="https://hexdocs.pm/live_toast/readme.html#installation"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border transition-colors shadow-sm phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-5 py-1 !bg-indigo-500 !border-indigo-500/50 !text-white hover:!bg-indigo-400"
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium border transition-colors shadow-xs phx-submit-loading:opacity-75 active:text-zinc-800 h-9 px-5 py-1 !bg-indigo-500 !border-indigo-500/50 !text-white hover:!bg-indigo-400"
>
Installation
</.link>
Expand Down
6 changes: 3 additions & 3 deletions demo/lib/demo_web/live/tabs/customization.html.heex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h2 class="mb-6 text-2xl font-bold">Customization</h2>

<div class="mb-10 border bg-zinc-50 rounded py-6 px-6 flex gap-4">
<div class="mb-10 border border-gray-200 bg-zinc-50 rounded-sm py-6 px-6 flex gap-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
Expand Down Expand Up @@ -42,8 +42,8 @@
<h2 class="text-lg font-semibold mb-3">Custom Components</h2>
<p>
A custom component renders the toast content inside the LiveToast shell. Use it to control the icon, title, body, actions, and application-specific markup while LiveToast continues to manage timing, animation, and dismissal. Pass it to an individual toast, or define
<code class="text-rose-600 bg-zinc-50 rounded px-1">toast_component_fn</code>
on <code class="text-rose-600 bg-zinc-50 rounded px-1">toast_group</code>
<code class="text-rose-600 bg-zinc-50 rounded-sm px-1">toast_component_fn</code>
on <code class="text-rose-600 bg-zinc-50 rounded-sm px-1">toast_group</code>
to use it for every programmatic toast and Phoenix flash in that host.
</p>
<pre class="overflow-x-auto rounded-md bg-zinc-950 p-4 text-sm text-zinc-100"><code>&lt;LiveToast.toast_group
Expand Down
Loading
Loading