Galleries
Overview
Gallery is a set on photos in a grid-like design. Based on your needs, JSXPine provides you 3 types of gallery.
Discover each one and their variants with examples below.
Installation
Notes
To use this component, you need to initialize your project first. If not done yet, run one of the following command:
npx jsxpine init or yarn jsxpine init or pnpm jsxpine init or bunx jsxpine init.
Go to the installation and usage page to learn more.
jsxpine add gallery
Copied !
-------------------- Components Dependencies -------------------------
import { Button } from "./button.component";
import { Icon } from "./icon.component";
import { Image } from "./image.component";
import clsx from "clsx";
/**
* @typedef CarouselNavigationProps
* @property {import("../common/types").DirectionType} direction
*/
/**
* @typedef CarouselIndicatorsProps
* @type {import("../common/props").HTMLTagWithChildren & { indicator?: boolean | import("../common/types").PositionType }}
*/
/**
* @typedef CarouselProps
* @type {{ slides?: import("./image.component").ImageType[], loop?: boolean, indicator?: boolean | import("../common/types").PositionType, direction?: import("../common/types").DirectionType, hideNavigations?: boolean, slidesToShow?: number, navigations?: JSX.Element, indicators?: JSX.Element } & import("../common/props").HTMLTag}
*/
/**
* Carousel Navigation component props
* @type {import("../common/props").JSXComponent<CarouselNavigationProps>}
*/
export function CarouselNavigations(props) {
const { direction } = props;
/**
* @type {Map<import("../common/types").DirectionType, { previous: string; next: string }>}
*/
const navigationButtonDirectionClassMap = new Map([
[
"horizontal",
{
previous: "top-1/2 -translate-y-1/2 left-4",
next: "top-1/2 -translate-y-1/2 right-4"
}
],
[
"vertical",
{
previous: "left-1/2 -translate-x-1/2 top-4",
next: "left-1/2 -translate-x-1/2 bottom-4"
}
]
]);
/**
* @type {Map<import("../common/types").DirectionType, { previous: import("../common/types").IconName; next: import("../common/types").IconName }>}
*/
const navigationIconDirectionClassMap = new Map([
[
"horizontal",
{ previous: "ri.arrow-left-s-line", next: "ri.arrow-right-s-line" }
],
[
"vertical",
{ previous: "ri.arrow-up-s-line", next: "ri.arrow-down-s-line" }
]
]);
const navIcon = navigationIconDirectionClassMap.get(direction);
return (
<>
<Button
x-bind:disabled="!loop && areFirstSlidesToShow()"
x-on:click="previous()"
borderRadius="circle"
class={`absolute ${
navigationButtonDirectionClassMap.get(direction)?.previous
}`}
>
{navIcon ? (
<Icon
name={navIcon.previous}
size={6}
fill="none"
stroke="currentColor"
/>
) : (
""
)}
</Button>
<Button
x-bind:disabled="!loop && areLastSlidesToShow()"
x-on:click="next()"
borderRadius="circle"
class={`absolute ${
navigationButtonDirectionClassMap.get(direction)?.next
}`}
>
{navIcon ? (
<Icon
name={navIcon.next}
size={6}
fill="none"
stroke="currentColor"
/>
) : (
""
)}
</Button>
</>
);
}
/**
* Carousel Indicators component props
* @type {import("../common/props").JSXComponent<CarouselIndicatorsProps>}
*/
export function CarouselIndicators(props) {
const { children, indicator } = props;
/**
* @type {Map<import("../common/types").PositionType, string>}
*/
const indicatorPositionClassMap = new Map([
["top", "top-0 left-1/2 -translate-x-1/2 gap-x-2"],
["bottom", "bottom-0 left-1/2 -translate-x-1/2 gap-x-2"],
["left", "left-0 top-1/2 -translate-y-1/2 flex-col gap-y-2"],
["right", "right-0 top-1/2 -translate-y-1/2 flex-col gap-y-2"]
]);
return (
<ul
class={`absolute flex ${
indicator && typeof indicator !== "boolean"
? indicatorPositionClassMap.get(indicator)
: indicatorPositionClassMap.get("bottom")
} p-4`}
>
{children ?? (
<template x-for="(indicator, index) in $refs.carousel.children">
<li>
<button
x-on:click="setActiveIndex(Number(index))"
x-bind:disabled="isActive(Number(index))"
class="cursor-pointer p-2 rounded-full btn"
></button>
</li>
</template>
)}
</ul>
);
}
/**
* Carousel component props
* @type {import("../common/props").JSXComponent<CarouselProps>}
*/
export function Carousel(props) {
const {
children,
slides,
class: className,
loop = false,
indicator = false,
direction = "horizontal",
hideNavigations = false,
slidesToShow = 1,
navigations,
indicators,
...restProps
} = props;
/**
* @type {Map<import("../common/types").DirectionType, string>}
*/
const directionClassMap = new Map([
["horizontal", "flex items-center"],
[
"vertical",
slidesToShow > 1 ? "flex flex-col items-center" : "grid grid-flow-dense"
]
]);
/**
* @type {Map<import("../common/types").DirectionType, string>}
*/
const slideDirectionClassMap = new Map([
["horizontal", slidesToShow > 1 ? "" : "min-w-full h-full"],
["vertical", slidesToShow > 1 ? "" : "min-w-full h-[inherit]"]
]);
const slideDirectionDimensionStyle =
direction === "horizontal"
? `width: ${100 / slidesToShow}%; height: 100%;`
: `width: 100%; height: ${100 / slidesToShow}%;`;
return (
<div
x-data={`carousel({ loop: ${loop}, slidesToShow: ${slidesToShow} })`}
class={clsx("relative flex", className)}
{...restProps}
>
<div class="relative flex grow h-full">
<div
x-ref="carousel"
class={`${directionClassMap.get(direction)} overflow-hidden`}
>
{children ??
slides?.map((slide) => (
<Image
class={slideDirectionClassMap.get(direction)}
style={slideDirectionDimensionStyle}
src={slide.src}
alt={slide.alt}
/>
))}
</div>
{!hideNavigations ? (
<>{navigations ?? <CarouselNavigations direction={direction} />}</>
) : null}
</div>
{indicator === true || typeof indicator !== "boolean" ? (
<>{indicators ?? <CarouselIndicators indicator={indicator} />}</>
) : null}
</div>
);
}
import clsx from "clsx";
/**
* @typedef HTMLButtonTagWithChildren
* @type {import("@kitajs/html").PropsWithChildren<Omit<JSX.HtmlButtonTag, "className"> & import("../common/props").CLSXClassProps>}
*/
/**
* @typedef {Object} ButtonProps
* @type {{ text?: string } & HTMLButtonTagWithChildren & import("../common/props").BorderRadiusProps & import("../common/props").SizeProps & import("../common/props").VariantColorProps}
*/
/**
* Button component props
* @param {ButtonProps} props
*/
export function Button({
children,
class: className,
size = "md",
text,
variant = "solid",
borderRadius = "rounded",
...restProps
}) {
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantColorMap = new Map([
["solid", "btn"],
["outlined", "btn-outlined"],
["inversed", "btn-inversed"]
]);
/**
* @type {Map<import("../common/types").SizeType, string>}
*/
const sizeMap = new Map([
["xs", "text-xs px-2 py-1"],
["sm", "px-4 py-2 text-sm"],
["md", "px-4 py-2"],
["lg", "px-8 py-4 text-lg"],
["xl", "px-12 py-6 text-xl"],
["2xl", "px-16 py-8 text-4xl"]
]);
/**
* @type {Map<import("../common/types").BorderRadiusType, string>}
*/
const borderRadiusMap = new Map([
["square", "rounded-none"],
["rounded", "rounded"],
["arc", "rounded-xl"],
["pill", "rounded-full"],
["curve", "rounded-lg"],
["circle", "aspect-square rounded-full"]
]);
return (
<button
class={clsx(
variantColorMap.get(variant),
sizeMap.get(size),
borderRadiusMap.get(borderRadius),
className
)}
{...restProps}
>
{text ? <span safe>{text}</span> : children}
</button>
);
}
/**
* Primary Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function PrimaryButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-primary"],
["outlined", "btn-primary-outlined"],
["inversed", "btn-primary-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
/**
* Secondary Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function SecondaryButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-secondary"],
["outlined", "btn-secondary-outlined"],
["inversed", "btn-secondary-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
/**
* Success Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function SuccessButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-success"],
["outlined", "btn-success-outlined"],
["inversed", "btn-success-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
/**
* Danger Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function DangerButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-danger"],
["outlined", "btn-danger-outlined"],
["inversed", "btn-danger-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
/**
* Info Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function InfoButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-info"],
["outlined", "btn-info-outlined"],
["inversed", "btn-info-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
/**
* Warning Button component props
* @type {import("../common/props").JSXComponent<ButtonProps>}
*/
export function WarningButton(props) {
const { children, class: className, variant = "solid", ...restProps } = props;
/**
* @type {Map<import("../common/types").VariantColorType, string>}
*/
const variantClassMap = new Map([
["solid", "btn-warning"],
["outlined", "btn-warning-outlined"],
["inversed", "btn-warning-inversed"]
]);
return (
<Button
class={[variantClassMap.get(variant), className].join(" ")}
{...restProps}
>
{children}
</Button>
);
}
import { icons } from "@iconify-json/ri/icons.json";
import clsx from "clsx";
import { SVG } from "./svg.component";
/**
* @typedef {`ri.${keyof typeof import("@iconify-json/ri/icons.json")["icons"]}`} IconName
*/
/**
* @typedef IconProps
* @type {{size?: number, name: IconName, color?: string, applyDefsId?: string} & import("./svg.component").SVGProps}
*/
/**
* Icon component props
* @param {Omit<IconProps, "viewBox">} props
*/
export function Icon({
children,
size = 4,
name,
applyDefsId,
class: className,
...restProps
}) {
const iconType = name.split(".")[0];
const iconName = /** @type {keyof typeof icons} */ (name.split(".")[1]);
if (!icons[iconName]) {
console.error(`"${name}" is not an icon from iconify/${iconType}`);
return "";
}
const { body } = icons[iconName];
// The purpose is to retrieve value from d attribute
let retrieveDValue = "";
const bodyMatch = body.match(/d=".+"/g);
const retrieveDAttribute = bodyMatch ? bodyMatch[0] : "";
if (retrieveDAttribute) {
retrieveDValue = retrieveDAttribute.slice(3, -2);
}
return (
<SVG
viewBox={"0 0 24 24"}
{...restProps}
class={clsx(`size-${size}`, className)}
>
{children}
<path
stroke-linecap="round"
stroke-linejoin="round"
fill={applyDefsId ? `url(#${applyDefsId})` : "currentColor"}
d={retrieveDValue}
/>
</SVG>
);
}
/**
* @typedef SVGProps
* @type {{fill?: string, stroke?: string, strokeWidth?: number, viewBox: string } & Omit<JSX.HtmlTag, "className"> & import("../common/props").CLSXClassProps}
*/
/**
* SVG component props
* @type {import("../common/props").JSXComponent<SVGProps>}
*/
export function SVG(props) {
const {
children,
class: className,
fill = "none",
stroke = "currentColor",
strokeWidth = 0.5,
...restProps
} = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill={fill}
stroke={stroke}
class={className}
style={{ strokeWidth: String(strokeWidth) }}
{...restProps}
>
{children}
</svg>
)
}
/**
* @typedef {Object} ImageType
* @property {string} src
* @property {string} [alt]
*/
/**
* @typedef ImageProps
* @type {Omit<JSX.HtmlImageTag, "className" | "loading"> & import("../common/props").CLSXClassProps}
*/
/**
* Image Component props
* @type {import("../common/props").JSXComponent<ImageProps>}
*/
export function Image(props) {
const { ...restProps } = props;
return (
<img {...{ ...restProps, loading: "lazy" }} />
)
}
import { Button } from "./button.component";
import { Icon } from "./icon.component";
import { Image } from "./image.component";
import clsx from "clsx";
/**
* @typedef ZoomProps
* @type {import("../common/props").HTMLTag & { selector?: string, showNavigation?: boolean }}
*/
/**
* Zoom component props
* @type {import("../common/props").JSXComponent<ZoomProps>}
*/
export function Zoom(props) {
const {
children,
class: className,
selector = "body",
showNavigation = false
} = props;
return (
<div
x-data={`zoom(${showNavigation})`}
class={clsx("w-full h-full select-none", className)}
>
{children}
<template x-teleport={selector}>
<div
x-bind="shower"
x-transition:enter="transition ease-in-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:leave="transition ease-in-in duration-300"
x-transition:leave-end="opacity-0"
{...{
"@click": "zoomOut",
"@keyup.window.escape": "zoomOut",
"@keyup.left.window": "showNavigation && previousImage($event)",
"@keyup.right.window": "showNavigation && nextImage($event)",
"x-trap.inert.noscroll": "zoom"
}}
class="fixed inset-0 z-[99] flex items-center justify-center bg-black bg-opacity-50 select-none cursor-zoom-out"
x-cloak="true"
>
<div class="relative flex items-center justify-center aspect-square xl:aspect-video">
{showNavigation && (
<Button
x-on:click="previousImage($event)"
x-bind:disabled="disablePreviousNavigation"
class="absolute left-0 flex items-center justify-center text-white translate-x-10 rounded-full cursor-pointer xl:-translate-x-24 2xl:-translate-x-32 bg-neutral-800/75 w-14 h-14 hover:bg-white/20"
>
<Icon name="ri.arrow-left-line" size={6} />
</Button>
)}
<Image
x-show="zoom"
x-transition:enter="transition ease-in-out duration-300"
x-transition:enter-start="opacity-0 transform scale-50"
x-transition:leave="transition ease-in-in duration-300"
x-transition:leave-end="opacity-0 transform scale-50"
class="object-contain object-center w-full h-full select-none cursor-zoom-out"
x-bind:src="selectedImage?.src"
x-bind:alt="selectedImage?.alt"
/>
{showNavigation && (
<Button
x-on:click="nextImage($event)"
x-bind:disabled="disableNextNavigation"
class="absolute right-0 flex items-center justify-center text-white -translate-x-10 rounded-full cursor-pointer xl:translate-x-24 2xl:translate-x-32 bg-neutral-800/75 w-14 h-14 hover:bg-white/20"
>
<Icon name="ri.arrow-right-line" size={6} />
</Button>
)}
</div>
</div>
</template>
</div>
);
}
-------------------- Gallery Component -------------------------
/**
* @typedef CarouselGalleryProps
* @type {Omit<import("./carousel.component").CarouselProps, "slidesToShow" | "indicators" | "slides"> & { images: import("./image.component").ImageType[], thumbnailWidth?: string, zoom?: boolean }}
*/
import { Carousel } from "./carousel.component";
import { Image } from "./image.component";
import { Zoom } from "./zoom.component";
import clsx from "clsx";
/**
* @typedef {"show-all" | "zoom"} RestGalleryRestEffectType
*/
/**
* @typedef RestGalleryProps
* @type {{ images?: import("./image.component").ImageType[], nbDisplayedImages?: number, restEffect?: RestGalleryRestEffectType, zoom?: boolean } & import("../common/props").HTMLTag}
*/
/**
* Carousel Gallery component props
* @type {import("../common/props").JSXComponent<CarouselGalleryProps>}
*/
export function CarouselGallery(props) {
const {
images = [],
thumbnailWidth = "10rem",
direction = "horizontal",
zoom = false,
...restProps
} = props;
return (
<Zoom>
<Carousel
slides={images}
{...restProps}
class={direction === "horizontal" ? "flex-col" : "flex-row"}
x-init={
zoom
? `
[...$refs.carousel.children].forEach((element) => {
element.classList.add("cursor-zoom-in");
element.addEventListener("click", () => {
zoomIn({ src: element.src, alt: element.alt });
})
})
`
: undefined
}
indicator
indicators={
<ul
class={clsx(
"items-center overflow-auto bg-black grow",
direction === "vertical"
? "flex flex-col -order-1"
: "grid grid-flow-col-dense"
)}
x-init={`
if (${direction === "vertical"}) {
$el.style.height = $refs.carousel.getBoundingClientRect().height + 'px';
}
`}
x-ref="thumbnails"
>
<template x-for="(indicator, index) in items">
<Image
x-bind:src="indicator.src"
x-bind:alt="indicator.alt"
x-on:click="setActiveIndex(Number(index))"
x-bind:class="{ 'opacity-50 cursor-not-allowed': isActive(Number(index)), 'cursor-pointer': !isActive(Number(index)) }"
x-init={`
$watch("activeIndex", () => {
if (isActive(Number(index))) {
xDistance = $el.getBoundingClientRect().x - $refs["thumbnails"].getBoundingClientRect().x;
yDistance = $el.getBoundingClientRect().y - $refs["thumbnails"].getBoundingClientRect().y;
$refs["thumbnails"].scrollBy(xDistance, yDistance);
}
})
`}
style={`min-width: ${thumbnailWidth}`}
class={clsx(
direction === "vertical" ? "w-full" : "h-full",
"object-cover"
)}
/>
</template>
</ul>
}
/>
</Zoom>
);
}
/**
*
* @type {import("../common/props").JSXComponent<RestGalleryProps>}
*/
export function RestGallery(props) {
const {
images,
nbDisplayedImages = 3,
restEffect = "show-all",
class: className,
zoom = false,
...restProps
} = props;
return (
<Zoom>
<ul
x-data={`restGallery(${JSON.stringify(
images
)}, ${nbDisplayedImages}, "${restEffect}")`}
class={className}
{...restProps}
x-ref="restGallery"
>
<template x-for="image in displayedImages">
<li>
<Image
x-on:click={zoom && `zoomIn(image)`}
x-bind:src="image.src"
x-bind:alt="image.alt"
class={clsx("object-cover h-full", { "cursor-zoom-in": zoom })}
/>
</li>
</template>
<template x-if="displayedImages.length !== images.length">
<li
class="flex items-center justify-center gap-x-2 bg-slate-800 text-white text-2xl cursor-pointer"
x-on:click="clickOnRestImage()"
x-ref="restImage"
>
<span class="">+</span>
<span x-text="nbRestImages"></span>
</li>
</template>
</ul>
</Zoom>
);
}
-------------------- Alpine Dependencies -------------------------
/**
* @typedef {Object} RestGalleryDataOutput
* @property {import("../../../components/image.component").ImageType[]} images
* @property {number} nbDisplayedImages
* @property {number} nbRestImages
* @property {import("../../../components/image.component").ImageType[]} displayedImages
* @property {Function} showAllImages
* @property {Function} updateRestImageHeight
* @property {(images: import("../../../components/image.component").ImageType[]) => Promise<void>} initializeImage
*/
/**
* Rest Gallery data props
* @param {import("../../../components/image.component").ImageType[]} images
* @param {number} nbDisplayedImages
* @param {import("../../../components/galleries.component").RestGalleryRestEffectType} restEffect
* @returns {import("alpinejs").AlpineComponent<RestGalleryDataOutput>}
*/
export function restGalleryData(
images = [],
nbDisplayedImages = 3,
restEffect = "show-all"
) {
return {
async init() {
this.initializeImage(this.images);
this.$watch("images", async (imagesValue) => {
this.initializeImage(imagesValue);
});
},
images,
nbDisplayedImages,
nbRestImages: 0,
displayedImages: [],
showAllImages() {
this.displayedImages = this.images;
},
clickOnRestImage() {
switch (restEffect) {
case "zoom":
break;
case "show-all":
default:
this.showAllImages();
break;
}
},
updateRestImageHeight() {
const firstChildrenHeight =
this.$refs.restGallery.children[1].getBoundingClientRect().height;
if (this.$refs.restImage) {
this.$refs.restImage.style.height = firstChildrenHeight + "px";
}
},
async initializeImage(images) {
this.displayedImages = images.slice(0, this.nbDisplayedImages);
this.nbRestImages = images.length - this.nbDisplayedImages;
await this.$nextTick();
this.updateRestImageHeight();
window.addEventListener("resize", async () => {
await this.$nextTick();
this.updateRestImageHeight();
});
}
};
}
Rest Gallery
Take RestGallery as a way to reduce display all photos. Because too many images can slower your page loading, it's pragmatic to display a few.
With RestGallery, you set a number of images to display, and then, choose to display the others. By default, 3 images are displayed.
Here below some examples with different displayed images.
P.S: Notice that it's up to you to design gallery layout with class props. Thanks to tailwind, you can fully customize it.
- +
- +7
- +
- +7
- +
- +6
Copied !
import { RestGallery } from "$components/gallery.component";
/**
* @type {import("$common/props").JSXComponent<{ images: import("$components/image.component").ImageType[]}>}
*/
export function DefaultRestGalleryExample(props) {
const { images } = props;
return (
<div class="flex flex-col gap-y-12">
<RestGallery class="flex flex-wrap [&_li]:w-1/2" images={images} />
<hr />
<RestGallery class="flex flex-wrap [&_li]:w-1/4" images={images} />
<hr />
<RestGallery
nbDisplayedImages={4}
class="grid grid-cols-2 w-3/4 mx-auto gap-2"
images={images}
/>
</div>
);
}
Carousel Gallery
A trendy gallery is obviously carousel. It's a combination of Carousel component with a set of thumbnails as indicators.
You can change direction with _direction_ props and define the thumbnail width with thumbnailWidth props.
See example below:




























































Copied !
import { CarouselGallery } from "$components/gallery.component";
/**
* @type {import("$common/props").JSXComponent<{ images: import("$components/image.component").ImageType[]}>}
*/
export function DefaultCarouselGalleryExample(props) {
const { images } = props;
return (
<div class="bg-slate-200 p-4 flex flex-col gap-y-8 relative w-full">
<CarouselGallery images={images} />
<CarouselGallery direction="vertical" images={images} />
<CarouselGallery thumbnailWidth="200px" images={images} />
</div>
);
}
Zoom Gallery
Because it's a well-known feature, gallery has zoom props which, you already guess, provides a zoom of the clicked image.
See these two examples below respectively about rest and carousel galleries.
- +
- +7
Copied !
import { RestGallery } from "$components/gallery.component";
/**
* @type {import("$common/props").JSXComponent<{ images: import("$components/image.component").ImageType[]}>}
*/
export function ZoomRestGalleryExample(props) {
const { images } = props;
return <RestGallery images={images} zoom />;
}




















Copied !
import { CarouselGallery } from "$components/gallery.component";
/**
* @type {import("$common/props").JSXComponent<{ images: import("$components/image.component").ImageType[]}>}
*/
export function ZoomCarouselGalleryExample(props) {
const { images } = props;
return <CarouselGallery images={images} zoom />;
}