Docs / Skins guide
Noodle skins
A skin changes how Noodle looks: colours, type, corners, spacing, and optionally the layout and page effects it suggests. A skin is a file ending in .skin. Open one from your file manager, drop it on the Noodle window, or use Settings > Appearance > Install skin. Noodle shows it for 15 seconds with Keep and Revert before anything is saved.
This page covers the whole format. To start right away, copy examples/skins/starter and go to Building a skin from scratch.
The built-in skins in resources/skins/ are laid out exactly like a .skin, unzipped, so they double as examples: noodle (light and dark), black, high-contrast, paper, classic-98, phosphor, phosphor-amber, glass-2000 and blueprint.
Contents
- The file
- skin.json
- Tokens
- Layout suggestions
- Page effects
- skin.css
- Pictures and fonts
- Limits
- What Noodle refuses, and why
- Skin dev mode
- Checking and packing from the command line
- Building a skin from scratch
- How Classic 98 is made
- The skin editor
The file
A .skin is a zip with:
| Path | Required | What it holds |
|---|---|---|
skin.json |
yes | Name, tokens and suggestions |
skin.css |
no | Extra CSS for the Noodle window |
assets/ |
no | PNG, JPEG, WebP, AVIF and SVG pictures, WOFF2 fonts |
LICENSE, *.txt, *.md |
no | Licence and notes; never served |
Anything else, a path that leaves the zip's root, or more than 10 MB in total makes Noodle refuse the skin. A zip that wraps everything in one top folder is fine. You don't need to make the zip by hand: npm run skin -- pack <folder> does it, and so does Save as .skin in the skin editor.
skin.json
A full example:
{
"name": "Night Shift",
"author": "You",
"version": "1.0.0",
"description": "Warm and dim for late reading.",
"license": "CC0-1.0",
"noodle": "0.0.1",
"mode": "dark",
"tokens": {
"bg": "#16120e",
"surface": "#1f1a14",
"text": "#f1e6d4",
"muted": "#bba98f",
"accent": "#f0a35e",
"radius": "6px"
},
"fonts": [{ "family": "My Font", "src": "assets/my-font.woff2", "weight": "400" }],
"wallpaper": "assets/wall.webp",
"layout": { "sidebarSide": "right", "density": "compact" },
"pageEffects": { "crt": "light", "scanlines": 0.4 },
"features": ["sounds"]
}
Every field:
| Field | Type | Default | Notes |
|---|---|---|---|
name |
string | required | Up to 60 characters. Also gives the installed skin its id |
author |
string | empty | Up to 60 characters |
version |
string | 1.0.0 |
Up to 20 characters, shown in Settings |
description |
string | empty | Up to 300 characters, shown when you hover the skin |
license |
string | empty | An SPDX id like CC0-1.0 or MIT is the clearest |
noodle |
string | none | The oldest Noodle version the skin works with. Newer requirements refuse it |
mode |
string | dark |
dark, light or both |
tokens |
object | {} |
Token values, see Tokens. With both, two sets |
fonts |
list of objects | [] |
Up to 8 WOFF2 fonts from assets/ |
wallpaper |
string | none | A picture in assets/, shown behind the sidebar and top bar |
layout |
object | none | Layout the skin suggests, see Layout suggestions |
pageEffects |
object | none | Page effects the skin suggests, see Page effects |
features |
list of strings | [] |
Extras Noodle draws itself: sounds, window-buttons |
Some details:
- With
"mode": "both",tokensholds two sets,{ "dark": { ... }, "light": { ... } }, and Noodle picks one from the theme setting (system, dark or light). Adarkorlightskin ignores the theme setting. - Token names are written without the leading dashes:
"bg", not"--bg". Unknown names are ignored. - A font entry looks like
{ "family": "My Font", "src": "/assets/my-font.woff2", "weight": "400", "style": "normal" }.familymay hold letters, digits, spaces,_and-.weightis one weight (700) or a range (100 900).styleisnormaloritalic. Use the family in thefont-uiorfont-monotoken. layoutandpageEffectsare suggestions. Settings > Appearance offers "Use its layout" and "Use its page effects"; nothing changes until the user accepts.soundsgives the skin short synthesized interface clicks, only when the user has turned sounds on in Settings.window-buttons(Windows and Linux) makes Noodle draw the minimize, maximize and close buttons itself instead of the system's, soskin.csscan restyle them (see Window buttons). A window can't swap its buttons while it is open, so this takes effect the next time Noodle starts; Settings > Appearance offers a Restart button. Without it, the system's buttons stay and only take thetitlebarandtitlebar-symbolcolours, which keeps the Windows 11 snap layouts on the maximize button. Ask for it only when colours aren't enough. The choice follows the skin picked in Settings, not a space's own skin. On macOS the system's buttons always stay. The window is created before the profile is unlocked, so the choice (and, for these skins only, thebgcolour shown before the first paint) is kept inwindow-frame.jsonin the Noodle data folder, outside the locked profile.- Keys Noodle doesn't know are ignored.
npm run skin -- validateand skin dev mode warn about them, which catches typos.
Tokens
Tokens are CSS custom properties on the Noodle window. Noodle's own styles read them everywhere, so setting a handful changes the whole window. A skin's values come after Noodle's defaults and win. The user's own choices in Settings (gap, corner radius, font, text size, a space's accent colour and wallpaper) win over the skin.
A token value may be a colour, a length, a shadow, a gradient or a font list. url(), image-set() and similar functions, ;, braces, comments, backslashes and unbalanced quotes are refused, so a value can't end its declaration or load anything. Pictures belong in skin.css or wallpaper.
The tables list every token with Noodle's own values. "same as dark" means the light palette keeps the dark value, and var(--bg) and similar mean the token follows another one unless you set it.
Colours
| Token | Dark | Light | Use |
|---|---|---|---|
bg |
#111113 |
#ececef |
Window background, behind the sidebar and top bar |
surface |
#1a1a1d |
#ffffff |
The page area, fields and panels |
elevated |
#1c1c1f |
#ffffff |
Command palette, menus and dialogs |
card |
#202024 |
#ffffff |
Cards on the canvas |
sidebar-bg |
transparent |
same as dark | Behind the sidebar, over bg |
topbar-bg |
transparent |
same as dark | Behind the top bar, over bg |
titlebar |
var(--bg) |
#ececef |
Strip behind the window buttons on Windows and Linux (solid colour) |
titlebar-symbol |
#d4d4d8 |
#3f3f46 |
The window button symbols on Windows and Linux (solid colour) |
hover |
rgba(255, 255, 255, 0.06) |
rgba(0, 0, 0, 0.05) |
Rows and buttons under the pointer |
active |
rgba(255, 255, 255, 0.1) |
rgba(0, 0, 0, 0.085) |
Pressed buttons and selected rows |
border |
rgba(255, 255, 255, 0.08) |
rgba(0, 0, 0, 0.1) |
Lines between parts |
border-strong |
rgba(255, 255, 255, 0.16) |
rgba(0, 0, 0, 0.2) |
Lines that need to show, like a focused field |
text |
#e4e4e7 |
#18181b |
Main text |
muted |
#a1a1aa |
#52525b |
Secondary text |
faint |
#71717a |
#71717a |
Hints, counts and the weakest text |
accent |
#a78bfa |
#7c3aed |
Highlights, focus rings and the selected thing |
accent-hover |
#b9a3fc |
#6d28d9 |
The accent under the pointer |
accent-text |
#18181b |
#ffffff |
Text on an accent background |
agent |
#c084fc |
#9333ea |
Anything an agent is doing |
warn |
#fbbf24 |
#b45309 |
Warnings |
danger |
#f87171 |
#dc2626 |
Errors and buttons that delete |
danger-text |
#fca5a5 |
#b91c1c |
Error text |
good |
#4ade80 |
#16a34a |
Success |
good-text |
#86efac |
#15803d |
Success text |
link |
#c4b5fd |
#6d28d9 |
Links, including links in the reader |
scrim |
rgba(9, 9, 11, 0.55) |
rgba(24, 24, 27, 0.25) |
Behind the palette and dialogs |
selection |
rgba(167, 139, 250, 0.35) |
rgba(124, 58, 237, 0.22) |
Selected text |
input-bg |
var(--surface) |
same as dark | The address field and other inputs |
tab-active-bg |
var(--active) |
same as dark | The selected tab |
tab-active-text |
var(--text) |
same as dark | Text of the selected tab |
scrollbar |
rgba(255, 255, 255, 0.14) |
rgba(0, 0, 0, 0.2) |
Scrollbar thumbs |
code-bg |
rgba(255, 255, 255, 0.08) |
rgba(0, 0, 0, 0.05) |
Code in the reader and in notes |
mark |
rgba(250, 204, 21, 0.28) |
rgba(250, 204, 21, 0.4) |
Highlighted text in the reader and in notes |
Shape and spacing
| Token | Dark | Light | Use |
|---|---|---|---|
radius |
8px |
same as dark | Corner radius of panels; the page views follow it |
radius-sm |
6px |
same as dark | Smaller corners: buttons, rows, fields |
gap |
8px |
same as dark | Space around the page and between large parts |
row-height |
34px |
same as dark | Height of tabs, bookmarks and similar rows |
shadow |
0 6px 20px rgba(0, 0, 0, 0.28) |
0 6px 20px rgba(0, 0, 0, 0.08) |
Small shadow: menus, the address suggestions |
shadow-lg |
0 24px 64px rgba(0, 0, 0, 0.5) |
0 24px 64px rgba(0, 0, 0, 0.18) |
Large shadow: the palette and dialogs |
Type
| Token | Dark | Light | Use |
|---|---|---|---|
font-ui |
system-ui, -apple-system, 'Segoe UI', sans-serif |
same as dark | Font of the whole window |
font-mono |
ui-monospace, 'Cascadia Code', Consolas, monospace |
same as dark | Font of code, the userscript editor and addresses |
font-size |
13px |
same as dark | Base text size; the rest scales from it |
Motion
| Token | Dark | Light | Use |
|---|---|---|---|
duration |
120ms |
same as dark | Length of small transitions; 0ms turns them off |
ease |
ease-out |
same as dark | Easing of those transitions |
The reader view's "Noodle" theme takes bg, surface, text, muted, border, hover, active, accent, link, code-bg, mark and danger from the active skin.
If you work on Noodle itself: npm run docs:tokens writes the tables above from SKIN_TOKENS in src/shared/skins.ts and the :root blocks in styles.css and theme.css. npm run build runs the same script with --check and stops when the tables are out of date or a token has no description, so a new token needs a line here first.
Layout suggestions
All keys are optional. A key with a value Noodle doesn't accept is dropped, and the validator says so.
| Key | Values | What it does |
|---|---|---|
sidebarSide |
left, right |
Which side the sidebar sits on |
sidebarMode |
shown, autohide, compact, hidden |
Always there, slides in at the edge, icons only, or gone |
tabsOnTop |
true, false |
Tabs in a strip along the top instead of the sidebar |
topBar |
shown, merged, hidden |
The address bar row, folded into the sidebar, or gone |
density |
comfortable, compact |
Row heights and padding |
gap |
whole number from 0 to 24 | Pixels around the page |
radius |
whole number from 0 to 24 | Corner radius in pixels, page views included |
motion |
full, reduced, none |
How much the window animates |
toolbar |
list of top bar button names, see below | Which top bar buttons show, in order; each name at most once |
Toolbar button names: split, reader, video, scripts, shield, fast, star, devtools and effects.
gap and radius here become the user's settings. Use the gap and radius tokens for the look itself, and the layout keys only when the skin needs those numbers to stick. Prompts, agent waits and find bring a hidden top bar back while they are up, whatever the skin suggests.
Page effects
Page effects change how web pages look, not the Noodle window. They are Noodle's own code: a skin can only suggest which ones to switch on and with which numbers, and the user turns them on (Use its page effects, or Settings > Appearance > Page effects).
{
"pageEffects": {
"crt": "light",
"curvature": 0.35,
"scanlines": 0.35,
"glow": 0.25,
"flicker": 0.1,
"mask": 0.3,
"tint": "#33ff66",
"pixelFont": true
}
}
| Key | Values | Default | Light tier | Full tier |
|---|---|---|---|---|
crt |
off, light, full |
off |
light turns on the overlay below |
full draws the page through the CRT shader |
curvature |
0 to 1 | 0.5 | Strength of the dark vignette | How far the tube bulges, plus vignette and fringe |
scanlines |
0 to 1 | 0.5 | Darkness of the 3 px lines | Depth of scanlines that follow the curve |
glow |
0 to 1 | 0.4 | A little more brightness and contrast | Bloom: bright areas bleed into their neighbours |
flicker |
0 to 1 | 0.2 | A slight brightness flicker | A slight brightness flicker |
mask |
0 to 1 | 0.4 | Faint red, green and blue columns | Aperture grille: each column favours one colour |
grayscale |
0 to 1 | 0 | Page in gray | Not used |
sepia |
0 to 1 | 0 | Page in brown tones | Not used |
tint |
#rrggbb or empty |
empty | One-colour screen: inverted, then tinted | One phosphor colour; light pages become lit text |
dither |
true, false |
false |
A 2 px checker pattern over the page | Not used |
pixelFont |
true, false |
false |
Page text in a pixel font (VT323) | Not used |
Flicker stops when the system asks for reduced motion or the user sets motion to reduced or none.
The light tier
The light tier is CSS that Noodle puts into each page: a filter on the page for grayscale, sepia, tint and glow, and one click-through overlay for scanlines, the mask, the vignette and dither. It costs close to nothing and works on every machine. With pixelFont on, icon fonts, code and preformatted text keep their own font.
The full CRT tier
With "crt": "full", Noodle renders the active tab offscreen and draws each frame through a WebGPU shader on the Noodle window: barrel curvature, an aperture grille, bloom, scanlines that follow the curve, slight colour fringing towards the edges, flicker, a rounded tube edge and the phosphor tint. Clicks, scrolling and typing are mapped back through the same curve, so the page stays usable. It draws at most 60 frames a second. The real tab is hidden and muted while the CRT copy shows, and moves to the address you ended up on when the CRT stops.
Noodle falls back to the light tier when WebGPU or shared GPU textures are missing, when the GPU device is lost, or when drawing stays under 50 frames a second for 3 seconds. Split view, reader view, the canvas and fullscreen use the light tier, and the CRT never starts on a tab an agent is working in. Since a skin can only suggest full, nothing heavy runs until the user turns it on.
skin.css
skin.css styles the Noodle window, after Noodle's own styles. Use the class names you see in the window's developer tools (.sidebar, .tab.active, .topbar, .address-input, .palette, .button and so on). Reach for tokens first: they are shorter, they keep working when Noodle's markup changes, and they cover parts you might forget. Use skin.css for what tokens can't say, like bevels, patterns or a different shape for one control.
/* The selected tab gets a bar in the accent colour on its left edge. */
.tab.active {
box-shadow: inset 3px 0 0 var(--accent);
}
@media (prefers-reduced-motion: no-preference) {
.tab {
transition: background var(--duration) var(--ease);
}
}
Window buttons
With the window-buttons feature, Noodle draws the window buttons as plain HTML in the top right corner, where the system's would be:
<div class="window-controls maximized inactive">
<button class="wc-button wc-min"><span class="wc-glyph"></span></button>
<button class="wc-button wc-max"><span class="wc-glyph"></span></button>
<button class="wc-button wc-close"><span class="wc-glyph"></span></button>
</div>
maximized is there while the window is maximized (the middle button then restores) and inactive while another window has focus. The glyphs are drawn with borders and the ::before and ::after pseudo-elements of .wc-glyph, so a skin can redraw them without pictures. The rest of the top strip still moves the window: drag it, double-click it to maximize, or drag it to a screen edge to snap. Windows' snap layouts menu only appears on the system's maximize button; Win+Z still opens it.
New tab page
Blank tabs show the new tab page (Settings > Appearance > New tab page): a clock, a search field and the user's shortcuts. Noodle draws it in the window, so it uses the skin's tokens (surface behind it, elevated and border for the icon tiles, hover, accent for focus) and the skin's or space's wallpaper. The user can pick their own colour or picture instead. Its classes, for skin.css:
| Class | What it is |
|---|---|
.ntp |
The page; on-dark, on-light or on-picture with the user's own background |
.ntp-clock, .ntp-time, .ntp-date |
The clock |
.ntp-search-input |
The search field |
.ntp-tile, .ntp-link, .ntp-add |
A shortcut, its link, the Add shortcut tile |
.ntp-icon, .ntp-name |
A shortcut's icon box and name |
.ntp-remove |
The remove button that shows on hover |
Cleaning
Noodle cleans the file before use:
@media,@supports,@keyframes,@font-faceand@containerstay.@import,@layerand every other at-rule are removed.url()works for files in the skin's ownassets/folder (url('assets/dots.svg')orurl(./assets/dots.svg)) and fordata:pictures and WOFF2 fonts. Anything else becomesnone.image-set(),src(),image(),cross-fade(),element(),attr(),env()andpaint()are removed.z-indexis capped at 9999.- A backslash escape outside a string makes Noodle refuse the skin, since an escape could spell one of the blocked names.
The install preview, skin dev mode and the validator all list what was removed.
Pictures and fonts
Put them in assets/ (subfolders are fine, up to five levels). File and folder names may use letters, digits, ., _ and - and must start with a letter or digit.
| Extension | Served as | Use it for |
|---|---|---|
.png |
image/png |
Pictures, patterns |
.jpg, .jpeg |
image/jpeg |
Photos |
.webp, .avif |
as named | Photos and pictures, smaller than PNG or JPEG |
.svg |
image/svg+xml |
Patterns and icons; scripts in SVG never run |
.woff2 |
font/woff2 |
Fonts, through fonts in skin.json or @font-face |
- The wallpaper is a picture from
assets/named inwallpaper. It sits behind the sidebar and the top bar, underbg. A user can also pick a wallpaper per space, and that one wins. - Fonts in
fontsgetfont-display: swap, so text shows in the next font in your list until the file has loaded. Endfont-uiwith a system font. - Check the licence of every font and picture you ship and put it next to the file (
assets/OFL.txt, say). Classic 98 and Phosphor do this for their Open Font License fonts. - Pictures and fonts are served on
noodle-skin://<skin>/assets/...to the Noodle window only. Web pages can't load them.
Limits
| What | Limit |
|---|---|
| Whole skin (all files) | 10 MB, and the .skin file itself 10 MB |
skin.json |
64 KB |
skin.css |
256 KB |
| A token value | 240 characters |
Fonts in fonts |
8 |
A data: URL in skin.css |
200,000 characters |
z-index in skin.css |
-9999 to 9999 |
Folders under assets/ |
5 levels, names up to 81 characters |
A folder in dev mode or pack |
500 files; hidden files (starting with .) are skipped |
What Noodle refuses, and why
Skins come from strangers, so a skin is data and never code.
- No JavaScript or any other code. A skin has nowhere to put it, SVG pictures are shown as images (where scripts never run), and Noodle serves skin files with a policy that blocks scripts even if one is opened as a document.
- No remote addresses. Fonts and pictures come from the skin itself, so a skin can't tell anyone when or where you use it. The window's content security policy only allows pictures and fonts from Noodle itself and from
noodle-skin://. - No files outside the skin's folder, and no file types beyond the list above. The zip reader refuses paths with
.., backslashes or a leading/. - No
@import(it could load other files), no@layer(see below) and no functions that fetch by name. - No backslash escapes outside strings, since they could spell a blocked name in a way a simple check would miss.
- No skins that need a newer Noodle than yours, since they might rely on tokens or classes that don't exist yet.
Some parts of the window are protected. Permission prompts, agent approval cards and waits, the agent bypass flag, the address field, download warnings and the Keep and Revert bar have their key styles in a cascade layer that skins can't override or join. A skin can't hide them, make them transparent, move them off screen, reword them or cover them, and the address field always uses the system's own field colours. Web pages are native views above the Noodle window, so nothing in a skin can draw over a page either. When the profile locks, the skin comes off and the unlock screen shows Noodle's own look.
Skin dev mode
Settings > Appearance > Skin development > Load skin folder picks an unpacked skin folder (the one holding skin.json) and shows it right away. Noodle watches the folder and reloads the skin about 150 ms after any file in it changes, pictures and fonts included, so you can keep your editor and Noodle side by side.
- Errors and warnings show under the button, worded the same as the command line checker. An error (broken JSON, a backslash escape, a file type Noodle refuses) stops the reload, and the last version that loaded stays on screen until you fix it.
- Nothing is installed or saved. Stop puts your own skin back, and so does restarting Noodle.
- Reload reads the folder again by hand, for editors that save in ways the watcher misses.
- Files and folders starting with
.are skipped, so a.gitfolder or editor swap files don't get in the way.
Checking and packing from the command line
Run these from inside a checkout of the Noodle source (Node 22.18 or newer). Relative paths count from the folder you type the command in; a skin folder outside the checkout needs its full path, like C:/skins/harbor.
npm run skin -- validate path/to/my-skin
npm run skin -- validate my-skin.skin other-skin
npm run skin -- validate C:/skins/harbor
npm run skin -- pack path/to/my-skin
npm run skin -- pack path/to/my-skin --out dist/my-skin.skin
validate takes folders and .skin files, prints each skin's problems and exits with 1 if any skin would be refused. pack checks the folder first and writes <folder name>.skin in the folder you ran it from (or the --out path) only when there are no errors. Both use src/main/skins/validate.ts, the same code Noodle runs when it installs a skin and in skin dev mode, so the messages match. A run looks like this:
ok Starter 1.0.0 (1.6 KB, 3 files)
FAIL Night Shift 1.0.0 (2.1 KB, 2 files)
warning: skin.json: tokens has "acent", which isn't a Noodle token.
error: skin.css uses a backslash escape outside a string, which skins may not.
Building a skin from scratch
This builds a small dark and light skin called Harbor. The result is close to examples/skins/starter, so you can compare.
Make a folder named
harboranywhere (this guide usesC:/skins/harbor), and in it a file namedskin.json:{ "name": "Harbor", "author": "Your name", "mode": "dark", "tokens": { "bg": "#12161a", "surface": "#1a2026", "accent": "#5fc490" } }In Noodle, open Settings > Appearance, scroll to Skin development and choose Load skin folder. Pick
harbor. The window turns blue-gray with a green accent. Everything you didn't set keeps Noodle's value.Change
"accent"to"#e0b252"and save the file. Noodle reloads the skin as soon as the file is written. Add"text","muted"and"accent-text"(the text on accent buttons) to suit. Tokens says what each name colours; most skins need ten or so.Give it a light version. Change
"mode"to"both"and splittokensin two:{ "name": "Harbor", "author": "Your name", "mode": "both", "tokens": { "dark": { "bg": "#12161a", "surface": "#1a2026", "accent": "#5fc490" }, "light": { "bg": "#e9eef0", "surface": "#ffffff", "accent": "#1f8a57" } } }Switch Settings > Appearance > Theme between Dark and Light to check both.
Round the corners a bit more: add
"radius": "12px"and"radius-sm": "8px"to both sets. The page views followradiustoo.Now some CSS. Make
skin.cssnext toskin.json:.tab.active { box-shadow: inset 3px 0 0 var(--accent); }With
var(--accent)the bar follows the dark or light accent by itself.Add a picture. Make a folder
assetsand save this asassets/dots.svg:<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"> <circle cx="6" cy="6" r="0.9" fill="#808080" fill-opacity="0.35" /> </svg>Then use it in
skin.css:.sidebar { background-image: url('assets/dots.svg'); background-size: 12px 12px; }If the path is wrong, dev mode warns that the
url()was removed.A font, if you want one: put a WOFF2 file in
assets/, list it inskin.json, and use it infont-uiwith a system font after it:{ "fonts": [{ "family": "Harbor Sans", "src": "assets/harbor-sans.woff2", "weight": "400" }] }and in each token set,
"font-ui": "'Harbor Sans', system-ui, sans-serif".Suggest a layout or page effects if the skin is meant for them, for example
"layout": { "gap": 10 }. People get a button to accept the suggestion; nothing changes on its own.Fill in
version,descriptionandlicense, then check and pack it. Run these from inside your Noodle checkout and give the skin folder's full path;packwritesharbor.skinin the folder you run it from.npm run skin -- validate C:/skins/harbor npm run skin -- pack C:/skins/harborWithout the Noodle source, use the skin editor's Save as .skin instead, or zip the folder's contents yourself and rename the zip to
harbor.skin.Choose Stop under Skin development, then install
harbor.skinthe way anyone else would (Install skin, or drop it on the window) and press Keep. Now you can send the file to others.
How Classic 98 is made
Classic 98 (resources/skins/classic-98) is the most detailed built-in skin, and almost all of it is plain CSS.
Tokens do most of the work
skin.json sets "mode": "light" and about forty tokens. The gray #c0c0c0 goes in bg, elevated, card, active, sidebar-bg and tab-active-bg; fields are white through surface and input-bg. accent and selection are navy (#000080), as on the real thing. The corners are square, nothing animates, and the shadows are hard offsets with no blur:
{
"tokens": {
"radius": "0px",
"radius-sm": "0px",
"shadow": "2px 2px 0 #000000",
"shadow-lg": "3px 3px 0 #000000",
"duration": "0ms"
}
}
titlebar is #1084d0 and titlebar-symbol is white for the moment before the skin's own buttons take over: Classic 98 asks for window-buttons, so after a restart Noodle draws gray beveled minimize, maximize and close keys on the blue strip, with close set a little apart. They gray out the strip when the window loses focus.
.wc-button {
width: 18px;
height: 16px;
color: #000000;
background: #c0c0c0;
box-shadow:
inset -1px -1px #0a0a0a,
inset 1px 1px #ffffff,
inset -2px -2px #808080,
inset 2px 2px #dfdfdf;
}
A pixel font
The skin ships Pixelify Sans (Open Font License, with assets/OFL.txt next to it) in two weights and points font-ui at it, with Segoe UI as the fallback:
{
"fonts": [
{ "family": "Pixelify Sans", "src": "assets/pixelify-sans-400.woff2", "weight": "400" },
{ "family": "Pixelify Sans", "src": "assets/pixelify-sans-700.woff2", "weight": "700" }
]
}
skin.css turns off font smoothing on .app so the pixels stay sharp.
The bevel
Every raised control uses the same four inset shadows: white and light gray on the top and left edges, dark gray and near black on the bottom and right. A pressed control swaps them. No pictures needed:
.button {
border: 0;
border-radius: 0;
background: #c0c0c0;
box-shadow:
inset -1px -1px #0a0a0a,
inset 1px 1px #ffffff,
inset -2px -2px #808080,
inset 2px 2px #dfdfdf;
}
.button:active:not(:disabled) {
box-shadow:
inset -1px -1px #ffffff,
inset 1px 1px #0a0a0a,
inset -2px -2px #dfdfdf,
inset 2px 2px #808080;
}
Fields (.address-input, .palette-input) use the pressed order, which makes them look sunken. Tabs and bookmarks are raised buttons, and the active tab is pressed in. The page area (.content) gets the sunken bevel and 2 px of padding, so the page sits in a frame.
The title strip and the dither
The top bar, the tab strip, the sidebar header and the settings header get the navy to blue gradient, linear-gradient(90deg, #000080, #1084d0), with white text. The checkered fill of a pressed tab and of scrollbar tracks is a 2 px conic gradient, again with no picture:
.tab.active {
background: #c0c0c0 repeating-conic-gradient(#ffffff 0 25%, #c0c0c0 0 50%) 0 0 / 2px 2px;
}
The start menu palette
The command palette gets a strip down its left side with "Noodle 98" written upwards. It is a ::before element with writing-mode: vertical-rl, turned 180 degrees, over the same gradient running bottom to top. The palette gets 30 px of left padding to make room.
Scrollbars
The scrollbars are 16 px wide with raised thumbs and buttons and dithered tracks, drawn with the ::-webkit-scrollbar pseudo-elements and the same bevel.
Suggestions
The skin suggests "layout": { "radius": 0, "gap": 6, "motion": "none" }, so square corners stick even if the user had set their own radius. It suggests a light CRT for pages (curvature 0.35, scanlines 0.35, glow 0.25, flicker 0.1, mask 0.3) and asks for sounds and window-buttons. The suggestions and sounds wait until the user accepts them; the window buttons follow the skin from the next start.
To take it apart yourself, copy resources/skins/classic-98 somewhere, load the copy in skin dev mode and start changing things.
The skin editor
Settings > Appearance > Skin editor starts from the skin you use, with colour pickers and sliders for every token and a box for extra CSS, all applied live. "Add to my skins" installs it; "Save as .skin" writes the file to share. A skin made in the editor keeps the CSS, fonts and pictures of the skin it started from. For anything beyond colours, skin dev mode with your own editor is quicker.