GKSHOP
GKSPHONE V2

Custom Widget

Build your own GKSPHONE home-screen widget — registration, sizes, and the iframe contract.

A widget is an HTML page from your own resource, rendered in an iframe on the phone's home screen.

Two starting points: the widget template on GitHub, or the custom-widget demo folder that ships in the download — copy it into resources, ensure it after gksphone, then add it in game from long-press home → + → Widgets.

Serve the page

Expose your files in fxmanifest.lua so the phone can load them:

fxmanifest.lua
files {
    'ui/widget.html',
    'ui/widget.css',
    'ui/widget.js'
}

The URL is then https://cfx-nui-<your-resource>/ui/widget.html.

Register it

exports["gksphone"]:AddCustomWidget({
    id          = "demo-widget",
    widgetUrl   = ("https://cfx-nui-%s/ui/widget.html"):format(GetCurrentResourceName()),
    title       = "Demo Widget",
    description = "Example custom home widget",
    icon        = "sparkles",
    size        = "2x2",
    show        = true,
    labelLangs  = { en = "Demo Widget", tr = "Demo Widget", de = "Demo Widget" },
})

Returns true on success, false if a required field is missing.

Field
idRequired. Unique. Registering the same id again replaces the entry
widgetUrlRequired. The iframe URL
titleGallery title. Falls back to name, then to id
descriptionGallery subtitle
iconFramework7 icon name for the gallery. Defaults to square_grid_2x2
appIconImage URL to use instead of the icon name. Also accepted as icons or iconUrl
size1x1, 2x2, 4x2 or 4x4. Anything else silently becomes 2x2
showListed in the widget gallery. Defaults to true
labelLangsTitle per language code
resourceOwning resource, for auto-cleanup. Derived from widgetUrl when it is a cfx-nui URL

Register at the right time

gksphone has to be running first, and your widget has to survive a phone restart:

local function register()
    if GetResourceState("gksphone") ~= "started" then return false end
    return exports["gksphone"]:AddCustomWidget({
        id        = "demo-widget",
        widgetUrl = ("https://cfx-nui-%s/ui/widget.html"):format(GetCurrentResourceName()),
        title     = "Demo Widget",
    }) == true
end

CreateThread(function()
    for _ = 1, 60 do
        if register() then return end
        Wait(1000)
    end
end)

AddEventHandler("onResourceStart", function(res)
    if res == "gksphone" then Wait(500) register() end
end)

Remove it

exports["gksphone"]:RemoveCustomWidget("demo-widget")

Home-screen instances of that id are cleaned up too.

You rarely need to call this on shutdown. When a resource stops, the phone drops every widget whose widgetUrl points at that resource automatically.

Sizes

SizeFootprint
1x1Compact
2x2Default
4x2Full width, 2 rows
4x4Full width, 4 rows

Size is stored per home-screen instance when the player adds the widget.

The iframe contract

The phone posts two messages to your page:

window.addEventListener("message", (event) => {
    const data = event.data
    if (typeof data !== "object" || !data) return

    if (data.type === "gksphone:widget:init") {
        // { widgetId, size, editing } — sent once, when the iframe loads
    }

    if (data.type === "gksphone:widget:editing") {
        // { editing } — sent when the player enters or leaves home edit mode
    }
})

While editing, the phone disables pointer events on the iframe, the same as built-in widgets. Use the editing flag if you want to show a hint instead of live content.

Minimal page

Fill the frame, keep the background transparent, and don't scroll:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent; }
    .widget {
      width: 100%; height: 100%;
      box-sizing: border-box;
      border-radius: 22px;
      padding: 14px;
      color: #fff;
      background: linear-gradient(155deg, #1a2744, #0d1526);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }
  </style>
</head>
<body>
  <div class="widget"><strong>Demo Widget</strong><div id="meta"></div></div>
  <script>
    const meta = document.getElementById("meta")
    window.addEventListener("message", ({ data }) => {
      if (data?.type === "gksphone:widget:init") meta.textContent = data.size
    })
  </script>
</body>
</html>

Static widgets

Config.CustomWidgets in gksphone/config/config.lua holds the registry, and you can seed it there. AddCustomWidget writes into the same list at runtime, which is the better option for a separate resource — it survives without editing the phone's own config.

On this page