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 payloadswindow.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 handlefetch()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:
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:
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
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).
message
string
Non-empty error message to display
Does nothing if
messageis empty or not a string.Does nothing when
isOnErrorEnabledisfalse.Does nothing when the bridge is not available.
setOnErrorEnabled(enabled) — enable/disable error delivery
When false:
sendError()calls are silently dropped.The
onErrorcallback registered viasetCallbacksis 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:
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
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.
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
:rootso the widget looks correct in the browser/editor beforegetTheme()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
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:
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 andsourceType.RAW_DATArequests return individual raw points and thegranularityfield isnullfor those streams. Aggregated source types (AVG,MIN,MAX,SUM,COUNT) use time-bucket aggregation and always return a non-nullgranularitystring. Always null-checkstream.granularitybefore 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:
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:
options.offset is provided but is not a non-negative integer
"'offset' must be a non-negative integer"
dataStreams validation:
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):
Initial call —
getHistoricalData({ period: 'RANGE_PICKER' })registers the widget as a subscriber and resolves with the data for whatever range the picker currently shows.Automatic updates — from that point on, whenever the user moves the range picker, the platform pushes a fresh
HistoricalDataMapdirectly to theonHistoricalDataUpdatedcallback registered viasetCallbacks. These are not new Promise resolutions — they arrive asynchronously outside the Promise chain.
Note: Register
onHistoricalDataUpdatedinsetCallbacksbefore callinggetHistoricalData({ period: 'RANGE_PICKER' })to avoid missing the first automatic push that may arrive immediately after subscription.
Note:
getHistoricalDatatimes 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()countbuildFields(n)/showNotConfigured()state managementinitTheme()→ applies brand/bg/text/online/offline colorsinitDeviceInfo()→ shows device name + status dot badgeReal-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 typographyFull CSS custom property system with dark-mode fallbacks
Tips & Gotchas
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?

