Vibe code your own Shots.so

Vibe code your own Shots.so in the browser. Paste a screenshot, add a gradient and padding, export a PNG at the exact social size. Skip the $10–$20 rent.

  • getting-started
  • vibe-coding
  • tonight

You can vibe code your own Shots.so as a paste-and-export frame. Shots.so is that product. The catalog line is “Your screenshot, but dressed for the timeline.”

You pay $10–$20 a month for it. That is $120–$240 a year. The basic replacement is 2 hours: a gradient background, padding, a device frame, and an export.

I would ship this if I still rented a frame for launch tweets. It is marked Tonight. The verdict on the card is “Gradient, padding, done”.

Earlier today I wrote about how Airtable works. That was a teardown. This one is a regular backlog topic. The useful v1 is the PNG you paste into a tweet.

What you need to vibe code your own Shots.so

The Shots.so page splits the work into three rows.

The first row is the one you want tonight. Gradient background, padding, a browser or device frame, export. Paste a screenshot. Download a PNG.

The second row is 6 hours. Style presets, batch export, annotations, social sizes. Those can wait until one real tweet already used the file.

The third row is 2 weeks and up. A template gallery, 3D mockups, video exports, accounts. The table labels that row as the part you should skip.

If you tell an agent to “clone Shots.so”, it aims at the third row. Copy the prompt, then pin it to the first row.

This one is different from QR Tiger. That needs a short URL and a bit of storage. The capability line here is Astro, Alpine, and the browser. No database. No login.

Here is the shape of the data you are building. Nothing else has to exist at 2 hours.

const composition = {
  filename: 'hero-home.png',
  shotWidth: 2400,
  shotHeight: 1350,
  preset: { width: 1600, height: 900 },
  padding: 0.12,
  radius: 18,
  shadow: 0.35,
  frame: 'browser-light',
  background: {
    type: 'gradient',
    from: '#c8ff45',
    to: '#7bc8ff',
  },
}

You paste hero-home.png. The tab paints a gradient, insets the shot, and writes hero-home.shot.png at 1600 by 900. Refreshing the page throws the work away, and that is fine. The screenshot never left the machine.

PASTE THE SCREENSHOT Clipboard, drop, or file picker Decode PNG, JPEG, WebP COMPOSE ON A GRADIENT Padding, radius, optional frame EXPORT AT 1600 BY 900 Offscreen canvas, not the preview Download or copy to clipboard
The 2-hour v1 paints a new canvas at the preset size. The on-screen preview is only a preview.

Who a Shots.so alternative is for

The audience line on the page is specific. Anyone posting product screenshots to social media.

The goal is a polished social image in seconds, entirely in the browser. Tonight the done state is one PNG at the size you asked for, plus a copy button that pastes into a chat.

If you need a template marketplace, 3D device mockups, or video exports, keep paying. The site puts that rebuild at 2 weeks on purpose.

The same v1 also replaces Xnapper and Pika, whichever of those you currently pay for.

Compressing a finished file is a different job. That is TinyPNG Pro, $25–$39 a month, about 60 minutes. The TinyPNG walkthrough stays in the tab too. Crop and annotate is Photoshop, $9.99–$22.99 a month, about 12 hours. Capture on a Mac is CleanShot X, $8–$10 a month, about 10 hours.

How does a screenshot frame tool work

I am describing the catalog’s model here, not reverse-engineering Shots.so. You take a bitmap, sit it on a background, and write a new bitmap at a known size.

The prompt names three end-to-end journeys. Tonight you need the first one.

Drop a screenshot on the canvas. Pick a gradient. Adjust padding and corner radius. Export a social-ready PNG.

The screens the prompt asks for are a canvas, a style panel, an export bar, and presets. For 2 hours you can live with the first three. Presets belong to the 6-hour row.

The canvas is a drop target, a framed shot, and sliders that repaint live. The style panel is swatches, padding, radius, shadow, and a frame picker. The export bar is size presets, a PNG download, and copy-to-clipboard.

The data model on the page is local Composition, StylePreset, and ExportJob objects only. Tonight those can be one object.

The capability line is Astro, Alpine, and browser APIs. This site is an Astro app on Cloudflare, so that is the path I default to. Use Rails if that is your weekday language.

Copy the prompt. Before you run it, add one extra line at the top: ship the 2-hour row only. Gradient, padding, frame, export. Stop.

Paste from the clipboard, then reject the rest

The fastest path is the screenshot key, then paste. Agents hide that behind a file picker because a picker is easier to demo.

The mini-prompt on the page is “Paste, drop, or reject”. Listen for paste events. Pull image data from clipboard items. Keep drag-and-drop as the second route, with a visible drop target during dragover. A file picker is the fallback.

Validate by decoding, not by trusting extensions or MIME strings. Accept PNG, JPEG, and WebP that decode into an image. Reject a PDF or a corrupt dump with an inline message, and leave the current composition untouched.

async function acceptShot(file) {
  try {
    return await createImageBitmap(file)
  } catch {
    throw new Error('Drop a PNG, JPEG, or WebP. The current frame is unchanged.')
  }
}

A new valid image replaces the screenshot and keeps the current style settings, so the next paste reuses the look.

Cap input dimensions. The prompt wants a 6K capture downscaled with a notice, so the tab does not stall. Tonight, refuse anything huge with a sentence on the canvas.

Never send image bytes to a server or to analytics. The guardrail on the page is explicit: render and export entirely in the browser so screenshots never leave the device.

Fit the screenshot without upscaling

A tall phone shot and a wide desktop shot both have to sit in a 1600 by 900 preset. Agents scale the preview by CSS and call it done. The export then either crops the shadow or smears the text.

The mini-prompt is “The layout solver behind the pretty padding”. Given output dimensions and a padding percentage, scale the screenshot to the largest size that fits the padded box, keep the aspect ratio, and center it. Cap at natural size so a small window capture stays sharp, and let the background own the extra room.

function fitScreenshot(shot, box) {
  const scale = Math.min(
    box.width / shot.width,
    box.height / shot.height,
    1
  )
  const width = Math.round(shot.width * scale)
  const height = Math.round(shot.height * scale)
  return {
    width,
    height,
    x: Math.round((box.width - width) / 2),
    y: Math.round((box.height - height) / 2),
  }
}

const box = { width: 1216, height: 684 }
fitScreenshot({ width: 2400, height: 1350 }, box)
//{ width: 1216, height: 684, x: 0, y: 0 }

That 1216 by 684 box is a 1600 by 900 preset with 12 percent padding on each side. A 2400 by 1350 hero fills it. A 800 by 500 window capture stays 800 by 500 and sits in the middle.

Layer order matters. Background, then shadow, then the frame, then the screenshot clipped to the corner radius. When a frame is present, put the radius on the frame’s outer edge so the screenshot’s own corners stay square inside it.

The frame’s header bar adds height. The fit has to account for that, or a tall shot overflows the padded box. Recompute the layout on every slider change and every preset switch.

Export at exact pixel dimensions

The preview canvas is sized for the viewport and scaled by device pixel ratio. Its bitmap is almost never 1600 by 900.

The mini-prompt is “Export at exactly 1600 by 900”. On export, create an offscreen canvas at the preset size and paint the composition again. Every measurement is computed for the target size. Do not scale the preview up.

async function exportPng(composition) {
  const canvas = document.createElement('canvas')
  canvas.width = composition.preset.width
  canvas.height = composition.preset.height
  paintComposition(canvas.getContext('2d'), composition)

  const blob = await new Promise((resolve, reject) => {
    canvas.toBlob((result) => {
      if (!result) {
        reject(new Error('Could not encode the PNG'))
        return
      }
      resolve(result)
    }, 'image/png')
  })

  return blob
}

paintComposition is the same function the live preview calls, with the preset width and height instead of the viewport box. Corner-radius clipping should antialias. Shadows should land inside the bounds. Use a high-quality resample when the screenshot shrinks.

Offer a PNG download and a copy-to-clipboard from that blob. Where the clipboard API refuses images, show a fallback message and keep the download.

The prompt’s done state is a file that measures exactly 1600 by 900, regardless of window size or display density. Open it and read the pixels.

function outputName(filename) {
  const dot = filename.lastIndexOf('.')
  const stem = dot === -1 ? filename : filename.slice(0, dot)
  return `${stem}.shot.png`
}

outputName('hero-home.png')
//'hero-home.shot.png'

What not to build in a Shots.so clone

Leave out accounts, cloud storage, a template marketplace, video mockups, and team libraries.

The non-goal on the page already names those. You are replacing the frame you currently rent for tweets.

If the agent starts an upload to R2 “so the composition survives a refresh”, stop it. Paste the non-goal again. Refreshing discards local work, and that is the product.

2-HOUR V1 Paste from the clipboard Gradient, padding, frame PNG at the preset size SKIP THIS HALF Template gallery 3D mockups and video Accounts and team libraries
Ship the 2-hour frame. Leave the mockup platform on the skip row.

The 6-hour row, after you post one

Use the tool as the real framer for a week. Then look at the second estimate.

Style presets, so the next shot matches the last one. Batch export. Arrows and a caption. A few social sizes beyond the first preset.

The preset mini-prompt is “Style presets that live in localStorage”. Save background, padding, radius, shadow, and frame under a name. Store a small thumbnail as a data URL, capped, because quota is finite. Version the schema so an old preset loads with defaults for a new field instead of crashing.

const preset = {
  version: 1,
  name: 'Launch lime',
  padding: 0.12,
  radius: 18,
  shadow: 0.35,
  frame: 'browser-light',
  background: {
    type: 'gradient',
    from: '#c8ff45',
    to: '#7bc8ff',
  },
}

Applying a preset changes style settings and never touches the loaded screenshot. Include rename, delete with a confirm, and a reset-to-defaults control. Persist last-used settings across sessions. Treat corrupt JSON as defaults.

The catalog says 6 hours for that row. Stop there.

Shots.so alternative or a TinyPNG compressor

A smaller file is still TinyPNG Pro. A Shots.so alternative is a new image with a background, not a smaller copy of the same pixels.

If you open Photoshop to crop one screenshot and draw an arrow, build that instead. Twelve hours, five edits, still in the tab.

The QR Tiger walkthrough is the other Tonight card from today if the rent is a printed code.

Host it, frame a real screenshot, cancel

The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.

This one can be a static page on a subdomain you already own, shots.flaviocopes.com, or a path next to a site you already deploy. There is no database to back up.

Use it as the real tool for a week. Paste one launch screenshot, download the PNG, and check the pixel size. If you miss presets or a second social size, add them from the 6-hour row.

Then cancel Shots.so, or Xnapper, or Pika, whichever one you pay for.

The audit is still the index if you want another Tonight card. TinyPNG Pro is about 60 minutes if the next bill is bytes.

Start now

Go to Build your own Shots.so. Copy the AHA prompt, or the stack you already write.

Tell the agent the first estimate only: gradient background, padding, device frame, export. Two hours.

Paste one real screenshot. Put the PNG on a tweet tonight.