Skip to content

feat: Add Svelte 5 spatial navigation package#238

Open
yehia2amer wants to merge 5 commits into
NoriginMedia:mainfrom
yehia2amer:feat/svelte5-support
Open

feat: Add Svelte 5 spatial navigation package#238
yehia2amer wants to merge 5 commits into
NoriginMedia:mainfrom
yehia2amer:feat/svelte5-support

Conversation

@yehia2amer

Copy link
Copy Markdown

What

Adds @noriginmedia/norigin-spatial-navigation-svelte — a Svelte 5 binding with full feature parity to the React package.

Components

  • <SpatialRoot> — Declarative init wrapper with initialFocusKey prop for timing-safe initial focus
  • <SpatialNode> — Renderless component that exposes a use:spatial action. No wrapper div injected. Consumer applies the action to their own element.
  • <Focusable> — Convenience component for simple leaf nodes. Renders an element and handles the action internally.

Key design decisions

  • No wrapper div — Uses a Svelte action (use:spatial) for DOM registration, matching how React's useFocusable returns a ref. Zero layout interference.
  • Automatic parent context — Every <SpatialNode> provides its navKey to descendants via Svelte context. No manual <FocusContext.Provider> needed (the onArrowPress How to use  #1 source of bugs in the React version is gone).
  • initialFocusKey on <SpatialRoot> — Handles Svelte's bottom-up mount timing internally. No setTimeout hacks for consumers.
  • autoFocus prop — For modals/dynamic content that should grab focus on mount.
  • Two-tier API<Focusable> for the 90% case (simple leaf), <SpatialNode> for full control (containers, custom layouts).

All React features supported

Every option from useFocusable is available: focusable, saveLastFocusedChild, trackChildren, autoRestoreFocus, forceFocus, isFocusBoundary, focusBoundaryDirections, preferredChildFocusKey, extraProps, accessibilityLabel, all callbacks (onEnterPress, onArrowPress, onFocus, onBlur, etc.), plus the full imperative API (setFocus, pause, resume, navigateByDirection, etc.).

Also included

  • Demo app (apps/svelte-demo) — Single-file demo mirroring the React demo (menu, content rows, scrolling, progress bar with arrow-hold, enter-to-select)
  • Docs — Installation guide update, Svelte quick start, Svelte API reference
  • AI agent skillskills/norigin-spatial-navigation-svelte/SKILL.md
  • Changeset for initial release

Consumer usage

<SpatialRoot initialFocusKey="menu">
  <SpatialNode navKey="menu" trackChildren>
    {#snippet children({ hasFocusedChild, spatial })}
      <nav use:spatial class:active={hasFocusedChild}>
        <SpatialNode onEnterPress={handleClick}>
          {#snippet children({ focused, spatial: btnSpatial })}
            <button use:btnSpatial class:focused>Click me</button>
          {/snippet}
        </SpatialNode>
      </nav>
    {/snippet}
  </SpatialNode>
</SpatialRoot>

Notes

  • The Svelte package uses eslint-plugin-svelte for linting. .svelte files are excluded from Prettier (prettier v2 can't parse Svelte 5 syntax).
  • No changes to existing CI workflow behavior. Added check task to turbo for svelte-check.
  • @sveltejs/package handles the build, shipping preprocessed .svelte files + type declarations.

Add @noriginmedia/norigin-spatial-navigation-svelte with full feature
parity to the React binding.

Components:
- <SpatialRoot> - declarative init with initialFocusKey prop
- <SpatialNode> - renderless, exposes use:spatial action
- <Focusable> - convenience wrapper for simple leaf nodes

Key design decisions:
- No wrapper div (action-based DOM binding)
- Automatic parent context (no manual Provider needed)
- autoFocus prop for dynamic content
- Two-tier API: Focusable (simple) vs SpatialNode (advanced)

Also includes:
- Demo app (apps/svelte-demo) mirroring the React demo
- Svelte quick start guide and API reference
- AI agent skill for Svelte usage
- Changeset for initial release
@yehia2amer
yehia2amer requested a review from a team as a code owner June 18, 2026 16:35
@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5f064be

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@noriginmedia/norigin-spatial-navigation-svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@predikament predikament added enhancement New feature or request dependencies Dependency changes labels Jul 1, 2026
@predikament

Copy link
Copy Markdown
Collaborator

Thanks a lot for this @yehia2amer - We'll make sure to get this reviewed ASAP and merged as soon as we can, since we've been wanting to add Svelte support. Appreciate the work! We'll get back to you.

@asgvard

asgvard commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Really nice work @yehia2amer. SpatialNode seems like 1:1 port of useFocusable, and I like that SpatialRoot/initialFocusKey solves Svelte's mount-timing problem, I remember I did some simple PoC in the past and faced similar issue.

One thing I want to flag before merging: Focusable and the autoFocus prop don't have a React equivalent, they're new API features, not parity with React. My concern is that every framework package starts inventing its own "convenience" layer and they drift apart over time, which makes the library harder to reason about and maintain across packages.
Would you be open to dropping those two from this PR and keeping it strictly to SpatialNode + SpatialRoot (parity with the React hook)? If there's a strong case for Focusable-style ergonomics, let's discuss it as its own proposal so we can decide if/how it should also apply to React, rather than have Svelte be first to diverge.
We actually have our own Focusable component with similar focusOnMount prop in one of the projects, so we might consider bringing these "convenience wrappers" into framework-specific repos, because sometimes they make it easier to create focusable components by adding a UI node instead of manually wiring hooks (i.e. in React). So that's a good idea, but in my opinion we should not expand the library features per framework. If someone adds Vue package tomorrow and adds another 3 convenience layers, they will drift apart from other frameworks.

Also the branch is a bit behind main, we have released a couple of things recently, so it's if you can merge main and resolve the conflict in lock file (or accept main and regen your lock file).

Otherwise this is solid, I pulled the branch, ran the demo locally, initial focus and arrow navigation both work as expected and identical to React version.

@yehia2amer

Copy link
Copy Markdown
Author

Thanks a lot for checking the PR. Glad it's working on your end too.

mmm yeah that's a fair what I can do is:

  • Dropping Focusable and autoFocus from this PR for now, so it stays a clean 1:1 with the hook (SpatialRoot + SpatialNode) until we agree on a consistent way to do this across all frameworks.
  • Merging main and regenerating the lock file to clear the conflict

Just One thing I'd love your input on: where should we take this discussion? I did the Focusable/autoFocus in the first place for the ergonomics in Svelte which is genuinely a bit different from React — the mount timing and the action-based DOM registration make some of these patterns feel more natural to solve at the component level rather than pushing everything onto the consumer. So I think it's worth a proper conversation rather than just copying the React approach 1:1.

I'll keep this PR clean without them, and spin up a separate branch that includes Focusable + autoFocus as a reference — that way we've got something concrete to look at and can figure out together.

Regenerate lockfile on top of latest main (core@4.0.0, react/legacy@3.2.1,
react-native-tvos) to include the svelte package + demo dependencies and
resolve the lockfile conflict flagged in review.
Keep the Svelte package as a strict 1:1 with useFocusable (SpatialRoot +
SpatialNode) per review feedback, avoiding per-framework API drift.

- Remove Focusable.svelte component and its index export
- Remove FocusableState type
- Remove autoFocus prop and its setTimeout auto-focus logic from SpatialNode
- Update docs (api-reference, quick-start) and README
- Remove unused Focusable import from svelte demo

Consumers needing mount-time focus can call setFocus(focusKey) explicitly,
matching React. SpatialRoot's initialFocusKey remains as it solves the
Svelte-specific mount-timing problem.
Bring the Svelte package to parity with useFocusable/core after rebasing
onto main (core@4.0.0).

- Add nextFocusResolver prop to SpatialNode (parity with useFocusable)
- Add focusOnPresetKey prop to SpatialRoot (core 3.2.0 init option)
- Add layoutAdapter prop to SpatialRoot and migrate off the deprecated
  useGetBoundingClientRect flag (still supported as a fallback)
- Fix renamed core type re-export: SpatialNavigationServiceInit ->
  SpatialNavigationServiceOptions
- Update API reference docs and changeset

Verified: svelte-check 0 errors, package builds, demo type-checks, builds,
and dev server serves cleanly against core 4.0.0.
@yehia2amer

Copy link
Copy Markdown
Author

@predikament @asgvard All done. pulled in the latest from main, removed Focusable and autoFocus as suggested and synced everything up with the new core 4.0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency changes enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants