Shoppingate Logo
HomePartnerSell on E-commAbout UsContact UsDevelopers
Documentation
  • Getting started
  • Choosing your path
  • Introduction
    • Introduction
    • Sign up & login
    • Dashboard & analytics
    • Mini-Programs
    • Submit & publish a version
    • App Binding
    • Privacy Settings
    • Allowed URL
    • Users & roles
    • Introduction
    • Install & log in
    • Create a project
    • The workspace
    • Preview & upload
  • app.json config
  • Lifecycle
  • Routing & query
  • Host functions
  • orderPayment
  • Permissions
  • User info
  • Deep links
  • Overview & SDK
  • The wrapper
  • What a page can reach
  • Host functions
  • Permissions
  • Look and feel
  • Testing
  • Authentication
  • Endpoints
  • Webhooks
Web view

Look and feel

A browser gives your page a set of defaults, and every one of them tells the user they have left the app — bouncing scroll, blue tap flashes, pinch zoom, a long-press menu. None of it is something the host can switch off for you. This is the page-level work that makes a wrap feel native.

This page is for web pages in a <web-view>. Native mini-app pages have no viewport meta, no browser chrome and no bounce, so none of it applies there — their styling is .ftss.

Do not build a navigation bar

Shoppingate draws the title bar and the back control. A header of your own puts a second title on the same screen, and the user ends up with two ways back that behave differently. Set the title in the shell's app.json and let the host render it.

Zoom

<meta name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">

maximum-scale=1 and user-scalable=no stop pinch zoom; viewport-fit=cover lets your page reach under the notch, which you then handle with safe-area insets. touch-action: manipulation kills the double-tap zoom delay.

iOS honours pinch gestures regardless of the meta tag, so the JavaScript below is not belt-and-braces — it is the part that actually works on iPhone.

// The meta tag and touch-action cover most of it. iOS still honours pinch gestures and
// the long-press menu, and both read as "this is a browser" the moment someone hits them.
["gesturestart", "gesturechange"].forEach(function (e) {
  document.addEventListener(e, function (ev) { ev.preventDefault(); }, { passive: false });
});
document.addEventListener("contextmenu", function (e) { e.preventDefault(); });

Scrolling and bounce

Rubber-banding the whole document is the single most website-ish thing a web-view page does. Pin the document and let an inner container scroll instead.

/* The app cannot do any of this for you: a page gets the browser's defaults unless it
   opts out, and every one of those defaults tells the user they left the app. */

html {
  touch-action: manipulation;        /* kills double-tap zoom */
  -webkit-text-size-adjust: 100%;
}

/* Pin the document and let an inner container scroll. This is what stops the whole page
   rubber-banding, which is the single most website-ish thing a web-view page does. */
html, body {
  height: 100%; width: 100%;
  overflow: hidden;
  position: fixed;
  margin: 0;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
}

#app {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: none;
  /* Clear of the home indicator. */
  padding-bottom: calc(2rem + env(safe-area-inset-bottom));
}

/* ...but anything worth copying stays selectable. Order numbers, reference ids, errors. */
input, textarea, [contenteditable], .selectable {
  user-select: text;
}

img { -webkit-user-drag: none; }
::-webkit-scrollbar { display: none; }

/* A phone has no hover, so the pressed state is the only feedback before the result
   arrives. 44px is the minimum touch target. */
button {
  min-height: 44px;
  transition: transform .08s ease;
}
button:active { transform: scale(.985); }

@media (prefers-reduced-motion: reduce) {
  button, button:active { transition: none; transform: none; }
}

Give any inner scrolling region — a long code block, a table — overscroll-behavior: contain, so reaching its end does not start scrolling the page behind it.

Keep the right things selectable. Blanket user-select: none is what makes a page feel like an app, but it also makes order numbers, reference ids and error messages impossible to copy. Opt those back in. People need to paste them into a support chat.

Safe areas

env(safe-area-inset-bottom) keeps content clear of the home indicator. Design between 320px and 430px wide, and make sure nothing scrolls sideways anywhere in that range.

Both themes

Your page renders in the viewer's theme, which you do not choose. Define a complete token set for light, then redefine only the tokens for dark.

/* Both themes, as a complete token set each. Define every colour on bare :root, then
   redefine ONLY the tokens for dark — a colour whose single definition sits inside the
   media query simply never applies in the other theme.

   :root is correct in a web page. Inside a mini-app's own .ftss the root element is
   `page`, and tokens on :root there match nothing at all. */
:root {
  --bg: #f5f5f7;
  --surface: #ffffff;
  --text: #111111;
  --muted: #6e6e73;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #121212;
    --surface: #1c1c1e;
    --text: #f2f2f2;
    --muted: #9b9ba1;
  }
}

body {
  /* Explicit, always. The host paints its own ground behind the page, so a transparent
     body borrows the host's theme and you get one theme's text on the other's background. */
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               "Noto Sans Arabic", sans-serif;
}

Two failure modes worth naming. A colour whose only definition sits inside the dark media query never applies in light — that is the classic unreadable page. And a body with no explicit background borrows the host's ground, so you get one theme's text on the other theme's background.

Arabic

Set dir="rtl" and use logical properties throughout. A layout built on margin-left needs a second stylesheet to mirror; one built on margin-inline-start mirrors itself.

/* Logical properties, so Arabic mirrors without a second stylesheet. */
.card {
  padding-inline-start: 1rem;   /* not padding-left */
  padding-inline-end: 1rem;
  margin-inline-start: auto;
}

Include "Noto Sans Arabic" in your font stack, and check the result with real Arabic copy — logical properties mean it can mirror, not that it does.

Touch and feedback

Touch targets at least 44×44px. Give every control a pressed state: a phone has no hover, so the press is the only feedback available before the result arrives, and without it people tap twice. Never put behaviour behind :hover alone.

Size type in rem so the system text-size setting is honoured, keep inputs at 16px or larger to stop iOS zooming on focus, and give keyboard and switch-control users a visible :focus-visible state.

Before you submit

Fifteen things, most of them a line of CSS:

Check
Why it matters
No navigation bar of your own
Shoppingate draws one. A second title on the same screen is the giveaway.
Pinch and double-tap zoom off
A page that zooms is a page in a browser.
No long-press menu, no tap flash, no visible scrollbars
Each one is a browser default the app does not have.
The document cannot rubber-band
Pin html/body and scroll an inner container.
Bottom safe area respected
Content under the home indicator is unreachable.
No horizontal scroll from 320px to 430px
The range real devices land in.
Touch targets at least 44×44px
Smaller is a miss, repeatedly.
Feedback within ~100ms of every tap
Without it people tap twice.
Nothing depends on :hover
There is no hover.
Both light and dark render correctly
The viewer's theme is not your choice.
Arabic reads correctly with dir="rtl"
Logical properties, and real Arabic copy to check it with.
Type in rem
So the system text-size setting is honoured.
No white flash on load
Set a background on the page and on the shell.
Every link stays in the app
A page that opens a browser has lost the user.
aria-label and alt text in both languages
Icon-only controls are unusable without them.

Most of these are statically checkable — whether the viewport meta is right, whether a 44px minimum is set, whether both themes are defined. A handful are not, and those need a real device: horizontal scroll across the width range, Arabic with real copy, and whether every tap actually feels instant.

Need help? We’re here.

Our team is ready to support you in every step of your experience.

Email Us
[email protected]
Sell With SGContact UsTerms of ServicePrivacy PolicyFAQTravel FAQ
Connect with us
X / TwitterLinkedInInstagramFacebookTiktok

Copyright © Shoppingate 2026

Mada
Visa
Mastercard
Apple Pay
Tamara