Basic image loading
Each page uses a normal image URL. This maintained styling reference composes the public viewer primitives with Tailwind utilities and deliberately does not import @publira/comic-viewer/default.css. Nothing here is decoration: the rail turns pages because of w-[300%] and the translateX(…) transforms, and the toolbar and the navigation hide through aria-hidden:opacity-0.
The whole reader is one component, and every other demo of this app is a component of its own carrying the same tree. Nothing is shared between them, so the file below is the whole of what this page runs, and the file on any other page is the whole of what that page runs.
Source code
1"use client";23import * as ComicViewer from "@publira/comic-viewer";4import type { ViewerPage } from "@publira/comic-viewer";5import { ChevronLeft, ChevronRight } from "lucide-react";67const navigationButtonClassName =8 "pointer-events-auto absolute top-1/2 grid size-10 -translate-y-1/2 place-items-center rounded-full bg-black/60 p-0 text-slate-100 shadow-lg outline-offset-2 outline-slate-100 transition hover:bg-black/80 focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50";910/**11 * The page-turn buttons over the page. The logical `start-3` and `end-3` place12 * them on the sides the reading direction gives them, and the icons point the13 * way a page turn goes, which the viewer context reports.14 */15const PageNavigation = () => {16 const { readingDirection } = ComicViewer.useViewerContext();17 const PreviousIcon = readingDirection === "rtl" ? ChevronRight : ChevronLeft;18 const NextIcon = readingDirection === "rtl" ? ChevronLeft : ChevronRight;1920 return (21 <ComicViewer.PageNavigation className="pointer-events-none absolute inset-0 z-10 transition duration-150 ease-out aria-hidden:translate-y-2 aria-hidden:opacity-0">22 <ComicViewer.PreviousPageButton23 className={`start-3 ${navigationButtonClassName}`}24 >25 <PreviousIcon aria-hidden="true" className="size-6" />26 </ComicViewer.PreviousPageButton>27 <ComicViewer.NextPageButton28 className={`end-3 ${navigationButtonClassName}`}29 >30 <NextIcon aria-hidden="true" className="size-6" />31 </ComicViewer.NextPageButton>32 </ComicViewer.PageNavigation>33 );34};3536interface ReaderProps {37 pages: readonly ViewerPage[];38}3940/**41 * A comic reader styled with Tailwind utilities alone: this demo composes the42 * public viewer primitives and never imports43 * `@publira/comic-viewer/default.css`. Nothing here is decoration — the rail44 * turns pages because of `w-[300%]` and the `translateX(…)` transforms, and the45 * toolbar and the navigation hide through `aria-hidden:opacity-0`.46 */47export const Reader = ({ pages }: ReaderProps) => (48 <ComicViewer.Root49 className="relative flex h-full min-h-0 w-full min-w-0 overflow-hidden rounded-xl bg-slate-950 text-slate-100 shadow-2xl shadow-slate-950/30"50 pages={pages}51 >52 {/* The track is three viewports wide and turns pages by translating itself.53 The page set, the slot, and the page each align the half of the spread they54 hold through the `data-page-side` attribute the rail reports. */}55 <ComicViewer.Viewport className="group/viewport relative flex min-h-0 min-w-0 flex-1 touch-pan-y overflow-hidden data-[pannable]:cursor-grab data-[pannable]:touch-none data-[panning]:cursor-grabbing">56 <ComicViewer.ViewportTrack className="flex h-full w-[300%] shrink-0 basis-[300%] [transform:translateX(calc(-33.3333%_+_var(--pcv-drag-offset)))] data-[dragging]:transition-none data-[transition-state=active]:transition-transform data-[transition-state=active]:duration-[260ms] data-[transition-state=active]:ease-out data-[transition-state=active]:data-[slide-direction=left]:[transform:translateX(calc(-66.6667%_+_var(--pcv-drag-offset)))] data-[transition-state=active]:data-[slide-direction=right]:[transform:translateX(var(--pcv-drag-offset))]">57 {/* The pan and the zoom of a pinch gesture apply to the spread on screen. */}58 <ComicViewer.ViewportPageSet className="flex h-full min-w-0 shrink-0 basis-1/3 data-[page-side=left]:justify-start data-[page-side=right]:justify-end data-[rail-slot=current]:[transform:translate(var(--pcv-pan-x,0)_var(--pcv-pan-y,0))_scale(var(--pcv-zoom-scale,1))]">59 {/* A page that is a whole spread on its own keeps both halves of the set,60 so the double-page basis it would otherwise take is given back to it. */}61 <ComicViewer.ViewportPageSlot className="flex min-w-0 flex-1 items-center justify-center data-[page-side=left]:justify-end data-[page-side=right]:justify-start data-[view-mode=double]:max-w-1/2 data-[view-mode=double]:basis-1/2 data-[view-mode=double]:data-[page-layout=spread]:max-w-full data-[view-mode=double]:data-[page-layout=spread]:basis-full">62 <ComicViewer.ViewportPage className="flex h-full w-full min-w-0 items-center justify-center data-[page-side=left]:justify-end data-[page-side=right]:justify-start">63 {/* The page fit mode the viewport reports is what sizes the image. */}64 <ComicViewer.PageCanvas className="h-full max-w-full bg-slate-900 object-contain transition-[filter] duration-150 group-data-[page-fit-mode=actual]/viewport:h-auto group-data-[page-fit-mode=actual]/viewport:w-auto group-data-[page-fit-mode=actual]/viewport:max-w-none group-data-[page-fit-mode=width]/viewport:h-auto group-data-[page-fit-mode=width]/viewport:w-full group-data-[page-fit-mode=width]/viewport:max-w-none data-[placeholder]:brightness-75 data-[placeholder]:saturate-75" />65 </ComicViewer.ViewportPage>66 </ComicViewer.ViewportPageSlot>67 </ComicViewer.ViewportPageSet>68 </ComicViewer.ViewportTrack>69 </ComicViewer.Viewport>70 {/* The toolbar slides out of sight with the `aria-hidden` the viewer sets71 on it while the reader controls are at rest. */}72 <ComicViewer.Toolbar className="absolute inset-x-0 bottom-0 z-10 flex items-center gap-2 bg-linear-to-t from-black/80 via-black/55 to-transparent px-3 pt-8 pb-3 transition duration-150 ease-out aria-hidden:translate-y-2 aria-hidden:opacity-0">73 {/* The slider reports the share of the document its thumb rests at as74 `--pcv-page-progress-fill`, which paints the part of the track behind it,75 and the fill runs the way the reader turns pages. */}76 <ComicViewer.PageProgress className="mx-auto min-w-0 shrink basis-3/5">77 <ComicViewer.PageProgressSlider className="block h-3.5 w-full cursor-pointer appearance-none bg-transparent p-0 outline-offset-4 outline-slate-100 [--pcv-page-progress-fill-direction:to_right] focus-visible:outline-2 disabled:cursor-not-allowed disabled:opacity-50 rtl:[--pcv-page-progress-fill-direction:to_left] [&::-moz-range-thumb]:size-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-slate-100 [&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-black/65 [&::-moz-range-track]:[background-image:linear-gradient(var(--pcv-page-progress-fill-direction),var(--color-slate-100)_var(--pcv-page-progress-fill),transparent_var(--pcv-page-progress-fill))] [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-black/65 [&::-webkit-slider-runnable-track]:[background-image:linear-gradient(var(--pcv-page-progress-fill-direction),var(--color-slate-100)_var(--pcv-page-progress-fill),transparent_var(--pcv-page-progress-fill))] [&::-webkit-slider-thumb]:-mt-[0.3125rem] [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:bg-slate-100" />78 <ComicViewer.PageStatus className="mt-1.5 block text-center text-sm text-slate-100" />79 </ComicViewer.PageProgress>80 </ComicViewer.Toolbar>81 <PageNavigation />82 </ComicViewer.Root>83);