{"id":"bff68bdf-d484-42ab-b17b-60d11e7e69d7","name":"iOS Notification","slug":"ios-notification","category":"post-screenshots","description":"One Apple notification or a stack of them, as the glass cards iOS actually draws: app icon and name, title and body, a grouped “3 more notifications” tail, and the optional “Keep Receiving Notifications?” panel. Dark and light materials over any wallpaper you pass.","sample_params":{"app_name":"media0","bg":"#7b61ff","content":"1,204 Open Graph images published from one template.","content2":"4,012 of 10,000 renders used. Nothing to do yet.","more":"3","theme":"dark","time":"1h ago","time2":"3h ago","title":"Render finished","title2":"Monthly quota at 40%"},"spec":{"name":"ios-notification","width":1000,"height":700,"scale":2,"html":"\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset='utf-8'\u003e\n\u003cstyle\u003e\n*{margin:0;padding:0;box-sizing:border-box}\nhtml,body{width:100%;height:100%}\nbody{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,Helvetica,Arial,sans-serif;\n  overflow:hidden;display:flex;align-items:center;justify-content:center}\n/* iOS notifications are a blurred material over the wallpaper, not a flat\n   fill: --card is translucent and the blur is what makes it read as glass. */\n/* Geometry is the Figma source scaled by 620/320 = 1.9375: card r12→23,\n   icon 20→39 inset 10→19 on every side, buttons 136x44 r8→264x85 r15.5\n   inset 16→31. Fills are the source's exact alphas, and because the\n   buttons paint over the card the same way they do in the file, .4 over\n   .45 lands dark and .4 over .5 lands light on its own. */\n.dark{--card:rgba(0,0,0,.45);--fg:#fff;--dim:rgba(255,255,255,.62);--faint:rgba(255,255,255,.46);\n  --rule:rgba(255,255,255,.14);--btn:rgba(0,0,0,.4)}\n.light{--card:rgba(255,255,255,.5);--fg:#0b0b0f;--dim:rgba(11,11,15,.6);--faint:rgba(11,11,15,.45);\n  --rule:rgba(11,11,15,.1);--btn:rgba(255,255,255,.4)}\n#stack{width:620px;zoom:1}\n.group{position:relative}\n.group + .group{margin-top:18px}\n.card{position:relative;z-index:1;border-radius:23px;background:var(--card);color:var(--fg);\n  -webkit-backdrop-filter:blur(45px);backdrop-filter:blur(45px);overflow:hidden}\n/* the grouped-notification cards peeking out from under the front one */\n.peek{position:absolute;z-index:0;height:40px;border-radius:23px;background:var(--card);\n  -webkit-backdrop-filter:blur(45px);backdrop-filter:blur(45px)}\n.p1{left:16px;right:16px;bottom:-12px}\n.p2{left:34px;right:34px;bottom:-23px}\n.body{padding:19px}\n.head{display:flex;align-items:center;gap:11px;margin-bottom:14px}\n.icon{width:39px;height:39px;border-radius:11px;flex:none;overflow:hidden;display:flex;\n  align-items:center;justify-content:center;font-size:20px;font-weight:700;\n  background:linear-gradient(160deg,rgba(255,255,255,.85),rgba(255,255,255,.45));color:#3b3b46}\n.icon img{width:100%;height:100%;object-fit:cover;display:block}\n.app{font-size:19px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--dim);\n  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.time{margin-left:auto;font-size:19px;color:var(--faint);white-space:nowrap;flex:none}\nh1{font-size:26px;font-weight:600;letter-spacing:-0.01em;line-height:1.3;\n  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.text{font-size:26px;font-weight:400;line-height:1.3;\n  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\nh1 + .text{margin-top:2px}\n.more{margin-top:11px;font-size:22px;color:var(--faint)}\n/* the \"Keep Receiving Notifications?\" panel, inside the same card */\n.ask{border-top:1px solid var(--rule);padding:22px 31px 31px}\n.ask p{font-size:22px;line-height:1.35;color:var(--fg)}\n.ask .row{display:flex;gap:31px;margin-top:20px}\n.ask button{flex:1;min-width:0;height:85px;font:inherit;font-size:24px;font-weight:500;color:var(--fg);\n  padding:0 10px;border:0;border-radius:15.5px;background:var(--btn);\n  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n\u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cdiv id='stack'\u003e\n  \u003cdiv class='group' data-title='{{.title}}' data-text='{{.content}}' data-time='{{.time}}'\u003e\u003c/div\u003e\n  \u003cdiv class='group' data-title='{{.title2}}' data-text='{{.content2}}' data-time='{{.time2}}'\u003e\u003c/div\u003e\n  \u003cdiv class='group' data-title='{{.title3}}' data-text='{{.content3}}' data-time='{{.time3}}'\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n// An absent param interpolates as null here, not \"\" — hence every || ''.\nvar APP = String({{.app_name}} || '').trim();\nvar ICON = String({{.app_icon}} || '').trim();\nvar MORE = String({{.more}} || '').trim();\nvar ASK = String({{.prompt}} || '').trim();\nvar KEEP = String({{.prompt_keep}} || '') || 'Keep…';\nvar OFF = String({{.prompt_off}} || '') || 'Turn off…';\nvar THEME = String({{.theme}} || '').trim().toLowerCase() === 'light' ? 'light' : 'dark';\n\ndocument.body.className = THEME;\ndocument.body.style.background = {{.bg}} || '#7b61ff';\n\n// The cards are assembled as markup, so every interpolated value is\n// escaped for both text and quoted-attribute context.\nfunction esc(s) {\n  return String(s).replace(/[\u0026\u003c\u003e\"']/g, function (c) {\n    return {'\u0026': '\u0026amp;', '\u003c': '\u0026lt;', '\u003e': '\u0026gt;', '\"': '\u0026quot;', \"'\": '\u0026#39;'}[c];\n  });\n}\n\n// A notification needs a title or a body; the rest are dropped so one, two\n// or three of them stack with the same rhythm.\nvar groups = [].slice.call(document.querySelectorAll('.group')).filter(function (g) {\n  var keep = (g.dataset.title || g.dataset.text).trim() !== '';\n  if (!keep) g.remove();\n  return keep;\n});\n\nvar iconInner = ICON\n  ? \"\u003cimg src='\" + esc(ICON) + \"' alt=''\u003e\"\n  : esc((APP.trim()[0] || '?').toUpperCase());\n\ngroups.forEach(function (g, i) {\n  var last = i === groups.length - 1;\n  var html = \"\u003cdiv class='card'\u003e\u003cdiv class='body'\u003e\" +\n    \"\u003cdiv class='head'\u003e\u003cdiv class='icon'\u003e\" + iconInner + \"\u003c/div\u003e\" +\n    \"\u003cspan class='app'\u003e\" + esc(APP) + \"\u003c/span\u003e\" +\n    \"\u003cspan class='time'\u003e\" + esc(g.dataset.time || '') + \"\u003c/span\u003e\u003c/div\u003e\";\n  if (g.dataset.title.trim()) html += \"\u003ch1\u003e\" + esc(g.dataset.title) + \"\u003c/h1\u003e\";\n  if (g.dataset.text.trim()) html += \"\u003cdiv class='text'\u003e\" + esc(g.dataset.text) + \"\u003c/div\u003e\";\n  // Both the grouped-count line and the prompt hang off the last card, the\n  // way iOS only ever collapses or asks at the bottom of a group.\n  if (last \u0026\u0026 MORE) {\n    var n = MORE.replace(/[^0-9]/g, '');\n    html += \"\u003cdiv class='more'\u003e\" + esc(/[a-z]/i.test(MORE) ? MORE : n + ' more notification' + (n === '1' ? '' : 's')) + \"\u003c/div\u003e\";\n  }\n  html += \"\u003c/div\u003e\";\n  if (last \u0026\u0026 ASK) {\n    html += \"\u003cdiv class='ask'\u003e\u003cp\u003e\" + esc(ASK) + \"\u003c/p\u003e\u003cdiv class='row'\u003e\" +\n      \"\u003cbutton type='button'\u003e\" + esc(KEEP) + \"\u003c/button\u003e\" +\n      \"\u003cbutton type='button'\u003e\" + esc(OFF) + \"\u003c/button\u003e\u003c/div\u003e\u003c/div\u003e\";\n  }\n  html += \"\u003c/div\u003e\";\n  if (last \u0026\u0026 MORE) html = \"\u003cdiv class='peek p2'\u003e\u003c/div\u003e\u003cdiv class='peek p1'\u003e\u003c/div\u003e\" + html;\n  g.innerHTML = html;\n});\n\n// Never clip: a stack taller than the canvas shrinks until it fits. zoom,\n// not transform — transform leaves the layout box at full height, which\n// pushes the stack off a centred page. Each pass reflows, so converge\n// instead of trusting the first ratio.\nvar stack = document.getElementById('stack');\nvar room = document.documentElement.clientHeight - 72;\nfor (var z = 1, n = 0; n \u003c 8 \u0026\u0026 z \u003e 0.42; n++) {\n  var h = stack.getBoundingClientRect().height;\n  if (h \u003c= room) break;\n  z = Math.max(0.4, z * Math.min(0.97, room / h));\n  stack.style.zoom = z;\n}\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n","params":{"app_icon":{"type":"image_url","maxLen":500,"description":"Optional app icon: an http(s) URL to a PNG, JPEG, WebP, GIF or AVIF (no SVG), fetched server-side. Without it the icon is a rounded square with the app's initial."},"app_name":{"type":"string","required":true,"maxLen":40,"description":"App name in the header row, shown uppercase as iOS does."},"bg":{"type":"string","default":"#7b61ff","maxLen":200,"description":"The wallpaper behind the notifications: any CSS background, from a flat colour to a gradient. Use transparent for a bare stack, though the glass material reads best over something."},"content":{"type":"string","maxLen":300,"description":"The first notification's body text, clamped to three lines. A notification needs a title or a body to show at all."},"content2":{"type":"string","maxLen":300,"description":"The second notification's body text, clamped to three lines. A notification needs a title or a body to show at all."},"content3":{"type":"string","maxLen":300,"description":"The third notification's body text, clamped to three lines. A notification needs a title or a body to show at all."},"more":{"type":"string","maxLen":30,"description":"Collapses the group: '3' renders '3 more notifications' on the last card with two more cards peeking out beneath it. Pass your own wording to override the sentence. Empty hides it."},"prompt":{"type":"string","maxLen":160,"description":"Optional 'Keep Receiving Notifications?' panel on the last card, e.g. 'Keep receiving notifications from the Acme app?'. Empty hides the panel and its buttons."},"prompt_keep":{"type":"string","default":"Keep…","maxLen":30,"description":"Label of the left prompt button. Only shown when prompt is set."},"prompt_off":{"type":"string","default":"Turn off…","maxLen":30,"description":"Label of the right prompt button. Only shown when prompt is set."},"theme":{"type":"string","default":"dark","maxLen":8,"description":"dark (default) or light — the two notification materials. Both are translucent and blur what is behind them, so they pick up the background."},"time":{"type":"string","maxLen":20,"description":"The first notification's timestamp, right-aligned next to the app name, e.g. '1h ago' or 'now'."},"time2":{"type":"string","maxLen":20,"description":"The second notification's timestamp, right-aligned next to the app name, e.g. '1h ago' or 'now'."},"time3":{"type":"string","maxLen":20,"description":"The third notification's timestamp, right-aligned next to the app name, e.g. '1h ago' or 'now'."},"title":{"type":"string","maxLen":120,"description":"The first notification's bold title line, clamped to two lines."},"title2":{"type":"string","maxLen":120,"description":"The second notification's bold title line, clamped to two lines; empty drops this notification."},"title3":{"type":"string","maxLen":120,"description":"The third notification's bold title line, clamped to two lines; empty drops this notification."}}}}
