Slide decks

A slide deck is an ordinary HTML page marked as a presentation. Once a page is a deck, Sharedrop adds a Present control that goes fullscreen with keyboard and tap navigation, so you can build a deck with an agent, drop it in, and present it on a TV or projector with no PowerPoint and no export step.

Nothing else about the page changes. A deck is sanitised, sandboxed, watermarked, shared, and made public or private exactly like any other HTML page. Slides is a presentation affordance layered on top, not a different security model.

Slide decks are available on every plan, including Free. Only present-only disappearing links require Pro.

What each surface can do

DashboardCLIREST APIMCP
Upload a deck via the in-file markerYesYesYesYes
Mark a deck explicitly at uploadn/aNo flag existsslides: true at finalizeslides: true at finalize
Turn an existing page into a deckYes, the Slides toggleNoNoNo
Open Present modePresent buttonOpen the URL with ?present=1?present=1?present=1
Present-only disappearing link (Pro)CheckboxNopresent_only: truepresent_only: true

The one path that exists only in the dashboard is converting a page you have already uploaded. If you are an agent, decide at upload time: either put the marker in the HTML or pass slides: true to finalize.

What makes a good deck

Structure your HTML as top-level <section> elements, one per slide:

<!doctype html>
<html>
  <head>
    <meta name="sharedrop:kind" content="slides" />
  </head>
  <body>
    <section><h1>Title slide</h1></section>
    <section><h2>Second slide</h2><p>...</p></section>
    <section><h2>Third slide</h2></section>
  </body>
</html>

In Present mode Sharedrop shows one <section> at a time and moves through them on key or tap. If a deck has no top-level <section> elements it still presents: it simply goes fullscreen as a single free-scrolling page.

Two details worth knowing:

  • Slide navigation and the counter appear only when there are two or more slides. A single-section deck goes fullscreen with no counter and nothing to step through.
  • If Sharedrop finds no top-level <section> elements directly inside <body>, it falls back to every <section> in the document, so a deck wrapped in a container still works.

Present mode supplies navigation only. It never restyles your deck: no background, no text colour, and no layout override on the visible slide. Your CSS is what the audience sees, so a deck that looks right in a browser looks right on the projector.

Animations and fragments

Present mode drives your deck like a presenter tool: it advances slide by slide, and within a slide it can reveal elements one at a time, the way a bullet list builds in PowerPoint or Canva. Two hooks make this work, and both are plain CSS you write on your own deck.

Slide-entry animations. When a slide becomes the visible one, Present adds the class __sd-active to that <section>. Key any entrance animation to it:

section { opacity: 0; transform: translateY(12px); transition: opacity .5s ease, transform .5s ease; }
section.__sd-active { opacity: 1; transform: none; }

Fragments (step-through reveal). Mark any element inside a slide with data-sd-fragment. Present hides those elements at first and reveals them one per step; only once every fragment on a slide is shown does the next step move to the next slide. Stepping back hides the last-revealed fragment first, then returns to the previous slide.

<section>
  <h2>Three shifts</h2>
  <ul>
    <li data-sd-fragment>Pilots became practice</li>
    <li data-sd-fragment>Hours returned to the business</li>
    <li data-sd-fragment>Governance kept pace</li>
  </ul>
</section>

Fragments get a gentle default fade-and-rise for free. To style the reveal yourself, key your CSS to __sd-frag-visible, the class Present adds to a fragment when it is shown:

[data-sd-fragment] { filter: blur(6px); opacity: 0; transition: .4s ease; }
[data-sd-fragment].__sd-frag-visible { filter: none; opacity: 1; }

Control the reveal order with data-sd-fragment-index (ascending); elements without an index follow document order. Fragments respect prefers-reduced-motion.

Static vs interactive, and what animates. A static deck has its scripts stripped at upload, so CSS transitions and the fragment/slide-entry classes work, but no JavaScript in the deck runs. An interactive deck keeps its own scripts, so canvas animations, counters, charts, and anything script-driven run while Present still drives the slides. If you want scripted animation, upload the deck interactive; if pure CSS is enough, static is the tighter, simpler choice.

Clicking. A click or tap advances (handy on a TV or with a presenter remote), but never when you click something interactive in the slide, a link, button, input, or an element you mark data-sd-no-advance, so a deck's own controls keep working. Arrow keys and the on-screen prev/next buttons always navigate.

Marking a page as a deck

There are three ways to designate a deck. Any one of them is enough.

1. In-file marker (works on every upload path)

Declare it inside the HTML, so a plain upload becomes a deck automatically. Either form works:

<meta name="sharedrop:kind" content="slides" />
<html data-sharedrop-kind="slides">

Because the marker lives in the file, this path needs no special flag: sharedrop upload deck.html, a drag-and-drop, an API upload, or an MCP upload all produce a deck when the marker is present.

The attribute name is matched exactly (sharedrop:kind, data-sharedrop-kind), but the value is case-insensitive, so slides, Slides, and SLIDES all work. Any other value, or no marker at all, leaves the page an ordinary HTML page.

Marker detection runs on HTML uploads only. An MHTML web archive is not promoted by a marker, though you can still mark it from the dashboard afterwards.

2. Explicit flag at upload (API and MCP)

Set slides: true when you finalize an HTML upload (see REST API and MCP below). This is the path for an agent that wants to tag a deck without editing the document.

The flag belongs on finalize, not on sign. The sign step accepts a slides field for convenience but does nothing with it, so signing alone never produces a deck. It is also HTML-only: sending slides: true with a PDF or an image is silently ignored and the page keeps its real kind.

3. Dashboard toggle

Open an HTML or MHTML page in the dashboard, click Slides, and choose Mark as slides. The same control reads Unmark slides once the page is a deck. The option does not appear on images, PDFs, videos, or archives.

One asymmetry to be aware of: unmarking always returns a page to the html kind. If you marked an MHTML page as a deck and then unmark it, it becomes html, not mhtml.

Marking is reversible and lossless: it changes only the page's kind, never its bytes. Re-uploading new HTML over a deck keeps it a deck, even if the replacement file has no marker, so you can iterate on a deck without re-tagging it every time.

There is no API or CLI equivalent of this toggle. Converting a page after upload is a dashboard action.

Presenting

Open a deck and click Present (or add ?present=1 to its URL). Present opens the deck fullscreen on the isolated view origin with minimal chrome:

ActionKeys / gesture
Next slideRight arrow, Space, Page Down, click, or tap
Previous slideLeft arrow, Page Up
First / last slideHome / End
Toggle fullscreenF
ExitEsc

Clicking or tapping the slide moves forward (a step at a time, so it reveals the next fragment before the next slide). It never fires when you click something interactive, a link, a button, a form field, or an element you mark data-sd-no-advance, so a deck's own controls keep working. There is no click-to-go-back; step back with Left arrow, Page Up, or the on-screen prev button.

On-screen prev/next buttons sit in the bottom left for a presenter working with a mouse or a TV pointer. The cursor and those controls auto-hide after about two and a half seconds of stillness and return as soon as you move the pointer, which keeps a stray cursor off the projector. A small slide counter (3 / 12) sits in the bottom right whenever the deck has more than one slide.

Browsers only allow fullscreen after a user gesture, so the deck opens filling the window and the first click or key both enters true fullscreen and advances. Esc leaves fullscreen; pressing it again returns to the previous page if there is one, so on a URL opened directly (a TV pointed at a link) it does nothing.

How Present mode is isolated

Present renders on the separate view origin under a policy written for this mode, and it is always access-gated: presenting a deck requires the same permission as viewing it. A private or shared deck still needs your signed-in access or a valid link; there is no way to present a deck you could not otherwise open.

For a static deck, the document runs no scripts of its own. The navigation controller is admitted by a one-time nonce rather than by allowing inline scripts generally, so a static deck runs exactly one script, the one Sharedrop injected, and nothing from the document itself. For an interactive deck, the controller is injected alongside the deck's own scripts under the interactive policy, so navigation and your animations run together. Styles, fonts, and images follow the page's own network setting in both cases.

Present mode does not apply the usual iframe sandbox, because a sandboxed document cannot enter fullscreen. That is why the tighter per-response policy above replaces it. See sandbox and safety for the model that applies to a deck everywhere else.

Who drives the slides

By default Sharedrop drives navigation for your deck, on both static and interactive decks, so you get slide and fragment stepping, the counter, kiosk auto-advance, and the on-screen controls without shipping any of it yourself.

A deck that brings its own presentation framework (reveal.js, impress.js) already owns navigation, and Sharedrop stays out of its way: such a deck self-drives on a bare fullscreen canvas. Sharedrop recognises reveal.js and impress.js automatically, and you can opt out explicitly with a marker, either form:

<meta name="sharedrop:present" content="self" />
<html data-sharedrop-present="self">

Use the opt-out only when your deck genuinely handles its own keys and clicks; for an ordinary <section> deck, leave it off and let Sharedrop drive.

Kiosk auto-advance

For an unattended screen, add autoplay=<seconds> to advance automatically and loop:

https://sharedrop.cloud/alex/abc123?present=1&autoplay=15

The deck advances every 15 seconds and loops back to the start at the end, so a screen pointed at that URL runs unattended indefinitely. Auto-advance steps the same way a presenter would: through each fragment on a slide, then to the next slide, so a kiosk plays your bullet reveals and animations on its own. autoplay must be a plain whole number of seconds: anything else (15s, 1.5, a negative) is ignored and the deck simply waits for input. It needs more than one slide, or at least one fragment, since there is otherwise nothing to advance to.

If you have a Pro plan, you can hand out a present-only disappearing link: a public URL that opens straight into fullscreen Present mode and expires after a time window, a view count, or both. It is a variant of a normal disappearing link, available for slide decks only.

  • Dashboard: open the deck, expand Disappearing link, tick Open in fullscreen Present mode, and create the link.
  • API / MCP: pass present_only: true when you create the ephemeral link (see below).

While the link is live the page is temporarily public; when it expires the page reverts to its previous visibility, exactly like any disappearing link. Asking for a present-only link on a page that is not a deck is refused.

What present-only does. The link always lands in Present mode. The URL carries &present=1, and the deck opens in Present even if that parameter is stripped along the way, so a link pasted through a chat client or a URL shortener that mangles the query still arrives at a presentation rather than an ordinary page view.

What it does not do. It is not an access restriction. While any disappearing link is live the page itself is public, so someone holding the link can drop the whole ?e=... query and reach the page at its plain URL until the link expires. That is how disappearing links work in general, not something specific to Present mode. The expiry window and the view count are the parts that actually restrict access; present-only decides how the deck is shown, not who may see it.

Dashboard

A deck shows a Slides badge and glyph in your page list, and Slides appears as its own option in the kind filter, so you can pull up every deck you own in one click.

Its detail view gains a Present button (available to anyone who can view the page, not only the owner). For the owner there is also the Slides control, which holds three things:

  • Mark as slides / Unmark slides, the post-upload toggle.
  • A copyable present link (the page URL with ?present=1). It works for anyone who can already see the page, so it is the right thing to paste into a calendar invite, or into a TV browser when the deck is public. A private deck opened on a signed-out screen bounces to sign-in, so for that case use a present-only disappearing link instead.
  • The present-only disappearing link option described above, on Pro.

CLI

Requires @sharedrop/cli. See the CLI guide for installation and authentication.

The CLI has no dedicated slides flag, and it does not need one: put the in-file marker in your HTML and a normal upload becomes a deck.

# deck.html contains <meta name="sharedrop:kind" content="slides" />
sharedrop upload deck.html --title "Q3 review"

# confirm it landed as a deck
sharedrop get <slug> --json | jq -r .data.kind    # -> slides

Present it by opening the returned URL with ?present=1, or from the dashboard:

https://sharedrop.cloud/alex/abc123?present=1

Re-uploading over a deck keeps it a deck, so sharedrop update <slug> deck.html needs no extra step. Present-only disappearing links have no CLI command; create those from the dashboard, the API, or MCP.

REST API

A deck is created through the standard streamed upload. To mark it explicitly, add slides: true to the finalize step; otherwise include the in-file marker in the HTML.

curl -X POST \
  -H "Authorization: Bearer sd_..." \
  -H "Content-Type: application/json" \
  -d '{"object_key": "...", "upload_token": "...", "slides": true}' \
  https://sharedrop.cloud/api/upload/finalize

slides applies to HTML only and is ignored for other kinds. The finished page reports "kind": "slides".

Put the flag on finalize. The sign step accepts slides but ignores it, so signing with the flag and finalizing without it produces an ordinary HTML page.

There is no endpoint that changes an existing page's kind to slides. If you need a page you have already uploaded to become a deck, either re-upload it with the marker or the flag, or use the dashboard toggle.

Create an ephemeral link with present_only: true on a deck:

curl -X POST \
  -H "Authorization: Bearer sd_..." \
  -H "Content-Type: application/json" \
  -d '{"expires_in_seconds": 86400, "present_only": true}' \
  https://sharedrop.cloud/api/v1/pages/<page_id>/ephemeral-links

The response echoes present_only and the returned url already carries &present=1. Listing the page's links returns both faithfully, so a link re-copied from GET /api/v1/pages/<page_id>/ephemeral-links behaves identically to the one you created. Note the field is snake_case here (present_only); a misspelled field is ignored rather than rejected, so check the echoed value rather than assuming it took.

Errors to expect:

  • 400 VALIDATION_ERROR, message Present-only links are available for slide decks only., when the page is not a deck.
  • 402 TIER_LIMIT on Free, since disappearing links are a Pro feature.
  • 409 if the page is shared (shared with named people), because a disappearing link makes a page temporarily public.

MCP

An agent marks a deck by passing slides: true to finalize_upload after the usual create_upload → PUT bytes flow:

// finalize_upload
{ "object_key": "...", "upload_token": "...", "slides": true }

The in-file marker works too, so a deck the agent generated with the <meta> tag becomes a deck without any flag. Either way, finalize_upload returns "kind": "slides", which is the thing to check before telling the user they have a deck.

Re-uploading with the same page_id keeps the page a deck, so an agent iterating on a presentation does not need to re-send the flag each round.

To hand the user a fullscreen link for a TV, create a present-only disappearing link with create_ephemeral_link:

// create_ephemeral_link
{ "page_id": "...", "expires_in_seconds": 86400, "present_only": true }

present_only is slides-only and returns an error on any other page, and the link itself is Pro-only. The response echoes present_only so you can confirm the flag took, which matters because an unrecognised field is ignored rather than rejected. list_ephemeral_links reports it faithfully too.

A complete agent flow for "build me a deck and put it on the TV":

1. create_upload({ filename: "q3.html", content_type: "text/html", size_bytes })
2. PUT the raw bytes to upload_url
3. finalize_upload({ object_key, upload_token, slides: true })   -> kind: "slides"
4. create_ephemeral_link({ page_id, expires_in_seconds: 14400, present_only: true })
5. give the user that url; it lands straight in Present mode on any screen it is pasted into