Skip to content
Native Animation
Advent Calendar Grid animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add advent-calendar-grid
HolidaysChristmasSkia

Advent Calendar Grid

An advent calendar with hinged doors and expanding rewards, with configurable days, colors, labels, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

An advent calendar with hinged doors and expanding rewards, with configurable days, colors, labels, and fonts.

Why this one

Opening a reward keeps its spatial connection to the calendar. Two paper leaves swing away from an illustrated treat, then the tile fills its host container. Locked dates respond with a short shake and a date hint. Eight original Skia illustrations keep the default artwork sharp at every size.

How it works

  1. The host is measured with onLayout. A two-column bento layout gives days 1, 6, 12, 18, and 24 full-width tiles. The internal ScrollView supports the entire campaign, including in a 300 × 400 container.
  2. Tiles are measured with Reanimated measure on the UI thread. Coordinates are relative to the component, so the same transition also works inside a card or modal.
  3. One progress value sequences the seal break (220 ms), door reveal (520 ms), pause (250 ms), and expansion (480 ms). Door rotation follows a spring with damping 16, stiffness 180, and explicit mass 1. Press feedback has its own spring.
  4. A second rendering of the selected illustration takes over at exactly the tile's position and size. The overlay animates its bounds and radius to fill the host. All grid illustrations share a canvas; locks share a second canvas.
  5. The reward floats above a real expo-blur card while 20 fine snowflakes fall. Closing reverses the spatial expansion; dragging down dismisses it too. The opened thumbnail stays in the grid.
  6. Controlled openedDays and callbacks let the host persist progress. onClaim requests redemption; it does not validate or redeem rewards itself.

Install

npx expo install @shopify/react-native-skia expo-blur expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-worklets

Place your screen under GestureHandlerRootView. For web, configure the Skia CanvasKit loader before importing this component, using the Skia web setup for your Expo project. The host applies safe-area insets; this component does not add them.

Usage

import { useState } from 'react';
import { AdventCalendarGrid } from './advent-calendar-grid';

const days = Array.from({ length: 24 }, (_, i) => ({
  day: i + 1,
  reward: i === 11 ? '20% off hot cocoa' : 'A little holiday treat',
}));

export function HolidayRewards() {
  const [opened, setOpened] = useState<number[]>([]);
  return (
    <AdventCalendarGrid
      days={days}
      today={12}
      openedDays={opened}
      onOpen={(day) => setOpened((current) => [...current, day])}
      onClaim={(day) => redeemReward(day)}
    />
  );
}

Customize the palette, artwork, copy, and timing:

const calendar = useRef<AdventCalendarGridRef>(null);

<AdventCalendarGrid
  ref={calendar}
  days={days}
  today={new Date(2026, 11, 12)}
  month={12}
  year={2026}
  colors={{
    background: '#102B25',
    mesh: '#1E4739',
    accent: '#B52E40',
    pastels: ['#BFDCCB', '#F3D1B2', '#D5CDED'],
    ink: '#14372B',
  }}
  labels={{ claim: 'Save my treat', day: (n) => `December ${n}` }}
  expansionDuration={480}
  tileScale={1.1}
  fontFamily="YourSansFont"
  numberFontFamily="YourMonoFont"
  titleFontFamily="YourDisplayFont"
  renderIllustration={(day, size) => (
    <YourRewardArtwork day={day.day} width={size} height={size} />
  )}
/>

renderIllustration receives the full day object and its intended square size, in points. Its return value is a React Native node, not a Skia child. Keep custom artwork local and preloaded; avoid creating a separate Skia canvas for every thumbnail on web. The built-in artwork supports gift, star, ornament, stocking, mug, snowflake, gingerbread, and candle.

Props

PropTypeDefaultPurpose
daysreadonly AdventDay[]RequiredUnique valid dates in the campaign month, sorted automatically. Each has day, reward, optional illustration, and optional color.
todaynumber | DateRequiredDays after this date are locked unless already opened. A date before the campaign locks all days; one after it unlocks all.
openedDaysreadonly number[]Local stateControlled opened dates. Update this array from onOpen.
onOpen(day: number) => void—First opening in the current opened state.
onClaim(day: number) => void—Requests a reward claim.
colorsPartial<AdventCalendarColors>Holiday paletteAll surfaces, text, illustration materials, mesh, shadows, glass, borders, and button text. An empty pastel array falls back to the default five.
labelsPartial<AdventCalendarLabels>EnglishOpen/opened hints, claim, close, scroll label, day label, locked date, and accessible date/state formatter.
renderIllustration(day, size) => ReactNodeBuilt-in Skia artOverride thumbnail and expanded artwork.
month, yearnumber12, 2026Campaign date context. Month is 1-based.
formatDate(date, style) => stringUS datesnumeric defaults to MM/DD; long to December 12.
headerReactNode—Campaign content inside the calendar scroll view.
fontFamilystringSystemInterface typeface.
numberFontFamilystringSystemDay numeral typeface.
titleFontFamilystringInterface fontExpanded reward typeface.
expansionDurationnumber480Expansion and return duration, in milliseconds.
tileScalenumber1Tile height multiplier; minimum height 116 pt.
styleStyleProp<ViewStyle>Flex 1Host layout. Give the parent a bounded height.
testIDstringadvent-calendar-gridRoot ID; controls append -day-N, -close, -claim, or -today.

The default palette uses pine #0F2A22, cranberry #C8323C, cream #F6EFE3, gold #E9B949, frost #DCEBF5, and five pastel tile colors. Theme overrides merge with defaults; replace the text and material colors too when changing the visual theme.

Ref methods

  • openDay(n) scrolls to a valid available or opened date, measures it, and opens it. A future date shakes and displays its opening date. Requests while another reward is open are ignored.
  • close() reverses the expansion and preserves the opened state. It can also cancel the opening sequence.
  • scrollToToday() centers today's tile. If that day is absent, it does nothing.

The overlay fills the measured component rather than creating an application-wide native modal. A screen-size host produces the full-screen presentation. On rotation, the overlay adapts to the new bounds. Avoid changing campaign data or rearranging the parent while a reward is open; the return target is the captured tile rectangle.

Accessibility

Every tile is a native button with a date and locked, available, or opened state in its accessible label. Locked buttons remain actionable so their opening date can be announced. Close, claim, and jump-to-today controls are at least 48 pt. The grid is removed from accessibility traversal while the modal reward is open. After expansion, focus moves to the close control; closing returns focus to the originating date. A screen-reader escape gesture also closes the reward. All visible and accessible strings are replaceable.

useReducedMotion replaces the door and spatial transition with a 200 ms fade. It removes falling snow, floating artwork, tile travel, shaking, and pulsing glow. The visible close button provides an alternative to the downward drag. Long reward text can scroll inside the bounded glass card without clipping the claim button.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. Uses only modules available in this project's Expo Go stack. Android blur uses BlurTargetView and dimezisBlurViewSdk31Plus; older Android versions use the translucent fallback.

Icons: Lucide (ISC). The license is included. Illustrations are original Skia geometry.