docs
/
AI skills

Make a page work on phones and tablets

How a Build Studio page adapts to phone and tablet widths. There are no per-device style settings, so the rules are media queries or Tailwind responsive classes. Where to put them, why inline styles beat them, how site CSS and the platform's Tailwind layers interact, hiding things per device, and checking at real widths.

Build Studio has no per-device styles. Everything it saves applies at every width:

  • The Props Editor's Style tab writes the element's inline style attribute: one value for every width.
  • The Monitor and Smartphone buttons at the top of the Style tab do nothing. They only log to the console (property-style-media-switch.tsx 9–11).
  • The Desktop, Tablet and Mobile buttons in the top bar only resize the canvas (step 4).
  • The page record's Breakpoints list (data.breakpoints, sm / md / lg) isn't read by the site.

A page adapts to width only through CSS that you write: media queries, or Tailwind responsive classes (md:, lg:). Both work on the hosted site (tested at 390, 768 and 1440 px). Variants and data-show can't help: the server always sees "desktop", and the page runtime has no device key (see Show different content to different visitors).

For the page record, see Build a website page. For root and its shared CSS, see Shared header and footer. For motion, see Animate elements on a page.

Ask first

  • Which page, or the whole site? One page's rules go in its head; rules for the header, footer and every page go in root's Page settings → Style.
  • What is wrong, and at what width? "Cut off on my iPhone", "three columns are too narrow on a tablet". If they don't know, check at 390, 768 and 1440 first (step 5) and show them.
  • What should change on a phone? Stack or hide, smaller type, a different image, a burger menu. Use their words. Don't drop content they didn't ask to hide.
  • Is the page live? On a site that serves drafts, a save is live at once.

Steps

1. Build Studio: read the page and what wraps it

Read the page with repo_find_one (the find result is partial) and the site's root page the same way. Note:

  • The active screen's html: its <head> <style>, and every inline style="…" that sets a size. Search for style=" with width, min-width, height, padding, margin, font-size or left / right. Each one is the same at every width, and a media query can't change it (step 2).
  • root's style.css: the site's shared CSS. Note its media queries and its breakpoints, and use the same ones on the page.
  • Animated elements (config.animations on the screen): poses that hold pixel widths are written inline by the site and break a phone layout. See Animate elements on a page step 2.

Hands on: the page id, the inline sizes to move into classes, and the site's breakpoints. Check: you can name each element that sets a fixed size.

2. Build Studio: the page's phone and tablet rules

Put the rules in a <style> in the screen's <head> (Build Studio keeps the head as written; edit it in code view). page_write with css (Page settings → Style) works too, and appends.

How the CSS reaches the page. The site writes one stylesheet holding root's Page settings CSS, then a parent page's, then the page's, then the site's own site.style.css (base-app page-assets.ts 30–71). The page's <head> styles come after it. Tested order on a page: the Tailwind build first, then root's CSS, then the page's head. At equal specificity the later rule wins, so a page's rule beats root's.

Write classes, not inline styles. An inline style beats any media query that doesn't use !important. Tested at 390 px: style="padding:80px" stayed 80px against @media (max-width:760px){ .x{ padding:10px } }; with !important it became 10px. Move the sizes from step 1 into a class and delete them from style.

The pattern, desktop first, with the breakpoints PhaZZer's site uses (its assets/site.css):

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.phone-only { display: none; }
.h1 { font-size: clamp(40px, 5.6vw, 96px); }      /* fluid type: no breakpoint needed */
img { max-width: 100%; height: auto; }

@media (max-width: 1100px) {                        /* tablet */
  .cards { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 760px) {                         /* phone */
  .cards { grid-template-columns: 1fr; }
  .desktop-only { display: none; }
  .phone-only { display: block; }
  .h1 { font-size: clamp(30px, 8.6vw, 40px); }
  input, select, textarea { font-size: 16px; }      /* iOS zooms into smaller fields */
}

Tested at 390 / 768 / 1440 px: the grid went 1 / 2 / 3 columns, and the desktop-only and phone-only blocks swapped.

Or Tailwind classes. Every site page loads the Tailwind v4 browser build (@tailwindcss/browser@4, base-app page-assets.ts 194), so Tailwind classes work on the hosted page, including responsive ones. Tested: hidden md:block was hidden at 390 and shown at 768; grid grid-cols-1 md:grid-cols-3 gave 1 then 3 columns; px-6 lg:px-12 gave 24px then 48px.

PrefixApplies from
(none)every width
sm:640 px
md:768 px (tested)
lg:1024 px (tested)
xl:1280 px
2xl:1536 px

Tailwind prefixes mean "this width and up", so write the phone style plain and add the larger ones with prefixes: class="text-2xl md:text-4xl".

A class base-app doesn't use itself is built by that script after the page loads (about 0.3 s on the local site), so it can flash at first. Colour names like bg-primary and text-secondary are Build Studio's sample palette (primary is #0cd6a0, tested), not the site's brand colours. Use the site's own colours.

Hiding per device: display: none inside a media query, or hidden md:block (phone hidden) and md:hidden (phone only). The hidden part is still in the page's HTML and still downloads its images.

Components with their own breakpoints:

ComponentResponsive setting
wm-gridcolumns (below 640 px, default 1), then columns-sm, columns-md, columns-lg, columns-xl (1–6, defaults 2 / 3 / 4 / 4) from 640 / 768 / 1024 / 1280 px
wm-mega-menumobile-breakpoint (default 768): below it, a burger and a full-screen menu
wm-sliderslides-per-view is one value for every width. To show fewer on a phone, set the attribute from page JavaScript with matchMedia; the slider rebuilds when it changes

All attributes are in Appmint components on a page.

Hands on: the rules in the screen's head (or classes on the elements). Check: no element the rules target still carries the same property in its inline style.

3. Page settings → Style on root: site-wide rules and resets

Rules for the header, footer and every page go in root's Page settings → Style → CSS (style.css on the root record; page_write on root's id with css). That stylesheet also loads on the built-in login, account and checkout pages (Shared header and footer step 2).

Tailwind's layers decide who wins. The platform's own stylesheet is Tailwind v4: its reset (preflight) sits in @layer base, and the utility classes that style the cart drawer, checkout, account and login screens sit in @layer utilities (tested layer order: theme, base, components, utilities). CSS outside any layer beats every layered rule, whatever its specificity. So:

  • Element resets go in @layer base. An unlayered *{margin:0;padding:0}, button{background:none;border:0} or a{color:inherit} overrides the platform's utilities and strips padding, borders and button colours from those screens. Tested: an unlayered .x p{padding:0} turned a p-8 paragraph's 32px padding into 0, while the same rule inside @layer base left it at 32px.
  • Your own classes stay unlayered. They then beat Tailwind on your pages, which is what you want.
/* root → Page settings → Style → CSS */
@layer base {
  * { box-sizing: border-box; margin: 0; padding: 0; }
  img { display: block; max-width: 100%; }
  a { color: inherit; text-decoration: none; }
  button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
}
body { background: #0a0b0f; color: #eeeae2; font: 16px/1.6 Inter, system-ui, sans-serif; }
/* …the site's own classes and media queries, unlayered… */

This is how PhaZZer's site is built (assets/site.css, uploaded as root's CSS).

What the platform's CSS does to a page that doesn't set things itself:

Platform ruleEffectDo
Tailwind preflight (@layer base)Headings have no size or weight of their own (tested: a plain <h2> was 16px); lists have no bullets; images are display: blockSize headings and style lists yourself
base-app styles/style.css (unlayered)body { font-family: Arial }Set the font on body
Dark mode (app/layout.tsx 90–104)A visitor whose device is in dark mode gets class="dark" on <html> (tested), so dark: classes apply (tested), and the platform's body background in @layer base turns near-blackAlways set body's background and color yourself
ViewportThe site already sends <meta name="viewport" content="width=device-width, initial-scale=1">Keeping your own in the head is harmless

Stop: root's CSS changes every page of the site, and the login, account and checkout pages. Show the person what changes and where, and wait for yes.

Hands on: nothing new. Check: repo_find_one on root shows style.css at the top level, with element resets inside @layer base { … }.

4. Build Studio: preview widths (a first look only)

ButtonCanvas width
Desktopthe full canvas
Tabletthe full canvas too: on a web page it changes nothing (app.tsx 226–230)
Mobile448 px (max-w-md)
Preview (projector), then Tablet or Mobile768 × 1024 or 385 × 874; Orientation swaps them (iframe-preview.tsx 83–93)
External preview/__preview/page/<site>/<page> on the site: the real page, in a new tab

Media queries in the head apply at the canvas width, so Mobile and Preview show phone rules. The canvas is still not the site: check the real URL in step 5.

5. Check it on the site at real widths

Open https://<site host>/<slug> in headless Chrome (CDP) or Chrome DevTools' device toolbar, with mobile emulation (Emulation.setDeviceMetricsOverride with mobile: true), at 390 × 844, 768 × 1024 and 1440 × 1000. Wait until the page has finished starting up. At each width:

  • window.innerWidth equals the width you set. If it is larger, something is wider than the screen and the phone zooms the whole page out. Tested: a 1200px-wide block at 390 made innerWidth 1224, and the header's cart and menu buttons ended up off the right edge. overflow-x: hidden on body doesn't prevent this. Find the culprit:

    [...document.querySelectorAll('body *')].filter(e => e.getBoundingClientRect().right > 391).map(e => e.outerHTML.slice(0, 80))
  • The header's menu and cart are visible; the burger shows on phones.

  • Each grid has the columns you meant (getComputedStyle(el).gridTemplateColumns); each phone-only or desktop-only part shows or hides.

  • Text is readable and no button is cut off. Take a screenshot at each width and look at it.

  • Open /login or /checkout once if root's CSS changed: their buttons and fields still have padding and colour.

When it's done

Tell the person: what changes at each width (390, 768, 1440) and what is hidden where, where the rules live (the page head, root's CSS, or classes), any inline sizes you moved into classes, and any element resets moved into @layer base. Show the three screenshots.

What it can't do yet

  • No per-device styles in Build Studio. The Style tab is inline and applies at every width; its Desktop/Mobile switch does nothing.
  • The Tablet canvas button doesn't narrow the canvas on a web page; use Preview.
  • The page's Breakpoints setting isn't used by the site.
  • AI › Improve Layout applies only the HTML the AI returns; any CSS it returns, media queries included, is dropped (AIContextMenu.tsx 913–950).
  • A different page per device, or a variant by device, isn't possible: the server can't tell a phone from a desktop.
  • wm-slider has one slides-per-view for every width.

Don'ts

  • Don't set sizes in the Style tab, and don't leave widths in inline style; media queries can't override them.
  • Don't put unlayered element resets in root's CSS; wrap them in @layer base.
  • Don't use Tailwind colour names for the brand.
  • Don't trust the Build Studio canvas; check the site at 390, 768 and 1440.
  • Don't hide content on phones that the person didn't ask to hide.
  • Don't promise a mobile-only variant or a separate mobile page.