The FocusableGroup and Focusable components are SceneGraph-native components that manage cross-component focus navigation.

You wrap each focusable element in a Focusable component, place the Focusable components in a FocusableGroup, and set the nextFocus* props (for example, nextFocusDown) to define where focus moves. Navigation decisions run on the render thread; therefore, focus navigation does not require round trips to the program thread.

For new TypeScript components, you should use FocusBoundary. It provides the same benefits with a fully TypeScript-side virtual focus system and reactive isFocused() accessors, and it does not have any SceneGraph focus mutation overhead. If you need BrightScript components to participate directly in focus navigation, you should use FocusableGroup.

Understanding how focus works

Focusing across components involves the following three concerns. The sections after this list describe each concern.

  1. Initial focus: setting focus after the component tree is fully built
  2. Moving focus between components: navigating between focusable elements
  3. Notifications on focus change: reacting to focus changes on the program thread

Setting initial focus

You must set initial focus on every screen with an imperative method or a declarative method. An imperative method sets focus by calling a function; a declarative method sets focus through a prop. The following sections explain why initial focus is required and describe both methods.

Understanding why initial focus is required

In traditional BrightScript, you call setFocus() within component init() methods during node initialization. However, RSG TypeScript works differently. In BrightScript, the component sets focus during component initialization on the render thread. In RSG TypeScript, the render thread applies transactions sequentially to create components.

When RSG TypeScript applies transactions to create the component tree, SceneGraph loses focus if the transactions add children after the initial node creation. This happens for the following reasons:

  1. The render thread creates the initial node, which can gain focus.
  2. A subsequent transaction adds children to the node.
  3. The SceneGraph focus state becomes inconsistent.
  4. SceneGraph loses focus, and you must re-establish it.

Because the render thread applies transactions one after another, you must set focus from the TypeScript program thread after the complete component tree is established, rather than during individual component initialization.

Low-level API

The low-level API sets focus on a node. The following code shows the FocusApi interface:

export type AnNodeRef = object;
export type TmNodeRef = object;

export interface FocusApi {
    setFocus: (anchorNodeRef: AnNodeRef, nodeRef: TmNodeRef, focus: boolean) => void;
}

Setting focus with the imperative method

Your app code calls the imperative method on the TypeScript program thread. You can use the method to do the following:

  • Set focus on any node in the SceneGraph tree
  • Set initial focus on a focusable node in a focusable group
  • Set focus on a new focusable node when you add new focusable nodes or delete existing focusable nodes

Calling the focus and blur methods

All primitive components on the program thread contain focus() and blur() methods. To call these methods, you must use a ref (a reference to the component) to access the component. The following example sets and removes focus:

// Set focus on the component
nodeRef.focus();

// Remove focus from the component
nodeRef.blur();

Setting focus with the declarative method

Components use the declarative method to set focus on a specific component with the gainFocus prop.

In RSG TypeScript, the declarative prop is more manageable because you can pass the prop through higher-order components (components that wrap other components).

At the end of each transaction, the SDK finds the node to pass to setFocus by traversing from the root of the anchor node and picking the first leaf node whose gainFocus prop is true.

You can also explicitly remove focus by setting gainFocus={false}, which calls the setFocus method with false. The following example sets and removes focus declaratively:

// Set focus declaratively
<button gainFocus={true} text="Focused Button" />

// Remove focus declaratively
<button gainFocus={false} text="Unfocused Button" />

Moving focus between components

SceneGraph handles focus within a single component automatically; however, moving focus between components requires explicit coordination.

Moving focus in BrightScript

In traditional BrightScript apps, you handle focus movement in onKeyEvent functions that call setFocus(). The following example moves focus from a RowList to a header navigation when the user presses up on the first item:

function onKeyEvent(key as String, press as Boolean) as Boolean
    if press and key = "up"
        if m.rowList.inFocus() and m.rowList.itemFocused = 0
            m.rowList.setFocus(false)
            m.headerNav.setFocus(true)
            return true
        end if
    end if
    return false
end function

Moving focus in RSG TypeScript

RSG TypeScript provides multiple methods for cross-component focus movement, with varying performance characteristics.

Moving focus with the round-trip method

You can use the initial focus methods (the gainFocus prop or the focus() method) to move focus between components; however, this method is less efficient because it requires a round trip: program thread → render thread → program thread → render thread.

This approach also requires the TypeScript program thread to do the following:

  1. Listen for focus change events from SceneGraph components
  2. Implement decision logic to determine when focus should move to another component
  3. Handle edge cases, such as focus reaching component boundaries

The following example shows verbose focus management on the program thread:

// Example of verbose focus management on program thread
const [currentFocus, setCurrentFocus] = createSignal("rowList");

const handleRowListFocusChange = (focusData: any) => {
    // Listen for focus events
    if (focusData.atTopEdge && key === "up") {
        // Decision logic on program thread
        headerNavRef.focus(); // Another round trip
        setCurrentFocus("headerNav");
    }
};

// This gets verbose quickly with multiple components

This approach is inefficient for the following reasons:

  • Threading overhead from round trips
  • Verbose focus event handling code on the program thread
  • Complex state synchronization between threads

Moving focus with the declarative approach

You should use declarative components that handle focus navigation entirely on the render thread. The following example shows basic cross-component navigation:

<FocusableGroup rememberLastFocus={true}>
  <Focusable nextFocusDown="mainContent">
    <HeaderNavigation id="headerNav" {...headerProps} />
  </Focusable>

  <Focusable  nextFocusUp="headerNav" nextFocusDown="footerNav">
    <rowList id="mainContent" {...props} />
  </Focusable>

  <Focusable nextFocusUp="mainContent">
    <FooterNavigation id="footerNav" {...footerProps} />
  </Focusable>
</FocusableGroup>

You can move focus across multiple nested FocusableGroup components as long as the target Focusable is within the same parent FocusableGroup. The following example nests two groups inside main-group:

<FocusableGroup id="main-group">
  {/* Top section with nested group */}
  <Focusable nextFocusDown="button2">
    <FocusableGroup id="top-group" rememberLastFocus={true}>
      <Focusable nextFocusRight="button1">
        <button id="button0" text="Button 0" />
      </Focusable>
      <Focusable nextFocusLeft="button0">
        <button id="button1" text="Button 1" />
      </Focusable>
    </FocusableGroup>
  </Focusable>

  {/* Bottom section with another nested group */}
  <Focusable nextFocusUp="button0">
    <FocusableGroup id="bottom-group" rememberLastFocus={true}>
      <Focusable nextFocusRight="button3">
        <button id="button2" text="Button 2" />
      </Focusable>
      <Focusable nextFocusLeft="button2" nextFocusRight="button4">
        <button id="button3" text="Button 3" />
      </Focusable>
      <Focusable nextFocusLeft="button3">
        <button id="button4" text="Button 4" />
      </Focusable>
    </FocusableGroup>
  </Focusable>
</FocusableGroup>

When you manage each FocusableGroup in its own file or as a shared component, you need to know the specific id of the focusable elements within that shared component to reference them from other components. As a result, each component depends on the internal structure of the other components, which makes the components less reusable.

To avoid this, you can specify the id of the target FocusableGroup instead of the id of individual focusable elements. The system then picks the appropriate focus automatically with the pickFocus functionality. The following example references the group instead of a specific button:

// Shared Navigation Component
interface NavigationBarProps {
  id: string;
}

const NavigationBar = ({ id }: NavigationBarProps) => (
  <FocusableGroup id={id} rememberLastFocus={true}>
    <Focusable nextFocusDown="search" hasPreferredFocus={true}>
      <button id="home" text="Home" />
    </Focusable>
    <Focusable nextFocusUp="home" nextFocusDown="settings">
      <button id="search" text="Search" />
    </Focusable>
    <Focusable nextFocusUp="search">
      <button id="settings" text="Settings" />
    </Focusable>
  </FocusableGroup>
);

// Main Layout - No need to know specific button IDs
<FocusableGroup id="main-layout">
  <Focusable nextFocusDown="nav-bar">  {/* Points to group, not specific button */}
    <ContentArea id="content-area" />
  </Focusable>

  <Focusable nextFocusUp="content-area">
    <NavigationBar id="nav-bar" />  {/* Pass id as prop */}
  </Focusable>
</FocusableGroup>

When focus moves to a FocusableGroup, the system selects the element to focus as follows:

  1. If a Focusable has hasPreferredFocus={true}, the system selects the focusable marked as preferred.
  2. If the group has rememberLastFocus={true}, focus returns to the last focused element in that group.
  3. Otherwise, the system focuses the first focusable element in the group.

Group-level navigation provides the following benefits:

  • Component isolation: shared components do not expose their internal focus structure.
  • Flexible focus strategy: groups can change their preferred focus without affecting the components that reference them.
  • Simplified navigation: parent components only need to know group IDs, not internal element IDs.

Group-level navigation has the following capabilities:

  • Focus can jump between different FocusableGroup components.
  • Navigation references work across group boundaries when the groups are within the same parent.
  • Each group can maintain its own rememberLastFocus behavior.
  • The system supports complex nested hierarchies.

The declarative approach provides the following benefits:

  • The render thread handles focus navigation entirely; therefore, navigation does not have any round-trip overhead.
  • The approach matches the declarative component style of RSG TypeScript.
  • The approach provides better performance and responsiveness than the round-trip method.

Receiving notifications on focus change

To update app state, trigger animations, or run other business logic, the program thread needs to know when focus moves within FocusableGroup components. This section describes the OnFocusedChild event, the sequence of events when focus changes, and example uses.

Using the OnFocusedChild event

All SceneGraph nodes can fire an OnFocusedChild event when one of their immediate children (or the node itself) gains or loses focus. SceneGraph calls all OnFocusedChild handlers that are on the focus path. This event is part of the built-in SceneGraph focus management system; RSG TypeScript does not introduce it.

See focusable-group-track-component-focus.skill.md for focused-component signal patterns based on this event.

The SceneGraph onFocusedChild event can detect focus changes only for the component itself or its immediate children. The event cannot detect focus changes in deeper nested components.

You can add onFocusedChild to any SceneGraph component (including FocusableGroup). If you want to know the exact component on which focus changed, you should place onFocusedChild on Focusable, for the following reasons:

  • A FocusableGroup contains Focusable components as immediate children.
  • The Focusable components contain the actual focusable elements (for example, Button) as direct children.
  • When a Button gains focus, only its immediate parent (Focusable) can detect the change.
  • A FocusableGroup with onFocusedChild shows only focus changes between Focusable components, not the actual focused elements within them.

The following example compares the two placement options:

// Option 1: On FocusableGroup (only shows Focusable-level changes)
<FocusableGroup onFocusedChild={(focusableId) => {
  // Fires when moving between Focusable components
  // focusableId would be null as Focusable IDs are empty, not Button IDs
}}>
  <Focusable nextFocusDown="button2">
    <button id="button1" text="Button 1" />
  </Focusable>
  <Focusable nextFocusUp="button1">
    <button id="button2" text="Button 2" />
  </Focusable>
</FocusableGroup>

// Option 2: On Focusable (shows actual focused element changes)
<FocusableGroup>
  <Focusable nextFocusDown="button2" onFocusedChild={(buttonId) => {
    // Fires when button1 gains or loses focus
    // buttonId would be "button1" or null
  }}>
    <button id="button1" text="Button 1" />
  </Focusable>
  <Focusable nextFocusUp="button1" onFocusedChild={(buttonId) => {
    // Fires when button2 gains or loses focus
    // buttonId would be "button2" or null
  }}>
    <button id="button2" text="Button 2" />
  </Focusable>
</FocusableGroup>

The Focusable component is a wrapper that holds navigation props (nextFocusUp, nextFocusDown, and so on). It does not provide special focus event handling; all SceneGraph nodes handle focus events by default. If each SceneGraph primitive component (Button, Label, and so on) had these navigation props built in, you would not need the Focusable wrapper. In that case, you could place onFocusedChild events directly on the primitive components or on the FocusableGroup.

The following example shows the current architecture:

<Focusable nextFocusRight="button1" onFocusedChild={handler}>
  <button id="button0" text="Button 0" />
</Focusable>

The following example shows how the same code would look if the primitive component held the navigation props:

<button
  id="button0"
  text="Button 0"
  nextFocusRight="button1"
  onFocusedChild={handler}
/>

The following example places an onFocusedChild handler on each Focusable in a navigation group:

<FocusableGroup id="navigation-group">
  <Focusable onFocusedChild={(focusedChildId: string) => {
    if (focusedChildId) {
      console.log("Home button focused:", focusedChildId);
      setCurrentFocusedItem(focusedChildId);
    } else {
      console.log("Home button lost focus");
      setCurrentFocusedItem(null);
    }
  }}>
    <button id="home" text="Home" />
  </Focusable>

  <Focusable onFocusedChild={(focusedChildId: string) => {
    if (focusedChildId) {
      console.log("Search button focused:", focusedChildId);
      setCurrentFocusedItem(focusedChildId);
    } else {
      console.log("Search button lost focus");
      setCurrentFocusedItem(null);
    }
  }}>
    <button id="search" text="Search" />
  </Focusable>

  <Focusable onFocusedChild={(focusedChildId: string) => {
    if (focusedChildId) {
      console.log("Settings button focused:", focusedChildId);
      setCurrentFocusedItem(focusedChildId);
    } else {
      console.log("Settings button lost focus");
      setCurrentFocusedItem(null);
    }
  }}>
    <button id="settings" text="Settings" />
  </Focusable>
</FocusableGroup>

You must place the onFocusedChild event on the Focusable component, not the FocusableGroup. SceneGraph calls the handler both when focus is gained and when focus is lost:

  • When focus is gained, focusedChildId contains the ID of the focused element (or an empty string "" if the element does not have an ID).
  • When focus is lost, focusedChildId is always null.

Understanding the event flow

This section describes the sequence of events when focus changes within a Focusable component. When focus is gained, the sequence is:

  1. The user navigates to a focusable element on the render thread.
  2. The focused element triggers the SceneGraph focus event system.
  3. The parent Focusable fires an OnFocusedChild event with the focused child's ID.
  4. The event handler runs on the program thread with the element ID.
  5. Your app can update reactive state, trigger effects, and so on.

When focus is lost, the sequence is:

  1. The user navigates away from the focusable element.
  2. The element loses focus, which triggers the SceneGraph focus event system.
  3. The parent Focusable fires an OnFocusedChild event with a null value.
  4. The event handler runs on the program thread with null.
  5. Your app can clean up state, hide related UI, and so on.

Using focus notifications

You can use the onFocusedChild event to synchronize state, track analytics, and update the UI conditionally. The following example synchronizes state with the focused section:

const [currentSection, setCurrentSection] = createSignal<string | null>('home');

<Focusable onFocusedChild={(id) => {
  if (id) {
    setCurrentSection(id);
  } else {
    setCurrentSection(null); // Focus lost
  }
}}>
  <button id="home" text="Home" />
</Focusable>

The following example tracks focus changes for analytics:

<Focusable onFocusedChild={(id) => {
  if (id) {
    analytics.track('focus_gained', { element: id });
  } else {
    analytics.track('focus_lost', { element: 'home' });
  }
}}>
  <button id="home" text="Home" />
</Focusable>

The following example shows or hides details depending on which button has focus:

const [showDetails, setShowDetails] = createSignal(false);

<Focusable onFocusedChild={(id) => {
  if (id === 'settings') {
    setShowDetails(true);  // Show details when settings focused
  } else {
    setShowDetails(false); // Hide details when focus lost
  }
}}>
  <button id="settings" text="Settings" />
</Focusable>

Using the onFocusedChild event provides the following benefits:

  • Automatic notification: you do not need to track focus manually.
  • SceneGraph integration: the event uses the existing SceneGraph focus event system.
  • Program thread awareness: the program thread can update state in response to focus changes.
  • Performance: an event-driven approach is more efficient than polling the focus state.

Avoiding common pitfalls

This section describes two pitfalls that cause focus to jump unexpectedly and a best practice for managing focus state dynamically.

Avoiding component recreation with gainFocus

Component recreation causes the focus system to lose track of the current focus state. When inefficient reactive patterns recreate components that have gainFocus props, the SDK reapplies the gainFocus props; therefore, the props override the current focus state and focus jumps back to the initial position.

State changes alone do not cause focus issues. The problem occurs only when state changes recreate or re-render components.

The following example shows a problematic pattern. The For component creates new Button components on each state update:

const ButtonRow = (props) => {
  const focusIdx = () => props.focusIdx || 0;

  // Problem: Using For with reactive array creates new components on each render
  return (
    <FocusableGroup>
      <For each={props.buttons}>
        {(buttonData, index) => {
          // Each state update recreates all Button components
          // This causes focus to be lost and re-applied based on gainFocus
          return (
            <Focusable>
              <button
                gainFocus={focusIdx() === index()}  // Problem: Applied to recreated components
                text={buttonData}
                id={`button-${index()}`}
              />
            </Focusable>
          );
        }}
      </For>
    </FocusableGroup>
  );
};

// When props.focusIdx changes → components recreated → focus jumps to gainFocus=true button

You should use a stable component structure. The following example creates a stable array of indices that changes only when the length changes:

// Stable component structure
const ButtonRow = (props) => {
  const focusIdx = () => props.focusIdx || 0;
  const buttonsLength = createMemo(() => props.buttons.length);

  // Create stable array of indices that only changes when length changes
  const buttonIndices = createMemo(() =>
    Array.from({ length: buttonsLength() }, (_, i) => i)
  );

  return (
    <FocusableGroup>
      <For each={buttonIndices()}>
        {(index) => {
          // Get button data reactively but do not recreate the button structure
          const buttonData = () => props.buttons[index];

          return (
            <Focusable>
              <button
                gainFocus={focusIdx() === index}  // Safe: Components are not recreated
                text={buttonData()}
                id={`button-${index}`}
              />
            </Focusable>
          );
        }}
      </For>
    </FocusableGroup>
  );
};

The following solutions avoid component recreation:

  • You can use a stable component structure (stable keys and structure) to prevent unnecessary component recreation:
// Use stable indices instead of reactive array iteration
const buttonIndices = createMemo(() => Array.from({ length: props.buttons.length }, (_, i) => i));
  • You can apply gainFocus conditionally, only during the initial mount:
const [hasInitialized, setHasInitialized] = createSignal(false);

<button
  gainFocus={!hasInitialized()}
  text="Home"
/>
  • You should use declarative navigation: move focus with the nextFocus* props instead of a reactive gainFocus.

You should use development tools and logging to identify when your app recreates components unnecessarily. You should look for patterns in which:

  • A For loop iterates over reactive arrays that change frequently.
  • Component keys are reactive and change with state.
  • Component props cause structural changes rather than only data updates.

See the focus-management examples in apps/focus-management/src/components/problematic-patterns/ for more information about these problematic patterns and their solutions.

Avoiding onFocusedChild events that cause component recreation

You should avoid using onFocusedChild events to update app state that recreates components with gainFocus props. When the app recreates those components, the SDK reapplies the gainFocus props. The reapplied props override the current focus state and cause focus to jump unexpectedly.

This pitfall applies in the following cases:

  • You use onFocusedChild to update frequently changing UI state that is tied to gainFocus.
  • Reactive focus state causes component recreation.
  • Focus state updates trigger unnecessary re-renders of the component tree.

This pitfall does not apply in the following cases:

  • You use legitimate dynamic focus management scenarios (for example, adding or removing items, or conditional rendering).
  • The focus state changes only when the component structure actually changes.
  • Your focus logic is properly separated and does not cause component recreation.

Managing focus state dynamically

The previous pitfalls show problematic uses of reactive focus state. However, in some legitimate scenarios you need to manage focus state dynamically, especially when the component structure changes. The following scenarios are valid use cases for dynamic focus state:

  • Adding or removing items: users add or remove items that affect the focusable structure.
  • Conditional rendering: components appear or disappear based on user actions.
  • Dynamic lists: list items are added, removed, or reordered.
  • Multi-step workflows: focus needs to move to newly created elements.

The following example adds and removes buttons and adjusts the focus index:

const DynamicButtonRow = () => {
  const [buttons, setButtons] = createSignal(['Button 1', 'Button 2']);
  const [focusedIndex, setFocusedIndex] = createSignal(0);

  const addButton = () => {
    const newButtons = [...buttons(), `Button ${buttons().length + 1}`];
    setButtons(newButtons);
    // Focus the newly added button
    setFocusedIndex(newButtons.length - 1);
  };

  const removeButton = (indexToRemove: number) => {
    const newButtons = buttons().filter((_, i) => i !== indexToRemove);
    setButtons(newButtons);

    // Adjust focus if needed
    const currentFocus = focusedIndex();
    if (currentFocus >= newButtons.length) {
      // Focus was on removed item or beyond, focus last item
      setFocusedIndex(Math.max(0, newButtons.length - 1));
    } else if (currentFocus > indexToRemove) {
      // Focus was after removed item, shift focus index down
      setFocusedIndex(currentFocus - 1);
    }
    // If focus was before removed item, keep same index
  };

  return (
    <FocusableGroup>
      <For each={buttons()}>
        {(buttonText, index) => (
          <Focusable>
            <button
              gainFocus={focusedIndex() === index()}
              text={buttonText}
              onButtonSelected={() => {
                if (buttonText === 'Add Button') {
                  addButton();
                } else {
                  removeButton(index());
                }
              }}
              onFocusedChild={(id) => {
                if (id) setFocusedIndex(index());
              }}
            />
          </Focusable>
        )}
      </For>
      <Focusable>
        <button
          text="Add Button"
          onButtonSelected={addButton}
        />
      </Focusable>
    </FocusableGroup>
  );
};

The following best practices apply to dynamic focus management:

  • You should use a stable component structure when possible. Even with dynamic content, maintain stable component keys and structure:
// Stable structure with dynamic content
const buttonsLength = createMemo(() => buttons().length);
const buttonIndices = createMemo(() =>
  Array.from({ length: buttonsLength() }, (_, i) => i)
);

<For each={buttonIndices()}>
  {(index) => {
    const buttonData = () => buttons()[index];
    return buttonData() ? (
      <Focusable>
        <button
          gainFocus={focusedIndex() === index}
          text={buttonData()}
          id={`button-${index}`}
        />
      </Focusable>
    ) : null;
  }}
</For>
  • You should separate focus logic from business logic by keeping focus state management separate from other reactive state:
// Separate signals for different concerns
const [items, setItems] = createSignal(initialItems); // Business data
const [focusedIndex, setFocusedIndex] = createSignal(0); // Focus state
const [uiState, setUIState] = createSignal("loading"); // UI state

// Only focusedIndex affects gainFocus, not other state changes
  • You should handle cases where the focus index becomes invalid:
const safeFocusIndex = createMemo(() => {
  const current = focusedIndex();
  const maxIndex = buttons().length - 1;
  return Math.min(Math.max(0, current), maxIndex);
});

<button gainFocus={safeFocusIndex() === index()} />
  • You should use onFocusedChild to keep your focus state in sync with the actual focus:
<Focusable onFocusedChild={(id) => {
  if (id) {
    // Update focus state when user navigates manually
    setFocusedIndex(index());
  }
}}>
  <button gainFocus={focusedIndex() === index()} />
</Focusable>

You can use dynamic focus state in the following cases:

  • The component structure changes (for example, adding or removing items).
  • You render components conditionally based on user actions.
  • Focus needs to move to newly created elements.
  • Multi-step workflows have dynamic content.

You should avoid dynamic focus state in the following cases:

  • Focus is tied to frequently changing UI state.
  • Focus changes on every render because of reactive dependencies.
  • You use focus state for logic that is not related to focus.
  • Reactive focus state does not correspond to actual structural changes.

Related documentation

  • FocusableGroup example: Describes the example app that demonstrates declarative SceneGraph-native focus navigation with Focusable and FocusableGroup.
  • FocusableGroup: Includes the generated reference for the FocusableGroup component.
  • Focusable: Includes the generated reference for the Focusable component.
  • Focus boundary: Describes the TypeScript-side virtual focus system that you should use for new TypeScript components.

Did this page help you?