Skip to content

File Uploads and Media

import { useState } from "react";
import { FileUploaderBase, SnapshotImageBase, CardBase } from "@lastshotlabs/snapshot/ui";
interface UploadFileEntry {
file: File;
id: string;
status: "pending" | "uploading" | "completed" | "error";
progress: number;
errorMessage?: string;
}
function AvatarUploader() {
const [files, setFiles] = useState<UploadFileEntry[]>([]);
const [preview, setPreview] = useState<string | null>(null);
return (
<CardBase title="Upload Avatar">
{preview && (
<SnapshotImageBase src={preview} alt="Preview" width={200} height={200} placeholder="skeleton" />
)}
<FileUploaderBase
variant="dropzone"
label="Drop an image here, or click to browse"
accept="image/*"
maxFiles={1}
maxSize={5 * 1024 * 1024}
files={files}
onFilesAdded={(newFiles) => {
const file = newFiles[0];
if (file) {
setPreview(URL.createObjectURL(file));
setFiles([{ file, id: crypto.randomUUID(), status: "pending", progress: 0 }]);
}
}}
onFileRemoved={(id) => {
setFiles((prev) => prev.filter((f) => f.id !== id));
setPreview(null);
}}
/>
</CardBase>
);
}

Drag-and-drop file uploader with three visual variants: dropzone, button, and compact.

Full drop area with label and description:

<FileUploaderBase
variant="dropzone"
label="Drop files here"
description="PNG, JPG, or PDF up to 10 MB"
accept="image/*,.pdf"
maxFiles={5}
maxSize={10 * 1024 * 1024}
onFilesAdded={(files) => uploadFiles(files)}
/>

Single-line upload trigger:

<FileUploaderBase
variant="button"
label="Upload document"
accept=".pdf,.doc,.docx"
maxFiles={1}
onFilesAdded={(files) => uploadFile(files[0])}
/>

Minimal inline variant:

<FileUploaderBase
variant="compact"
accept="image/*"
maxFiles={10}
onFilesAdded={(files) => uploadFiles(files)}
/>

Use controlled files with UploadFileEntry objects to show progress, completion, and errors:

function DocumentUploader() {
const [files, setFiles] = useState<UploadFileEntry[]>([]);
async function handleUpload(newFiles: File[]) {
const entries: UploadFileEntry[] = newFiles.map((file) => ({
file,
id: crypto.randomUUID(),
status: "uploading" as const,
progress: 0,
}));
setFiles((prev) => [...prev, ...entries]);
for (const entry of entries) {
try {
await uploadToServer(entry.file);
setFiles((prev) =>
prev.map((f) => f.id === entry.id ? { ...f, status: "completed", progress: 100 } : f)
);
} catch {
setFiles((prev) =>
prev.map((f) => f.id === entry.id ? { ...f, status: "error", errorMessage: "Upload failed" } : f)
);
}
}
}
return (
<FileUploaderBase
variant="dropzone"
label="Upload documents"
accept=".pdf,.doc,.docx"
maxFiles={10}
maxSize={20 * 1024 * 1024}
files={files}
onFilesAdded={handleUpload}
onFileRemoved={(id) => setFiles((prev) => prev.filter((f) => f.id !== id))}
/>
);
}

Each UploadFileEntry has:

FieldTypeDescription
fileFileThe native File object
idstringUnique identifier
status"pending" | "uploading" | "completed" | "error"Upload state
progressnumberProgress percentage (0-100)
errorMessagestring?Error text if status is "error"
PropTypeDefaultDescription
variant"dropzone" | "button" | "compact""dropzone"Visual variant
labelstringLabel text
descriptionstringDescription below label
acceptstringAccepted file types
maxFilesnumber1Maximum number of files
maxSizenumberMaximum file size in bytes
filesUploadFileEntry[]Controlled file entries
onFilesAdded(files: File[]) => voidCalled when files are added
onFileRemoved(id: string) => voidCalled when a file is removed

Optimized image component with placeholder loading, responsive srcset generation, and format conversion:

import { SnapshotImageBase } from "@lastshotlabs/snapshot/ui";
<SnapshotImageBase
src="/photos/hero.jpg"
alt="Hero image"
width={800}
height={400}
quality={85}
format="webp"
placeholder="blur"
sizes="(max-width: 768px) 100vw, 800px"
/>

Use priority for above-the-fold images to skip lazy loading:

<SnapshotImageBase
src="/photos/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
placeholder="skeleton"
/>
PropTypeDefaultDescription
srcstringrequiredImage URL
altstringrequiredAlt text
widthnumberrequiredDisplay width in px
heightnumberDisplay height in px
qualitynumber75Output quality (1-100)
format"avif" | "webp" | "jpeg" | "png" | "original""original"Output format
placeholder"blur" | "empty" | "skeleton""empty"Loading placeholder
prioritybooleanfalsePreload the image
sizesstringResponsive sizes attribute
aspectRatiostringCSS aspect ratio override

HTML5 video player with poster image support:

import { VideoBase } from "@lastshotlabs/snapshot/ui";
<VideoBase
src="/videos/demo.mp4"
poster="/videos/demo-poster.jpg"
controls
autoPlay={false}
loop={false}
muted={false}
/>

When autoPlay is set, muted defaults to true (required by most browsers for autoplay):

<VideoBase src="/videos/background.mp4" autoPlay loop />
PropTypeDefaultDescription
srcstringrequiredVideo source URL
posterstringPoster image URL
controlsbooleantrueShow playback controls
autoPlaybooleanAuto-play on mount
loopbooleanLoop playback
mutedbooleantrue if autoPlayMute audio

Slide carousel with auto-play, arrow navigation, and dot indicators. Pauses on hover:

import { CarouselBase, SnapshotImageBase } from "@lastshotlabs/snapshot/ui";
<CarouselBase autoPlay interval={5000} showArrows showDots>
<SnapshotImageBase src="/slides/1.jpg" alt="Slide 1" width={800} height={400} />
<SnapshotImageBase src="/slides/2.jpg" alt="Slide 2" width={800} height={400} />
<SnapshotImageBase src="/slides/3.jpg" alt="Slide 3" width={800} height={400} />
</CarouselBase>

Children can be any React elements — not just images:

<CarouselBase showArrows showDots>
<CardBase title="Feature 1">Fast builds with Bun</CardBase>
<CardBase title="Feature 2">113 standalone components</CardBase>
<CardBase title="Feature 3">Full auth out of the box</CardBase>
</CarouselBase>
PropTypeDefaultDescription
childrenReactNode[]requiredSlide elements
autoPlaybooleanAuto-advance slides
intervalnumber5000Auto-advance interval in ms
showArrowsbooleantrueShow prev/next arrows
showDotsbooleantrueShow dot indicators

Responsive iframe embed for videos and external content:

import { EmbedBase } from "@lastshotlabs/snapshot/ui";
<EmbedBase
url="https://www.youtube.com/embed/dQw4w9WgXcQ"
aspectRatio="16/9"
title="Video embed"
/>

URL preview card with metadata. Supports automatic iframe embeds for YouTube, Vimeo, and other platforms:

import { LinkEmbedBase } from "@lastshotlabs/snapshot/ui";
<LinkEmbedBase
url="https://github.com/lastshotlabs/snapshot"
meta={{
title: "Snapshot",
description: "Build React apps with 113 components and 108 hooks",
image: "/og-image.png",
siteName: "GitHub",
}}
allowIframe
maxWidth="600px"
/>

Markdown-based WYSIWYG editor with toolbar, edit/preview/split modes, and custom preview rendering:

import { useState } from "react";
import { RichTextEditorBase } from "@lastshotlabs/snapshot/ui";
function PostEditor() {
const [content, setContent] = useState("");
return (
<RichTextEditorBase
content={content}
onChange={setContent}
placeholder="Write your post..."
mode="edit"
toolbar={["bold", "italic", "heading", "link", "code", "image"]}
minHeight="200px"
maxHeight="500px"
/>
);
}
PropTypeDefaultDescription
contentstringMarkdown content
onChange(content: string) => voidContent change handler
placeholderstringPlaceholder text
readonlybooleanRead-only mode
mode"edit" | "preview" | "split""edit"Editor mode
toolbarboolean | string[]Toolbar config
minHeightstringMinimum editor height
maxHeightstringMaximum editor height
renderPreview(content: string) => ReactNodeCustom preview renderer

Compact rich text input designed for chat and comment fields. Supports formatting features and send-on-Enter:

import { RichInputBase } from "@lastshotlabs/snapshot/ui";
<RichInputBase
placeholder="Type a message..."
features={["bold", "italic", "link"]}
sendOnEnter
maxLength={2000}
showSendButton
onSend={({ html, text }) => sendMessage(text)}
onChange={({ text }) => updateDraft(text)}
/>

Render Markdown content with syntax highlighting and Snapshot design tokens:

import { MarkdownBase } from "@lastshotlabs/snapshot/ui";
<MarkdownBase content={markdownString} maxHeight="400px" />

Syntax-highlighted code block with copy button and line numbers:

import { CodeBlockBase } from "@lastshotlabs/snapshot/ui";
<CodeBlockBase
code={`function hello() {\n console.log("world");\n}`}
language="typescript"
title="hello.ts"
showCopy
showLineNumbers
maxHeight="300px"
/>

Side-by-side text comparison with line numbers and diff highlighting:

import { CompareViewBase } from "@lastshotlabs/snapshot/ui";
<CompareViewBase
left={originalCode}
right={modifiedCode}
leftLabel="Before"
rightLabel="After"
showLineNumbers
maxHeight="400px"
/>
ComponentDescription
BannerBaseAnnouncement banners
HeadingBaseStyled headings
LinkBaseStyled links
TextBaseStyled text
TimelineBaseVertical timeline