Loading
Show a placeholder in the bracket's own shape while the draw loads, so nothing jumps when it arrives.
This page shows how to fill the screen while the draw is on its way, so the bracket appears in place instead of pushing everything around it.

A placeholder in the bracket's own shape
A spinner tells people something is coming, but not what. A placeholder bracket does both: it has the real rounds, lines and spacing, with blank cards where the matches will go. When the draw arrives, the cards fill in and nothing else moves.
Build a stage from placeholder participants, the same size as the one you are waiting for, and draw every match as a blank card:
import { Bracket, type Stage, singleElimination } from '@react-brackets/native';
import { useEffect, useState } from 'react';
import { View } from 'react-native';
/** Eight blank entries make the same rounds as eight real teams. */
const placeholder = singleElimination(
Array.from({ length: 8 }, (_, index) => ({ id: `placeholder-${index}`, name: '' })),
);
export function Draw({ tournamentId }: { tournamentId: string }) {
const [stage, setStage] = useState<Stage | null>(null);
useEffect(() => {
let current = true;
fetch(`https://example.com/tournaments/${tournamentId}`)
.then((response) => response.json())
.then((draw: Stage) => current && setStage(draw));
return () => {
current = false;
};
}, [tournamentId]);
return stage ? (
<Bracket stage={stage} />
) : (
<Bracket stage={placeholder} renderMatch={() => <BlankCard />} />
);
}
function BlankCard() {
return (
<View
style={{
height: 84,
padding: 14,
gap: 12,
justifyContent: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: '#e4e4e7',
}}
>
<View style={{ width: '62%', height: 10, borderRadius: 5, backgroundColor: '#e4e4e7' }} />
<View style={{ width: '48%', height: 10, borderRadius: 5, backgroundColor: '#e4e4e7' }} />
</View>
);
}The current flag stops a slow response for an earlier tournament from replacing a newer one. With React Query, SWR or your own store, swap the effect for their hook and keep the rest.
Keep the swap from shifting
The placeholder only hides the wait if it lines up with what replaces it.
- Use the same format and options. A
thirdPlacematch, a double elimination stage or aroundRobinwith groups all change the rounds, so build the placeholder with the same call as the real stage. - Use the likely size. If you don't know how many entries there will be, use the most likely count. A placeholder a round too big still looks better than an empty screen.
- Match the card height. Each card keeps its own measured height, so a placeholder card taller or shorter than the real one changes the bracket's height when the draw lands. Either give it the height of your real cards, or build it from the real rows, as below.
To match the default card exactly, keep Bracket.Participant and fill it with invisible text. The text takes one line at the bracket's font size, so each row is as tall as a real one:
import { Bracket, useBracket } from '@react-brackets/native';
import { Text, View } from 'react-native';
function BlankCard() {
const { theme } = useBracket();
const bar = { borderRadius: 6, backgroundColor: theme.colors.border };
const hidden = { color: 'transparent', fontSize: theme.fontSize };
return (
<Bracket.Match>
<Bracket.Participant slot={0}>
<View style={bar}>
<Text style={hidden}>Placeholder</Text>
</View>
</Bracket.Participant>
<Bracket.Participant slot={1}>
<View style={bar}>
<Text style={hidden}>Team</Text>
</View>
</Bracket.Participant>
</Bracket.Match>
);
}useBracket works inside anything renderMatch returns, so the placeholder takes its colours and font size from the same theme as the real bracket, in light and dark mode.
Blank participants are still participants, so pressing one would highlight a path. Pass highlighted={null} to the placeholder bracket to keep it still:
<Bracket stage={placeholder} highlighted={null} renderMatch={() => <BlankCard />} />Make it breathe
A slow pulse shows the screen is working rather than stuck. Animate the card's opacity with Reanimated, so it runs on the UI thread and keeps going while JavaScript is busy parsing the response:
import { useEffect } from 'react';
import { View } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
function BlankCard() {
const pulse = useSharedValue(0);
useEffect(() => {
pulse.value = withRepeat(
withTiming(1, { duration: 800, easing: Easing.inOut(Easing.quad) }),
-1,
true,
);
}, [pulse]);
const breathing = useAnimatedStyle(() => ({ opacity: 0.55 + pulse.value * 0.45 }));
return (
<Animated.View
style={[
{ height: 84, padding: 14, gap: 12, justifyContent: 'center', borderRadius: 10 },
{ borderWidth: 1, borderColor: '#e4e4e7' },
breathing,
]}
>
<View style={{ width: '62%', height: 10, borderRadius: 5, backgroundColor: '#e4e4e7' }} />
<View style={{ width: '48%', height: 10, borderRadius: 5, backgroundColor: '#e4e4e7' }} />
</Animated.View>
);
}Reanimated follows the system's reduce motion setting by default, so people who have turned it on see a still card, as they do with the bracket's own animations. The Loading example puts the placeholder, the pulse and a reload button together.
Screen readers
A blank bracket read aloud is a list of empty rows. Mark the area as busy while it loads, and hide the placeholder cards from assistive technology:
<View accessibilityState={{ busy: !stage }}>
{stage ? (
<Bracket stage={stage} />
) : (
<Bracket stage={placeholder} highlighted={null} renderMatch={() => <BlankCard />} />
)}
</View>Inside BlankCard, give the outer view, or Bracket.Match if you build it from the real rows, both of these:
<Bracket.Match accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
{/* the blank rows */}
</Bracket.Match>accessibilityElementsHidden hides the card on iOS and importantForAccessibility="no-hide-descendants" on Android. Bracket.Match passes any view prop through to its view.
With Suspense
If your data library suspends, render the placeholder bracket as the fallback:
<Suspense fallback={<Bracket stage={placeholder} renderMatch={() => <BlankCard />} />}>
<Draw tournamentId="spring-cup" />
</Suspense>Refreshing a draw you already have
Only show the placeholder the first time. When people pull to refresh, or a screen comes back into focus, keep showing the stage you have until the new one arrives. Swapping a real bracket back to blank cards for a second looks like the data went missing.
Before there is a draw
With no participants yet, there is no bracket to draw. Show your own empty state, like "The draw opens on 12 June", and render the bracket once you have a stage.