Created
October 31, 2025 09:58
-
-
Save spicychickensauce/092fa3a58daa77c3bec5b173b5801936 to your computer and use it in GitHub Desktop.
LiveView View Transitions Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Mix.install([ | |
| {:phoenix_playground, "~> 0.1"}, | |
| # Use this repo: https://github.com/spicychickensauce/phoenix_live_view/tree/implement-view-transitions | |
| # And run `mix assets.build` to get the compiled js files | |
| {:phoenix_live_view, "~> 1.1", path: "../../oss/phoenix_live_view", override: true} | |
| ]) | |
| defmodule DemoLive do | |
| use Phoenix.LiveView | |
| alias Phoenix.LiveView.JS | |
| def mount(_params, _session, socket) do | |
| {:ok, socket |> assign(entries: Agent.get(DB, & &1)) |> start_view_transition(%{types: ["page"]})} | |
| end | |
| def handle_event("add", _, socket) do | |
| Agent.update(DB, &(&1 ++ [%{id: "#{:rand.uniform(1_000_000)}"}])) | |
| {:noreply, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1)) | |
| |> start_view_transition(%{types: ["same-document"]})} | |
| end | |
| def handle_event("remove", %{"id" => id}, socket) do | |
| Agent.update(DB, fn entries -> Enum.reject(entries, &(&1.id == id)) end) | |
| {:noreply, | |
| socket | |
| |> assign(entries: Agent.get(DB, & &1)) | |
| |> start_view_transition(%{types: ["same-document"]})} | |
| end | |
| def render(assigns) do | |
| ~H""" | |
| <Assets.styles /> | |
| <button class="btn" phx-click="add">Add</button> | |
| <.card :for={entry <- @entries} entry={entry} /> | |
| """ | |
| end | |
| defp card(assigns) do | |
| ~H""" | |
| <div id={"card-#{@entry.id}"} | |
| style={"view-transition-name: card-#{@entry.id}; view-transition-class: card;"} | |
| class="flex gap-2 bg-gray-800 p-2 m-4 rounded"> | |
| <.card_img id={@entry.id} class="h-24" style="" /> | |
| <div class="flex gap-2"> | |
| <.link phx-click={JS.start_view_transition(temp_name: "img-full", to: "#card-#{@entry.id} > div.bg-contain") | |
| |> JS.navigate("/card/#{@entry.id}")}>Entry {@entry.id}</.link> | |
| <div><button class="btn" phx-click="remove" phx-value-id={@entry.id}>Remove</button></div> | |
| </div> | |
| </div> | |
| """ | |
| end | |
| def card_img(assigns) do | |
| assigns = assign(assigns, color: "hsl(#{String.to_integer(assigns.id)/1_000_000}turn 40% 25%)") | |
| ~H""" | |
| <div class={["bg-contain aspect-square bg-[url(https://www.phoenixframework.org/images/icon.svg)]", @class]} | |
| style={"background-color: #{@color};" <> @style}/> | |
| """ | |
| end | |
| end | |
| defmodule CardLive do | |
| use Phoenix.LiveView | |
| def mount(%{"id" => id}, _session, socket) do | |
| {:ok, assign(socket, id: id) |> start_view_transition(%{types: ["page"]})} | |
| end | |
| def render(assigns) do | |
| ~H""" | |
| <Assets.styles /> | |
| <div> | |
| <.link navigate="/">← back</.link> | |
| <h2 class="m-4 font-bold text-xl">Entry {@id}</h2> | |
| <div class="bg-gray-800"> | |
| <DemoLive.card_img id={@id} style={"view-transition-name: img-full"} class="mx-auto h-128" /> | |
| </div> | |
| </div> | |
| """ | |
| end | |
| end | |
| defmodule DemoRouter do | |
| use Phoenix.Router | |
| import Phoenix.LiveView.Router | |
| pipeline :browser do | |
| plug(:accepts, ["html"]) | |
| plug(:fetch_session) | |
| plug(:put_root_layout, html: {PhoenixPlayground.Layout, :root}) | |
| plug(:put_secure_browser_headers) | |
| end | |
| scope "/" do | |
| pipe_through(:browser) | |
| live("/", DemoLive) | |
| live("/card/:id", CardLive) | |
| end | |
| end | |
| defmodule Assets do | |
| use Phoenix.Component | |
| def styles(assigns) do | |
| ~H""" | |
| <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> | |
| <style type="text/tailwindcss"> | |
| html { | |
| background-color: var(--color-gray-950); | |
| } | |
| :root { | |
| color-scheme: dark; | |
| --enter-anim: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, | |
| 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; | |
| --exit-anim: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, | |
| 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; | |
| } | |
| .btn { | |
| @apply bg-slate-600 p-1 rounded | |
| } | |
| @media (prefers-reduced-motion) { | |
| ::view-transition-group(*), | |
| ::view-transition-old(*), | |
| ::view-transition-new(*) { | |
| animation: none !important; | |
| } | |
| } | |
| @view-transition { | |
| navigation: auto; | |
| types: page | |
| } | |
| @keyframes fade-in { | |
| from { opacity: 0; } | |
| } | |
| @keyframes fade-out { | |
| to { opacity: 0; } | |
| } | |
| @keyframes slide-from-right { | |
| from { transform: translateX(30px); } | |
| } | |
| @keyframes slide-to-left { | |
| to { transform: translateX(-30px); } | |
| } | |
| html:active-view-transition-type(page) { | |
| &::view-transition-group(root) { | |
| z-index: -10; | |
| } | |
| &::view-transition-old(root) { | |
| animation: var(--exit-anim); | |
| } | |
| &::view-transition-new(root) { | |
| animation: var(--enter-anim); | |
| } | |
| &::view-transition-new(img-full) { | |
| animation: none; | |
| } | |
| &::view-transition-group(.card) { | |
| z-index: -1; | |
| } | |
| &::view-transition-old(.card) { | |
| animation: var(--exit-anim); | |
| } | |
| &::view-transition-new(.card) { | |
| animation: var(--enter-anim); | |
| } | |
| } | |
| </style> | |
| """ | |
| end | |
| end | |
| Agent.start_link(fn -> [] end, name: DB) | |
| PhoenixPlayground.start(plug: DemoRouter) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment