ui/focus

Focus management utilities for Glizzy components

Provides effect-based focus management and focus trapping for overlays. Use this for dialogs, modals, popovers, and other overlay components.

Usage

import lustre/effect.{type Effect, batch}
import glizzy/ui/focus

type Model {
  Model(
    is_open: Bool,
    focus_scope_id: String,
  )
}

type Msg {
  Open
  Close
  FocusScopeCreated(String)
}

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    Open -> #(
      Model(..model, is_open: True),
      batch([
        focus.trap_focus(model.focus_scope_id, True),
        focus.focus_first(model.focus_scope_id),
      ])
    )
    Close -> #(
      Model(..model, is_open: False),
      focus.trap_focus(model.focus_scope_id, False)
    )
    FocusScopeCreated(scope_id) -> #(
      Model(..model, focus_scope_id: scope_id),
      effect.none()
    )
  }
}

Types

A unique identifier for a focus scope.

pub type FocusScopeId =
  String

Values

pub fn create_scope(
  container_id: String,
  on_created: fn(String) -> msg,
) -> effect.Effect(msg)

Create a focus scope for the given container element. Returns an effect that creates the scope and dispatches the scope ID.

Use this in your component’s init function to set up focus management.

Example

fn init() -> #(Model, Effect(Msg)) {
  #(
    Model(is_open: False, focus_scope_id: ""),
    focus.create_scope("dialog-content", FocusScopeCreated),
  )
}
pub fn destroy_scope(scope_id: String) -> effect.Effect(msg)

Destroy a focus scope and clean up resources. Returns an effect that destroys the scope.

Use this when your component unmounts to prevent memory leaks.

pub fn disable_trap(container_id: String) -> effect.Effect(msg)

Disable focus trap on a container element by ID. Returns an effect that disables the trap after paint.

pub fn enable_trap(container_id: String) -> effect.Effect(msg)

Enable focus trap on a container element by ID. This is a simpler alternative to using focus scopes.

When enabled, Tab key cycles through focusable elements within the container. Shift+Tab reverses the cycle.

Returns an effect that enables the trap after paint.

pub fn focus_by_id(element_id: String) -> effect.Effect(msg)

Focus an element by its ID. Returns an effect that focuses the element after paint.

Use this to restore focus to a trigger element when closing an overlay.

pub fn focus_first(scope_id: String) -> effect.Effect(msg)

Focus the first focusable element within the scope. Returns an effect that focuses the first element after paint.

Use this when opening an overlay to move focus inside.

pub fn focus_last(scope_id: String) -> effect.Effect(msg)

Focus the last focusable element within the scope. Returns an effect that focuses the last element after paint.

Use this for reverse focus navigation or when closing from the first element.

pub fn get_focused_id(
  on_got_id: fn(option.Option(String)) -> msg,
) -> effect.Effect(msg)

Get the currently focused element’s ID. Returns an effect that dispatches the focused element ID.

Use this to save the currently focused element before opening an overlay, so you can restore focus when closing.

pub fn trap_focus(
  scope_id: String,
  enabled: Bool,
) -> effect.Effect(msg)

Enable or disable focus trapping within the scope. When enabled, Tab key cycles through focusable elements within the scope.

Returns an effect that enables/disables the trap after paint.

Example

// Enable focus trap when opening dialog
focus.trap_focus(scope_id, True)

// Disable focus trap when closing dialog
focus.trap_focus(scope_id, False)
Search Document