Appearance
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:
| Layer | File | Purpose |
|---|---|---|
| Engine base | styles/engine.css | Zone layout, components, dark defaults in :root |
| Theme | styles/themes/dark.css etc. | Color palette via html[data-theme="name"] selector |
| Game overrides | styles/game.css | Structural layout tweaks (padding, border-radius, etc.) |
| Inline | (css: "...") in passages | Per-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
- Copy
styles/themes/dark.csstostyles/themes/my-theme.css - Change the selector to
html[data-theme="my-theme"]and edit the color values - Link the file in
index.htmlafter 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"> - 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.cssana
// In any passage:
(layout: Layout_Phone) ← engine loads layout_phone.css on first switchTemplate 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.
Modal Overlays
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.