Navigation
Control the bottom tab bar, side drawer, top dropdown menu, and bottom sheet from the web.
At a Glance
The Navigation Bridge lets you control all native navigation panels directly from your web page.
| Component | Bridge namespace | config.json key |
|---|---|---|
| Bottom Tabs | unveilyBridge.bottomTabs | modules.bottomTabs.enabled |
| Side Drawer | unveilyBridge.sideDrawer | modules.sideDrawer.enabled |
| Top Down Menu | unveilyBridge.topDownMenu | modules.topDownMenu.enabled |
| Bottom Sheet | unveilyBridge.bottomSheet | modules.bottomSheet.enabled |
Panel Exclusivity
Bottom Sheet, Top Down Menu, and Side Drawer can only be open one at a time. Opening a new panel automatically closes any other open panel.
Timing — Wait for unveilyGlueReady
window.unveilyBridge is registered before the page loads, but the module namespaces (bottomTabs, sideDrawer, etc.) are only attached after onPageFinished fires on the native side.
In SPAs (React, Vue, etc.) useEffect or onMounted may run before the glue is injected, causing calls like setConfig() to silently do nothing.
Always listen for the unveilyGlueReady event before calling any bridge method on mount:
window.addEventListener('unveilyGlueReady', () => {
window.unveilyBridge.bottomTabs.setConfig({ items: [...] });
window.unveilyBridge.sideDrawer.setConfig({ items: [...] });
}, { once: true });For user-triggered actions (button clicks, etc.) this is not needed — the glue is always ready by the time a user can interact.
Enable Modules
The distributed config.json ships with every UI module disabled (opt-in). Turn on only the panels your app actually uses.
Enable only what you use
Modules are off by default so unrequested tabs, drawers, and gesture observers aren't injected into your app — and so you avoid pulling in unnecessary permissions or store-review complications. Flip only the components you use to "enabled": true.
{
"modules": {
"bottomTabs": { "enabled": true, "autoHide": false, "heightDp": 60 },
"sideDrawer": { "enabled": true },
"topDownMenu": { "enabled": true },
"bottomSheet": { "enabled": true }
}
}To explore quickly, a demo config with every panel enabled ships as config.sample.json. Back up config.json, swap it in, and restore when done. (Android: app/src/main/assets/, iOS: Resources/)
cp config.json config.json.bak && cp config.sample.json config.jsonDon't judge tier (plan) from local config
Plan gating applies only on the remote/license path. Local config.json bypasses it, so a local demo can show panels your actual plan would block. Verify production availability against your plan.
Panel Color Customization
Set a panelColor field on any module to apply your brand color to that panel's background. The value is stored in config.json and survives SDK updates without any MainActivity.kt changes.
Single color (applied to both light and dark mode):
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": "#CC1A1A2E"
}
}
}Separate light/dark colors (automatically switches with the system theme):
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": {
"light": "#CCF0F0F5",
"dark": "#CC1A1A2E"
}
},
"sideDrawer": {
"enabled": true,
"panelColor": {
"light": "#CCFFFFFF",
"dark": "#CC2C2C2C"
}
},
"topDownMenu": {
"enabled": true,
"panelColor": "#CC101020"
},
"bottomSheet": {
"enabled": true,
"panelColor": "#CC1C1C1E"
}
}
}Color format: "#AARRGGBB" (with alpha) or "#RRGGBB" (fully opaque). Omit panelColor to use the default SDK background.
Priority
panelColor takes priority over the High Contrast setting from the accessibility panel. If you need to support High Contrast mode alongside brand colors, either leave panelColor unset or adjust colors dynamically from JS when the accessibility settings change.
Icon / Label Colors
Use iconColor and labelColor to tint icons and set label text color independently. Supported on Bottom Tabs, Top Down Menu, and Side Drawer. Same single-string or {light, dark} object format as panelColor.
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": "#CC1A1A2E",
"iconColor": { "light": "#1A1A2E", "dark": "#FFFFFF" },
"labelColor": { "light": "#1A1A2E", "dark": "#FFFFFF" },
"activeAlpha": 1.0,
"inactiveAlpha": 0.5
},
"sideDrawer": {
"enabled": true,
"panelColor": "#CC2C2C2C",
"iconColor": "#FFFFFF",
"labelColor": "#FFFFFF"
},
"topDownMenu": {
"enabled": true,
"panelColor": "#CC101020",
"iconColor": "#FFFFFF",
"labelColor": "#CCCCCC"
}
}
}| Option | Applies to | Default |
|---|---|---|
iconColor | Icon ImageView tint (SRC_IN blend) | Original image colors |
labelColor | Label TextView text color | #FFFFFF (white) |
activeAlpha | Active tab opacity (Bottom Tabs only) | 1.0 |
inactiveAlpha | Inactive tab opacity (Bottom Tabs only) | 0.6 |
Icon tinting
iconColor works best with single-color icons (SVG or flat PNG). Applying it to multi-color icons will flatten them to a single hue.
Active Tab Indicator
Use activeIndicator to add a visual marker to the currently active tab. If activeIndicatorColor is not set, iconColor is used as the fallback.
"bottomTabs": {
"enabled": true,
"panelColor": "#E8101020",
"iconColor": { "light": "#1F2937", "dark": "#FFFFFF" },
"activeIndicator": "dot",
"activeIndicatorColor": { "light": "#2563EB", "dark": "#60A5FA" }
}| Option | Description | Default |
|---|---|---|
activeIndicator | Indicator style — "none" / "dot" / "pill" / "bar" | "none" |
activeIndicatorColor | Indicator color (same single/object format as panelColor) | #6366F1 / #818CF8 (Indigo) |
activeIndicatorWidth | "bar" width — "short" (20dp centered) / "full" (full tab width) | "short" |
activeIndicatorPosition | "bar" position — "top" (above icon) / "bottom" (below label) | "top" |
dot— small filled circle below the labelbar— short horizontal line above the iconpill— rounded-rect background wrapping the icon and label
Pill opacity
For "pill" style, include an alpha channel in activeIndicatorColor (e.g. "#332563EB" ≈ 20% opacity). A fully opaque color will obscure the icon and label.
Bottom Tabs
A tab bar fixed at the bottom of the screen.
Tab Bar Height
Use the heightDp option in config.json to adjust the tab bar height.
{
"modules": {
"bottomTabs": {
"enabled": true,
"heightDp": 64
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
heightDp | number | 54 | Tab bar height in dp. Automatically clamped to the 44–96 range. |
Accessibility minimum
Values below 44dp are raised to 44dp (WCAG 2.5.5 minimum touch target). Values above 96dp are clamped to 96dp.
show / hide / toggle
window.unveilyBridge.bottomTabs.show();
window.unveilyBridge.bottomTabs.hide();
window.unveilyBridge.bottomTabs.toggle();setConfig
window.unveilyBridge.bottomTabs.setConfig({
visible: false, // optional — default: true. Pass false to keep the bar hidden on first load.
items: [
{ id: "home", label: "Home", icon: "home.svg", url: "/home" },
{ id: "search", label: "Search", icon: "search.svg", url: "/search" },
{ id: "my", label: "My", icon: "person.svg", url: "/my" }
]
});The tab bar is shown automatically on the first setConfig() call. Pass visible: false to keep it hidden and call show() manually when ready. Subsequent setConfig() calls do not change visibility.
Icons: Place icon files in assets/menu_icons/ and reference them by filename. Both .svg and raster formats (.png, .webp, etc.) are supported. Local .svg files are rendered via AndroidSVG for crisp display at any density; other formats use Glide.
setBadge
Show a notification badge on a specific tab. Pass 0 to remove.
window.unveilyBridge.bottomTabs.setBadge("home", 5); // show badge
window.unveilyBridge.bottomTabs.setBadge("home", 0); // remove badgeCallback
function onBottomTabClick(result) {
console.log(result.id); // tab id
console.log(result.url); // url configured for the tab
}Side Drawer
A navigation list that slides in from the side of the screen.
open / close / toggle
window.unveilyBridge.sideDrawer.open();
window.unveilyBridge.sideDrawer.close();
window.unveilyBridge.sideDrawer.toggle();setConfig
window.unveilyBridge.sideDrawer.setConfig({
widthDp: 320, // optional — drawer width in dp (default: 320)
items: [
{ id: "profile", label: { ko: "프로필", en: "Profile", ja: "プロフィール" }, icon: "person.png", url: "/profile" },
{ id: "settings", label: { ko: "설정", en: "Settings", ja: "設定" }, icon: "settings.png", url: "/settings" },
{ id: "refresh", type: "button", label: { ko: "새로고침", en: "Refresh", ja: "更新" } },
{ id: "lang", type: "select", value: "en", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]}
]
});setItemEnabled / setItemVisible
window.unveilyBridge.sideDrawer.setItemEnabled("settings", false);
window.unveilyBridge.sideDrawer.setItemVisible("settings", false);setBadge
Show a notification badge on an icon-type item. Pass 0 to remove.
window.unveilyBridge.sideDrawer.setBadge("profile", 3);
window.unveilyBridge.sideDrawer.setBadge("profile", 0); // removeCallbacks
// icon-type item tapped (backward compatible)
function onSideDrawerItemClick(result) {
console.log(result.id); // item id
console.log(result.url); // url of the tapped item
}
// button or select item interacted with
function onSideDrawerAction(result) {
console.log(result.id); // item id
console.log(result.type); // "button" | "select"
console.log(result.value); // "" for button; selected option value for select
}Top Down Menu
A grid menu that slides down from the top of the screen.
open / close / toggle
window.unveilyBridge.topDownMenu.open();
window.unveilyBridge.topDownMenu.close();
window.unveilyBridge.topDownMenu.toggle();setConfig
window.unveilyBridge.topDownMenu.setConfig({
columns: 4, // grid columns (1–5, default: 4)
maxHeightFraction: 0.5, // maximum height fraction — a cap (0.1–1.0, default: 0.5). Shows shorter than the cap with little content
items: [
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" },
{ id: "search", label: { ko: "검색", en: "Search", ja: "検索" }, icon: "search.png" },
{ id: "refresh", type: "button", label: { ko: "새로고침", en: "Refresh", ja: "更新" } },
{ id: "lang", type: "select", value: "en", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]},
{ id: "banner", type: "image", icon: "promo.svg", heightDp: 80 }
]
});Column behavior
columns applies only to type:'icon' grid cells. button/select/toggle/image/header/separator items are always full-width rows. columns is also a fixed value — on tablets the column count does not auto-increase; each column gets wider instead. To use more columns on tablets, branch on app.isTablet().
var cols = window.unveilyBridge.app.isTablet() ? 5 : 3;
window.unveilyBridge.topDownMenu.setConfig({ columns: cols, items: [ /* ... */ ] });setItemEnabled / setItemVisible
window.unveilyBridge.topDownMenu.setItemEnabled("search", false);
window.unveilyBridge.topDownMenu.setItemVisible("search", false);Dismiss gestures
- Swipe up: A fast upward swipe while the menu is open closes it.
- Back button: Closes the menu instead of exiting the app.
Callbacks
// icon-type item tapped (backward compatible)
function onTopDownMenuItemClick(result) {
console.log(result.id); // item id
console.log(result.url); // url of the tapped item
}
// button or select item interacted with
function onTopDownMenuAction(result) {
console.log(result.id); // item id
console.log(result.type); // "button" | "select"
console.log(result.value); // "" for button; selected option value for select
}Bottom Sheet
A panel that slides up from the bottom of the screen. Compatible with the Bottom Tab Bar.
show
window.unveilyBridge.bottomSheet.show({
title: "Product Details", // optional
content: "Description", // optional
heightFraction: 0.5, // fraction of screen height (0.1–1.0, default: 0.5)
heightDp: 400 // fixed height in dp — takes priority over heightFraction
});hide / toggle
window.unveilyBridge.bottomSheet.hide();
window.unveilyBridge.bottomSheet.toggle();setConfig
Set the sheet height with setConfig. If the sheet is open it applies immediately (live resize); if closed it applies on the next show().
// Closed — height applies on the next show()
window.unveilyBridge.bottomSheet.setConfig({ heightFraction: 0.7 });
// Called while open — immediate live resize
window.unveilyBridge.bottomSheet.setConfig({ heightFraction: 0.3 });Unified panel sizing rule
For all three panels (top-down menu, side drawer, bottom sheet) sizing is done through setConfig. Called while open it applies immediately; while closed it applies on the next open()/show(). open()/show() only display the panel — calling them again on an already-open panel does not change its size.
Size options: top-down menu maxHeightFraction (a cap) · side drawer widthDp (fixed width) · bottom sheet heightFraction or heightDp (fixed height, heightDp takes precedence).
Dismiss gestures
- Swipe down: A fast downward swipe closes the sheet.
- Back button: Closes the sheet instead of exiting the app.
- Tap outside: Tapping the scrim above the sheet closes it.
Item Types
Both Top Down Menu and Side Drawer items support a type field that controls how the item is rendered.
type | Rendering | Closes panel on interaction | Callback |
|---|---|---|---|
"icon" | Icon + label grid/list cell (default) | Yes | onTopDownMenuItemClick / onSideDrawerItemClick |
"image" | Full-width image row (SVG recommended) | Only if url set | onTopDownMenuItemClick / onSideDrawerItemClick |
"button" | Full-width native Button | No | onTopDownMenuAction / onSideDrawerAction |
"select" | Full-width native Spinner (dropdown) | No | onTopDownMenuAction / onSideDrawerAction |
"toggle" | Full-width Switch (label + on/off) | No | onTopDownMenuAction / onSideDrawerAction |
"separator" | Thin horizontal divider line | — | None |
"header" | Non-interactive section title label | — | None |
"button", "select", and "toggle" do not auto-close the panel on interaction — your JS callback decides what to do.
type: "icon" (default)
{
"id": "settings",
"label": { "ko": "설정", "en": "Settings", "ja": "設定" },
"icon": "settings.png",
"url": "/settings"
}label can also be a plain string for single-language setups: "label": "Settings"
type: "image"
Full-width image row. Local .svg files are rendered via AndroidSVG; other formats use Glide.
{
"id": "banner",
"type": "image",
"icon": "promo.svg",
"heightDp": 80,
"url": "/promo"
}| Field | Type | Default | Description |
|---|---|---|---|
icon | string | — | Asset filename (e.g. "banner.svg") or remote URL |
heightDp | number | 80 | Row height in dp (20–400) |
url | string | — | Optional. If set, tapping fires onTopDownMenuItemClick and closes the panel |
Place the SVG file in assets/menu_icons/ and reference it by filename.
type: "button"
Full-width native Button. Clicking fires onTopDownMenuAction / onSideDrawerAction.
{
"id": "refresh",
"type": "button",
"label": { "ko": "새로고침", "en": "Refresh", "ja": "更新" }
}type: "select"
Full-width native Spinner (dropdown). Selecting an option fires onTopDownMenuAction / onSideDrawerAction.
{
"id": "lang",
"type": "select",
"value": "en",
"options": [
{ "value": "ko", "label": { "ko": "한국어", "en": "Korean", "ja": "韓国語" } },
{ "value": "en", "label": { "ko": "영어", "en": "English", "ja": "英語" } },
{ "value": "ja", "label": { "ko": "일본어", "en": "Japanese", "ja": "日本語" } }
]
}| Field | Type | Default | Description |
|---|---|---|---|
value | string | — | Pre-selected option value |
options | array | — | Array of { value, label } objects |
type: "toggle"
Full-width Switch with label on the left and the toggle on the right. Fires onTopDownMenuAction / onSideDrawerAction with value: "true" or "false".
{
"id": "dark",
"type": "toggle",
"label": { "ko": "다크 모드", "en": "Dark Mode", "ja": "ダークモード" },
"value": false
}| Field | Type | Default | Description |
|---|---|---|---|
label | string | i18n | — | Description text shown left of the switch |
value | boolean | false | Initial checked state |
type: "separator"
Thin horizontal divider line for grouping items. No id, no callback.
{ "type": "separator" }type: "header"
Non-interactive section title label. No callback.
{
"type": "header",
"label": { "ko": "설정", "en": "Settings", "ja": "設定" }
}title Field
button, select, toggle, and image items support an optional title field.
When set, a small secondary-text label is rendered directly above the control.
Accepts a plain string or an i18n object.
{
"id": "dark",
"type": "toggle",
"title": { "ko": "화면", "en": "Appearance", "ja": "画面" },
"label": { "ko": "다크 모드", "en": "Dark Mode", "ja": "ダークモード" },
"value": false
}Multilingual Labels
All item label fields accept either a plain string or an i18n object with ko, en, ja keys.
// Plain string — same label regardless of locale
{ id: "home", label: "Home", icon: "home.png" }
// i18n object — label switches with setLocale()
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" }When an i18n object is used, the displayed label is selected by the current locale (see setLocale below).
The fallback order is: requested locale → en → item id.
setLocale
Override the locale used for menu item label display. Call this when the user changes the language in your web app.
window.unveilyBridge.app.setLocale("ko"); // "ko" | "en" | "ja"
window.unveilyBridge.app.setLocale(""); // reset to device localeLabels in open panels update immediately without reopening.
Example — language selector in Top Down Menu
window.unveilyBridge.topDownMenu.setConfig({
items: [
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" },
{ id: "lang", type: "select", value: "en", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]}
]
});
function onTopDownMenuAction(result) {
if (result.id === "lang" && result.type === "select") {
window.unveilyBridge.app.setLocale(result.value);
}
}