Custom App
GKS Tablet lets you add custom iframe apps at runtime. Create a separate resource with a NUI page, register it with AddCustomApp, and it appears in the App Store / home screen like a built-in app.
The API matches GKSPhone Custom App. Use the starter template: gksphone-app on GitHub.
Custom apps using UI
Create a separate resource and set appurl to your HTML file (https://cfx-nui-{resource}/{path}).
When the app opens, the tablet loads your page in an iframe and injects window.gkstablet (and window.gksphone as an alias for shared phone/tablet UIs).
Adding the app
Use the AddCustomApp export from your client script:
exports['gks-tablet']:AddCustomApp({
name = "FleetTracker", -- unique app name
appurl = "https://cfx-nui-my-tablet-app/ui/index.html", -- your NUI URL
icons = "https://cfx-nui-my-tablet-app/ui/icon.png", -- app icon
description = "Track department vehicles", -- App Store description
show = true, -- show in App Store
startapp = false, -- auto-install on home screen
allowjob = { "police", "lspd" }, -- job whitelist (optional)
blockedjobs = {}, -- job blacklist (optional)
labelLangs = { -- localized name (optional)
en = "Fleet Tracker",
tr = "Filo Takip",
},
onOpen = function(tabletUniqueId) -- runs when app opens (optional)
print("Opened on tablet:", tabletUniqueId)
end,
onClose = function(tabletUniqueId) -- runs when app closes (optional)
print("Closed")
end,
})Call AddCustomApp again after a resource restart — install state is saved in the database, but metadata (URL, icon, jobs) comes from your export each session.
When the iframe loads, the tablet POSTs to https://{your-resource}/getInfo. Handle it to sync data on open:
RegisterNUICallback('getInfo', function(data, cb)
cb('ok')
end)You can also register lifecycle callbacks by resource name:
exports['gks-tablet']:onAppOpen('my-tablet-app', function(tabletUniqueId) end)
exports['gks-tablet']:onAppClose('my-tablet-app', function(tabletUniqueId) end)
exports['gks-tablet']:removeAppCallbacks('my-tablet-app')InputChange
Prevent the player from walking while typing in iframe inputs.
exports['gks-tablet']:InputChange(true) -- allow movement
exports['gks-tablet']:InputChange(false) -- block movementOr use the bridge in HTML:
<input onfocus="window.gkstablet?.inputFocused(false)" onblur="window.gkstablet?.inputFocused(true)">Sending a message to the UI
Use NuiSendMessage instead of SendNUIMessage:
exports['gks-tablet']:NuiSendMessage({ event = 'update', value = 42 })Listen in your frontend the same way as a normal NUI message:
window.addEventListener('message', (event) => {
if (event.data?.event === 'update') {
console.log(event.data.value)
}
})Imported functions
When the iframe loads, the tablet injects a bridge into window.gkstablet. window.gksphone points to the same object (for shared phone/tablet code).
| Name | Type | Description |
|---|---|---|
url | string | Your resource name (from appurl) |
isDarkMode() | function | Returns tablet dark mode state |
onChangeDarkMode(cb) | function | Called when theme changes |
loadingPopup(text) | function | Show loading dialog |
closeLoadingPopup() | function | Close loading dialog |
notify(text, timeout) | function | Show toast |
fetchNui(event, data) | function | Send NUI callback |
inputFocused(allowWalk) | function | Block/unblock movement while typing |
GetGallery(...) | function | Open gallery picker |
FullScreenImage(url) | function | Open image viewer |
SelectEmoji(open) | function | Open emoji picker |
setStatusBarColor(color) | function | Set page background color |
calling(number, anon) | function | Not supported on tablet (no-op) |
videoCall(number) | function | Not supported on tablet (no-op) |
CameraOpen(...) | function | Returns null on tablet |
fetchNui
window.gkstablet.fetchNui('my-event', { foo: 'bar' })isDarkMode
const dark = window.gkstablet.isDarkMode()notify
window.gkstablet.notify('Saved!', 2000)GetGallery
const result = await window.gkstablet.GetGallery(false, true, false, false)
// { data: 'url', opencamera: false }The tablet's picker ignores onlyVideo, onlyPicture and multiple — it always shows the same sheet. Only the fourth argument does anything: pass camera = true and it returns { data: false, opencamera: true } straight away, without opening the picker at all.
FullScreenImage
window.gkstablet.FullScreenImage('https://example.com/photo.jpg')SelectEmoji
window.gkstablet.SelectEmoji(true)
window.addEventListener('message', (event) => {
if (event.data?.type === 'emojiSelected') {
console.log(event.data.eventData)
}
})setStatusBarColor
window.gkstablet.setStatusBarColor('#000000')
