NII IZNANKAIZNANKA.TECH · VPN
EN

Author docs · format 1 · client 6.0+

How to make a theme

From a new palette to your own scene in HTML and JS. All on one page.

Levels
3
Tokens
17
Window labels
24

00Overview

What a theme is

A theme changes how the NII Iznanka desktop client looks and sounds.

  • Since version 6.0 themes are not built into the client
  • Each theme sits in the catalog as a separate package .nftheme
  • The client downloads a theme only when it is selected
  • One base theme remains in the build: NII IZNANKA

Anyone can make a theme. A package is a plain zip: manifest.json, images, sounds and, if needed, your own HTML, CSS and JS. You upload it in the studio, a moderator reviews it, the server signs the version, and the theme appears in everyone’s catalog.

01Palette

Just manifest.json: colors, fonts, background, sounds, music, intro, your own wording, window size. No code.

02Style

Plus theme.css on top of the client window: any shape of buttons, panels and dialogs, animations based on channel state.

03Scene

Your own window entirely: a page in HTML, CSS and JS. It receives state from the client and controls it via the SDK.

Levels stack. A scene can also have tokens and theme.css: they color the client dialogs that open on top of the scene.

What you can change

WhatWithLevel
All window and dialog colorstokenspalette
Fonts: body, headings, monospacefontspalette
Background: an image or video, dimming, blurbackgroundpalette
Sounds for connect, disconnect, error, clickssoundspalette
Background musicmusicpalette
An intro when the theme is selected, with subtitlesintropalette
Wording: “CONNECT” → “ENTER THE SPHERE”wordspalette
Window size, rounding and shape, down to a torn edgewindowpalette
Window and tray iconiconpalette
Any window elements: shape, layout, animationstheme.cssstyle
The whole window: your own button, node list and screensscenescene
A live background behind the regular windowscene.mode: backdropscene
Your own theme settings in the client settings windowsettingsany

A theme’s journey

  1. 01Authornftheme pack
  2. 02Studioupload to the website
  3. 03Moderationtheme and code
  4. 04Signaturestudio Ed25519
  5. 05Catalogiznanka.tech
  6. 06Clientthe CATALOG tab
▸ Unsigned
A theme without the studio signature installs only in author mode and is marked “NOT VERIFIED”.

01Quick start

A theme in 10 minutes

You will need:

  • Node.js 18 or newer
  • the author kit nftheme-kit.zip: the nftheme tool, the SDK, three templates and a preview

Unpack the archive. Nothing to install, no dependencies.

terminal
# 1. template: palette, style or scene (default)
node nftheme-kit/bin/nftheme.js init my-theme --level scene --name "MY THEME"

# 2. preview in the browser, edits are picked up automatically
node nftheme-kit/bin/nftheme.js preview my-theme

# 3. validation, the same as in the catalog
node nftheme-kit/bin/nftheme.js check my-theme

# 4. package for upload
node nftheme-kit/bin/nftheme.js pack my-theme

You get the file my-theme-1.0.0.nftheme.

  • Check it in the client: THEMES → AUTHORS → “Open theme folder”
  • Everything works? Drag the package into the studio
Where to start
With a palette. Five tokens and a background already make a recognizable theme. A scene is for when you want your own window behavior.

Minimal manifest.json

manifest.json
{
  "format": 1,
  "id": "night-owl",
  "name": "NIGHT OWL",
  "version": "1.0.0",
  "author": "your_nick",
  "tokens": { "accent": "#ffd23f", "bg": "#07060a" }
}

Put preview.webp (or .png, .jpg) next to it: a 16:9 screenshot of the theme, ideally 1280×720. That is enough, such a package will pass validation.

02Package

Package .nftheme

A package is a zip archive. manifest.json sits at the root; arrange the rest however you like. Paths in the manifest and in CSS are relative to the package root.

  • Zipped the whole folder? The outer folder is stripped automatically
  • System junk (__MACOSX, .DS_Store, Thumbs.db) is skipped
package layout
my-theme.nftheme
├── manifest.json        required
├── preview.webp         required: the catalog card, 16:9
├── icon.png             window and tray icon, 256×256
├── theme.css            the “style” level
├── scene/index.html     the “scene” level
├── scene/scene.js
└── assets/              images, sound, video, fonts

File names and types

  • Names: Latin letters, digits, dot, hyphen, underscore. No spaces or Cyrillic
  • Two files that differ only in case will not pass: on Windows they are the same file
  • The __nf/ folder is reserved by the client
  • The type is checked by content. An image renamed to .mp3 will not pass
KindExtensions
text and codejson css js mjs html txt md vtt, in UTF-8
imagespng jpg jpeg webp gif avif svg
fontswoff2 woff ttf otf
soundmp3 ogg opus m4a wav
videomp4 webm

Limits

WhatLimit
whole package64 MB
unpacked package160 MB
a single file48 MB
files in the package600
text file2 MB
image8 MB, preview up to 4 MB, icon up to 1 MB
file path160 characters, up to 8 levels
manifest.json64 KB
▸ Video
The heaviest part of a theme. A 1280×720 H.264 clip at 2-3 Mbit/s weighs about 20 MB per minute. The shorter the intro, the faster the theme downloads.

03Reference

manifest.json

  • The first five fields are required, the rest are optional
  • Text can be a string or per language: "name": { "ru": "НОЧЬ", "en": "NIGHT" }
  • The client uses the user’s language, then Russian, then English
FieldTypeWhat it is
formatnumberalways 1
idstring3-40 characters: lowercase Latin letters, digits, hyphen. It is yours forever. Studio theme names are taken
nametext up to 40the name in the catalog and in the client
versionstring1.0.0. Every new upload must have a higher number
authorstring up to 40the author in the package. The catalog shows your nickname from the studio
descriptiontext up to 400the description in the catalog
tagsup to 6 stringsoptional. The client and the catalog no longer show them
accent#rrggbbthe card color in the catalog
minClientstringminimum client version, at least 6.0.0
previewpaththe preview. By default preview.webp, .png or .jpg
screenshotsup to 8 pathsscreenshots for the theme page
iconpath to a pngwindow and tray icon
licensestring up to 60the license, for example CC-BY-4.0
creditsup to 12 stringscredits: whose art, music, fonts
homepagehttps://…the author’s link on the theme page
windowobjectwindow size and shape, section 07
tokensobjectcolors and sizes, section 04
fontsobjectfonts, section 04
backgroundobjectbackground, section 05
soundsobjectevent sounds, section 05
musicpath or objectbackground music, section 05
intropath or objectintro, section 05
wordsobjectyour own wording, section 06
csspathstylesheet, section 08
scenepath or objectscene, section 09
settingsarraytheme settings, section 11
manifest.json: all fields at once
{
  "format": 1,
  "id": "night-signal",
  "name": { "ru": "НОЧНОЙ СИГНАЛ", "en": "NIGHT SIGNAL" },
  "version": "1.2.0",
  "author": "iznanka",
  "description": { "ru": "Серая панель, сигнал вместо кнопки, голос на подъём канала." },
  "accent": "#c42b47",
  "license": "CC-BY-4.0",
  "credits": ["art and sound: fawn", "font Overused Grotesk: OFL"],
  "screenshots": ["shots/on.webp", "shots/servers.webp"],
  "icon": "icon.png",
  "window": { "width": 1280, "height": 720 },
  "tokens": { "bg": "#000", "panel": "#a0a0a0", "text": "#fff", "accent": "#c42b47" },
  "fonts": { "body": "assets/grotesk.woff2", "display": "assets/pixel.woff2" },
  "background": { "image": "assets/field.webp", "dim": 0.2 },
  "sounds": { "connect": "assets/up.mp3", "disconnect": "assets/down.mp3", "click": "assets/tick.mp3" },
  "music": { "src": "assets/menu.ogg", "volume": 0.35 },
  "intro": { "video": "assets/briefing.mp4", "subtitles": "assets/briefing.vtt" },
  "words": { "ru": { "connect": "ВЫСАДКА", "disconnect": "ЭВАКУАЦИЯ", "servers": "СФЕРЫ" } },
  "css": "theme.css",
  "scene": { "entry": "scene/index.html", "mode": "full" },
  "settings": [
    { "id": "glitch", "type": "toggle", "label": "Glitch", "default": true }
  ]
}

04Palette

Colors and fonts

Tokens are the variables the client uses to color the window and all dialogs.

  • Colors: #hex, rgb(), rgba(), hsl(), transparent
  • Sizes: px, em, rem, %
  • In CSS and in the scene they are available as var(--nf-name)
TokenIn the base themeWhat it colors
bg#000000the window background beneath everything else
surface#0b0b12the glow in the center of the window
panelrgba(12,10,22,.78)the background of panels, menus and dialogs
line#23232eborders and dividers
text#e9e9f2body text
muted#6f6f80captions and secondary text
accent#a855f7the main color: connect button, selected node, headings
accent2#7c3aedthe second accent: hover, gradients
ok#4ade80channel up, “PROTECTED”
warn#f97316maintenance, warnings
danger#ef4444errors, channel drop
glowrgba(168,85,247,.45)the glow around accents
shadowrgba(0,0,0,.6)dialog shadows
overlayrgba(0,0,0,.72)the dimming under a dialog
radius0pxthe corner rounding of buttons and panels
border1pxborder thickness
gap14pxthe spacing between panels

Try it

Tweak the colors on the right. Below is a ready snippet for manifest.json.

NII IZNANKA VPN
HELPTHEMESSERVERSSETTINGS
Netherlands · 41 msLithuania · 33 msItaly · 58 msCONNECT
STATUSPROTECTEDRECEIVED 1.3 GB

    

Fonts

Three roles. The client loads the file and sets --nf-font-body, --nf-font-display, --nf-font-mono.

  • body: all text
  • display: large labels and the button
  • mono: numbers, the log, addresses
manifest.json
"fonts": {
  "body": "assets/fonts/grotesk.woff2",
  "display": "assets/fonts/pixel.woff2",
  "mono": "assets/fonts/mono.woff2"
}
▸ Languages
The client has four languages. If a font lacks Cyrillic or CJK characters, the client substitutes a system font. Check your theme in every language. Use a font licensed for embedding; OFL works.

05Palette

Background, sound, intro

Background

FieldWhatDefault
imagethe background imagenone
videothe background video: muted, looped, over the imagenone
colorthe color under the imagenone
fitcover, contain or fillcover
positionas in CSS: center, top, left 30%center
dimdimming 0…10
blurblur 0…40 px0

Event sounds

The client plays the sound for each event itself. Each theme has its own on/off switch and volume in the client settings.

EventWhen it plays
connectthe channel came up
disconnectthe channel went down
errorconnection failed or the channel dropped
switcha node switch on a live channel
clicka button press in the client window
hoverhovering over a button
open, closea dialog opened or closed
notifya notification
manifest.json
"sounds": { "connect": "assets/sfx/up.mp3", "disconnect": "assets/sfx/down.mp3", "hover": "assets/sfx/tick.mp3" },
"music": { "src": "assets/music/menu.ogg", "volume": 0.35 }

Intro

  • The clip plays full screen when the theme is selected
  • Esc skips it
  • Subtitles in WebVTT format
  • "once": true: the intro plays only the first time the theme is selected
manifest.json
"intro": { "video": "assets/intro.mp4", "subtitles": "assets/intro.vtt", "volume": 0.85, "once": false }
assets/intro.vtt
WEBVTT

00:00.500 --> 00:03.200
Link established. Welcome.

00:03.400 --> 00:06.000
Channel open.

06Palette

Your own wording

The client can speak in the theme’s words. That is how BERSERK did it: servers became LANDS, settings became GEAR. Now any theme can do this.

  • Words are set per language, up to 40 characters
  • If a language is not specified, the client’s usual word stays
manifest.json
"words": {
  "ru": { "connect": "ПРИНЯТЬ КЛЕЙМО", "disconnect": "СНЯТЬ КЛЕЙМО", "servers": "ЗЕМЛИ", "settings": "СНАРЯЖЕНИЕ" },
  "en": { "connect": "TAKE THE BRAND", "servers": "LANDS" }
}
KeyIn RussianIn English
connectПОДКЛЮЧИТЬCONNECT
disconnectОТКЛЮЧИТЬDISCONNECT
connectingПОДКЛЮЧАЮ…CONNECTING…
disconnectingОТКЛЮЧАЮ…DISCONNECTING…
switchingПЕРЕКЛЮЧАЮ…SWITCHING…
connectedЗАЩИЩЕНОPROTECTED
disconnectedНЕ ЗАЩИЩЕНОNOT PROTECTED
serversСЕРВЕРЫSERVERS
settingsНАСТРОЙКИSETTINGS
aboutО ПРОГРАММЕABOUT
helpПОМОЩЬHELP
themesТЕМЫTHEMES
catalogКАТАЛОГ ТЕМTHEME CATALOG
splitРАЗДЕЛЬНЫЙ ТОННЕЛЬSPLIT TUNNEL
updateОБНОВЛЕНИЕUPDATE
autoАВТОВЫБОРAUTO
pingОТКЛИКPING
receivedПРИНЯТОRECEIVED
sentОТДАНОSENT
sessionСЕССИЯSESSION
nodeУЗЕЛNODE
favoritesИЗБРАННОЕFAVORITES
maintenanceТЕХ. РАБОТЫMAINTENANCE
menuМЕНЮMENU
closeЗАКРЫТЬCLOSE
backНАЗАДBACK
Clarity
A word must stay understandable. “LANDS” instead of “SERVERS” is clear from context. A button that does not make clear it connects will not pass moderation.

07Palette

Window

FieldWhatLimits
width, heightwindow size in pixels800-1920 × 560-1080, default 1000×700
radiuswindow corner rounding0-48
transparenta transparent window: where nothing is drawn, the desktop shows throughtrue or false
clipwindow shape: a polygon of [x, y] points in percent3-96 points
  • The window cannot be resized with the mouse: the design is drawn for one size
  • In full screen (F11) the client scales the whole window and paints the margins with bg
manifest.json: a torn edge, like BERSERK
"window": {
  "width": 1280, "height": 720,
  "clip": [[0,4],[6,0],[38,2],[61,0],[100,3],[98,40],[100,71],[97,100],[55,97],[30,100],[2,98],[0,60]]
}
Window shape
With clip the window becomes transparent. Make sure the window buttons stay inside the shape, otherwise they cannot be clicked.

08Style

theme.css

The theme.css file is applied on top of the client styles.

  • Hook onto the data-nf labels: they do not change between client versions
  • Classes inside the window (.screen, .rows and others) are internal and may be renamed without notice
  • Paths in url() are relative to theme.css
theme.css
/* connect button: a neon ring */
[data-nf='connect'] {
  border-radius: 999px;
  box-shadow: 0 0 32px var(--nf-glow);
}
/* a pulse while the channel comes up */
[data-nf-status='connecting'] [data-nf='connect'] { animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 0 8px var(--nf-glow); } }

/* nodes under maintenance are paler */
[data-nf='server'][data-nf-maint] { opacity: .5; }

/* a custom cursor */
body { cursor: url('assets/cursor.png') 4 0, default; }

Window labels

data-nfWhat it is
screenthe whole main window of the base theme
mapthe dotted world map behind the panels
brandthe client logo and name
markthe ring logo inside brand
menuthe menu column
menu-itema menu item. Which one is shown by data-nf-item: help, themes, servers, settings, about
serversthe panel with the node list and the button
servera node row. Selected: aria-selected="true", under maintenance: data-nf-maint, favorite: data-nf-fav
mode-switchthe “auto / advanced” switch
connectthe connect button
statusthe tunnel status panel
status-textthe PROTECTED / NOT PROTECTED label
progressthe channel progress bar
logthe log window
metaaddress, traffic and session time
telegramthe Telegram button
versionthe version line
update-chipthe old update button. Since 6.0.1 updates are shown by the client’s shared banner at the bottom center; a theme neither hides nor moves it
window-controlsthe “minimize”, “full screen”, “close” buttons
dialogany dialog. Which one is shown by data-nf-dialog: servers, settings, about, help, themes, catalog, split, update, sound
dialog-titlethe dialog title
buttona regular button in a dialog
toasta pop-up notification
key-screenthe key entry screen. It can only be recolored; a scene is not drawn over it

State: attributes on <body>

AttributeValues
data-nf-statusoff, connecting, on, disconnecting, switching, error
data-nf-themeyour theme’s id
data-nf-langru, en, zh, ja
data-nf-fullpresent in full screen mode
Do not hide the exit
The theme switch button and the window buttons must be visible: it must always be possible to leave a theme. Ctrl+Shift+Backspace restores the base theme anyway, but a moderator will not pass such a theme.

09Scene

Your own window, entirely

A scene is an HTML page inside the package. The client opens it across the whole window, in a sandbox, and passes it state via the SDK. Everything a browser can do works inside: canvas, WebGL, CSS animations, video, sound, custom fonts.

manifest.json
"scene": { "entry": "scene/index.html", "mode": "full" }
modeHow it behaves
fullthe scene fills the whole window and the client’s main screen is hidden. Client dialogs (settings, servers, help) open on top of the scene and are colored by tokens and theme.css
backdropthe scene becomes a live background behind the regular client window. It receives no mouse or keyboard input

Window layers

  1. Introintro, above everything
  2. Client dialogstokens + theme.css
  3. Scene fullscene/index.html
  4. Client windowtokens + theme.css
  5. Scene backdroplive background
  6. Backgroundbackground

Scene skeleton

scene/index.html
<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="scene.css">
  <script src="/__nf/sdk.js"></script>
</head>
<body>
  <div id="frame">
    <button id="core">-</button>
    <div id="nodes"></div>
  </div>
  <script src="scene.js"></script>
</body>
</html>
scene/scene.js
const frame = document.getElementById('frame');
nf.fit(frame, 1280, 720);                                // a 1280×720 frame
nf.window.dragRegions([{ x: 0, y: 0, w: 1180, h: 48 }]); // the window can be dragged by the top

const core = document.getElementById('core');
core.addEventListener('click', () => nf.toggle());

function render() {
  const s = nf.state;
  core.textContent = s.connected ? nf.t('disconnect') : nf.t('connect');
  document.body.dataset.status = s.status;
}

nf.on('init', render);
nf.on('state', render);
nf.on('nodes', (nodes) => {
  document.getElementById('nodes').replaceChildren(...nodes.map((node) => {
    const row = document.createElement('button');
    row.textContent = node.name + ' · ' + (node.ping || '-') + ' ms';
    row.onclick = () => nf.select(node.id);
    return row;
  }));
});

What you should know

  • SDK is always at /__nf/sdk.js. You do not need to put it in the package
  • Size. The window can also be full screen. Draw in a fixed-size frame and fit it with nf.fit()
  • Dragging. The window has no frame. Specify regions with nf.window.dragRegions(), otherwise the window cannot be moved
  • Window buttons. By default ("hostChrome": true) the client places a block in the top-right corner over the scene: theme, minimize, full screen, close. Drawing your own buttons with nf.window.*? Set "hostChrome": false, as the template does
  • Client hotkeys (F1-F12, Esc) also work inside the scene. If the scene needs a key itself, call event.preventDefault() and the client will not receive it
  • Leaving. When the theme changes the scene receives leave. Return a Promise and the client waits up to 6 seconds
Without a GPU
In a VM or over remote desktop the canvas is rendered by the CPU. shadowBlur and filter in the frame loop drop such a machine to 2 frames per second. Draw glow as a pre-made sprite with drawImage. When the window is minimized, stop requestAnimationFrame; state will receive visible: false.

10Scene

SDK: the object nf

Properties

PropertyTypeWhat
nf.stateobjectchannel state, explained below. Before the init event it equals null
nf.statsobject{ rx, tx, rxRate, txRate, ping, at }: session bytes, speed in bytes/s, latency in ms. Updated once a second while the channel is up
nf.nodesarraynodes available to the key: { id, name, country, city, ping, load, maintenance, favorite, selected }. The scene does not receive node addresses
nf.settingsobjecttheme setting values from manifest.settings
nf.themeobject{ id, version, name, base }. base is the package root address, ending with “/”
nf.langstringclient language: ru, en, zh, ja
nf.previewbooleantrue in a preview (the website or nftheme preview), false in the client
nf.scalenumberthe frame scale after nf.fit()
nf.versionstringSDK version

Events

EventPayloadWhen
initeverything at onceonce, when the client has passed the data. Start drawing here
statenf.statethe channel went up or down, or the node, language, sound or full screen changed
statsnf.statsonce a second while the channel is up
nodesnf.nodesa new node list or latency measurements arrived (F5, nf.ping())
settingsnf.settingsthe user changed a theme setting in the client
langstringthe client language changed
leavethe new theme’s idthe theme is being changed. Return a Promise and the client waits up to 6 seconds for the scene to finish its exit

Methods

MethodWhat it does
nf.on(event, fn)subscribe; returns an unsubscribe function
nf.off(event, fn)unsubscribe
nf.ready()the scene has finished drawing and the client removes the curtain. Without the call this happens automatically after load. To keep the curtain longer: nf.manualReady = true before loading
nf.connect()bring the channel up → Promise
nf.disconnect()bring the channel down → Promise
nf.toggle()bring it up or down → Promise
nf.select(id)select a node. On a live channel the client switches by itself
nf.auto()return to automatic node selection
nf.ping()measure latency of all nodes, like F5
nf.favorite(id)add a node to favorites or remove it
nf.open(screen)open a client screen: servers, settings, about, help, themes, catalog, split, update
nf.toast(text)a notification at the bottom of the window, up to 140 characters
nf.window.minimize()minimize the window
nf.window.close()hide to the tray
nf.window.fullscreen()full screen and back
nf.window.dragRegions(rects)which parts of the scene drag the window: [{ x, y, w, h }] in scene pixels, up to 16
nf.t(key)a client word in the current language, taking into account manifest.words
nf.asset(path)the full address of a package file
nf.storage.get(key)read from the theme storage → Promise
nf.storage.set(key, value)write any JSON, up to 64 KB in total per theme → Promise
nf.storage.remove(key)delete → Promise
nf.sound.play(path, { volume, loop })play a package file, respecting the theme’s on/off switch and volume
nf.sound.enabled / nf.sound.volumewhether the theme sound is on and the overall volume 0..1
nf.fit(el, w, h, mode)fit a w×h frame into the window. mode: contain or cover
nf.format.bytes(n) / rate(n) / duration(ms)«1.3 GB», «350 KB/s», «03:46:32»

All client calls return a Promise. If the client refuses (for example, the key is not activated), the Promise rejects with a clear error.

The state object

nf.state
{
  status: 'on',            // off | connecting | on | disconnecting | switching | error
  connected: true,
  busy: false,             // connecting, disconnecting or switching
  node: { id: 'nl-1', name: 'Netherlands · Amsterdam', country: 'NL', city: 'Amsterdam',
          ping: 41, load: 23, maintenance: false, favorite: true, selected: true },
  auto: true,              // the client picks the node
  since: 1791232800000,    // when the channel came up (ms), otherwise null
  error: null,             // the error text when status === 'error'
  lang: 'ru',
  words: { connect: 'CONNECT', servers: 'SERVERS', … },   // with manifest.words applied
  sound: { enabled: true, volume: 0.8 },
  visible: true,           // the window is on screen: not minimized, not in the tray
  fullscreen: false,
  reduceMotion: false,     // “reduce motion” is enabled in the system
  platform: 'windows',     // windows | linux
  version: '6.0.0',
  sub: { plan: 'NII IZNANKA', daysLeft: 27 },
  update: { available: false, version: null }
}
▸ What state does not have
The key, node addresses and the user’s IP. The scene does not need them.

11Any level

Theme settings

  • Describe the settings in the manifest and the client draws them in the settings window, in your theme’s block
  • The client stores the values. The scene receives them in nf.settings and the event settings
  • Up to 16 settings
typeFieldsValue
toggledefault: true or falseboolean
rangemin, max, step, defaultnumber
selectoptions: 2-12 options { value, label }, defaultstring
colordefault: #rrggbba #rrggbb string
manifest.json
"settings": [
  { "id": "glitch", "type": "toggle", "label": { "ru": "Помехи", "en": "Glitches" }, "default": true },
  { "id": "speed", "type": "range", "label": "Ring speed", "min": 0, "max": 2, "step": 0.1, "default": 1 },
  { "id": "scene", "type": "select", "label": "Scene", "default": "grid",
    "options": [ { "value": "grid", "label": "Grid" }, { "value": "signal", "label": "Signal" } ] },
  { "id": "tint", "type": "color", "label": "Color", "default": "#c42b47", "hint": "The color of the rings around the button" }
]
scene/scene.js
nf.on('settings', (values) => {
  document.body.classList.toggle('glitch', values.glitch);
  rings.speed = values.speed;
});

You do not need to describe the theme sound separately. The client adds an on/off switch and volume itself if the theme has sounds, music or intro.

12Security

Sandbox

A theme is someone else’s code in an app that holds the user’s VPN key. That is why the client trusts no theme, not even a reviewed one.

Scene · sandboxHTML, CSS, JS, canvas, videonftheme://id/: only its own files
NII Iznanka clientkey, configuration, addressestunnel, files, network

networkkeyfileswindowscookie

  • The network is closed entirely. fetch, XMLHttpRequest, WebSocket, images and fonts from other addresses, WebRTC: the client blocks all of it. A theme works only with files from its own package
  • The scene is isolated. It is an iframe with no shared origin with the client window: no client DOM, no cookies, no localStorage. For your own data there is nf.storage
  • Nothing secret. No key, no node addresses, no IP; see state
  • No windows, no navigation. A scene cannot open a new window, navigate the page away or open a link in the browser
  • theme.css has no network. url() only works with package files
  • Signature. From the catalog the client installs only packages signed by the studio. If a file is swapped, the sha256 will not match and the signature fails
  • Revocation. If something harmful is found in a published theme, it is revoked. On the next catalog check, clients turn it off and remove it themselves

Emergency exit

  • Ctrl+Shift+Backspace: restore the base theme at any time. The client catches the shortcut before the scene, so a theme cannot intercept it
  • Launching with the --safe-theme flag opens the client straight in the base theme

13Debugging

How to test

In the browser

nftheme preview my-theme starts a client stub at 127.0.0.1:4790:

  • a window with data-nf labels, your background, tokens and theme.css
  • the scene in a sandbox
  • a control panel on the right: channel state, language, sound, your settings, validation errors, an SDK call log
  • files are reloaded on every save
▸ The stub
The stub shows the scene and SDK accurately. The regular client window is similar but not identical: check the final theme.css result in the client.

In the client

  1. THEMES → AUTHORS → “Open theme folder”: the theme installs from the folder and reloads on every save
  2. “Open .nftheme file”: check a built package
  3. “Developer tools”: DevTools with the scene console, layout and network, where you can see what is blocked

A theme from a folder or file is marked NOT VERIFIED and runs in the same sandbox as a catalog theme.

Common errors

MessageWhat to do
File “…” is not in the packagethe path in the manifest does not match the file. Case matters
invalid namerename the file using Latin letters, without spaces
The content does not look like .mp3the file is in another format. Re-encode it; renaming will not help
The scene does not include the SDKadd <script src="/__nf/sdk.js"> to <head>
id “…” is takenit belongs to another author or a studio theme. Pick your own
The version is not newer than the published onebump version
the window does not movethe scene has no nf.window.dragRegions()
an image from the internet does not loadthat is by design. Put it in the package

14Publishing

Review and the catalog

  1. Sign in to the studio via your account: with email or a link from the bot
  2. Drag the package. The website validates it with the same code as nftheme check and adds it to the queue
  3. A moderator opens the theme in the sandbox and in the client and reviews the code. The decision arrives in Telegram, with a reason if rejected
  4. The server signs the approved version and it appears in the catalog. For those who already have the theme, the client updates it automatically

What a moderator checks

Will passWill not pass
the theme works: connecting, node selection, dialogsthe connect button cannot be found or understood
preview: an honest screenshot of the theme itselfa preview from another theme or someone else’s art used as bait
readable code. Well-known libraries with version and licenseobfuscated or minified code without source, eval, code from strings
art, music and fonts are your own or used with permission, sources listed in creditsother people’s work without permission. On a rights holder’s complaint the theme is removed
the theme can be left: theme switch, minimize, closea hidden theme button, traps, full-screen flashing without warning
sound via sounds or nf.sound, respecting the settingssound that bypasses the settings, loud surprises
when idle the theme barely loads the CPUthe fan takes off while the window is minimized
fake key-entry or payment screens, ads, links, referrals, 18+, calls to action and harassment

Versions, hiding, revocation

  • Every new version goes through review again. While it is under review, the previous one stays in the catalog
  • A new submission withdraws your previous one that has not been reviewed yet
  • A rejected version can be resubmitted under the same number
  • Hidden: the theme was removed from the storefront; those who have it keep it
  • Revoked: the theme is dangerous; clients will turn it off and remove it
  • No more than 12 uploads per day

15Recipes

Ready-made techniques

An image instead of the connect button

theme.css
[data-nf='connect'] {
  width: 220px; height: 220px; padding: 0;
  border: 0; border-radius: 50%;
  background: url('assets/sigil-off.png') center / contain no-repeat;
  color: transparent;            /* we hide the label; a screen reader will still read it */
  filter: grayscale(1) brightness(.7);
  transition: filter .4s, transform .2s;
}
[data-nf-status='on'] [data-nf='connect'] {
  background-image: url('assets/sigil-on.png');
  filter: drop-shadow(0 0 24px var(--nf-glow));
}
[data-nf='connect']:hover { transform: scale(1.04); }

Flashing on a drop

theme.css
[data-nf-status='error'] [data-nf='screen'] { animation: lost .12s steps(2) 6; }
@keyframes lost { 50% { filter: invert(1) hue-rotate(160deg); } }

A live background behind the regular window

manifest.json
"tokens": { "bg": "transparent", "panel": "rgba(0,0,0,.55)" },
"scene": { "entry": "stars/index.html", "mode": "backdrop" }
stars/stars.js
const c = document.querySelector('canvas');
const ctx = c.getContext('2d');
const stars = Array.from({ length: 220 }, () => ({ x: Math.random(), y: Math.random(), z: Math.random() }));
let speed = 0.0004;
nf.on('state', (s) => { speed = s.connected ? 0.003 : s.busy ? 0.008 : 0.0004; });
(function frame() {
  c.width = innerWidth; c.height = innerHeight;
  for (const s of stars) {
    s.z -= speed; if (s.z <= 0) s.z = 1;
    const k = 0.5 / s.z;
    ctx.fillStyle = 'rgba(255,255,255,' + (1 - s.z) + ')';
    ctx.fillRect((s.x - 0.5) * k * c.width + c.width / 2, (s.y - 0.5) * k * c.height + c.height / 2, 2, 2);
  }
  if (!nf.state || nf.state.visible !== false) requestAnimationFrame(frame);
  else nf.on('state', function wake(s) { if (s.visible) { nf.off('state', wake); requestAnimationFrame(frame); } });
})();

Session record

scene/scene.js
nf.on('state', async (s) => {
  if (s.status !== 'off' || !nf.state.since) return;
  const minutes = Math.round((Date.now() - nf.state.since) / 60000);
  const best = (await nf.storage.get('best')) || 0;
  if (minutes > best) {
    await nf.storage.set('best', minutes);
    nf.toast('New record: ' + minutes + ' min');
  }
});

A drop-in screen on connect

scene/scene.js
let was = null;
nf.on('state', (s) => {
  if (was !== 'on' && s.status === 'on') {
    deploy.querySelector('b').textContent = s.node ? s.node.name : '';
    deploy.hidden = false;
    nf.sound.play('assets/deploy.mp3');
    setTimeout(() => { deploy.hidden = true; }, 2200);
  }
  was = s.status;
});

16Questions

The essentials

Will themes come to Android?

Yes, in the same format. Palettes, wording and sounds will work there too, and scenes will run via WebView. Make sure your theme also lives in a 1000×700 window: phone screens are vertical.

Can I make a theme based on a game or anime?

Yes, if you have rights to the art and sound or the owner does not mind. List the sources in credits. If a rights holder complains, the theme will be removed.

Can I use three.js, PixiJS, GSAP?

Yes. Put the library file in the package (it will not load from a CDN, the network is closed), do not modify it, and list the version and license in credits. A moderator will compare the file with the official one.

Where does a theme store settings and records?

The client stores the settings from manifest.settings. Put your own data in nf.storage, up to 64 KB. When the theme is deleted, the data goes with it.

How long does review take?

Usually up to a day. The decision arrives in Telegram.

Are studio themes built the same way?

Almost. BERSERK, CYBERPUNK and others sit in the catalog as separate studio-signed packages. The main NII IZNANKA theme is also built into the client. Studio themes can integrate deeper into the window: the host field is available only to studio-signed packages.