Kosmesis
Components

Product Gallery

A main-image + thumbnail-strip product gallery. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add product-gallery
pnpm dlx kosmesis add product-gallery
yarn dlx kosmesis add product-gallery
bunx kosmesis add product-gallery

Copy and paste the following code into your project.

product-gallery.tsx
import { StatefulComponent } from "@praxisjs/core";import { Component, Prop, State } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export interface ProductGalleryProps {  images: string[];  alt?: string;  class?: string;}@Component()export class ProductGallery extends StatefulComponent {  @Prop() images: string[] = [];  @Prop() alt = "";  @Prop() class?: string;  @State() _index = 0;  select(index: number): void {    this._index = index;  }  render() {    return (      <div data-slot="product-gallery" class={cn("flex flex-col gap-2", this.class)}>        <div class="aspect-square overflow-hidden rounded-xl bg-muted">          <img src={() => this.images[this._index]} alt={this.alt} class="size-full object-cover" />        </div>        {this.images.length > 1 && (          <div class="flex gap-2">            {this.images.map((src, i) => (              <button                key={src}                type="button"                aria-label={`Show image ${String(i + 1)}`}                data-active={() => (this._index === i ? "" : undefined)}                class="size-14 shrink-0 overflow-hidden rounded-lg border-2 border-transparent opacity-70 transition-opacity data-[active]:border-primary data-[active]:opacity-100"                onClick={() => { this.select(i); }}              >                <img src={src} alt="" class="size-full object-cover" />              </button>            ))}          </div>        )}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

product-gallery.tsx
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Prop, State } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class ProductGalleryStyles extends Stylesheet {  $root = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem" });  $main = this.css({ aspectRatio: "1 / 1", overflow: "hidden", borderRadius: "0.75rem", backgroundColor: t.muted });  $mainImage = this.css({ width: "100%", height: "100%", objectFit: "cover" });  $thumbs = this.css({ display: "flex", gap: "0.5rem" });  $thumb = this.css({    height: "3.5rem",    width: "3.5rem",    flexShrink: 0,    overflow: "hidden",    borderRadius: "0.5rem",    border: "2px solid transparent",    opacity: 0.7,    transition: "opacity 150ms ease, border-color 150ms ease",    cursor: "pointer",  }).on("&[data-active]", { borderColor: t.primary, opacity: 1 });  $thumbImage = this.css({ width: "100%", height: "100%", objectFit: "cover" });}export interface ProductGalleryProps {  images: string[];  alt?: string;  class?: string;}@Component()export class ProductGallery extends StatefulComponent {  @Styled(ProductGalleryStyles) $s!: ProductGalleryStyles;  @Prop() images: string[] = [];  @Prop() alt = "";  @Prop() class?: string;  @State() _index = 0;  select(index: number): void {    this._index = index;  }  render() {    return (      <div data-slot="product-gallery" class={cx(this.$s.$root, this.class)}>        <div class={this.$s.$main}>          <img src={() => this.images[this._index]} alt={this.alt} class={this.$s.$mainImage} />        </div>        {this.images.length > 1 && (          <div class={this.$s.$thumbs}>            {this.images.map((src, i) => (              <button                key={src}                type="button"                aria-label={`Show image ${String(i + 1)}`}                data-active={() => (this._index === i ? "" : undefined)}                class={this.$s.$thumb}                onClick={() => { this.select(i); }}              >                <img src={src} alt="" class={this.$s.$thumbImage} />              </button>            ))}          </div>        )}      </div>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. The thumbnail strip only renders when there's more than one image.

Usage

import { ProductGallery } from "@/components/ui/product-gallery";

<ProductGallery images={["/front.jpg", "/back.jpg", "/detail.jpg"]} alt="Product photo" />

Props

PropTypeDefault
imagesstring[]
altstring""
classstring

On this page