FloatingIndicator

Give the parent position: 'relative'. On native, placement updates when the target or parent changes or the screen dimensions change.

Basics

The indicator follows the selected target and remains behind the controls.

Loading demo…

import { useMemo, useState } from 'react'; import { Pressable, View } from 'react-native'; import { Block, FloatingIndicator, Text } from '@plocks/ui'; export function Demo() { const [parent, setParent] = useState<View | null>(null); const [targets, setTargets] = useState<(View | null)[]>([]); const [active, setActive] = useState(0); const refs = useMemo( () => [0, 1, 2].map( (i) => (node: View | null) => setTargets((current) => current[i] === node ? current : Object.assign([...current], { [i]: node }), ), ), [], ); return ( <Block fullWidth> <View ref={setParent} style={{ position: 'relative', flexDirection: 'row', padding: 4 }}> <FloatingIndicator parent={parent} target={targets[active] ?? null} bg="primary.2" style={{ borderRadius: 8 }} /> {['React', 'Vue', 'Svelte'].map((label, i) => ( <Pressable key={label} ref={refs[i]} onPress={() => setActive(i)} style={{ padding: 12 }}> <Text>{label}</Text> </Pressable> ))} </View> </Block> ); }

Multiple Rows

The indicator can move between targets in a grid.

Loading demo…

import { useMemo, useState } from 'react'; import { Pressable, View } from 'react-native'; import { Block, FloatingIndicator, Text } from '@plocks/ui'; export function Demo() { const [parent, setParent] = useState<View | null>(null); const [targets, setTargets] = useState<(View | null)[]>([]); const [active, setActive] = useState(4); const refs = useMemo( () => Array.from( { length: 9 }, (_, i) => (node: View | null) => setTargets((current) => current[i] === node ? current : Object.assign([...current], { [i]: node }), ), ), [], ); return ( <Block fullWidth> <View ref={setParent} style={{ position: 'relative', flexDirection: 'row', flexWrap: 'wrap', width: 180 }} > <FloatingIndicator parent={parent} target={targets[active] ?? null} bg="primary.2" style={{ borderRadius: 8 }} /> {Array.from({ length: 9 }, (_, i) => ( <Pressable key={i} ref={refs[i]} onPress={() => setActive(i)} style={{ width: 60, height: 50, alignItems: 'center', justifyContent: 'center' }} > <Text>{i + 1}</Text> </Pressable> ))} </View> </Block> ); }

Tab Highlight

A decorative indicator can follow an accessible tab selection.

Loading demo…

import { useMemo, useState } from 'react'; import { Pressable, View } from 'react-native'; import { Block, FloatingIndicator, Text } from '@plocks/ui'; export function Demo() { const [parent, setParent] = useState<View | null>(null); const [targets, setTargets] = useState<(View | null)[]>([]); const [active, setActive] = useState(0); const refs = useMemo( () => [0, 1, 2].map( (i) => (node: View | null) => setTargets((current) => current[i] === node ? current : Object.assign([...current], { [i]: node }), ), ), [], ); return ( <Block fullWidth> <View ref={setParent} role="tablist" style={{ position: 'relative', flexDirection: 'row' }}> <FloatingIndicator parent={parent} target={targets[active] ?? null} bg="primary.2" style={{ borderRadius: 8 }} /> {['Overview', 'Details', 'Activity'].map((label, i) => ( <Pressable key={label} ref={refs[i]} role="tab" aria-selected={active === i} onPress={() => setActive(i)} style={{ padding: 12 }} > <Text>{label}</Text> </Pressable> ))} </View> </Block> ); }
Scan to open this page on your phoneplocks.dev/ui/FloatingIndicator