PraxisJS

DOM Utilities

@praxisjs/composables DOM composable classes — WindowSize, ScrollPosition, ElementSize, Intersection, and Focus. Used via the @Compose decorator.

DOM Utilities

DOM composables from @praxisjs/composables. Attach them to any component with @Compose to bind reactive DOM state directly to component properties.

npm install @praxisjs/composables
pnpm add @praxisjs/composables
yarn add @praxisjs/composables
bun add @praxisjs/composables

WindowSize

Tracks the browser window's inner dimensions reactively. Updates on every resize event.

import { Compose } from '@praxisjs/decorators'
import { WindowSize } from '@praxisjs/composables'

@Component()
class App extends StatefulComponent {
  @Compose(WindowSize)
  window!: WindowSize

  render() {
    return (
      <div>
        <p>Viewport: {() => this.window.width} × {() => this.window.height}</p>
        {() => this.window.width < 768 && <MobileMenu />}
      </div>
    )
  }
}

Properties: width: number, height: number

Storybook
Live demo — WindowSize

ScrollPosition

Tracks scroll position. Omit the argument to track the window scroll; pass a ref string to track a specific scrollable element.

// Track window scroll
@Compose(ScrollPosition)
scroll!: ScrollPosition

render() {
  return <p>Scroll Y: {() => this.scroll.y}px</p>
}
// Track a specific scrollable element
containerRef = { current: null as HTMLDivElement | null }

@Compose(ScrollPosition, 'containerRef')
scroll!: ScrollPosition

render() {
  return (
    <div
      ref={(el) => { this.containerRef.current = el }}
      style="height:300px;overflow:auto"
    >
      <p>Scroll: {() => this.scroll.y}px</p>
      {/* scrollable content */}
    </div>
  )
}

Pass the ref string, not the element

Pass the property name as a string ('containerRef') rather than the element directly. The element doesn't exist at decoration time — the string is resolved to the actual ref object at mount when the DOM is ready.

Properties: x: number, y: number

Storybook
Live demo — ScrollPosition

ElementSize

Tracks an element's dimensions reactively via ResizeObserver. Updates whenever the element's size changes.

@Component()
class ResizeWatcher extends StatefulComponent {
  containerRef = { current: null as HTMLDivElement | null }

  @Compose(ElementSize, 'containerRef')
  size!: ElementSize

  render() {
    return (
      <div ref={(el) => { this.containerRef.current = el }}>
        <p>Width: {() => this.size.width}px, Height: {() => this.size.height}px</p>
      </div>
    )
  }
}

Properties: width: number, height: number

Storybook
Live demo — ElementSize

Intersection

Tracks whether an element is intersecting the viewport (or a scroll container) via IntersectionObserver.

@Component()
class LazySection extends StatefulComponent {
  sectionRef = { current: null as HTMLElement | null }

  @Compose(Intersection, 'sectionRef', { threshold: 0.5 })
  visibility!: Intersection

  render() {
    return (
      <section ref={(el) => { this.sectionRef.current = el }}>
        {() => this.visibility.visible ? <HeavyContent /> : <Placeholder />}
      </section>
    )
  }
}

Constructor: new Intersection(ref, options?)options matches IntersectionObserverInit.

Properties: visible: boolean

Storybook
Live demo — Intersection

Focus

Tracks whether an element currently has focus.

@Component()
class SearchBar extends StatefulComponent {
  inputRef = { current: null as HTMLInputElement | null }

  @Compose(Focus, 'inputRef')
  focus!: Focus

  render() {
    return (
      <div>
        <input ref={(el) => { this.inputRef.current = el }} placeholder="Search..." />
        {() => this.focus.focused && <span class="hint">Press Enter to search</span>}
      </div>
    )
  }
}

Properties: focused: boolean

Storybook
Live demo — Focus

On this page