On this pageOverview
Tabs
Tab panel navigation with roving tabindex keyboard support, horizontal and vertical orientation, and automatic or manual activation modes. Tabs renders a tab list with buttons and corresponding panels — only the active panel is visible.
See it in an app
Check out how Tabs is wired up in a real Foldkit app.
The view function is generic over your tab type. Pass a typed tabs array and a tabToConfig callback that maps each tab to its button and panel content.
Model-View-Update with Effect. A single immutable model holds all state, messages describe what happened, and a pure update function produces the next state. Side effects are explicit Commands, never hidden in the view layer.
Composable “The Elm Architecture” modules, Schema-typed state, and controlled side effects via Effect.
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt — fit them into your own Model, init, Message,
// update, and view definitions.
import { Effect } from 'effect'
import { Command, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, p, span } from './html'
// Add a field to your Model for the Tabs Submodel:
const Model = S.Struct({
tabs: Ui.Tabs.Model,
// ...your other fields
})
// In your init function, initialize the Tabs Submodel with a unique id:
const init = () => [
{
tabs: Ui.Tabs.init({ id: 'framework-tabs' }),
// ...your other fields
},
[],
]
// Embed the Tabs Message in your parent Message:
const GotTabsMessage = m('GotTabsMessage', {
message: Ui.Tabs.Message,
})
// Inside your update function's M.tagsExhaustive({...}), delegate to Tabs.update:
GotTabsMessage: ({ message }) => {
const [nextTabs, commands] = Ui.Tabs.update(model.tabs, message)
return [
// Merge the next state into your Model:
evo(model, { tabs: () => nextTabs }),
// Forward the Submodel's Commands through your parent Message:
commands.map(
Command.mapEffect(Effect.map(message => GotTabsMessage({ message }))),
),
]
}
type Framework = 'Foldkit' | 'React' | 'Elm'
const frameworks: ReadonlyArray<Framework> = ['Foldkit', 'React', 'Elm']
const descriptions: Record<Framework, string> = {
Foldkit: 'Model-View-Update with Effect.',
React: 'Component-based with hooks.',
Elm: 'The original MVU architecture.',
}
// Inside your view function, render the tabs:
Ui.Tabs.view<Message, Framework>({
model: model.tabs,
toParentMessage: message => GotTabsMessage({ message }),
tabs: frameworks,
tabListAriaLabel: 'Framework comparison',
tabToConfig: (tab, { isActive }) => ({
buttonClassName:
'px-4 py-2 rounded-t-lg border data-[selected]:bg-white data-[selected]:border-b-0',
buttonContent: span([], [tab]),
panelClassName: 'p-6 border rounded-b-lg',
panelContent: p([], [descriptions[tab]]),
}),
tabListAttributes: [Class('flex')],
})Pass orientation: 'Vertical' to switch to up/down arrow navigation.
Model-View-Update with Effect. A single immutable model holds all state, messages describe what happened, and a pure update function produces the next state. Side effects are explicit Commands, never hidden in the view layer.
Composable “The Elm Architecture” modules, Schema-typed state, and controlled side effects via Effect.
// Pseudocode walkthrough — same Model, init, Message, and update as the
// basic tabs; only the view config changes to set orientation and use flex
// + flex-col for layout.
import { Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { Class, p, span } from './html'
const GotTabsMessage = m('GotTabsMessage', {
message: Ui.Tabs.Message,
})
type Framework = 'Foldkit' | 'React' | 'Elm'
const frameworks: ReadonlyArray<Framework> = ['Foldkit', 'React', 'Elm']
const descriptions: Record<Framework, string> = {
Foldkit: 'Model-View-Update with Effect.',
React: 'Component-based with hooks.',
Elm: 'The original MVU architecture.',
}
// Inside your view function, set orientation to 'Vertical' and use flex +
// flex-col for layout:
Ui.Tabs.view<Message, Framework>({
model: model.tabs,
toParentMessage: message => GotTabsMessage({ message }),
tabs: frameworks,
tabListAriaLabel: 'Framework comparison',
orientation: 'Vertical',
tabToConfig: (tab, { isActive }) => ({
buttonClassName:
'px-4 py-2 text-left rounded-l-lg border mr-[-1px] data-[selected]:bg-white data-[selected]:border-r-0',
buttonContent: span([], [tab]),
panelClassName: 'flex-1 p-6 border rounded-r-lg',
panelContent: p([], [descriptions[tab]]),
}),
attributes: [Class('flex')],
tabListAttributes: [Class('flex flex-col')],
})Tabs is headless — the tabToConfig callback controls all tab and panel markup. A common styling trick is to use a negative margin (mb-[-1px] for horizontal, mr-[-1px] for vertical) on the active tab to overlap the panel border.
| Attribute | Condition |
|---|---|
data-selected | Present on the active tab button and its panel. |
data-disabled | Present on disabled tab buttons. |
Tabs uses roving tabindex — only the focused tab is in the tab order. Arrow direction depends on orientation: left/right for horizontal, up/down for vertical. Disabled tabs are skipped during navigation.
| Key | Description |
|---|---|
| Arrow Right / Down | Move to the next tab. In Automatic mode, also selects it. |
| Arrow Left / Up | Move to the previous tab. In Automatic mode, also selects it. |
| Home | Move to the first tab. |
| End | Move to the last tab. |
| Enter / Space | Select the focused tab (Manual mode only — Automatic selects on focus). |
The tab list receives role="tablist" with aria-orientation and aria-label. Each tab button gets role="tab" with aria-selected and aria-controls linking to its panel. Panels receive role="tabpanel" with aria-labelledby pointing back to the tab.
Configuration object passed to Tabs.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the tabs instance. |
activeIndex | number | 0 | Initially active tab index. |
activationMode | 'Automatic' | 'Manual' | 'Automatic' | In Automatic mode, arrow keys select tabs on focus. In Manual mode, arrow keys focus only — Enter or Space is required to select. |
Configuration object passed to Tabs.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Tabs.Model | - | The tabs state from your parent Model. |
toParentMessage | (childMessage: Tabs.Message) => ParentMessage | - | Wraps Tabs Messages in your parent Message type for Submodel delegation. |
tabs | ReadonlyArray<Tab> | - | The list of tabs. The generic Tab type narrows the value passed to tabToConfig. |
tabToConfig | (tab, context) => TabConfig | - | Maps each tab to its button content and panel content. The context provides isActive. |
tabListAriaLabel | string | - | Accessible label for the tab list. |
orientation | 'Horizontal' | 'Vertical' | 'Horizontal' | Controls arrow key direction and aria-orientation. Horizontal uses left/right, vertical uses up/down. |
isTabDisabled | (tab, index) => boolean | - | Disables individual tabs. |
persistPanels | boolean | false | When true, renders all panels with the hidden attribute on inactive ones instead of removing them from the DOM. |
onTabSelected | (index: number) => Message | - | Alternative to Submodel delegation — fires your own Message on tab selection. Use with Tabs.selectTab() to update the Model. |
tabListAttributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the tab list container. |
tabListClassName | string | - | CSS class for the tab list container. |
attributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the outer wrapper. |
className | string | - | CSS class for the outer wrapper. |
Object returned by the tabToConfig callback for each tab.
| Name | Type | Default | Description |
|---|---|---|---|
buttonContent | Html | - | Content rendered inside the tab button. |
panelContent | Html | - | Content rendered inside the tab panel. |
buttonClassName | string | - | CSS class for the tab button. |
buttonAttributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the tab button. |
panelClassName | string | - | CSS class for the tab panel. |
panelAttributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the tab panel. |