app.json sits in the mini-app root and configures the whole mini-app. The file is a single JSON object.
Extension file: you can add an app.ext.json alongside it. The two are merged, and app.ext.json wins on any key they share. The same works for pages and components (index.ext.json).
theme.json — required when darkmode is true"v2" for the upgraded base-component styles"requiredComponents" — load component code lazilyEach entry is a path without a file extension. The framework finds the matching .json, .js, .fxml and .ftss itself.
For this directory layout:
├── app.js ├── app.json ├── app.ftss ├── pages │ ├── index │ │ ├── index.fxml │ │ ├── index.js │ │ ├── index.json │ │ ├── index.ftss │ ├── logs │ ├── logs.fxml │ ├── logs.js ├── utils
…you write:
{
"pages": ["pages/index/index", "pages/logs/logs"]
}Rules that bite:
entryPagePath, the first item is the home page index.js.path sent by the host, or a navigateTo url, must match an entry here exactly — or the mini-app quietly opens the home page instead.Sets which page opens first. Defaults to the first item in pages.
{ "entryPagePath": "pages/home/home" }It takes no query parameters — a path only.
Status bar, navigation bar, title, background.
black or whitedefault, custom, customV2, hidedark or lightonReachBottomauto, portrait, landscapestatic or dynamicstatic or manual{
"window": {
"navigationBarTitleText": "Luxe Spa",
"navigationBarBackgroundColor": "#1A2E2A",
"navigationBarTextStyle": "white",
"backgroundTextStyle": "light",
"enablePullDownRefresh": true
}
}navigationBarTextStyle and backgroundTextStyle take two values each. Any other value is ignored, and the default silently applies — a common reason a title looks unreadable on a dark bar.
You get the top-right capsule and nothing else — you draw the entire bar yourself, including the status-bar inset. Budget for that before choosing it. It has no effect inside a web-view component.
black or whitebottom or topEach entry in list:
pages{
"tabBar": {
"color": "#999999",
"selectedColor": "#C5A059",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/index/index", "text": "Home" },
{ "pagePath": "pages/services/services", "text": "Explore" },
{ "pagePath": "pages/bookings/bookings", "text": "Bookings" }
]
}
}Things that catch people:
position: "top", icons are not displayed at all.ft.switchTab, never ft.navigateTo — and switchTab drops the query string, so pass data through globalData instead.The sentence a user sees when the mini-app asks for something. Without it the prompt has no explanation, and users deny more often.
{
"permission": {
"scope.userLocation": {
"desc": "Your location is used to show the nearest branch"
},
"scope.camera": {
"desc": "The camera is used to attach a photo to a support ticket"
}
}
}desc is the only field, and it is required. Supported scopes:
scope.userLocation · scope.userLocationBackground · scope.record · scope.camera · scope.bluetooth · scope.writePhotosAlbum · scope.addPhoneContact · scope.addPhoneCalendar · scope.userInfo · scope.getPhoneNumber
Milliseconds. Each defaults to 60000.
ft.requestft.connectSocketft.uploadFileft.downloadFile{
"networkTimeout": {
"request": 15000,
"uploadFile": 60000
}
}60 seconds is a long time to stare at a spinner. Drop request to 10–20 s and handle the failure; leave uploads and downloads high.
A component declared here is available in every page and component without re-declaring it:
{
"usingComponents": {
"loading-spinner": "/components/loading-spinner/index"
}
}Use this sparingly. A global component is treated as a dependency of every page, is initialised at startup, and counts against the main package size. A component used by one page belongs in that page's own .json.
{
"darkmode": true,
"themeLocation": "theme.json"
}themeLocation is required when darkmode is true. Base components then pick their own default styles, and the navigation and tab bars switch automatically.
Everything beyond the base styles is yours to adapt. Two warnings:
prefers-color-scheme. Drive your colours from theme.json instead.style — "v2" opts into the upgraded styles for button, icon, radio, checkbox, switch and slider.
useExtendedLib — pulls in an extended library at no package-size cost:
{ "useExtendedLib": { "kbone": true, "weui": true } }subpackages / preloadRule — split a large mini-app so the first launch downloads less, and declare which chunks to fetch in the background.
lazyCodeLoading — "requiredComponents" loads only the component code a page actually needs, which cuts startup time on component-heavy apps.
resizable — lets a desktop window be resized or maximised, and allows rotation on tablets. Off by default.
referer — adds a referer to HTTP requests from video, image (Android only), request, downloadFile, uploadFile, loadFontFace, previewImage, previewMedia and getImageInfo. The value is built as {referer}/{appid}/{version}/view.html.
requiredBackgroundModes — declares what must keep running in the background, such as audio playback.
plugins — declares plugins the mini-app depends on.
debug — deprecated. Use ft.setEnableDebug instead, or enable debugging from the More menu on a non-production build.
Present in the wider mini-program spec, but inert here. Setting them does nothing:
entranceDeclare · functionalPages · sitemapLocation · workers · singlePage · supportedMaterials · serviceProviderTicket · embeddedAppIdList · halfPage · restartStrategy · visualEffectInBackground
Each page can have its own .json that overrides the global window block for that page only. Write the window properties at the top level — there is no window wrapper:
{
"navigationBarTitleText": "Order Details",
"enablePullDownRefresh": true,
"onReachBottomDistance": 100,
"usingComponents": {
"order-card": "/components/order-card/index"
}
}This is where enablePullDownRefresh usually belongs — global pull-to-refresh on every page, including ones with nothing to refresh, feels broken.
{
"entryPagePath": "pages/index/index",
"pages": [
"pages/index/index",
"pages/services/services",
"pages/bookings/bookings",
"pages/order-detail/order-detail"
],
"window": {
"navigationBarTitleText": "Luxe Spa",
"navigationBarBackgroundColor": "#1A2E2A",
"navigationBarTextStyle": "white",
"backgroundTextStyle": "light"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#C5A059",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/index/index", "text": "Home" },
{ "pagePath": "pages/services/services", "text": "Explore" },
{ "pagePath": "pages/bookings/bookings", "text": "Bookings" }
]
},
"permission": {
"scope.camera": {
"desc": "The camera is used to attach a photo to a support ticket"
}
},
"networkTimeout": {
"request": 15000
},
"style": "v2"
}pages arraypath opens the home page insteadpath does not match a pages entry exactlynavigationBarTextStyle only accepts black or whitelist, or a missing required colourposition: "top"switchTab drops it — use globalDataenablePullDownRefresh not set globally or on that pagepermission.<scope>.descusingComponents, or no subpackagesprefers-color-scheme has no effecttheme.jsonnetworkTimeout.request