> For the complete documentation index, see [llms.txt](https://docs.gkshop.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.gkshop.org/gksphone-v2/custom-widget.md).

# Custom Widget

### Adding the widget <a href="#adding-the-widget" id="adding-the-widget"></a>

To add a home-screen widget, use the [AddCustomWidget export](/gksphone-v2/exports-and-events/client-exports.md#add-custom-widget).

We have [template widget](https://github.com/GKSHOP/gksphone-widget-template) that you can use for reference.

### Install the demo resource

1. Copy the `custom-widget` folder into your server `resources` directory.
2. Add to `server.cfg`:

```cfg
ensure gksphone
ensure custom-widget
```

3. In-game: open the phone → long-press home → **+** → **Widgets** → **Demo Widget**.

### Resource setup

Your resource must expose the widget HTML via FiveM `files` so the phone can load it as an iframe:

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

Prefer a `cfx-nui` URL:

```
https://cfx-nui-<your-resource-name>/ui/widget.html
```

### Exports - Client

#### AddCustomWidget

Registers (or updates) a widget in the phone gallery.

```lua
local widgetData = {
    id = "demo-widget", --- A unique id (required)
    widgetUrl = "https://cfx-nui-custom-widget/ui/widget.html", -- iframe URL (required)
    title = "Demo Widget", -- Gallery title (fallback if labelLangs missing)
    description = "Example custom home widget", -- Gallery subtitle
    icon = "sparkles", -- Framework7 icon name used in the gallery
    size = "2x2", -- "1x1" | "2x2" | "4x2" | "4x4" (default: "2x2")
    show = true, -- Show in widget gallery (default: true)
    labelLangs = { -- Widget name by languages
        af = "Demo Widget",
        ar = "Demo Widget",
        cs = "Demo Widget",
        de = "Demo Widget",
        en = "Demo Widget",
        es = "Demo Widget",
        fr = "Demo Widget",
        id = "Demo Widget",
        nl = "Demo Widget",
        ["pt-PT"] = "Demo Widget",
        ro = "Demo Widget",
        sv = "Demo Widget",
        th = "Demo Widget",
        tr = "Demo Widget",
        uk = "Demo Widget",
        ["zh-TW"] = "Demo Widget"
    }
}

exports["gksphone"]:AddCustomWidget(widgetData)
```

Recommended pattern (wait until `gksphone` is started):

```lua
local WIDGET_ID = "demo-widget"
local RESOURCE = GetCurrentResourceName()

local function registerWidget()
    if GetResourceState("gksphone") ~= "started" then
        return false
    end

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

CreateThread(function()
    local tries = 0
    while tries < 60 do
        if registerWidget() then
            return
        end
        tries = tries + 1
        Wait(1000)
    end
end)

AddEventHandler("onResourceStart", function(resourceName)
    if resourceName == "gksphone" then
        Wait(500)
        registerWidget()
    end
end)
```

#### RemoveCustomWidget

Removes a widget from the gallery registry. Home-screen instances of that `id` are cleaned up by the phone.

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

Example on resource stop:

```lua
AddEventHandler("onResourceStop", function(resourceName)
    if resourceName ~= GetCurrentResourceName() then return end
    if GetResourceState("gksphone") == "started" then
        pcall(function()
            exports["gksphone"]:RemoveCustomWidget("demo-widget")
        end)
    end
end)
```

{% hint style="info" %}
If your `widgetUrl` uses `https://cfx-nui-<resource>/...`, GKSPHONE also auto-removes matching widgets when that resource stops.
{% endhint %}

### Widget sizes

| Size  | Cells               | Notes                           |
| ----- | ------------------- | ------------------------------- |
| `1x1` | 1×1                 | Compact                         |
| `2x2` | 2×2                 | Default (recommended for demos) |
| `4x2` | Full width × 2 rows | Wide strip                      |
| `4x4` | Full width × 4 rows | Large                           |

The size is stored on each home-screen instance when the player adds the widget from the gallery.

### Javascript UI

The phone embeds your page in an iframe and sends `postMessage` events.

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

    if (data.type === "gksphone:widget:init") {
        // data.widgetId — registry id
        // data.size — "2x2" | "4x2" | ...
        // data.editing — true while home edit mode is active
        console.log("Widget init", data.widgetId, data.size, data.editing)
        return
    }

    if (data.type === "gksphone:widget:editing") {
        // data.editing — edit mode toggled
        console.log("Editing", data.editing)
    }
})
```

#### Init message

Sent when the iframe finishes loading:

```javascript
{
  type: "gksphone:widget:init",
  widgetId: "demo-widget",
  size: "2x2",
  editing: false
}
```

#### Editing message

Sent when the player enters or leaves home edit mode:

```javascript
{
  type: "gksphone:widget:editing",
  editing: true
}
```

{% hint style="info" %}
While editing, the phone disables pointer events on the iframe (same jiggle behavior as built-in widgets). Use the `editing` flag if you want to show an in-widget hint.
{% endhint %}

### Minimal HTML example

```html
<!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" />
  <title>Demo Widget</title>
  <style>
    html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent; }
    .widget {
      width: 100%; height: 100%;
      border-radius: 22px;
      padding: 14px;
      color: #fff;
      background: linear-gradient(155deg, #1a2744, #0d1526);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div class="widget" id="root">
    <strong id="title">Demo Widget</strong>
    <div id="meta"></div>
  </div>
  <script>
    const meta = document.getElementById("meta")
    window.addEventListener("message", (event) => {
      const data = event.data
      if (!data || typeof data !== "object") return
      if (data.type === "gksphone:widget:init") {
        meta.textContent = data.size + (data.editing ? " · editing" : "")
      }
      if (data.type === "gksphone:widget:editing") {
        meta.textContent = (meta.textContent || "").replace(/ · editing/, "") + (data.editing ? " · editing" : "")
      }
    })
  </script>
</body>
</html>
```

### Config (optional)

You can also list static widgets in `gksphone` config:

```lua
-- gksphone/config/config.lua
Config.CustomWidgets = {
    -- {
    --     id = "demo-widget",
    --     widgetUrl = "https://cfx-nui-custom-widget/ui/widget.html",
    --     title = "Demo Widget",
    --     size = "2x2",
    --     show = true
    -- }
}
```

Runtime registration via `AddCustomWidget` is preferred for third-party resources.
