FUNSTACK UI

HomeGetting startedConceptsComponentsRoadmap
GitHub

FUNSTACK UI is experimental. Part of the FUNSTACK family with FUNSTACK Router and FUNSTACK Static, which build this site.

Introduction

  • Overview
  • Getting started

Concepts

  • Action props
  • Pending & optimistic state
  • Transitions & ViewTransition
  • Suspense
  • Server Components
  • Browser-owned state
  • Experimental features

Components

  • Toggle
  • Tabs
  • Accordion
  • Tooltip
  • Dialog
  • Popover
  • Menu
  • Switch
  • Checkbox
  • RadioGroup
  • CheckboxGroup

Resources

  • Roadmap & changelog
  • GitHub ↗

Transitions & ViewTransition

Every state change the library makes is a React transition. That is what lets React keep old content on screen, and it is also what lets <ViewTransition> animate the change.

The transition prop

Each part whose mount, unmount or content is driven by library state has a transition prop. When you pass it, the part renders a <ViewTransition default={transition}> as the outermost node of its output; without it, the part renders no boundary at all. The value has exactly React's type: "auto", "none", a View Transition class, or a map from transition type to class.

Treat transition as static. Changing it between undefined and a value remounts the part (development warns); switching between "none" and a class does not. Any value, "none" and a type map that does not match included, has a cost: when the part mounts or is revealed in a transition (a tab switch with keepMounted false or true, a part inside a dialog that opens), React starts a document view transition. The browser then cross-fades the whole page for 250 ms, and the next transition commit waits for it.

<Tabs.Panel value="billing" transition="fade">...</Tabs.Panel>

<Tabs.Panel
  value="billing"
  transition={{ "fsui-tabs-next": "slide-left", "fsui-tabs-prev": "slide-right", default: "none" }}
>
  ...
</Tabs.Panel>
::view-transition-old(.slide-left) { animation: 160ms ease-in both slide-out-left; }
::view-transition-new(.slide-left) { animation: 160ms ease-out both slide-in-right; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

The library ships no animation, so there is nothing to turn off for reduced motion; keep the media query above in your own stylesheet.

If you want only your own named animations, you can opt out of the page cross-fade with the CSS below. It is optional and applies to every view transition on the page, including your router's. The last rule keeps clicks working during a transition in Firefox, which otherwise suppresses hit testing.

::view-transition-old(root),
::view-transition-new(root) { animation: none; }
/* or: :root { view-transition-name: none; } */

::view-transition { pointer-events: none; }

Transition types

Each dispatch adds fsui-<component> and, where the change has a direction or kind, fsui-<component>-<change> through addTransitionType:

ComponentTypes
Togglefsui-toggle, fsui-toggle-press, fsui-toggle-release
Tabsfsui-tabs, fsui-tabs-next, fsui-tabs-prev (DOM order of the new tab relative to the committed one)
Accordionfsui-accordion, fsui-accordion-expand, fsui-accordion-collapse
Tooltipnone (no transitions; see below)
Dialogfsui-dialog, fsui-dialog-open, fsui-dialog-close
Switchfsui-switch, fsui-switch-on, fsui-switch-off
Checkboxfsui-checkbox, fsui-checkbox-check, fsui-checkbox-uncheck
RadioGroupfsui-radio-group
CheckboxGroupfsui-checkbox-group, fsui-checkbox-group-check, fsui-checkbox-group-uncheck (a reset can carry both)

Use them in a transition map, or in CSS with :active-view-transition-type(fsui-tabs-next). Types you add yourself with addTransitionType in the synchronous part of an action join the same transition.

Types are attached only to the commit that ends a dispatched action. A rollback after a rejection, or a controlled parent snapping the value back, carries no fsui-* type: a type map needs a default entry if rollbacks should animate.

What animates when

React starts a view transition only for commits made entirely of transition lanes. The optimistic flip is a synchronous commit, so it never animates through <ViewTransition>:

The part rendersAnimate withExamples
optimistic statea CSS transition on the data attributeToggle [data-pressed], Tabs.Tab [data-selected], Accordion.Trigger [data-open], Tabs.Indicator (anchor insets)
committed statethe transition propTabs.Panel swaps, Accordion.Item / Panel in library mode
pending ending, a snap-backthe transition propa spinner rendered from pending disappearing
browser-owned stateCSS: @starting-style, transition-behavior: allow-discreteTooltip popup, Accordion in JS-free mode (::details-content)

Toggle has a transition prop too

It animates the commit that ends the action (for example a spinner turning back into an icon), not the pressed flip itself. Use CSS on [data-pressed] for that.

Tabs.Indicator follows the optimistic tab with CSS Anchor Positioning; a plain CSS transition on its inset properties makes it slide on click and slide back on rollback.

Rules the library follows for you

  • It never calls document.startViewTransition and never uses flushSync.
  • <ViewTransition> renders no DOM and is safe in server-rendered trees; hydration is not a transition, so nothing animates on first paint.
  • Kept-mounted panels (keepMounted) switch their <Activity> mode inside the library's transition, so nested <ViewTransition> boundaries get enter and exit animations.