Packages

A Phoenix LiveView component library

Current section

Files

Jump to
ptah_ui lib ptah_ui components hover_gallery.ex
Raw

lib/ptah_ui/components/hover_gallery.ex

defmodule PtahUi.Components.HoverGallery do
@moduledoc """
Hover Gallery — stacks images and reveals each one by hovering
the corresponding invisible column over the container.
Requires the `HoverGallery` JS hook in your `app.js`:
Hooks.HoverGallery = {
mounted() {
const imgs = this.el.querySelectorAll("[data-hg-img]");
const cols = this.el.querySelectorAll("[data-hg-col]");
const dots = this.el.querySelectorAll("[data-hg-dot]");
const show = (i) => {
imgs.forEach((el, j) => el.style.opacity = j === i ? "1" : "0");
dots.forEach((el, j) => {
el.style.opacity = j === i ? "1" : "0.5";
el.style.width = el.style.height = j === i ? "8px" : "6px";
});
};
cols.forEach((col, i) => col.addEventListener("mouseenter", () => show(i)));
this.el.addEventListener("mouseleave", () => show(0));
}
};
## Example
<.hover_gallery id="gallery-1" class="aspect-video rounded-xl">
<:item src="/images/photo1.jpg" alt="Photo 1" />
<:item src="/images/photo2.jpg" alt="Photo 2" />
<:item src="/images/photo3.jpg" alt="Photo 3" />
</.hover_gallery>
<%!-- With custom content --%>
<.hover_gallery id="gallery-2" class="h-48 rounded-xl">
<:item>
<div class="w-full h-full bg-sky-400 flex items-center justify-center text-white font-bold">A</div>
</:item>
<:item>
<div class="w-full h-full bg-rose-400 flex items-center justify-center text-white font-bold">B</div>
</:item>
</.hover_gallery>
"""
use Phoenix.Component
attr :id, :string, required: true, doc: "Required for the HoverGallery JS hook."
attr :dots, :boolean, default: true, doc: "Show indicator dots at the bottom."
attr :class, :string, default: nil
attr :rest, :global
slot :item, required: true, doc: "An image or content panel (up to 10)." do
attr :src, :string, doc: "Image src — renders an <img> tag automatically."
attr :alt, :string, doc: "Alt text when using src."
end
def hover_gallery(assigns) do
~H"""
<div
id={@id}
phx-hook="HoverGallery"
class={["relative overflow-hidden select-none", @class]}
{@rest}
>
<div
:for={{item, i} <- Enum.with_index(@item)}
data-hg-img
class={[
"absolute inset-0 transition-opacity duration-300",
if(i == 0, do: "opacity-100", else: "opacity-0")
]}
>
<img
:if={item[:src]}
src={item.src}
alt={item[:alt] || ""}
class="w-full h-full object-cover"
draggable="false"
/>
<%= render_slot(item) %>
</div>
<div class="absolute inset-0 z-10 flex">
<div :for={_ <- @item} data-hg-col class="flex-1 h-full" />
</div>
<div
:if={@dots and length(@item) > 1}
class="absolute bottom-2.5 inset-x-0 z-20 flex justify-center gap-1.5 pointer-events-none"
>
<span
:for={{_item, i} <- Enum.with_index(@item)}
data-hg-dot={i}
class={[
"rounded-full transition-all duration-300",
if(i == 0, do: "w-2 h-2 bg-white", else: "w-1.5 h-1.5 bg-white/50")
]}
/>
</div>
</div>
"""
end
end