Welcome to the context project tutorial! This is the first chapter, so we are going to start with a fundamental challenge in building Terminal User Interfaces (TUIs): Layers.
Imagine you are building a text interface. You have a main scrollable area where the chat history lives. You decide to add an Autocomplete menu that pops up when the user starts typing.
You hit two major problems immediately:
Escape key, should it close the autocomplete menu, or should it cancel the whole program?
This chapter explains how Overlay & Input Coordination solves these problems using two specific tools: promptOverlayContext (Visuals) and overlayContext (Input).
promptOverlayContext)
In web development, we have z-index to make things float on top. In a terminal using Ink (our rendering engine), we don't have z-index. If an element is inside a box with overflow: hidden, it gets cut off.
To fix this, we use a Portal strategy. We "teleport" the UI element out of the scrollable box and render it near the root of the application, where nothing can clip it.
PromptOverlayProvider.Let's say we want to show a "Confirmation Dialog" floating above the prompt.
// Inside your deep component (e.g., PromptInput)
import { useSetPromptOverlayDialog } from './promptOverlayContext';
function MyInputComponent() {
// Access the "teleporter" hook
const setDialog = useSetPromptOverlayDialog();
// When we mount, send our JSX to the top layer
useEffect(() => {
setDialog(<Text color="green">I am floating!</Text>);
// Cleanup: remove dialog when we unmount
return () => setDialog(null);
}, [setDialog]);
return <Text>Normal Input Here...</Text>;
}
What happens?
Even though MyInputComponent might be buried inside a scrollable box, the text "I am floating!" will render at the very bottom of the terminal, completely visible and unclipped.
overlayContext)Now that our dialog is visible, we need to manage the keyboard. Specifically, the Escape key.
If a dropdown is open, Escape should close the dropdown. If nothing is open, Escape might cancel the current generation or exit a mode. To do this, the app needs to know: Is an overlay currently active?
activeOverlays.useIsOverlayActive(). If true, Escape closes the overlay. If false, it does the default action.Here is how a component tells the system "I am open and active":
// Inside your overlay component
import { useRegisterOverlay } from './overlayContext';
function MyDropdown() {
// 1. Register this component as an active overlay
// The ID is 'my-dropdown'. The second arg 'true' means it's active.
useRegisterOverlay('my-dropdown', true);
return <Border>Select an option...</Border>;
}
Other parts of the app can now check if they should listen to keys:
import { useIsOverlayActive } from './overlayContext';
function KeyHandler() {
const isBlocked = useIsOverlayActive();
// If an overlay is up, don't run global shortcuts!
if (isBlocked) return null;
return <HandleGlobalKeys />;
}
Let's look under the hood. The implementation relies heavily on React Context to pass data up and down the tree without "prop drilling" (passing data through every single parent).
Here is what happens when you open an Autocomplete menu:
Open promptOverlayContext.tsx. The magic happens in two parts.
First, the Provider holds the state (data or dialog) and exposes a "Setter".
// promptOverlayContext.tsx (Simplified)
export function PromptOverlayProvider({ children }) {
// This state holds the floating UI node
const [dialog, setDialog] = useState(null);
return (
// We provide the "setter" to children...
<SetDialogContext.Provider value={setDialog}>
{/* ...and we provide the "value" to the Root Layout */}
<DialogContext.Provider value={dialog}>
{children}
</DialogContext.Provider>
</SetDialogContext.Provider>
);
}
Second, the Hook gives components a safe way to write to that state.
// promptOverlayContext.tsx (Simplified)
export function useSetPromptOverlayDialog(node) {
const set = useContext(SetDialogContext);
// This effect runs whenever 'node' changes
useEffect(() => {
set(node); // "Teleport" the node up
return () => set(null); // Clear it when unmounting
}, [set, node]);
}
Open overlayContext.tsx. This file manages a Set of strings.
// overlayContext.tsx (Simplified)
export function useRegisterOverlay(id, enabled = true) {
const store = useContext(AppStoreContext);
useEffect(() => {
if (!enabled) return;
// Add ID to the global Set
store.setState(prev => ({
...prev,
activeOverlays: new Set(prev.activeOverlays).add(id)
}));
// Remove ID on cleanup
return () => {
store.setState(prev => {
const next = new Set(prev.activeOverlays);
next.delete(id);
return { ...prev, activeOverlays: next };
});
};
}, [id, enabled]);
}
This logic ensures that if the component crashes or unmounts for any reason, the "Active Overlay" lock is automatically removed, preventing the app from getting stuck.
In this chapter, we learned how context handles complex UI layering in a terminal:
promptOverlayContext to "teleport" UI elements out of clipped containers so they float on top.overlayContext to register active components, ensuring keys like Escape interact with the popup instead of the main app.This foundation allows us to build complex, window-like interfaces inside a simple text terminal.
In the next chapter, we will see how these overlays are used inside larger layout structures.
Next Chapter: Modal & Portal Layouts
Generated by Code IQ