Skip to main content
On this pageFunctions

Ui/Tabs

Functions

create

functionsource
/**
 * Pairs the tabs `view`, `update`, and `selectTab` behind a single
 *  Value-typed entry point. Declare once at module scope so consumers
 *  receive `tab.value: Value` in `toView` without an `as` cast:
 * 
 *  ```ts
 *  const DemoTabs = Ui.Tabs.create<DemoTab>()
 * 
 *  // In view:
 *  h.submodel({ view: DemoTabs.view, ... })
 * 
 *  // In update:
 *  const [next, commands] = DemoTabs.update(model, message)
 *  ```
 * 
 *  The internal view stays typed `ReadonlyArray<string>`; consumers can
 *  pass a `ReadonlyArray<MyUnion>` (assignable) and the fenced cast inside
 *  `create` types `TabInfo.value` as `MyUnion`.
 */
<Value extends string = string>(): Readonly<{
  reflectSelectedTab: Reflect2<Model, Value, ReadonlyArray<Value>>
  selectTab: (model: Model, value: Value, index: number) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Value>>]
  update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Value>>]
  view: SubmodelView<Model, Message, ViewInputs<Value>>
}>

init

functionsource
/** Creates an initial tabs model from a config. Defaults to first tab and automatic activation. */
(config: InitConfig): Tabs.Model

Types

ActivationMode

typesource
/** Controls whether tabs activate on focus (`Automatic`) or require an explicit selection (`Manual`). */
type ActivationMode = Literals<readonly ["Automatic", "Manual"]>

InitConfig

typesource
/** Configuration for creating a tabs model with `init`. */
type InitConfig = Readonly<{
  activationMode: ActivationMode
  activeIndex: number
  id: string
}>

Orientation

typesource
/** Controls the tab list layout direction and which arrow keys navigate between tabs. */
type Orientation = Literals<readonly ["Horizontal", "Vertical"]>

OutMessage

typesource
/**
 * Generic over `Value extends string` so consumers using
 *  `Ui.Tabs.create<MyUnion>()` receive `value: MyUnion` in the
 *  `Selected` OutMessage. Defaults to `string`.
 */
type OutMessage = Selected<Value>

RenderInfo

typesource
/**
 * Render-time payload published to the consumer's `toView`.
 * 
 *  - `tablist`: ARIA + role attributes for the wrapping tablist element.
 *  - `tabs`: one entry per tab in `viewInputs.tabs`, in the same order, with
 *    the tab button's attribute bundle, the panel's attribute bundle,
 *    and derived state.
 *  - `activeIndex`: the currently-active tab index, convenient when the
 *    consumer wants to render only the active panel (vs all panels with
 *    `Hidden` for transitions).
 */
type RenderInfo = Readonly<{
  activeIndex: number
  tablist: ReadonlyArray<ChildAttribute>
  tabs: ReadonlyArray<TabInfo<Value>>
}>

Selected

typesource
/** Sent to the parent when a tab is committed via click or keyboard. Carries both the tab's value (typed as `Value` via `Ui.Tabs.create<Value>()`) and its index. Generic at the type level; the schema stores `value: string` and the factory's fenced cast types it as `Value`. */
type Selected = Readonly<{
  _tag: "Selected"
  index: number
  value: Value
}>

TabInfo

typesource
/**
 * Per-tab render info passed to the consumer's `toView`. Generic over
 *  `Value extends string`: when `Ui.Tabs.create<MyUnion>()` is declared,
 *  `tab.value` is typed `MyUnion` so the consumer can switch on it without
 *  casting.
 */
type TabInfo = Readonly<{
  index: number
  isActive: boolean
  isDisabled: boolean
  isFocused: boolean
  panel: ReadonlyArray<ChildAttribute>
  tab: ReadonlyArray<ChildAttribute>
  value: Value
}>

ViewInputs

typesource
/**
 * Per-render view inputs passed to `view` via `h.submodel`'s `viewInputs` field.
 *  Generic over `Value extends string` so consumers using
 *  `Ui.Tabs.create<MyUnion>()` receive `tab.value: MyUnion` in `toView`
 *  and `(value: MyUnion, index) => boolean` in `isTabDisabled`, without
 *  casting.
 */
type ViewInputs = Readonly<{
  ariaLabel: string
  isTabDisabled: (value: Value, index: number) => boolean
  orientation: Orientation
  tabs: ReadonlyArray<Value>
  toView: (render: RenderInfo<Value>) => Html
}>

Constants

CompletedFocusTab

constsource
/** Sent when the focus-tab command completes. */
const CompletedFocusTab: CallableTaggedStruct<"CompletedFocusTab", {}>

FocusTab

constsource
/** Moves focus to the tab at the given index. */
const FocusTab: CommandDefinitionWithArgs<"FocusTab", {
  id: String
  index: Number
}, Effect<{
  _tag: "CompletedFocusTab"
}, never, never>>

FocusedTab

constsource
/** Sent when a tab receives keyboard focus in `Manual` mode without being activated. */
const FocusedTab: CallableTaggedStruct<"FocusedTab", {
  index: Number
}>

Message

constsource
/** Union of all messages the tabs component can produce. */
const Message: S.Union<[typeof SelectedTab, typeof FocusedTab, typeof CompletedFocusTab]>

Model

constsource
/** Schema for the tabs component's state, tracking active/focused indices and activation mode. */
const Model: Struct<{
  activationMode: Literals<readonly ["Automatic", "Manual"]>
  activeIndex: Number
  focusedIndex: Number
  id: String
}>

OutMessage

constsource
/** Union of out-messages the tabs component can produce. Surfaced as the third element of `update`'s return tuple and pattern-matched by the parent. */
const OutMessage: Union<readonly [
  CallableTaggedStruct<"Selected", {
    index: Number
    value: String
  }>
]>

Selected

constsource
/** Sent to the parent when a tab is committed via click or keyboard. Carries both the tab's value (typed as `Value` via `Ui.Tabs.create<Value>()`) and its index. Generic at the type level; the schema stores `value: string` and the factory's fenced cast types it as `Value`. */
const Selected: CallableTaggedStruct<"Selected", {
  index: Number
  value: String
}>

SelectedTab

constsource
/** Sent when a tab is selected via click or keyboard. Updates both the active and focused indices. */
const SelectedTab: CallableTaggedStruct<"SelectedTab", {
  index: Number
  value: String
}>

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson