Switch to infinite scroll (Full book)

Tabs

The tabs widget is primarily a navigation control, where we switch between sections.

We show a tab bar with Tabs.List:

<Tabs> {/* container */}
	<Tabs.List> {/* header, horizontal or vertical */}
		<Tabs.Tab>	{/* tab */}
    <Tabs.Tab>	{/* tab */}
    <Tabs.Tab>	{/* tab */}
  </Tabs.Tab>
	<Tabs.Panel> {/* tab panel */}
  <Tabs.Panel> {/* tab panel */}
  <Tabs.Panel> {/* tab panel */}

uncontrolled mode

  • We select the initial tab with defaultValue.
  • Pairs of matching Tabs.Tab and Tabs.Panel share the same value.
  • (optional) We make the list mobile-friendly with Scroller: we wrap the .Tab components with it.
<Tabs defaultValue="first" color="dimmed">
    <Tabs.List>
        <Scroller>
            <Tabs.Tab value="first">Proteins</Tabs.Tab>
            <Tabs.Tab value="second">Workouts</Tabs.Tab>
        </Scroller>
    </Tabs.List>
    <Tabs.Panel value="first">{/* Proteins */}</Tabs.Panel>
    <Tabs.Panel value="second">{/* Workouts */}</Tabs.Panel>
</Tabs>

controlled mode

Note: In controlled mode, the selected tab derives from state. The state is subscribed to through value, while onChange handles tentative tab changes:

const [authMode, setAuthMode] = useState("login")

;<Tabs value={authMode} onChange={(v) => setAuthMode(v!)}>
    <Tabs.Tab value="login">Login</Tabs.Tab>
    <Tabs.Tab value="signup">Sign up</Tabs.Tab>
</Tabs>

Tabs.Panel are optional in control mode

A Tabs.Panel wraps its content as a div and doesn't bring style by default

When its value is not matched, it uses display:none to hide its content.

We technically don't need a Tabs.Panel: we can do the conditional display manually based on state, or use an always-on component whose data derives from the selected tab name (do not use).

panels default to lazy-mounted, and sticking around when unselected

A given panel mounts the first time it is selected.

  • By default, it doesn't unmount when unselected. It uses the React <Activity> hidden feature to hide itself:

    • Its state is preserved.
    • The Effects are destroyed, aka the effect cleanup functions run just the same as if the component had unmounted, and the effects run again when the tab is selected back. (the default is keepMountedMode="activity")
  • We can ask a "real" unmount with keepMounted={false} which adds the display=none HMTL attribute.

  • The keepMountedMode="display-none" option is special because, when combined with the default keepMounted={true}, all tabs mount immediately (not lazily), and their effect all run at the same time. Since they stay mounted, the cleanup effect doesn't run when switching tabs and the one-time effect doesn't run when switching to a tab. They appear hidden only through CSS.

earlymorning logo

Tabs

The tabs widget is primarily a navigation control, where we switch between sections.

We show a tab bar with Tabs.List:

<Tabs> {/* container */}
	<Tabs.List> {/* header, horizontal or vertical */}
		<Tabs.Tab>	{/* tab */}
    <Tabs.Tab>	{/* tab */}
    <Tabs.Tab>	{/* tab */}
  </Tabs.Tab>
	<Tabs.Panel> {/* tab panel */}
  <Tabs.Panel> {/* tab panel */}
  <Tabs.Panel> {/* tab panel */}

uncontrolled mode

  • We select the initial tab with defaultValue.
  • Pairs of matching Tabs.Tab and Tabs.Panel share the same value.
  • (optional) We make the list mobile-friendly with Scroller: we wrap the .Tab components with it.
<Tabs defaultValue="first" color="dimmed">
    <Tabs.List>
        <Scroller>
            <Tabs.Tab value="first">Proteins</Tabs.Tab>
            <Tabs.Tab value="second">Workouts</Tabs.Tab>
        </Scroller>
    </Tabs.List>
    <Tabs.Panel value="first">{/* Proteins */}</Tabs.Panel>
    <Tabs.Panel value="second">{/* Workouts */}</Tabs.Panel>
</Tabs>

controlled mode

Note: In controlled mode, the selected tab derives from state. The state is subscribed to through value, while onChange handles tentative tab changes:

const [authMode, setAuthMode] = useState("login")

;<Tabs value={authMode} onChange={(v) => setAuthMode(v!)}>
    <Tabs.Tab value="login">Login</Tabs.Tab>
    <Tabs.Tab value="signup">Sign up</Tabs.Tab>
</Tabs>

Tabs.Panel are optional in control mode

A Tabs.Panel wraps its content as a div and doesn't bring style by default

When its value is not matched, it uses display:none to hide its content.

We technically don't need a Tabs.Panel: we can do the conditional display manually based on state, or use an always-on component whose data derives from the selected tab name (do not use).

panels default to lazy-mounted, and sticking around when unselected

A given panel mounts the first time it is selected.

  • By default, it doesn't unmount when unselected. It uses the React <Activity> hidden feature to hide itself:

    • Its state is preserved.
    • The Effects are destroyed, aka the effect cleanup functions run just the same as if the component had unmounted, and the effects run again when the tab is selected back. (the default is keepMountedMode="activity")
  • We can ask a "real" unmount with keepMounted={false} which adds the display=none HMTL attribute.

  • The keepMountedMode="display-none" option is special because, when combined with the default keepMounted={true}, all tabs mount immediately (not lazily), and their effect all run at the same time. Since they stay mounted, the cleanup effect doesn't run when switching tabs and the one-time effect doesn't run when switching to a tab. They appear hidden only through CSS.