Theme your app
A scaffolded Fresh app has one theme and one chrome, and every page gets both. New apps start on Studio Utility: ink on neutral gray, sharp corners, hairline rules, and compact density.
Paths are relative to the Fresh app, such as apps/dashboard/. In a workspace with several apps,
add --app <name> to the ui:* and tokens commands. For routes, islands, and registry
components, see Customize Fresh UI.
How the theme reaches every page
Generated files hold the values, and the app shell stamps the selected preset on <html>.
- apps/dashboard/
- client.ts # imports assets/styles.css on every route [owned]
- navigation.ts # top-bar links and chrome opt-outs [owned]
- tokens.config.json # the selected preset [owned]
- tokens/semantic.tokens.json # your alias overrides [owned]
- lib/theme.ts # reads theme-preset.json and fonts.json [owned]
- assets/
- styles.css # Tailwind, tokens, component CSS [owned]
- tokens.css # the --ns-* tokens, light and dark [generated]
- theme-bridge.css # Tailwind utilities such as bg-ns-surface [generated]
- theme-preset.json # the preset's language, density, and flavor [generated]
- fonts.json # font stylesheet URLs [generated]
- tokens.json # the token catalog behind /design/tokens [generated]
- routes/
- _app.tsx # stamps the theme on <html> [owned]
- _layout.tsx # the app chrome [owned]
client.tsimportsassets/styles.css, so every route loads the tokens, the Tailwind utilities, and the component CSS.routes/_app.tsxwraps every page. It readsassets/theme-preset.jsonthroughlib/theme.ts, stamps the attributes below on<html>, and links the font stylesheets fromassets/fonts.json.assets/tokens.cssselects on those attributes, so one stamp restyles the whole app.- A script in
<head>setsdata-schemebefore first paint, fromns-themeinlocalStorageor else the OS preference.ThemeToggleflipsdata-schemeand writesns-theme, keeping the language.
| Attribute | Value |
|---|---|
data-theme |
The design language, such as studio. |
data-flavor |
The Studio flavor, such as utility. Absent for other languages. |
data-density |
compact or comfortable. The preset fixes it. |
data-scheme |
light or dark. |
Pick a preset
A preset sets a design language: type, colors, shape, and density. NetScript ships 24, and the design system shows each one.
| Group | Preset ids |
|---|---|
| Studio flavors | studio-utility (default), studio-cloud, studio-workshop, studio-playful, studio-ledger, studio-atelier, studio-signal |
| Other languages | ledger, signal, glass, tonal, fluent, brutal, filigrane, pixel, tactile, bloom, cupertino, liquid, terminal |
| Platform copies and the earlier default | macos-27, apple-classic, fluent-2-web, ns-one |
Switch the whole app with ui:apply:
netscript ui:apply --preset studio-cloud
The command rebuilds the generated files, records the preset in tokens.config.json, and installs
the theme's DESIGN.md and SKILL.md under .agents/skills/fresh-ui-theme-<preset-id>/. It keeps
your tokens/semantic.tokens.json and leaves your components as they are. An unknown preset fails
before any file changes.
--preset accepts four forms:
| Form | Example |
|---|---|
| A preset id | studio-cloud |
| A preset code that can also carry a style, font, and icon library | ns1_eyJ2ZXJzaW9uIjoxLCJ0aGVtZSI6InN0dWRpby1jbG91ZCJ9 |
A share link: an HTTPS URL with a ?preset= code, decoded locally |
https://example.com/?preset=ns1_... |
An HTTPS URL to preset JSON or to a registry:theme item |
https://example.com/themes/acme.json |
netscript ui:preset url --theme studio-cloud prints a share link, and ui:preset decode <code>
reads a code.
A new app takes the same flag:
netscript init my-app --preset studio-cloud
To compare presets first, run netscript tokens build --previews, start the app in development,
and open /design/tokens?preset=<id>.
Fine-tune a preset
An override reassigns a semantic role to another token of the same type. Create
tokens/semantic.tokens.json with { "semantic": {} }. If the app has a tokens.config.json, which
ui:apply and init --preset write, add "override": "tokens/semantic.tokens.json" to it, because
tokens build ignores an override the config does not name. ui:apply keeps that key when you
switch presets. An app without a tokens.config.json needs nothing more: tokens build finds the
file.
netscript tokens init is not the way in after ui:apply. It refuses to run without --force,
which replaces tokens/source/ and resets the override file.
Edit the override file. This example gives Studio Utility a blue primary, with text that reads at
4.5:1 or better in both schemes, and pill-shaped controls. radius-control shapes buttons, progress
bars, and other controls, and Studio Utility sets it to 0:
{
"semantic": {
"primary": { "$type": "color", "$value": "{semantic.blue-11}" },
"primary-fg": { "$type": "color", "$value": "{semantic.gray-1}" },
"primary-hover": { "$type": "color", "$value": "{semantic.blue-12}" },
"primary-stroke": { "$type": "color", "$value": "{semantic.blue-12}" },
"radius-control": { "$type": "dimension", "$value": "{semantic.radius-full}" }
}
}
Then run netscript tokens build. An override can only alias an existing semantic role. A raw value
such as #0057ff, an unknown role, or a changed type fails before any file is replaced. One
override covers light and dark, because ramp steps resolve per scheme. The build does not check
contrast, so check text on primary in both schemes yourself. Each preset shapes through its own
roles: in Studio Utility, radius-md and radius-lg follow roles that are already 0. Every build
replaces the generated files, so keep edits in the override file. The
tokens reference has the
details.
Registry theme items
The Fresh UI registry publishes each preset as a registry:theme item named theme-<preset-id>,
such as theme-studio-utility. An item carries the six theme files in assets/ and that theme's
DESIGN.md and SKILL.md, which ui:apply and ui:add place under
.agents/skills/fresh-ui-theme-<preset-id>/. netscript ui:list shows the items, and
netscript init copies the theme-studio-utility files into a new app.
Your own registry can publish registry:theme and registry:base items, and
ui:apply --preset <url> applies one. The ui:* reference
lists the registry commands.
Add a page that inherits the chrome and theme
netscript ui:add page status
The command writes routes/status/index.tsx, registers a typed route in router.ts, and links the
page in navigation.ts. The generated layout holds the page's own content and no theme or frame
code:
// routes/status/index.tsx
export const statusPage = definePage()
.withRoute(appRoutes['status'])
.withMeta(() => ({ title: 'Status' }))
.withLayer('status', StatusContent, () => ({}))
.withLayout((slots) => (
<main class='ns-shell ns-section ns-stack ns-stack--lg'>
<PageHeader>
<PageHeader.Main>
<PageHeader.Intro>
<h1>Status</h1>
</PageHeader.Intro>
</PageHeader.Main>
</PageHeader>
{slots.status()}
</main>
))
.build();
Fresh applies the shell and the layout to every page under routes/, so a hand-written page gets
the same result:
| Source | What the page inherits |
|---|---|
routes/_app.tsx |
The <html> shell: theme attributes, fonts, the scheme script, and the skip link. |
assets/styles.css through client.ts |
The tokens, Tailwind utilities, and component CSS. |
routes/_layout.tsx |
The top bar: brand, the appNavigation links, and ThemeToggle. |
The links in the top bar are the appNavigation list in navigation.ts. ui:add page links
one-segment pages for you. Pass --nav (or --nav "Daily reports" to set the label) to link a nested
page such as reports/daily, or --no-nav to skip the link. netscript ui:remove page <path>
removes the page, its route registration, and its link; add --dry-run to preview.
A page that needs its own frame, such as a sign-in screen, adds its route prefix to
chromelessRoutePrefixes in navigation.ts. The /design showcase does this: it renders its own
sidebar layout.
Keep the page to its own content:
- Give it one
<main>, then aPageHeaderwith one<h1>, then sections built from@app/components/ui/mod.ts. - Lay out with
ns-shell,ns-section,ns-stack, andns-cluster, or with Tailwind utilities such asbg-ns-surface,text-ns-muted-fg, andp-ns-4. - Use no hex colors or pixel values. A literal does not follow the preset or the dark scheme.
- Never copy a header, navigation, or
ThemeToggleinto a page. Duplicated chrome drifts and breaks client navigation. Editnavigation.tsor_layout.tsxinstead.
Upgrade an older app
See Upgrade an app to tokens v2 to adopt the theme attributes and migrate earlier component copies.
Check your work
In development, open /design/components and /design/tokens to see the components and tokens
under the active preset. Then open the pages you changed, flip ThemeToggle, and check light and
dark, including the empty, loading, and error states.
Next steps
Edit routes, add islands, and install more components in
Customize Fresh UI. For how the pieces fit, see
Fresh UI & design and the @netscript/fresh-ui reference.