Welcome to the CustomSelect project!
In this tutorial series, we will build a powerful, interactive command-line selection tool. We are starting at the very top level: the Multi-Select Container.
Imagine you are building a command-line tool to order a pizza. You need to ask the user to select toppings: Mushrooms, Pepperoni, Onions, etc. The user needs to scroll through the list, press Space to select multiple items, and press Enter to finish.
To make this happen, we need a "Boss" component. This component needs to:
In our project, this Boss is the SelectMulti function. It is the entry point that brings all the logic and visuals together.
The Multi-Select Container acts as the frame for your UI. It doesn't draw every single pixel itself; instead, it coordinates three main things:
Here is how a developer uses this container. We simply provide a list of options and tell it what to do onSubmit.
// Example Usage
const toppings = [
{ label: 'Pepperoni', value: 'pep' },
{ label: 'Mushrooms', value: 'mush' },
{ label: 'Extra Cheese', value: 'cheese' },
];
<SelectMulti
options={toppings}
onSubmit={(selected) => console.log('You ordered:', selected)}
/>
When you run this, SelectMulti takes over and renders the interactive list.
Let's look under the hood. When SelectMulti renders, it follows a specific sequence.
Let's break down the code in SelectMulti.tsx into small, digestible pieces.
First, the component receives props (configuration). It immediately passes them to a custom hook. This hook is the "Brain" of our operation.
export function SelectMulti(props: SelectMultiProps<T>) {
// We use a custom hook to manage all the complex logic.
// This handles cursor movement, selection tracking, etc.
const state = useMultiSelectState({
...props,
// defaults are handled here
});
// Calculate width for layout purposes
const maxIndexWidth = props.options.length.toString().length;
To learn how the "Brain" works, check out Chapter 3: Selection Behavior Hooks.
The container doesn't want to render every option (what if there are 1000?). It only renders what is currently visible.
return (
<Box flexDirection="column">
<Box flexDirection="column">
{/* We iterate ONLY over the visible options provided by the state */}
{state.visibleOptions.map((option, index) => {
// Logic to determine render details happens here...
// ...
To understand how we determine which options are visible, see Chapter 5: Navigation Engine & Viewport.
Inside the loop, the Container acts as a traffic controller. If an option is type 'input', it renders a SelectInputOption. Otherwise, it renders a standard SelectOption.
// ... inside the map loop ...
if (option.type === "input") {
return (
<Box key={String(option.value)}>
{/* Render a complex input row */}
<SelectInputOption option={option} {...inputProps} />
</Box>
);
}
We will cover the specific renderers in Chapter 2: Option Renderers.
If it's not an input, it renders the standard text row. Notice how the Container passes down visual flags like isFocused or isSelected.
// Standard text option rendering
return (
<Box key={String(option.value)}>
<SelectOption
isFocused={isOptionFocused}
isSelected={isSelected}
description={option.description}
>
{/* Children (Label and Index) go here */}
</SelectOption>
</Box>
);
})}
Finally, after the loop finishes, the Container decides if it needs to draw a specific "Submit" button at the bottom of the list.
</Box> {/* End of options list */}
{/* If submit text is provided, render the button */}
{submitButtonText && onSubmit && (
<Box marginTop={0}>
<Text color={state.isSubmitFocused ? "suggestion" : undefined}>
{submitButtonText}
</Text>
</Box>
)}
</Box>
);
}
The Multi-Select Container (SelectMulti) is the orchestrator.
It essentially says: "I have a list of 50 items. The state tells me items 5 through 10 are visible. Item 7 is an input field, the rest are text. Renderers, get to work!"
Now that we understand the Container, let's look at the workers responsible for drawing the individual rows.
Next Chapter: Option Renderers
Generated by Code IQ