giggles
Input

Examples

Worked examples for common input patterns

A filterable file list composed from Select and TextInput. Because each component owns its keybindings, the parent scope has no navigation or character-capture logic — it only routes focus between fields. / bubbles up from the Select (which doesn't consume it) and triggers focusChild('search'); escape from the TextInput (which ignores it) triggers the reverse:

File manager
function FileManager() {
  const [query, setQuery] = useState('');

  const options = FILES.filter(
    f => !query || f.value.toLowerCase().includes(query.toLowerCase())
  );

  const scope = useFocusScope({
    keybindings: ({ focusChild, next, prev }) => ({
      '/': () => focusChild('search'),
      escape: () => { setQuery(''); focusChild('list'); },
      tab: next,
      'shift+tab': prev,
    }),
  });

  return (
    <Box flexDirection="column" gap={1}>
      <FocusScope handle={scope}>
        <TextInput
          focusKey="search"
          label="/"
          value={query}
          onChange={setQuery}
          onSubmit={() => scope.focusChild('list')}
          placeholder="filter files…"
        />
        <Select focusKey="list" options={options} onSubmit={open} />
      </FocusScope>
    </Box>
  );
}

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:

Modal with FocusTrap
function ConfirmDialog({ message, onConfirm, onCancel }) {
  const scope = useFocusScope({
    keybindings: ({ next, prev }) => ({
      left: prev,
      right: next,
      escape: onCancel,
    })
  });

  return (
    <Box flexDirection="column" borderStyle="double">
      <Text bold color="yellow">Confirm Action</Text>
      <Text>{message}</Text>
      <FocusScope handle={scope}>
        <MenuItem label="Confirm" onSelect={onConfirm} />
        <MenuItem label="Cancel" onSelect={onCancel} />
      </FocusScope>
    </Box>
  );
}

function App() {
  const [showModal, setShowModal] = useState(false);
  const menuScope = useFocusScope({
    keybindings: ({ next, prev }) => ({ j: next, k: prev })
  });

  return (
    <Box flexDirection="column">
      <FocusScope handle={menuScope}>
        <MenuItem label="New File" onSelect={() => setShowModal(true)} />
        <MenuItem label="Delete" onSelect={() => setShowModal(true)} />
      </FocusScope>

      {showModal && (
        <FocusTrap>
          <ConfirmDialog
            message="Are you sure?"
            onConfirm={() => setShowModal(false)}
            onCancel={() => setShowModal(false)}
          />
        </FocusTrap>
      )}
    </Box>
  );
}

On this page