# GigglesProvider import { TypeTable } from 'fumadocs-ui/components/type-table'; Root provider for giggles applications. Wrap your app in this to enable focus, input, and theming. By default it sizes the root container to fill the full terminal dimensions and updates automatically on resize. Set `fullScreen={false}` to opt out and let Ink determine the container size. ```tsx title="Basic Setup" import { render } from 'ink'; import { GigglesProvider } from 'giggles'; render( ); ``` ', }, fullScreen: { description: 'Sizes the root container to fill the terminal dimensions, updating on resize.', type: 'boolean', default: 'true', }, }} /> # Getting Started The core systems power giggles' declarative approach to TUI development. # Theme import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Swatch } from '@/components/swatch'; ThemeProvider [#themeprovider] Override theme values for a subtree. Nest multiple providers to apply different themes to different parts of the app. Merges with the parent theme, so you only need to specify the values you want to change. ```tsx title="Nested Themes" import { GigglesProvider, ThemeProvider } from 'giggles'; render( ); ``` ', }, children: { description: 'Components that inherit this theme', type: 'React.ReactNode', required: true, }, }} /> GigglesTheme [#gigglestheme] , }, borderColor: { description: 'Color for component borders (Modal, etc.)', type: 'string', default: , }, selectedColor: { description: 'Color for checked/selected state indicators', type: 'string', default: , }, hintColor: { description: 'Color for key labels in CommandPalette hints', type: 'string', default: , }, hintDimColor: { description: 'Color for command names and separators in CommandPalette hints', type: 'string', default: , }, hintHighlightColor: { description: 'Color for the highlighted key label in interactive CommandPalette', type: 'string', default: , }, hintHighlightDimColor: { description: 'Color for the highlighted command name in interactive CommandPalette', type: 'string', default: , }, indicator: { description: 'Character used for collapsed or inactive state (e.g. closed directory, unselected item)', type: 'string', default: "'▶'", }, indicatorOpen: { description: 'Character used for expanded or active state (e.g. open directory)', type: 'string', default: "'▼'", }, checkedIndicator: { description: 'Character shown for selected items in MultiSelect', type: 'string', default: "'▣'", }, uncheckedIndicator: { description: 'Character shown for unselected items in MultiSelect', type: 'string', default: "'◻'", }, }} /> useTheme [#usetheme] Returns the current `GigglesTheme` from the nearest `ThemeProvider`. Use this in custom `render` functions to stay consistent with the app theme. ```tsx title="Usage" import { useTheme } from 'giggles'; function CustomOption({ highlighted }: { highlighted: boolean }) { const theme = useTheme(); return ...; } ``` # Agentic Coding This page is a reference for AI coding agents (Claude, Cursor, Copilot, etc.) working in a project built with giggles. Run `npx skills add https://github.com/zion-off/giggles --skill giggles` to install the skill, or copy the block below into your `CLAUDE.md`, `.cursorrules`, or equivalent context file. ````md ## giggles This project uses giggles — a batteries-included React/Ink framework for terminal UIs. It provides focus management, keyboard input routing, screen navigation, and theming on top of Ink (React for CLIs). Full documentation is available at https://giggles.zzzzion.com. An index of all pages is available at https://giggles.zzzzion.com/llms.txt — fetch this to discover specific pages before searching for information. ### Setup Every app wraps with `GigglesProvider`. Never call focus hooks in the same component as the provider — put them in a child component. ```tsx import { render } from 'ink'; import { GigglesProvider } from 'giggles'; function Root() { return ( ); } render(); ``` ### Focus primitives **`useFocusNode(options?)`** — registers a leaf node in the focus tree. Returns `{ id, hasFocus }`. Used directly in custom interactive components. Accepts `focusKey` so the parent scope can address this node via `focusChild`/`focusChildShallow`. **`useFocusScope(options?)`** — registers a scope node. Returns `{ id, hasFocus, isPassive, next, prev, nextShallow, prevShallow, escape, drillIn, focusChild, focusChildShallow }`. - `hasFocus` is true when the scope node **or any descendant** has focus (ancestor walk, not strict equality). - Wrap children in `` to set the implicit parent for nested hooks. Omitting `` throws a `GigglesError`. - `keybindings` accepts a plain object or a factory `(helpers) => object`. Handlers are re-registered every render so closures are never stale. - `focusKey` — optional string that lets the parent scope address this scope by name via `focusChild`/`focusChildShallow`. **Navigation helpers** — available directly on the handle and as the `keybindings` factory argument. Stable references; safe to call from effects and event handlers. ```ts next() // move to next child, drill into its first leaf prev() // move to prev child, drill into its first leaf nextShallow() // move to next child, land on scope node (don't drill) prevShallow() // move to prev child, land on scope node escape() // make this scope passive — yields input to parent drillIn() // focusFirstChild — queues focus if no children yet; first child to register claims it focusChild(key) // focus the direct child registered with focusKey=key, drilling into its first leaf focusChildShallow(key) // same, but land on the scope node without drilling ``` ### Controlled focus Use `focusKey` to give a child a stable name, then call `focusChild` from the parent to jump directly to it — bypassing `next`/`prev` iteration. Keys are scoped to the immediate parent scope. ```tsx // Child declares its key const scope = useFocusScope({ focusKey: 'editor' }); // or on a UI component: // Parent jumps to it directly const root = useFocusScope({ keybindings: ({ focusChild }) => ({ '1': () => focusChild('files'), '2': () => focusChild('editor'), }) }); // Also available on the handle for effects useEffect(() => { if (error) root.focusChild('errors'); }, [error]); ``` `focusChild` drills into the first leaf of the target (same as `next`). Use `focusChildShallow` to land on the scope node itself. If the key is not found, both are no-ops. If the target scope has no children yet, `focusChild` queues focus — the first child to register claims it automatically. ### Key bubbling Keys walk from the focused node up to the root. At each node: 1. Skip if passive. 2. If a binding matches, call it and **stop** — no further bubbling. 3. If this is the trap node, stop. **Implication for composition:** keys not handled by a UI component bubble to the parent scope. A vertical `Select` consumes `j`/`k`/`↑`/`↓`/`enter` — everything else (e.g. `h`, `l`, `e`) bubbles freely. You only need `escape()` when the same key must serve both an inner and outer scope simultaneously. ### `next` vs `nextShallow` Use `nextShallow`/`prevShallow` at a scope that contains children which may have no registered descendants (e.g. collapsed tree nodes). `next` drills into children — if a child scope has no registered descendants, it will queue indefinitely. `nextShallow` lands on the scope node itself, where the user can press a key to open it. ### Passive mode Call `escape()` to make a scope passive. The scope node receives focus and is skipped during dispatch, so parent bindings fire instead. Passive clears automatically when focus leaves the scope's subtree. Only use passive mode when parent and child scopes compete for the **same keys**. If different keys are used at each level, normal bubbling separates them — no `escape()` needed. ### Border color convention ```tsx ``` ### UI components (`giggles/ui`) All interactive components use `useFocusNode()` internally — they are leaf nodes, not scopes. Their keybindings fire before bubbling to any parent scope. All interactive components accept a `focusKey` prop so their parent scope can address them via `focusChild`/`focusChildShallow`. Available components: `Select`, `MultiSelect`, `TextInput`, `Autocomplete`, `Confirm`, `Viewport`, `Modal`, `Badge`, `Spinner`, `Paginator`, `Panel`, `Markdown`, `VirtualList`, `CommandPalette`, `CodeBlock`. Key behaviour for interactive components: | Component | Keys consumed | Notes | |---|---|---| | `Select` (vertical) | `j` `k` `↑` `↓` `enter` | All other keys bubble | | `Select` (horizontal) | `h` `l` `←` `→` `enter` | All other keys bubble | | `MultiSelect` (vertical) | `j` `k` `↑` `↓` `space`; `enter` if `onSubmit` set | | | `TextInput` | All printable, `←` `→` `home` `end` `backspace` `delete` `enter` | Passthroughs: `tab` `shift+tab` `escape` | | `Autocomplete` | All printable, `←` `→` `home` `end` `backspace` `delete` `enter` | Passthroughs: `tab` `shift+tab` `escape` | | `Confirm` | `y` `n` `enter` | | | `Viewport` | `j` `k` `↑` `↓` `pageup` `pagedown` `g` `G` | | | `Modal` | — | Wraps `FocusTrap`; `escape` closes | `Select` and `MultiSelect` work in controlled or uncontrolled mode. Pass `value`/`onChange` to own state yourself; omit them to let the component manage it internally. Use `onHighlight` to observe cursor movement without owning state. ### `useKeybindings` Register keybindings independently from focus scope navigation. You can call it multiple times in the same component — all bindings are active simultaneously, later calls override earlier ones for duplicate keys. ```tsx // Base navigation useKeybindings(focus, { j: moveDown, k: moveUp }); // Search mode — fallback intercepts unbound keys; escape bubbles to named bindings above useKeybindings(focus, searchMode ? { escape: exitSearch } : {}, searchMode ? { fallback: handleInput, bubble: ['escape'] } : undefined); ``` **Fallback handler:** pass `fallback` to catch keys that don't match any named binding — useful for text input. Keys listed in `bubble` skip the fallback and propagate to parent scopes. `TextInput` and `Autocomplete` use this internally. **App-wide shortcuts:** register them on the root scope — unhandled keys bubble up naturally, so a binding at the root fires whenever no child consumes the key first. ### `FocusTrap` Locks input to a subtree — nothing outside it receives keys until the trap unmounts. Used internally by `Modal`. Use it directly for custom modal-like components. ### Screen router ```ts const { push, pop, replace, reset, currentRoute } = useNavigation(); push('screenName', params) // open on top of stack pop() // return to previous screen replace('screenName', params) // swap current screen reset('screenName', params) // clear stack, show this screen ``` All screens stay mounted but hidden — state is preserved across navigation. Focus position is saved on push and restored on pop. Use `restoreFocus={false}` on `` to always focus the first child instead. ### Theme Access the active theme with `useTheme()`. Override values by passing a `theme` prop to `GigglesProvider`: ```tsx ``` ### Common pitfalls | Pitfall | Fix | |---|---| | Focus hooks in the same component as `GigglesProvider` | Move them into a child component | | `useFocusScope()` without a corresponding `` | Throws `GigglesError` — always render `` wrapping the children | | `next()` hangs on a scope with no registered children | Use `nextShallow()` at the parent level | | `h`/`l` bubbling unexpectedly to parent scope | Vertical `Select` does not consume `h`/`l` — use different keys at each level, or use `escape()` if the same keys must serve both | | Parent bindings not firing while child is focused | Expected — add `escape()` to the child scope to yield control | ```` # Core Concepts Building a terminal UI means answering one question over and over: when the user presses a key, what should happen? The answer depends on what's active, where it sits in the component tree, and what screens are on the stack. giggles handles all of this through three connected systems — focus, input routing, and screen navigation. A component that knows when it's active [#a-component-that-knows-when-its-active] Start with a single panel. You want it to show a green border when it has focus and a grey one when it doesn't. Call `useFocusNode()` to register the component as a focusable target — it returns a handle with a `hasFocus` boolean you can use directly in your render: ```tsx title="A focusable panel" function Panel() { const focus = useFocusNode(); return ( Panel content ); } ``` Wrap your application in `GigglesProvider` to activate the focus, input, and theme systems: ```tsx title="Setup" import { GigglesProvider } from 'giggles'; function App() { return ( {/* your app */} ); } ``` The first component to call `useFocusNode` receives focus automatically. When it unmounts, focus moves to its nearest ancestor — so your app always has an active component. Navigating between components [#navigating-between-components] One panel isn't interesting. Add a second one and you immediately have a new problem: how does focus move between them? This is where `useFocusScope` comes in. A scope groups focusable children and defines the keybindings that move between them: ```tsx title="useFocusScope" function Layout() { const scope = useFocusScope({ keybindings: ({ next, prev }) => ({ tab: next, 'shift+tab': prev, }) }); return ( ); } ``` `next` and `prev` navigate between children, automatically descending to the first leaf so navigation feels natural without manual coordination. `` sets the parent context — any `useFocusNode` or `useFocusScope` call inside it registers as a child of this scope. The scope exposes its own `hasFocus` — true whenever any descendant has focus. Use it for container-level visual indicators: ```tsx title="Scope-level focus indicator" {children} ``` How a keypress reaches its handler [#how-a-keypress-reaches-its-handler] With two panels, pressing `tab` moves focus. But what actually happens between the keypress and the handler firing? When a key is pressed, giggles walks up the focus tree from the currently focused component toward the root, checking each node for a matching handler. If the focused component has a binding for that key, it fires and the walk stops. If not, the key bubbles to the parent scope, then the grandparent, and so on — the same pattern as DOM event bubbling, just over the focus tree instead of the element tree ([with one exception](/core/input#keybindingoptions)). This means deeply nested components naturally take precedence over their parents, and unhandled keys always find their way up. You don't need a special global shortcut API — register app-wide shortcuts on the outermost scope and they'll fire whenever nothing deeper claims the key first: ```tsx title="App-level shortcuts" const root = useFocusScope({ keybindings: () => ({ 'ctrl+p': openCommandPalette, 'ctrl+q': quit, }) }); ``` Navigating between screens [#navigating-between-screens] Sometimes you want to show an entirely different view — a detail page, a settings panel, a confirmation dialog — without losing the state of what's behind it. giggles handles this with a screen router: a stack of full-screen views where only the top one is visible, but all of them stay mounted. Stack operations map directly to what you'd expect: * `push(name, params)` — open a new screen on top * `pop()` — close the current screen, return to previous * `replace(name, params)` — swap the current screen * `reset(name, params)` — clear the stack and show this screen All screens in the stack stay mounted but hidden. When you navigate back, the previous screen's state is exactly as you left it — no re-initialization or state loss. This makes back navigation instant and predictable. Each screen also maintains its own focus state. Pushing a new screen saves where focus was; popping it restores focus to exactly that position. Routers can be nested — a screen can contain its own `Router` for tabbed layouts or sub-navigation, each managing its own independent stack. Use `useNavigation()` inside screen components to access these functions: ```tsx title="useNavigation" function DetailScreen() { const { pop, push, currentRoute } = useNavigation(); return ( Viewing {currentRoute.params.id} ); } ``` # Getting Started import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Terminal } from '@/components/terminal'; import GettingStartedExample from '@/components/examples/framework/getting-started'; giggles is a batteries-included framework for building terminal user interfaces (TUIs) with React and Ink. It handles focus management, input routing, screen navigation, and theming out of the box — along with a component library covering most TUI use cases and terminal utilities for things like handing off control to external programs. Everything is declarative and composable, so you can build polished CLI apps without wiring up the low-level details yourself. Installation [#installation] Scaffold a new project: ```bash npx create-giggles-app ``` ```bash pnpm create giggles-app ``` ```bash yarn create giggles-app ``` Your First TUI [#your-first-tui] Here's a simple menu with keyboard navigation: ```tsx title="app.tsx" import { useState } from 'react'; import { Box, Text, render } from 'ink'; import { GigglesProvider } from 'giggles'; import { Select } from 'giggles/ui'; const menuItems = [ { label: 'New Game', value: 'new' }, { label: 'Continue', value: 'continue' }, { label: 'Settings', value: 'settings' }, { label: 'Quit', value: 'quit' }, ]; function App() { const [choice, setChoice] = useState('new'); return ( My App console.log(lang)} />; } // controlled — caller owns selection state function ControlledLanguagePicker() { const [lang, setLang] = useState('ts'); return setPreview(loadPreview(v))} immediate /> {preview} ); } ``` Custom render [#custom-render] ```tsx title="Custom Render" ({ label: i, value: i }))} /> ); } function App() { const root = useFocusScope({ keybindings: ({ next, prev }) => ({ j: next, k: prev }) }); return ( ); } ``` Collapsible file tree [#collapsible-file-tree] A two-level tree where `j`/`k` move between top-level directories without entering them, and `l`/`enter` open the focused directory and move focus inside. The root uses [`nextShallow`/`prevShallow`](/core/focus#navigation-variants) to land on each directory node without drilling into it. The directory scope registers three bindings with a stable shape: `l` guards against re-entry when already open, `enter` is safe without a guard because `Select` always consumes it, and `h` is a harmless no-op when already closed. [`drillIn`](/core/focus#navigation-variants) is deferred — if the directory's children haven't rendered yet when it fires, the store queues the focus and delivers it as soon as the first child registers: ```tsx title="File tree" import { FocusScope, useFocusScope, useTheme } from 'giggles'; import { Select } from 'giggles/ui'; function DirItem({ name, files }: { name: string; files: string[] }) { const [open, setOpen] = useState(false); const { indicator, indicatorOpen } = useTheme(); const scope = useFocusScope({ keybindings: ({ drillIn }) => ({ l: () => { if (!open) { setOpen(true); drillIn(); } }, enter: () => { setOpen(true); drillIn(); }, h: () => setOpen(false), }) }); return ( {open ? indicatorOpen : indicator} {name}/ {open && ( ); } ``` Modal with focus trap [#modal-with-focus-trap] A confirmation dialog that completely blocks input to the menu behind it. Without `FocusTrap`, `j`/`k` on the menu scope would still fire while the dialog is open — the trap prevents that by stopping all key bubbling at the modal boundary: ```tsx title="Modal with FocusTrap" function ConfirmDialog({ message, onConfirm, onCancel }) { const scope = useFocusScope({ keybindings: ({ next, prev }) => ({ left: prev, right: next, escape: onCancel, }) }); return ( Confirm Action {message} ); } function App() { const [showModal, setShowModal] = useState(false); const menuScope = useFocusScope({ keybindings: ({ next, prev }) => ({ j: next, k: prev }) }); return ( setShowModal(true)} /> setShowModal(true)} /> {showModal && ( setShowModal(false)} onCancel={() => setShowModal(false)} /> )} ); } ``` # Overview Quick start [#quick-start] ```tsx title="File list navigation" import { useFocusNode, useKeybindings } from 'giggles'; function FileList() { const focus = useFocusNode(); const [selected, setSelected] = useState(0); const files = ['index.ts', 'utils.ts', 'types.ts']; useKeybindings(focus, { j: () => setSelected(i => Math.min(files.length - 1, i + 1)), k: () => setSelected(i => Math.max(0, i - 1)), }); return ( {files.map((file, i) => ( {file} ))} ); } ``` How a keypress reaches its handler [#how-a-keypress-reaches-its-handler] When a key is pressed, giggles walks up the [focus tree](/core/focus) from the currently focused component toward the root, checking each node for a matching handler. The first match wins; unmatched keys keep moving up. Components outside the focused path never receive input. This is what makes bindings local: the same key can be bound differently at every nesting level, and only the focused path's bindings are consulted. Register app-wide shortcuts on the outermost scope — they fire whenever nothing deeper claims the key first. Registering keybindings [#registering-keybindings] `useKeybindings` takes a focus handle as its first argument — bindings only fire when that component is in the active focus path. It works with any focus handle: nodes from `useFocusNode`, or scopes from `useFocusScope`. For **scopes**, there's an alternative: the `keybindings` option on `useFocusScope` itself. The difference is that the option gives you the [navigation helpers](/core/focus#navigation-variants) — `next`, `prev`, `drillIn`, and friends — which aren't available through `useKeybindings`. Use the option when you need to navigate between children; use `useKeybindings` for everything else: action bindings, `fallback`, `bubble`, or adding bindings to a scope that already has navigation set up. ```tsx useKeybindings(focus, { j: moveDown, k: moveUp, enter: select, }); ``` You can call `useKeybindings` multiple times in the same component. Bindings from all calls are merged; later calls override earlier ones for duplicate keys. This lets you split navigation and mode-specific bindings into separate calls, which makes conditional spreading cleaner: ```tsx useKeybindings(focus, { ...(!editing && { j: moveDown, k: moveUp }), escape: exitMode, }); useKeybindings(focus, editing ? { enter: confirmEdit } : {}); ``` Text input and the fallback [#text-input-and-the-fallback] Named bindings work well for discrete actions, but they can't capture arbitrary typed characters. For that, use the `fallback` option — a handler that receives any keystroke that doesn't match a named binding: ```tsx useKeybindings(focus, { escape: exitSearch }, { fallback: (input, key) => { if (key.backspace) setQuery(q => q.slice(0, -1)); else if (input) setQuery(q => q + input); } }); ``` Some keys should always reach their named binding even when a fallback is active. Use `bubble` to let specific keys skip the fallback and propagate normally: ```tsx useKeybindings(focus, { escape: exitSearch }, { fallback: handleInput, bubble: ['escape', 'enter'], }); ``` Named bindings always take priority over the fallback across all `useKeybindings` calls on the same component. If a named binding and the fallback need to compete for the same key at different times, disable the binding conditionally: `...(!searchMode && { j: moveDown })`. Named bindings and the registry [#named-bindings-and-the-registry] Bindings can carry a human-readable name, making them discoverable for help screens, command palettes, and key hint bars: ```tsx useKeybindings(focus, { j: { action: moveDown, name: 'Move down' }, k: { action: moveUp, name: 'Move up' }, '/': { action: openSearch, name: 'Search' }, }); ``` `useKeybindingRegistry` reads all named bindings filtered by the current focus path: ```tsx function HelpScreen() { const registry = useKeybindingRegistry(); return ( {registry.available.map((cmd) => ( {cmd.key} {cmd.name} ))} ); } ``` `available` returns only the bindings reachable from the current focus path — so the help screen always reflects what's actually active, not everything registered across the entire app. Trapping focus [#trapping-focus] Key bubbling normally walks all the way to the root. For modal dialogs and confirmation prompts, you may want to cut that path entirely — nothing outside the modal should receive input until it's dismissed. `FocusTrap` does this: mount it and all input is confined to its subtree. Unmount it and focus returns to where it was before. ```tsx function App() { const [showModal, setShowModal] = useState(false); return ( <> {showModal && ( setShowModal(false)} /> )} ); } ``` * [API Reference](/core/input/api-reference) — full type signatures for `useKeybindings`, `useKeybindingRegistry`, and `FocusTrap` * [Examples](/core/input/examples) — complete worked examples for common input patterns # API Reference import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; Router [#router] Manages a stack of screens. Only the top screen is visible; others remain mounted but hidden. ', }, restoreFocus: { description: 'Restore the previously focused element when returning to a screen. When false, always focuses the first child.', type: 'boolean', default: 'true', }, children: { description: 'Screen components defining available routes', type: 'React.ReactNode', required: true, }, }} /> Throws a `GigglesError` if `initialScreen` does not match any registered Screen name. Screen [#screen] Declarative route definition. Renders nothing — used by Router to register screens. useNavigation() [#usenavigation] Returns navigation methods and current route state. Must be used within a Router. ```ts title="Type Signature" function useNavigation(): NavigationContextValue ``` ) => void', required: true, }, pop: { description: 'Remove the top screen from the stack. No-op if only one screen remains.', type: '() => void', required: true, }, replace: { description: 'Replace the top screen with a new one', type: '(name: string, params?: Record) => void', required: true, }, reset: { description: 'Clear the stack and set a single screen', type: '(name: string, params?: Record) => void', required: true, }, }} /> `push`, `replace`, and `reset` throw a `GigglesError` if the screen name is not registered. ScreenRoute [#screenroute] ', }, }} /> # Examples import { Terminal } from '@/components/terminal'; import NestedRouterExample from '@/components/examples/router/nested-router'; Nested routers [#nested-routers] Each `Router` manages its own independent stack, so you can nest them for tabbed layouts or sub-navigation. The outer router handles top-level navigation; inner routers handle navigation within a screen: ```tsx title="Nested routers" function App() { return ( ); } function Tabs() { return ( ); } ``` # Overview import { Terminal } from '@/components/terminal'; import BasicRouterExample from '@/components/examples/router/basic-router'; Quick start [#quick-start] ```tsx title="Basic router" import { GigglesProvider, Router, Screen, useNavigation, useFocusNode, useKeybindings } from 'giggles'; function App() { return ( ); } function Home() { const { push } = useNavigation(); const focus = useFocusNode(); useKeybindings(focus, { s: () => push('settings'), }); return Home Press s for settings; } function Settings() { const { pop, canGoBack } = useNavigation(); const focus = useFocusNode(); useKeybindings(focus, { q: () => pop(), }); return Settings {canGoBack && Press q to go back}; } ``` How the stack works [#how-the-stack-works] `Router` maintains a stack of screens. Only the top screen is visible — but every screen below it stays mounted, preserving its state. Navigating back is instant: there's no re-initialization, no lost scroll position, no refetched data. The four stack operations map directly to what you'd expect: * `push(name, params)` — add a screen on top * `pop()` — remove the top screen, return to the previous one * `replace(name, params)` — swap the current screen without adding to the stack * `reset(name, params)` — clear the stack and start fresh from a single screen Call these from `useNavigation()` inside any screen component. Focus integration [#focus-integration] When a screen becomes the top of the stack, its first focusable child receives focus automatically. When navigating back, focus is restored to whichever element was focused before the screen was covered — so the user's position is preserved across navigation. To always focus the first child on return instead, set `restoreFocus={false}` on the `Router`. *** * [API Reference](/core/router/api-reference) — full type signatures for `Router`, `Screen`, and `useNavigation` * [Examples](/core/router/examples) — nested routers and tabbed layouts