Examples
Worked examples for common focus patterns
Jumping to a named child
Iterating with next and prev works well when children are ordered, but sometimes you want to jump directly to a specific pane — press 1 for the editor, 2 for the terminal. Give each child a focusKey and call focusChild from the parent keybindings:
function Dashboard() {
const root = useFocusScope({
keybindings: ({ focusChild }) => ({
'1': () => focusChild('editor'),
'2': () => focusChild('terminal'),
'3': () => focusChild('sidebar'),
})
});
return (
<FocusScope handle={root}>
<Editor focusKey="editor" />
<Terminal focusKey="terminal" />
<Sidebar focusKey="sidebar" />
</FocusScope>
);
}focusChild drills into the first leaf of the target scope — the same behaviour as next. Use focusChildShallow to land on the scope node without entering it. focusChild is also available directly on the handle for imperative use outside of keybindings:
const root = useFocusScope();
function handleFileOpen(name: string) {
openFile(name);
root.focusChild('preview');
}Nested scopes with shared keys
Two panels side by side, each containing a Select. j/k navigate items inside the active panel — but at the root level, j/k should also switch between panels. Because the inner scope consumes those keys first, the user presses e to yield, putting the panel into passive mode so the root's j/k can take over:
function Panel({ title, items }: { title: string; items: string[] }) {
const scope = useFocusScope({
keybindings: ({ escape }) => ({ e: escape })
});
return (
<FocusScope handle={scope}>
<Box
borderStyle="round"
borderColor={scope.isPassive ? 'yellow' : scope.hasFocus ? 'green' : 'grey'}
>
<Text>{title}</Text>
<Select options={items.map(i => ({ label: i, value: i }))} />
</Box>
</FocusScope>
);
}
function App() {
const root = useFocusScope({
keybindings: ({ next, prev }) => ({ j: next, k: prev })
});
return (
<FocusScope handle={root}>
<Panel title="Navigation" items={['Home', 'About', 'Contact']} />
<Panel title="Tools" items={['Grep', 'Find', 'Awk']} />
</FocusScope>
);
}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 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 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:
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 (
<FocusScope handle={scope}>
<Box flexDirection="column">
<Text color={scope.hasFocus ? 'green' : 'white'}>
{open ? indicatorOpen : indicator} {name}/
</Text>
{open && (
<Select options={files.map(f => ({ label: f, value: f }))} />
)}
</Box>
</FocusScope>
);
}
function FileTree() {
const root = useFocusScope({
keybindings: ({ nextShallow, prevShallow }) => ({
j: nextShallow, k: prevShallow
})
});
return (
<FocusScope handle={root}>
<DirItem name="src" files={['index.ts', 'utils.ts', 'types.ts']} />
<DirItem name="tests" files={['unit.test.ts', 'e2e.test.ts']} />
</FocusScope>
);
}The directory header's color reflects scope.hasFocus, which is true whenever any descendant — including files deep inside — has focus. No separate state needed to track whether the container is active.