Skip to content

UI components reference

Prop tables for @media-sdk/ui-react@^0.3.0. All components are presentational — your app owns data fetching and state.

bash
pnpm add @media-sdk/ui-react@^0.3.0

Wiring diagram

mermaid
flowchart TB
  subgraph app ["Your app"]
    state["Query, tab, filter state"]
    caps["useMediaCapabilities()"]
    search["useMediaSearch / useMediaVideos"]
  end

  subgraph ui ["@media-sdk/ui-react"]
    bar["SearchBar"]
    tabs["MediaTabs"]
    grid["PhotoGrid / VideoGrid"]
    pag["Pagination"]
    load["LoadingState / ErrorState"]
    prev["PhotoPreview / VideoPreview"]
  end

  state --> bar
  caps --> state
  search --> grid
  search --> pag
  search --> load
  grid --> prev

Controlled search input with a submit button. Your app owns the query string and submit handler.

PropTypeRequiredDescription
valuestringYesCurrent input value
onChange(value: string) => voidYesCalled when the input value changes
onSubmit(event: FormEvent<HTMLFormElement>) => voidYesCalled on form submit (typically event.preventDefault() then trigger search)

Example

tsx
import { SearchBar } from "@media-sdk/ui-react";
import { useState } from "react";

function AppSearch({
  onSearch,
}: {
  onSearch: (query: string) => void;
}) {
  const [query, setQuery] = useState("");

  return (
    <SearchBar
      value={query}
      onChange={setQuery}
      onSubmit={(event) => {
        event.preventDefault();
        onSearch(query);
      }}
    />
  );
}

MediaTabs

Photos / Videos tab switcher. Your app controls which tab is active and responds to changes.

PropTypeRequiredDescription
activeTabMediaTabYesCurrent tab: "photos" or "videos"
onTabChange(tab: MediaTab) => voidYesCalled when the user selects a tab

Types

ts
type MediaTab = "photos" | "videos";

Example

tsx
import { MediaTabs, type MediaTab } from "@media-sdk/ui-react";
import { useState } from "react";

function TabbedSearch() {
  const [activeTab, setActiveTab] = useState<MediaTab>("photos");

  return (
    <MediaTabs
      activeTab={activeTab}
      onTabChange={setActiveTab}
    />
  );
}

Gate the Videos tab in your app with useMediaCapabilities().operations.searchVideos if needed.


PhotoGrid

Renders a grid of PhotoCard items.

PropTypeRequiredDescription
photosPhoto[]YesPhotos to display (from useMediaSearch or direct client calls)
onPhotoSelect(photo: Photo) => voidYesCalled when the user clicks a photo card

Example

tsx
import { PhotoGrid } from "@media-sdk/ui-react";
import type { Photo } from "@media-sdk/core";

function Photos({
  photos,
  onPhotoSelect,
}: {
  photos: Photo[];
  onPhotoSelect: (photo: Photo) => void;
}) {
  return (
    <PhotoGrid
      photos={photos}
      onPhotoSelect={onPhotoSelect}
    />
  );
}

VideoGrid

Renders a grid of VideoCard items. Same pattern as PhotoGrid.

PropTypeRequiredDescription
videosVideo[]YesVideos to display (from useMediaVideos or direct client calls)
onVideoSelect(video: Video) => voidYesCalled when the user clicks a video card

Example

tsx
import { VideoGrid } from "@media-sdk/ui-react";
import type { Video } from "@media-sdk/core";

function Videos({
  videos,
  onVideoSelect,
}: {
  videos: Video[];
  onVideoSelect: (video: Video) => void;
}) {
  return (
    <VideoGrid
      videos={videos}
      onVideoSelect={onVideoSelect}
    />
  );
}

PhotoPreview

Modal overlay for a selected photo. Your app controls visibility — render only when a photo is selected.

PropTypeRequiredDescription
photoPhotoYesPhoto to display
onClose() => voidYesCalled when the user closes the preview (overlay click or close button)

Behavior

  • Renders a modal dialog with the large photo and photographer caption
  • Clicking the overlay or the close button calls onClose
  • Focus management via internal usePreviewDialog hook

Example

tsx
import { PhotoPreview } from "@media-sdk/ui-react";
import type { Photo } from "@media-sdk/core";

function PhotoWithPreview({
  selectedPhoto,
  onClose,
}: {
  selectedPhoto: Photo | null;
  onClose: () => void;
}) {
  if (!selectedPhoto) {
    return null;
  }

  return (
    <PhotoPreview
      photo={selectedPhoto}
      onClose={onClose}
    />
  );
}

Pair with client.trackView({ mediaId: photo.id, mediaType: "photo" }) in your app when the preview opens — see Events.


VideoPreview

Modal overlay with an HTML5 video player.

PropTypeRequiredDescription
videoVideoYesVideo to display
onClose() => voidYesCalled when the user closes the preview

Behavior

  • Selects the best playable MP4 link from video.video_files
  • Auto-plays with controls when a source is available
  • Shows a fallback message when no playable file exists
  • Displays duration caption

Example

tsx
import { VideoPreview } from "@media-sdk/ui-react";
import type { Video } from "@media-sdk/core";

function VideoWithPreview({
  selectedVideo,
  onClose,
}: {
  selectedVideo: Video | null;
  onClose: () => void;
}) {
  if (!selectedVideo) {
    return null;
  }

  return (
    <VideoPreview
      video={selectedVideo}
      onClose={onClose}
    />
  );
}

Pagination

Previous / next page navigation controls. Pass pagination state from hooks or direct client responses.

PropTypeRequiredDefaultDescription
pagenumberYesCurrent page number
hasPreviousbooleanYesWhether a previous page exists
hasNextbooleanYesWhether a next page exists
loadingbooleanNofalseWhen true, disables both navigation buttons
ariaLabelstringYesAccessible label for the navigation landmark
onPrevious() => voidYesCalled when the user clicks Previous
onNext() => voidYesCalled when the user clicks Next

Example

tsx
import { Pagination } from "@media-sdk/ui-react";

function PhotoPagination({
  page,
  hasPrevious,
  hasNext,
  loading,
  onPrevious,
  onNext,
}: {
  page: number;
  hasPrevious: boolean;
  hasNext: boolean;
  loading: boolean;
  onPrevious: () => void;
  onNext: () => void;
}) {
  return (
    <Pagination
      page={page}
      hasPrevious={hasPrevious}
      hasNext={hasNext}
      loading={loading}
      ariaLabel="Photo results"
      onPrevious={onPrevious}
      onNext={onNext}
    />
  );
}

Wire to hook pagination:

tsx
import { useMediaSearch } from "@media-sdk/react";
import { Pagination } from "@media-sdk/ui-react";

function SearchResults({ query }: { query: string }) {
  const { data, loading, nextPage, previousPage } = useMediaSearch({ query });

  if (!data) return null;

  return (
    <Pagination
      page={data.pagination.page}
      hasPrevious={data.pagination.hasPrevious}
      hasNext={data.pagination.hasNext}
      loading={loading}
      ariaLabel="Photo results"
      onPrevious={() => void previousPage()}
      onNext={() => void nextPage()}
    />
  );
}

LoadingState

Accessible loading message with aria-live="polite".

PropTypeRequiredDescription
messagestringYesText shown while loading

Example

tsx
import { LoadingState } from "@media-sdk/ui-react";

function Results({ loading }: { loading: boolean }) {
  if (loading) {
    return <LoadingState message="Loading photos…" />;
  }

  return null;
}

ErrorState

Accessible error message with role="alert".

PropTypeRequiredDescription
messagestringYesError text to display

Example

tsx
import { ErrorState } from "@media-sdk/ui-react";

function Results({ error }: { error: Error | null }) {
  if (error) {
    return <ErrorState message={error.message} />;
  }

  return null;
}

For structured error handling, branch on MediaError.code in your app before choosing the message string. See Errors.


PhotoCard

Single photo thumbnail button. Used internally by PhotoGrid; available for custom layouts.

PropTypeRequiredDescription
photoPhotoYesPhoto to render
onSelect(photo: Photo) => voidYesCalled when the card is clicked

VideoCard

Single video thumbnail button with play icon and duration. Used internally by VideoGrid.

PropTypeRequiredDescription
videoVideoYesVideo to render
onSelect(video: Video) => voidYesCalled when the card is clicked

End-to-end example

Full wiring of hooks → props → components:

tsx
import { useMediaSearch, useMediaVideos } from "@media-sdk/react";
import {
  SearchBar,
  MediaTabs,
  PhotoGrid,
  PhotoPreview,
  VideoGrid,
  VideoPreview,
  Pagination,
  LoadingState,
  ErrorState,
  type MediaTab,
} from "@media-sdk/ui-react";
import { useState } from "react";
import type { Photo, Video } from "@media-sdk/core";

function MediaBrowser() {
  const [query, setQuery] = useState("");
  const [submittedQuery, setSubmittedQuery] = useState("");
  const [activeTab, setActiveTab] = useState<MediaTab>("photos");
  const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);
  const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);

  const photoSearch = useMediaSearch({
    query: submittedQuery,
    enabled: activeTab === "photos" && submittedQuery.length > 0,
  });

  const videoSearch = useMediaVideos({
    query: submittedQuery,
    enabled: activeTab === "videos" && submittedQuery.length > 0,
  });

  const active = activeTab === "photos" ? photoSearch : videoSearch;

  return (
    <>
      <SearchBar
        value={query}
        onChange={setQuery}
        onSubmit={(event) => {
          event.preventDefault();
          setSubmittedQuery(query);
        }}
      />

      <MediaTabs activeTab={activeTab} onTabChange={setActiveTab} />

      {active.loading && <LoadingState message="Loading…" />}
      {active.error && <ErrorState message={active.error.message} />}

      {activeTab === "photos" && photoSearch.data && (
        <PhotoGrid
          photos={photoSearch.data.items}
          onPhotoSelect={setSelectedPhoto}
        />
      )}

      {activeTab === "videos" && videoSearch.data && (
        <VideoGrid
          videos={videoSearch.data.items}
          onVideoSelect={setSelectedVideo}
        />
      )}

      {active.data && (
        <Pagination
          page={active.data.pagination.page}
          hasPrevious={active.data.pagination.hasPrevious}
          hasNext={active.data.pagination.hasNext}
          loading={active.loading}
          ariaLabel={`${activeTab} results`}
          onPrevious={() => void active.previousPage()}
          onNext={() => void active.nextPage()}
        />
      )}

      {selectedPhoto && (
        <PhotoPreview
          photo={selectedPhoto}
          onClose={() => setSelectedPhoto(null)}
        />
      )}

      {selectedVideo && (
        <VideoPreview
          video={selectedVideo}
          onClose={() => setSelectedVideo(null)}
        />
      )}
    </>
  );
}