Skip to content

Theming & Modals

Visual customization with themes and CSS variables, and modal overlay screens.

Reference: Modals, Themes & Audio.

Themes and Visual Customization

Ana's CSS is organized in four layers, applied in this order:

LayerFilePurpose
Engine basestyles/engine.cssZone layout, components, dark defaults in :root
Themestyles/themes/dark.css etc.Color palette via html[data-theme="name"] selector
Game overridesstyles/game.cssStructural layout tweaks (padding, border-radius, etc.)
Inline(css: "...") in passagesPer-screen overrides

Colors belong in a theme file. styles/game.css is for structural/layout adjustments that don't change between themes.

The theme system

The engine ships three themes: dark (default), light, and default (warm sepia).

In GameInit, register the themes you want available to players:

ana
:: GameInit @system(init)

(theme-define: "dark",    "Dark")
(theme-define: "light",   "Light")
(theme-define: "default", "Default")

Switch theme at runtime from a settings screen, or for narrative effects:

ana
// Settings screen — player picks
(dropdown: $world.theme, "Theme", (theme-list:))

:: _Settings_ApplyTheme
(theme: $world.theme)

// Narrative effect — temporary switch, no localStorage save
(theme: "dracula", persist, false)

(theme-list:) returns an array of registered theme IDs for use in dropdowns. (theme-current:) returns the active theme ID string.

The active theme persists to localStorage across sessions automatically.

Creating a custom theme

  1. Copy styles/themes/dark.css to styles/themes/my-theme.css
  2. Change the selector to html[data-theme="my-theme"] and edit the color values
  3. Link the file in index.html after the other theme files:
    html
    <link rel="stylesheet" href="/styles/themes/dark.css">
    <link rel="stylesheet" href="/styles/themes/light.css">
    <link rel="stylesheet" href="/styles/themes/my-theme.css">  <!-- add this -->
    <link rel="stylesheet" href="/styles/game.css">
  4. Register it: (theme-define: "my-theme", "My Theme") in GameInit

All --ana-* CSS custom properties are available to override. See styles/engine.css :root for the full list.

game.css: structural overrides only

styles/game.css loads after theme CSS. Use it for:

  • Sidebar padding, image border-radii
  • Zone spacing that doesn't vary by theme

Don't put color overrides in game.css; those belong in a theme file. Any --ana-* var override in game.css will win over the active theme, which breaks theme switching.

Custom layout CSS

Custom layouts define their CSS file in the .layout file's css: field. The engine injects the layout's <link> tag automatically when that layout is first activated, with no manual index.html edits required:

// layouts/Layout_Phone.layout
name: Layout_Phone
zones: text, options, notify
css: layouts/layout_phone.css
ana
// In any passage:
(layout: Layout_Phone)    ← engine loads layout_phone.css on first switch

Template CSS: built in

The CSS for the engine-provided modal templates (achievements, char panel, inventory, quest log, char creator) is included in engine.css under labeled sections. Override any .ach-*, .char-panel-*, .inv-*, .ql-*, .cc-* class in game.css.

Modals use the modal zone, a full-viewport overlay that blocks game input while open.

Opening a modal

ana
(modal-open: "InventoryScreen")

The named passage executes as an (action:) into the modal zone. The overlay captures pointer/keyboard events, so the game scene is functionally paused.

Closing a modal

ana
(modal-close:)

Ana uses a stack, so you can open a modal from within a modal. Each (modal-close:) pops one level; the overlay hides when the stack is empty.

Designing modal passages

Modal passages write to the modal zone using standard zone syntax. The engine provides the (html:), (css:), and (script:) escape hatches for richer layouts (the settings screen template is the best example).

ana
:: InventoryScreen

@zone(modal)
Inventory

(css: "
  .inv-row { display: grid; grid-template-columns: 1fr auto auto; gap: .6rem; padding: .4rem; }
")

(each: _id in (ids: $inv))[
    (set: _name  to (get: $item, _id, "name"))
    (set: _type  to (get: $item, _id, "type"))
    (set: _count to (count: $inv, _id))
    (html: "<div class='inv-row'>")
    (html: _name)
    (if: _count > 1)[(html: "×" + _count)]
    // action buttons keyed on item type go here
    (html: "</div>")
]

(link: "Close")[(modal-close:)]

Equipment display

There's no built-in equipment widget; the (equip-display:) macro was removed in favor of a template you can read and restyle. (equip-slots:) returns your registered slot names, so a few lines of Ana build the grid:

ana
(each: _slot in (equip-slots:))[
    (set: _uid to (arr-first: (get: equip, _slot)))
    (proper: _slot): (if: _uid)[(get: $item, _uid, "name")](else:)[—]
]

A complete, copy-pasteable equipment screen (with Unequip links that return items to inventory) lives in templates/equipment.ana. Unequipping with (remove: equip, _slot) returns the item to carry inventory; use (destroy: equip, _slot) to delete it instead. Equipment macros are documented in the Macro Reference's Equipment section.

Tables inside modals

Use (table:) / (tr:) / (td:) / (th:) for any multi-column data: achievements, leaderboards, item comparisons, NPC rosters. These are full Ana macros (not raw HTML), so they compose naturally with (if:), (each:), (set:), (html:), and any other macro.

ana
(table:)[
    (tr:)[(th: "Name")(th: "Tier")(th: "Status")]
    (each: _id in (ids: $npc))[
        (tr:)[
            (td: (get: $npc, _id, "name"))
            (td: (tier: _id))
            (td: (if: $rel._id > 50)["Friendly"](else:)["Neutral"])
        ]
    ]
]

Style per-table with a second class and a (css:) block. See Table Macros for the full API and CSS override documentation.

Two-panel master/detail: (panel:)

(panel:) builds a left-list / right-detail layout in one macro call. Authors declare what goes in each column; the macro handles click wiring, tab switching, and lazy detail rendering automatically. No JS required.

ana
(panel: $world.selectedQuest)[
    (panel-filter: "Active",    (quest-list: active))
    (panel-filter: "Available", (quest-list: available))
    (panel-filter: "Completed", (quest-list: complete))
    (panel-item: _id)[(quest-title: _id)]
    (panel-detail: _id)[
        (if: _id)[(quest-title: _id)(quest-desc: _id)]
        (else:)[Select a quest to view details.]
    ]
]

When an item is clicked, $world.selectedQuest is updated and the right panel re-renders immediately with the detail template. The detail is lazy: only the current item's content is in the DOM.

Filters ((panel-filter:)) create tab buttons at the top that show/hide item groups. Without filters, use (panel-item: _id, collection) for a flat list.

The same pattern works for inventory, NPC rosters, shop categories, or any data that benefits from a list-select-show-detail flow. See the Panel Macro for the full API and CSS override documentation.

Using templates

templates/ contains ready-to-use modal passages for the most common screens. Copy the file you need into passages/ui/ and it works out of the box; the templates loop through your actual game data (inventory, achievements, quests, statuses) rather than hardcoding specific item names. See templates/inventory_screen.ana, achievements.ana, quest_log.ana, and char_panel.ana.