Skip to main content
RapidDev - Software Development Agency
App Featurespersonalization-ux18 min read

How to Add Presentation Mode to Your App — Copy-Paste Prompts Included

Presentation mode needs four pieces: a fullscreen trigger using document.requestFullscreen(), a slide renderer with animated transitions, keyboard and swipe navigation, and slide data stored as a JSONB array in Supabase. With Lovable or V0 you can ship a working deck presenter in 3–5 hours for $0–$25/month. All logic runs client-side; the only recurring cost is Supabase for deck storage.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

personalization-ux

Build with AI

3–5 hours with Lovable or V0

Custom build

3–7 days custom dev

Running cost

$0–25/mo up to 10K users

Works on

Web

Everything it takes to ship Presentation Mode — parts, prompts, and real costs.

TL;DR

Presentation mode needs four pieces: a fullscreen trigger using document.requestFullscreen(), a slide renderer with animated transitions, keyboard and swipe navigation, and slide data stored as a JSONB array in Supabase. With Lovable or V0 you can ship a working deck presenter in 3–5 hours for $0–$25/month. All logic runs client-side; the only recurring cost is Supabase for deck storage.

What Presentation Mode Actually Is

Presentation mode turns a web app into a full-screen stage: all chrome disappears, slides fill the viewport, and the user navigates with keyboard arrows or swipes. It is used in pitch-deck tools, interactive tutorials, onboarding flows, and product demos. The Fullscreen API and BroadcastChannel are browser-native — no third-party service needed. The real product decisions are how slide content is stored (JSONB array gives the most flexibility), how speaker notes are surfaced on a secondary monitor, and whether slides need auto-advance timing. AI tools handle the core slide renderer and Supabase wiring well in one or two prompts; the speaker notes window and keyboard listener scoping are where builds slip.

What users consider table stakes in 2026

  • One-click entry and exit with keyboard shortcut (F5 to enter, Escape to exit) — no buried menu required
  • Full-screen takeover hiding all app chrome: navigation, sidebars, toolbars, and notification banners
  • Keyboard arrow navigation (ArrowLeft / ArrowRight / Space) and swipe support on tablets
  • Persistent slide counter overlay showing current position and total (e.g. 3 of 12)
  • Speaker notes panel visible only to the presenter on a secondary display via a dedicated window
  • Per-slide auto-advance timer with configurable duration, pause-on-interaction, and visible countdown

Anatomy of the Feature

Seven components build a complete presentation mode. The Fullscreen API and BroadcastChannel are browser-native and zero cost; Framer Motion handles slide transitions; Supabase stores deck data with RLS.

Layers:UIDataBackend

Fullscreen Trigger Button

UI

A button that calls document.requestFullscreen() on the presentation container element on user click. Toggle label switches between 'Present' and 'Exit'. Paired with a global keydown listener that fires Escape to call document.exitFullscreen() and F5 to enter. The fullscreenchange event updates component state so the UI stays in sync.

Note: requestFullscreen() must be called inside a direct user gesture handler (click, keydown). Calling it on page load or inside a promise chain breaks the browser security model and silently fails.

Slide Renderer

UI

A React component that renders the current slide from a slides[] array held in local state or fetched from Supabase. Uses Framer Motion AnimatePresence with a slide-left or fade variant for slide-to-slide transitions. Reads title, body, and notes fields from each JSONB slide object. Scales content with CSS viewport units so text stays readable at any screen size.

Note: Framer Motion is MIT-licensed and zero cost. The AnimatePresence key prop must be the slide index or transitions will not fire on navigation.

Navigation Controls

UI

Previous and Next buttons with a global keydown listener for ArrowLeft, ArrowRight, and Space. Touch and swipe support added via the react-swipeable library (onSwipedLeft / onSwipedRight handlers). Buttons are hidden when at the first or last slide rather than disabled, to keep the presenter UI uncluttered.

Note: react-swipeable is the most reliable swipe library for React; alternatives like @use-gesture/react work but add bundle weight for no functional gain here.

Slide Counter

UI

A fixed-position overlay in the lower-right corner displaying currentIndex + 1 / slides.length. Updates on every navigation event. Fades in after 1 second of no interaction and fades out during active navigation to avoid visual noise during fast advancement.

Note: Keep z-index above 9999 so the counter renders above Framer Motion slides without fighting stacking contexts.

Speaker Notes Overlay

UI

A secondary window opened via window.open('/notes', 'speaker-notes', 'width=800,height=600') for display on a secondary monitor. Receives the current slide index from the main presentation window via BroadcastChannel API (channel name: 'presentation-sync'). Renders the notes field of the current slide alongside a miniature slide preview.

Note: BroadcastChannel requires both windows to share the same origin. Verify the channel name is identical in both components. Falls back gracefully if the user has no secondary display.

Slide Data Store

Data

A Supabase table storing slide decks with an ordered JSONB slides array. Each element is a JSON object with title (string), body (string), notes (string), duration_seconds (integer), and an optional image_url (Supabase Storage signed URL). Row-level security restricts all operations to the owner via auth.uid().

Note: Storing slide images as Supabase Storage signed URLs keeps the deck row compact. Load full-resolution URLs only for the current and next slide; lazy-load the rest.

Auto-Advance Timer

Backend

A client-side setInterval that reads the current slide's duration_seconds field and advances the slide index when it elapses. The timer is configurable per slide via a small settings panel. Any user interaction (click, keydown, swipe) resets the interval countdown and shows a 'paused' badge so the presenter knows the timer is suspended.

Note: Clear the interval with clearInterval on component unmount and on manual navigation to prevent unexpected slide jumps while the presenter is mid-sentence.

The data model

Two tables cover deck storage and session tracking. Run this in the Supabase SQL Editor — it is copy-paste ready.

schema.sql
1create table public.presentations (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete cascade not null,
4 title text not null,
5 slides jsonb not null default '[]'::jsonb,
6 created_at timestamptz not null default now()
7);
8
9alter table public.presentations enable row level security;
10
11create policy "Users can view own presentations"
12 on public.presentations for select
13 using (auth.uid() = user_id);
14
15create policy "Users can insert own presentations"
16 on public.presentations for insert
17 with check (auth.uid() = user_id);
18
19create policy "Users can update own presentations"
20 on public.presentations for update
21 using (auth.uid() = user_id);
22
23create table public.slide_sessions (
24 id uuid primary key default gen_random_uuid(),
25 presentation_id uuid references public.presentations(id) on delete cascade not null,
26 started_at timestamptz not null default now(),
27 ended_at timestamptz
28);
29
30alter table public.slide_sessions enable row level security;
31
32create policy "Users can view own sessions"
33 on public.slide_sessions for select
34 using (
35 exists (
36 select 1 from public.presentations p
37 where p.id = presentation_id and p.user_id = auth.uid()
38 )
39 );
40
41create policy "Users can insert own sessions"
42 on public.slide_sessions for insert
43 with check (
44 exists (
45 select 1 from public.presentations p
46 where p.id = presentation_id and p.user_id = auth.uid()
47 )
48 );
49
50create policy "Users can update own sessions"
51 on public.slide_sessions for update
52 using (
53 exists (
54 select 1 from public.presentations p
55 where p.id = presentation_id and p.user_id = auth.uid()
56 )
57 );
58
59create index presentations_user_idx
60 on public.presentations (user_id, created_at desc);

Heads up: The slides JSONB array stores each slide as {title, body, notes, duration_seconds, image_url}. The slide_sessions table tracks presentation start and end times for analytics. You can extend it with a slides_viewed integer column to measure how far presenters typically get.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Best UI, Next.js ecosystemFit for this feature:

V0 excels at polished slide UI — clean typography scaling, dark presenter overlays, and smooth transitions. Next.js dynamic route /present/[id] handles deck loading via Server Components. Best path when quality and extensibility matter most.

Step by step

  1. 1Prompt V0 with the spec below; it will generate the SlideRenderer, NavigationControls, and SpeakerNotesWindow components
  2. 2Add Supabase credentials in the Vars panel (NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY) and run the data model SQL in your Supabase project's SQL Editor
  3. 3Use Design Mode to refine slide typography, background, and transition feel without spending credits
  4. 4Deploy to production and open the published URL on a second monitor to test the window.open() speaker notes — V0 sandbox cannot open secondary windows
  5. 5Set a Vercel environment variable NEXT_PUBLIC_SITE_URL to your production domain so the notes window resolves correctly
Paste into v0
Build a presentation mode feature in Next.js App Router. Create route /present/[id] as a client component. Load the presentation from Supabase using the id param — table: presentations (id, user_id, title, slides jsonb). Each slide object: {title, body, notes, duration_seconds, image_url}. Implement: (1) a 'Present' button that calls document.requestFullscreen() on the presentation container on click, label toggles to 'Exit' on fullscreenchange event, (2) Framer Motion AnimatePresence slide-left transition keyed by slide index, (3) keydown listener for ArrowLeft/ArrowRight/Space (navigation) and Escape (exit fullscreen) — gate all handlers behind a presentationActive ref, remove listener in useEffect cleanup, (4) react-swipeable onSwipedLeft/onSwipedRight for tablet swipe support, (5) fixed overlay showing slide N of total in bottom-right corner, (6) auto-advance timer per slide reading duration_seconds — clears and resets on any user interaction, shows 'paused' badge, (7) speaker notes: a button that calls window.open('/notes', 'speaker-notes') and sends slide index via BroadcastChannel('presentation-sync') on every navigation, (8) edge case: if slides array has exactly 1 item hide both Previous and Next buttons, (9) lazy-load image_url for slide N+1 while slide N is displayed. Create /notes as a separate page that joins BroadcastChannel('presentation-sync') and renders the current slide's notes field. Include Supabase RLS so users only access their own decks.

Where this path bites

  • BroadcastChannel speaker notes requires both windows on the same origin — verify the channel name string is identical in both components
  • V0 does not auto-provision Supabase; set NEXT_PUBLIC_SUPABASE_URL and credentials in Vercel Vars before deploying
  • V0 sandbox cannot enter fullscreen or open secondary windows — all testing must happen on the deployed URL

Third-party services you'll need

Presentation mode costs nothing beyond your database. All rendering and animation runs client-side with free open-source libraries.

ServiceWhat it doesFree tierPaid from
SupabaseStores slide decks (JSONB), session history, and user authFree tier: 500MB DB, 2 projectsPro $25/mo for multiple projects or higher storage
Framer MotionSlide transition animations (AnimatePresence, motion.div)Open-source MIT licenseFree
Fullscreen API + BroadcastChannel APIBrowser-native fullscreen entry/exit and speaker notes sync between windowsBrowser-native Web APIsFree
react-swipeableTouch swipe navigation between slides on tabletsOpen-source MIT licenseFree

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier covers deck storage and auth. All presentation rendering and navigation is client-side. No third-party API costs.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Clicking 'Present' inside Lovable preview does nothing

Symptom: The Lovable editor wraps your app in a sandboxed iframe without the allowfullscreen attribute. Calling document.requestFullscreen() inside a restricted iframe is silently blocked by the browser — no error is thrown, the button just appears unresponsive.

Fix: Test all fullscreen behavior exclusively on the published Lovable URL. Open the live link in a real browser tab on your actual domain, grant any permissions, and confirm fullscreen works on the production domain before assuming the feature is broken.

Speaker notes window opens but never updates as slides advance

Symptom: BroadcastChannel requires both windows to share the exact same origin. AI tools sometimes generate postMessage with a hardcoded origin string that differs between local and production environments, or use a different channel name in the notes page than in the main presenter — causing messages to never arrive.

Fix: Verify the BroadcastChannel channel name ('presentation-sync') is an identical string literal in both the main presentation component and the /notes page. If using postMessage alongside BroadcastChannel, always use window.location.origin as the targetOrigin — never a hardcoded domain string.

Keyboard navigation triggers other app shortcuts simultaneously

Symptom: A global document.addEventListener('keydown', handler) added without a cleanup function and without an active-state guard fires for ArrowLeft, ArrowRight, Space, and Escape even when the user is not in presentation mode. This conflicts with search shortcuts, navigation shortcuts, or modal dismiss behavior elsewhere in the app.

Fix: Add a presentationActive boolean ref and wrap all keyboard handler logic with an early return if it is false. Remove the event listener in the useEffect cleanup (return () => document.removeEventListener(...)) so it stops firing when the component unmounts or the user exits presentation mode.

Slide deck loads slowly on first open

Symptom: AI tools often fetch the full presentations row — including all JSONB slides and embedded image URLs — in a single query at mount time. If users store high-resolution images inline or as long signed URLs, the initial load stalls for several seconds on mobile connections.

Fix: Store slide images as Supabase Storage signed URLs rather than inline base64 data. Lazy-load the image_url for slide N+1 while slide N is displayed using a useEffect that fires on each navigation event. The deck structure loads instantly; images trickle in just ahead of when the presenter needs them.

Auto-advance timer fires while the presenter is mid-sentence

Symptom: A setInterval set to duration_seconds keeps running regardless of user activity. If the presenter pauses on a slide to answer a question or read notes, the timer advances to the next slide without warning. There is no visible indicator that a countdown is running.

Fix: Reset the interval timer on any click, keydown, or swipe event. Add a visible countdown badge (e.g. 'Next slide in 12s') in the presenter overlay so the countdown is visible. Display a 'Timer paused' indicator whenever the user has interacted within the last few seconds.

Best practices

1

Call document.requestFullscreen() only inside a direct user gesture handler — never on page load or in async chains; browsers silently reject it otherwise

2

Store slides as a JSONB array with explicit named fields (title, body, notes, duration_seconds) rather than freeform markup — this keeps the renderer predictable and simplifies the slide editing UI

3

Guard every keyboard listener behind a presentationActive boolean and clean up event listeners on unmount to prevent shortcut conflicts elsewhere in the app

4

Lazy-load slide images one position ahead of the current slide — preload N+1 while N is showing to eliminate the flash of missing content during fast navigation

5

Test the speaker notes BroadcastChannel on the deployed URL with two real browser windows before launch — it cannot be verified in the preview pane

6

Give the auto-advance timer a visible countdown badge and reset it on any user interaction so presenters are never surprised by an unattended slide jump

7

Add Supabase Storage signed URLs with short expiry (1 hour) for slide images so sharing a presentation URL does not expose permanent public asset links

When You Need Custom Development

AI tools handle the single-presenter fullscreen deck experience comfortably. These scenarios require custom work:

  • Live multi-user presentations where all viewers must see the same slide simultaneously — requires Supabase Realtime channel subscription and reconnect logic
  • Audience interaction features: live polls with per-slide vote tallies, moderated Q&A queues, or emoji reactions broadcast to all viewers in real time
  • Embedded video or audio clips that must auto-play timed to slide transitions — browser autoplay policies block this without careful user-gesture propagation handling
  • Branded presenter themes with per-client custom fonts, animations, and logo watermarks that differ for each customer of your platform

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I use presentation mode offline without internet?

Yes, once the deck is loaded. All slide rendering, navigation, fullscreen entry/exit, and auto-advance run client-side. If the user loads the deck while connected, they can disconnect and continue presenting. New deck saves or speaker notes sync require connectivity, but the presentation itself does not.

How do I show speaker notes on a second monitor?

Call window.open('/notes', 'speaker-notes', 'width=800,height=600') from the presenter controls. Drag the opened window to the secondary display. The main presentation window sends the current slide index to the notes window via BroadcastChannel API — both windows must be on the same domain. Works in Chrome, Edge, and Firefox on desktop.

Does fullscreen work on all browsers?

The Fullscreen API works in all modern desktop browsers (Chrome, Firefox, Safari 16.4+, Edge). On iOS Safari, requestFullscreen() is not supported for arbitrary elements — the user must use the browser's native full-screen gesture. Test on Safari iOS and add a CSS fallback (position:fixed, inset:0, z-index:9999) as a near-fullscreen substitute for iOS users.

Can I embed videos in slides?

Yes — add a video_url field to the JSONB slide object and render a video element in the slide component. The challenge is browser autoplay policy: video autoplays reliably only if it is muted. For video synchronized to slide timing, use the muted autoplay attribute and provide an unmute button as an overlay so users can opt in to audio.

How many slides can a deck have?

No hard limit is imposed by the JSONB schema. For practical performance, decks beyond 100 slides with large image URLs should lazy-load images rather than fetching all signed URLs at once. Text-only decks of 300+ slides load in under a second on typical connections.

Can multiple users present the same deck simultaneously?

With the standard AI-built version, each presenter controls their own slide position independently. For synchronized multi-user presenting — where all viewers see the same slide in real time — you need Supabase Realtime channel subscription to broadcast navigation events. This is a custom development requirement not covered by a single AI prompt.

How do I share a presentation link with view-only access?

Add an is_public boolean column to the presentations table and a separate Supabase RLS policy: CREATE POLICY 'Public read' ON presentations FOR SELECT USING (is_public = true). Generate a shareable URL like /present/[id]?view=true that hides editing controls but renders all slides normally. The viewer cannot edit or delete the deck.

Does auto-advance work on mobile?

Yes — the setInterval timer advances slides on any device. Mobile swipe navigation via react-swipeable works alongside it. The main mobile limitation is fullscreen: iOS Safari does not support requestFullscreen() for app-embedded elements. Use the CSS full-viewport positioning fallback to simulate fullscreen on iOS rather than calling requestFullscreen() directly.

RapidDev

Need this feature production-ready?

RapidDev builds presentation mode into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.