For the complete documentation index, see llms.txt. This page is also available as Markdown.

HTML Widget Developer Guide

HTML lets you embed a fully custom HTML + JavaScript page inside the Blynk dashboard as a first-class widget. The page runs in a Blynk WebView that exposes a native bridge (BlynkBridge) so your HTML can read datastream values, push values back to the server, react to real-time updates, pick up app theme colours, query device metadata, fetch historical datastream data, and navigate between dashboard pages - all without any server-side code.


How It Works

HTML <script>                    Blynk WebView bridge
  BlynkBridge.getValue(index)  ─► read current value of a single datastream (0-based)
  BlynkBridge.getValues()      ─► read current values of all assigned datastreams
                                   On Web, every resolved value object always carries the FULL
                                   field set { index, value, lastUpdated, type, label, pinType, color,
                                               min, max, unit, suffix, decimalFormat,
                                               mappings, enumFallbackValue }
                                   — fields that don't apply to the stream's type are present with
                                   default values (null / '' / []), not omitted.
  BlynkBridge.sendValue()      ─► write a value to a datastream
  BlynkBridge.sendError()      ─► display an error toast notification in the Dashboard
  BlynkBridge.getTheme()       ─► theme colors and optional font-family names for current mode
  BlynkBridge.getDeviceInfo()  ─► device name, status, and metadata
  BlynkBridge.isExternalRequestsAllowed() ─► boolean — whether outbound HTTP/fetch calls are permitted
  BlynkBridge.isRangePickerSupported()   ─► boolean — whether RANGE_PICKER period is available
  BlynkBridge.getHistoricalData(options) ─► historical datastream values for a given period
                                            resolves with { streams: [{ index, granularity, values: [{x,y}] }] }
                                            granularity is null when sourceType was RAW_DATA
  BlynkBridge.isPageActionsSupported()   ─► boolean — whether page navigation calls are available

  // Real-time updates — register once after constructing the bridge:
  bridge.setCallbacks({
    onValueUpdated: ({ index, value, ... }) => { /* update your UI */ },
    onThemeUpdated: (theme) => { /* re-apply theme on light/dark switch */ },
    onError: (message) => { /* handle platform/validation errors */ }
  });
  // The Blynk WebView calls these handlers whenever the server pushes
  // new data, the app theme changes, or an error occurs.

The BlynkBridge JavaScript class (injected before your page's own scripts) wraps all platform calls and keeps things consistent.


Minimal HTML Skeleton


BlynkBridge JavaScript API

The BlynkBridge class is injected by the Blynk WebView before your page scripts run. Always guard against it being absent (browser / editor preview):

Initialization

Constructing BlynkBridge also exposes:

  • window.blynkWidgetUpdate(json) — called by the Blynk WebView with real-time push payloads

  • window.blynkOnError(message) — called by the Blynk WebView when a platform error occurs

Checking Bridge Availability

Returns true when running inside the Blynk WebView. Returns false in a browser or during the HTML editor preview.


Checking External Request Permission

Returns true when the host platform permits the widget to make outbound HTTP/HTTPS requests (e.g. calling the Blynk Platform REST API or any third-party endpoint) directly from widget JavaScript.

Important: Even when this returns true, individual requests may still be blocked by CORS policy or other network-level restrictions. Always handle fetch() rejections gracefully.

Recommended guard before making any external network request:


Reading Values

getValue(index) — single datastream

index is 0-based. Rejects when the index is missing (Error: 'getValue() has no index') or refers to a datastream that isn't configured on the widget (Error: `Datastream index ${index} is not configured` ).

All fields below are always present on the resolved object, even ones that don't apply to the current stream's type — in that case they default to null, '', or [] rather than being left out. Use the type field to decide which ones are meaningful:

Field
Type
Description

index

number

0-based datastream index (mirrors the position in widget settings)

value

string

Current datastream value as a string

lastUpdated

`number

null`

type

string

Datastream type:"INT", "DOUBLE", "STRING", "ENUM", …

label

string

Datastream label if configured, otherwise''

pinType

string

Pin type name, e.g."VIRTUAL", or ''

color

string

Datastream accent color configured in widget settings, or''

min

`number

null`

max

`number

null`

unit

string

Measurement unit name, or''

suffix

string

Unit suffix for display, or''

decimalFormat

string

Decimal format pattern, e.g."#.##", or ''

mappings

string[]

Enum option labels;[] for non-enum streams

enumFallbackValue

`string

null`

Example resolved object for an integer temperature datastream:

Example for a double speed datastream:

Usage pattern — rendering a human-readable value:


Enum value model ("ENUM")

For "ENUM" streams, mappings and enumFallbackValue are the fields that matter:

Field
Type
Description

mappings

string[]

Array of option labels indexed from0. The raw numeric value is an index into this array.

enumFallbackValue

`string

null`

The value field holds the numeric index of the currently selected option as a string ("0", "1", …). Use mappings[parseInt(v.value)] to get the human-readable label.

Example resolved object for a mode selector with three options:

Usage pattern — rendering the selected label:

Usage pattern — building a dropdown from enum options:


getValues() — all datastreams

Returns an array of ValueObject (same shape as above), one per assigned datastream, in the same order as the datastreams appear in widget settings. The index field on each object reflects its 0-based position in that ordering. The array length equals the number of assigned datastreams. An empty array means no datastreams are configured.

Usage pattern — handling mixed stream types:


Sending Values

Parameter
Type
Description

index

number

0-based datastream index

value

`string

number`

Example:

sendValue is fire-and-forget — it doesn't return a Promise. If index refers to a datastream that isn't configured on the widget, you'll see `Datastream index ${index} is not configured` come through onError (or as a toast, if sendError-driven UI is enabled) instead of a thrown exception.


Error Handling

sendError(message) — push an error toast to the Dashboard

Displays message as an error toast notification in the Blynk Dashboard UI. Use this to surface widget errors to the user (e.g. a failed external fetch, invalid input, or unsupported configuration).

Parameter
Type
Description

message

string

Non-empty error message to display

  • Does nothing if message is empty or not a string.

  • Does nothing when isOnErrorEnabled is false.

  • Does nothing when the bridge is not available.


setOnErrorEnabled(enabled) — enable/disable error delivery

When false:

  • sendError() calls are silently dropped.

  • The onError callback registered via setCallbacks is not invoked.


onError callback — receive platform errors

Register via setCallbacks:

The platform calls window.blynkOnError(message) directly; BlynkBridge forwards it to your registered handler.

Platform behaviour:

Platform

sendError

onError callback

Web (Blynk Console)

No-op

Fires via JS bridge


Real-time Updates

BlynkBridge dispatches new values, theme changes, and range-picker updates to onValueUpdated, onThemeUpdated, and onHistoricalDataUpdated respectively, as they happen.


Device Info

On Web, this never resolves with a bare null.

  • It rejects when the bridge isn't available, or after a 10-second timeout (Error: 'Device info request timeout (10s)').

  • Otherwise it resolves with an object — when there's no device linked to the widget, the individual fields fall back to null/'' instead.

Field notes:

Field
Type
Notes

id

`number

null`

orgId

`number

null`

name

string

Device display name;'' when there's no linked device

status

string

'ONLINE', 'OFFLINE', or a custom status string; '' when unset

lastReportedAt

`number

null`

Example — showing an online/offline badge:

Example — safe guard pattern (recommended):


App Theme

On Web, getTheme() rejects (does not resolve null) when the bridge isn't available (Error: 'Blynk bridge is not available') or after a 10-second response timeout (Error: 'Theme request timeout (10s)'). Otherwise it resolves with an object whose color fields are CSS hex strings already resolved for the current light/dark mode — no prefers-color-scheme adaptation needed — plus optional font-family strings supplied by the host app:

Color fields

Field
Maps to attribute
Typical use

brand

blynkBrandColor6

Primary accent, buttons, focus rings

primary

blynkPrimaryColor6

Secondary accent

positive

blynkPositiveColor6

Success / online status

warning

blynkWarningColor6

Warning badges

critical

blynkCriticalColor6

Errors / offline status

background

blynkBackgroundColor

Page / widget background

textPrimary

blynkTextPrimaryColor

Body text

onPrimary

blynkOnPrimaryColor

Text on primary-colored surfaces

onBrand

blynkOnBrandColor

Text on brand-colored surfaces

neutral

blynkNeutralColor9

Muted text, borders

Font fields

These are string | null — present only when the host app has configured custom typography. Use them as CSS font-family values; keep a reasonable system-font fallback for when they are absent.

Field
Typical use

primaryFont

Body text and general labels

secondaryFont

Sub-labels, captions, helper text

buttonsFont

Button and interactive-element text

widgetValuesFont

Numeric / data-value display

Custom theme fonts (primaryFont, secondaryFont, buttonsFont, widgetValuesFont) render correctly inside your widget automatically — you don't need to load or declare your own @font-face rules for them.

Recommended pattern

Apply all theme properties as CSS custom properties so the whole page updates atomically:

And reference them in your CSS:

Note: Always keep CSS fallback values in :root so the widget looks correct in the browser/editor before getTheme() resolves.


Historical Data

Checking Range Picker Support

Returns true when the host platform supports the RANGE_PICKER period mode (an interactive date-range picker driven by the native UI). |


getHistoricalData(options)

Fetches historical datastream values for the widget's assigned datastreams.

Parameters

Parameter
Type
Required
Description

options

object

yes

Request options object

options.period

string

yes

One of:'ONE_HOUR', 'SIX_HOURS', 'DAY', 'WEEK', 'MONTH', 'THREE_MONTHS', 'SIX_MONTHS', 'ONE_YEAR', 'CUSTOM'

options.from

number

CUSTOM only

Start of the custom range — Unix timestamp inmilliseconds

options.to

number

CUSTOM only

End of the custom range — Unix timestamp inmilliseconds. The range to - from must not exceed 365 days.

options.offset

number

no

Number of data points toskip from the start of the result. Only meaningful with sourceType: 'RAW_DATA' (which can return a very large number of points). Use it to paginate: fetch with offset: 0, then increment by the number of points received. Must be a non-negative integer; defaults to 0 when omitted.

options.dataStreams

array

yes

Non-empty array of datastream descriptors

options.dataStreams[].index

number

yes

0-based datastream index (must be within the number of datastreams configured in widget settings)

options.dataStreams[].sourceType

string

yes

Aggregation function — one of:'RAW_DATA', 'MIN', 'MAX', 'AVG', 'SUM', 'COUNT'

sourceType values:

Value
Description

RAW_DATA

No aggregation — individual raw data points.granularity is null in the response for these streams.

MIN

Minimum value per time bucket

MAX

Maximum value per time bucket

AVG

Average value per time bucket

SUM

Sum of values per time bucket

COUNT

Number of data points per time bucket

Return value

Resolves with a HistoricalDataResult object containing a streams array:

Note on granularity: The server automatically picks the appropriate bucket size based on the requested period and sourceType. RAW_DATA requests return individual raw points and the granularity field is null for those streams. Aggregated source types (AVG, MIN, MAX, SUM, COUNT) use time-bucket aggregation and always return a non-null granularity string. Always null-check stream.granularity before displaying or comparing it.

Validation rules

The following conditions are checked before the native call is made; the Promise is rejected immediately if any fail:

Period validation:

Condition
Error message

options is absent or options.period is null/undefined

"'period' is required"

period is not in the supported list

"'period' must be one of: ONE_HOUR, SIX_HOURS, DAY, WEEK, MONTH, THREE_MONTHS, SIX_MONTHS, ONE_YEAR, CUSTOM"

period === 'CUSTOM' and from or to is null/undefined

"CUSTOM period requires 'from' and 'to' timestamps"

from or to is not a finite number (NaN, Infinity)

"'from' and 'to' must be finite numbers"

from > to

"'from' must be less than or equal to 'to'"

to - from exceeds 365 days

"'to' - 'from' must not exceed one year (365 days)"

offset validation:

Condition
Error message

options.offset is provided but is not a non-negative integer

"'offset' must be a non-negative integer"

dataStreams validation:

Condition
Error message

options.dataStreams is not an array

"'dataStreams' must be an array"

dataStreams[N] is not an object

'dataStreams[N] must be an object'

dataStreams[N].index is missing or non-numeric

'dataStreams[N].index must be a number'

dataStreams[N].sourceType is not one of the valid values

'dataStreams[N].sourceType must be one of: RAW_DATA, MIN, MAX, AVG, SUM, COUNT'

Out-of-range dataStreams[].index on Web: unlike getValue/sendValue, an out-of-range index here does not produce an error. If every entry in dataStreams is out of range, the whole call rejects with Error: 'No valid datastream indexes'. If only some entries are invalid, the call still resolves — just without streams for the invalid indexes, with no error at all. Validate indexes yourself against the count from getValues() if you need to detect this case.

Timeout on Web: requests time out after 30 seconds, rejecting with Error: 'Historical data request timeout (30s)'.

Examples

RANGE_PICKER period

RANGE_PICKER is a special period that delegates range selection to the native date-range picker UI provided by the host app rather than using a fixed window or explicit timestamps. It is only available on platforms where bridge.isRangePickerSupported() returns true.

How it works (subscription model):

  1. Initial callgetHistoricalData({ period: 'RANGE_PICKER' }) registers the widget as a subscriber and resolves with the data for whatever range the picker currently shows.

  2. Automatic updates — from that point on, whenever the user moves the range picker, the platform pushes a fresh HistoricalDataMap directly to the onHistoricalDataUpdated callback registered via setCallbacks. These are not new Promise resolutions — they arrive asynchronously outside the Promise chain.

Note: Register onHistoricalDataUpdated in setCallbacks before calling getHistoricalData({ period: 'RANGE_PICKER' }) to avoid missing the first automatic push that may arrive immediately after subscription.

Note: getHistoricalData times out after after 30 seconds on web (see above).


Logging

Messages are forwarded to the Blynk platform logger.


CSS Custom Properties & Theming

Define all theme-sensitive values as CSS custom properties in :root with sensible defaults; override them from getTheme() at runtime:

The @media (prefers-color-scheme: dark) block serves as a fallback for the editor/browser. Inside the Blynk WebView getTheme() returns colors already matched to the current Blynk theme (which may differ from the OS preference), so always call getTheme() and apply its values.


Responsive Layout with Container Queries

The WebView uses a fixed pixel canvas, but the widget tile can be any size. Use CSS Container Queries to adapt:

Use clamp() for fluid type:

Container query units (cqw, cqh, cqmin, cqmax) are fully supported in Blynk WebViews.


Initialization Flow

The recommended boot sequence:

Important: All bridge calls return a Promise. Always chain .catch() on every call to handle the no-bridge and timeout cases.


Complete Annotated Example

The built-in sample HTML is a production-quality example that demonstrates all of the above patterns:

  • Four input fields, dynamically built from getValues() count

  • buildFields(n) / showNotConfigured() state management

  • initTheme() → applies brand/bg/text/online/offline colors

  • initDeviceInfo() → shows device name + status dot badge

  • Real-time updates via bridge.setCallbacks({ onValueUpdated, onError })

  • Numeric validation, decimal formatting, dirty-state tracking

  • Send button with animated confirmation dot

  • Container-query responsive layout + clamp() fluid typography

  • Full CSS custom property system with dark-mode fallbacks


Tips & Gotchas

Topic
Guidance

isExternalRequestsAllowed()

Returns false on Blynk Console (Web). Always check this before calling fetch() and handle rejections — CORS may still block individual requests.

No external scripts in production

Fonts and icon libraries loaded from CDN add latency. Bundle assets inline or use system fonts when file size matters.

getValue index is 0-based

bridge.getValue(0) reads the first assigned datastream, bridge.getValue(1) the second, etc. On Web, an index that isn't configured on the widget rejects with `Datastream index ${index} is not configured` (not an index_out_of_bounds-style message); a missing index rejects with 'getValue() has no index'.

getValues() returns [] when not configured

Always checkvalues.length === 0 and show a "not configured" state.

getValue/getValues always return the full field set

min, max, unit, suffix, decimalFormat, mappings, enumFallbackValue, etc. are always present, defaulting to null/''/[] when not applicable to the stream's type. Don't rely on field presence to detect type — check the type field itself.

lastUpdated and color fields

EveryValueObject (from getValue, getValues, and onValueUpdated) also carries lastUpdated (Unix ms timestamp of the last push, or null) and color (the datastream's configured accent color, or '') — easy to miss since they weren't part of earlier examples.

Color values from getTheme()

Color fields are CSS hex color strings already resolved for the current light/dark theme.

Font values from getTheme()

primaryFont, secondaryFont, buttonsFont, widgetValuesFont are `string

Custom theme fonts just work

Font names fromgetTheme() (e.g. an org's branded font) render correctly in your widget automatically — no extra setup needed.

onThemeUpdated callback

Register it viasetCallbacks({ onThemeUpdated }) to react live when the user flips light/dark mode or an org's branding changes, without a page reload. Not covered by onValueUpdated/onError alone.

getDeviceInfo()/getTheme() do not resolve null

Bothreject (bridge unavailable, or a 10-second timeout) instead of ever resolving to a bare null. When there's no device linked, getDeviceInfo() still resolves — just with id/orgId/lastReportedAt as null and name/status as ''. Guard on info.id, not on info itself.

getDeviceInfo() has a flat status field

status is a plain string ('ONLINE', 'OFFLINE', a custom value, or '' when unset) — not a nested object.

value field is always a string

Even forINT and DOUBLE streams, value is returned as a string. Parse it with parseFloat() / parseInt() before arithmetic.

ENUM value is an index string

Fortype === 'ENUM' streams, value holds the numeric index of the selected option (e.g. "1"), not the label. Resolve it via mappings[parseInt(v.value)]. Always guard with v.mappings?.[idx] ?? v.enumFallbackValue ?? v.value in case the index is out of range.

suffix/unit/decimalFormat are always present

These fields are always in the object, defaulting to'' when not set — check for an empty string, not for the field's existence.

sendError(message)

Displays an error toast in the Dashboard. No-op whenisOnErrorEnabled is false, when message is empty, or when the bridge is unavailable.

onError callback

Registered viasetCallbacks({ onError }). Fired for timeouts, server errors, and misconfigured-index errors from getValue/sendValue/getHistoricalData. Only invoked when isOnErrorEnabled is true.

setOnErrorEnabled(false)

Suppresses bothsendError() and the onError callback. Useful during init or when you want to handle errors silently without surfacing them to the user.

getHistoricalData period is required

period must be provided and must be one of the exact supported strings. Passing an unrecognised value (or omitting period) rejects immediately with a client-side error before any request is sent.

getHistoricalData dataStreams is required

A non-arraydataStreams rejects with "'dataStreams' must be an array". Each entry must supply a numeric index and a valid sourceType, or the specific dataStreams[N]... message is thrown.

getHistoricalData out-of-range dataStreams[].index

UnlikegetValue/sendValue, an out-of-range index here doesn't error per-entry — it's just missing from the response. If none of the requested indexes are valid, the call rejects with 'No valid datastream indexes'. See "Historical Data" above.

getHistoricalData CUSTOM range

Bothfrom and to must be finite millisecond timestamps with from ≤ to and to - from ≤ 365 days. The Promise rejects immediately otherwise — no request is sent.

getHistoricalData offset for RAW_DATA pagination

offset skips N data points from the beginning of the result. Only meaningful with sourceType: 'RAW_DATA'. Paginate by keeping the same period/from/to and incrementing offset by the number of points received in each batch. Must be a non-negative integer; omit or pass 0 for the first batch.

getHistoricalData timeout on Web

The Web bridge enforces its own 30-second client-side timeout regardless of any backend timeout — rejects with'Historical data request timeout (30s)'. Plan your UI for a loading / timeout state.

isRangePickerSupported()

Returnstrue on Web (the Dashboard's global time-range picker).

RANGE_PICKER subscription order

RegisteronHistoricalDataUpdated via setCallbacks before calling getHistoricalData({ period: 'RANGE_PICKER' }). The first push may arrive immediately after subscription and would be missed otherwise.

onHistoricalDataUpdated is not a Promise

It is a callback invoked by the platform on each range change. Do notawait it — handle it only inside setCallbacks.

overscroll-behavior: none

Always set this onhtml, body to prevent the WebView from hijacking swipe gestures in the dashboard scroll.

touch-action: manipulation

Eliminates the 300 ms tap delay without disabling pinch-zoom declarations.

-webkit-user-select: none

Prevents accidental text selection when the user is trying to scroll the dashboard.

No innerHTML with user data

Never insert untrusted strings viainnerHTML. Use textContent / setAttribute / DOM APIs for values coming from the bridge.

window.blynkWidgetUpdate

Blynk calls this global directly. Do not delete or replace it afterBlynkBridge is constructed.

Preview / editor mode

Whenbridge.isBridgeAvailable() is false, show MAX_INPUTS fields with placeholder content so the widget looks meaningful in the editor.

Per-plan limit on HTML Widgets per template

Each plan allows only a limited number of HTML Widget instances per template (shown to users as*"HTML Widget limit reached. Your plan allows {{count}} per template."*). Once reached, the dashboard editor shows an "Upgrade" prompt and blocks adding more — this is a platform/billing constraint, not something your widget's JavaScript can detect or work around.

Last updated

Was this helpful?