giggles
Focus

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:

Dashboard with named children
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:

Focus child from an event handler
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:

Two panels
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:

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 (
    <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.

On this page