Skip to main content
NetScriptdocs Search docs ⌘K v0.0.9
Browse documentation

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]
  1. client.ts imports assets/styles.css, so every route loads the tokens, the Tailwind utilities, and the component CSS.
  2. routes/_app.tsx wraps every page. It reads assets/theme-preset.json through lib/theme.ts, stamps the attributes below on <html>, and links the font stylesheets from assets/fonts.json.
  3. assets/tokens.css selects on those attributes, so one stamp restyles the whole app.
  4. A script in <head> sets data-scheme before first paint, from ns-theme in localStorage or else the OS preference. ThemeToggle flips data-scheme and writes ns-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:

Shell
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:

Shell
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:

JSON
{
  "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

Shell
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.tsxTypeScript
// 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 a PageHeader with one <h1>, then sections built from @app/components/ui/mod.ts.
  • Lay out with ns-shell, ns-section, ns-stack, and ns-cluster, or with Tailwind utilities such as bg-ns-surface, text-ns-muted-fg, and p-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 ThemeToggle into a page. Duplicated chrome drifts and breaks client navigation. Edit navigation.ts or _layout.tsx instead.

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.

Search all documentation

Type a term to search the documentation.