Skip to content
All Taste components

Animated screens

Skeleton to feed

A shimmering placeholder that resolves into a real community feed, post by post.

shimmer skeleton · staggered fade-in · photo post · event post

CommonsKoramangala · 5th Block
AR
Ananya Rao2h · 5th Block

Lost: grey tabby near the 5th Block park. Answers to Pepper, blue collar with a bell.

249Share
HR
HSR Runners5h · Club
Sun 19 Oct · 6:00 AM

Easy 10K loop from Agara Lake. All paces welcome, filter coffee after.

// Skeleton → content: a shimmering placeholder while the feed loads, then posts fade up one after another.
// Mobile-first page on the Taste preset (animate-shimmer and animate-rise come with it); maps to React Native via NativeWind.
"use client";

import { useEffect, useState } from "react";
import { Bell, CalendarDays, Heart, MessageCircle, Search, Share } from "lucide-react";

type Post = { id: string; name: string; initials: string; meta: string; body: string; photo?: boolean; event?: string; likes: number; comments: number };

const demo: Post[] = [
  { id: "1", name: "Ananya Rao", initials: "AR", meta: "2h · 5th Block", body: "Lost: grey tabby near the 5th Block park. Answers to Pepper, blue collar with a bell.", photo: true, likes: 24, comments: 9 },
  { id: "2", name: "HSR Runners", initials: "HR", meta: "5h · Club", event: "Sun 19 Oct · 6:00 AM", body: "Easy 10K loop from Agara Lake. All paces welcome, filter coffee after.", likes: 41, comments: 12 },
];

// Swap for your data call.
const loadFeed = () => new Promise<Post[]>((done) => setTimeout(() => done(demo), 1200));

const bone = "block rounded-md bg-[linear-gradient(90deg,var(--tst-surface-2)_36%,var(--tst-surface)_50%,var(--tst-surface-2)_64%)] bg-[length:300%_100%] animate-shimmer motion-reduce:animate-none";

export default function Feed() {
  const [posts, setPosts] = useState<Post[] | null>(null);
  useEffect(() => {
    let live = true;
    loadFeed().then((p) => live && setPosts(p));
    return () => {
      live = false;
    };
  }, []);

  let n = 0; // stagger index across every block that fades in
  const rise = () => ({ className: "animate-rise motion-reduce:animate-none", style: { animationDelay: n++ * 110 + "ms" } });

  return (
    <main className="mx-auto flex min-h-dvh max-w-md flex-col bg-bg font-sans text-fg">
      <header className="flex h-14 items-center gap-1 pl-4 pr-1.5">
        <div className="min-w-0 flex-1">
          <h1 className="text-lg font-semibold leading-tight tracking-tight">Commons</h1>
          <p className="text-xs text-fg-muted">Koramangala · 5th Block</p>
        </div>
        <button type="button" aria-label="Search" className="grid size-10 place-items-center rounded-full hover:bg-surface-2"><Search className="size-5" /></button>
        <button type="button" aria-label="Notifications" className="grid size-10 place-items-center rounded-full hover:bg-surface-2"><Bell className="size-5" /></button>
      </header>

      <section aria-busy={!posts} aria-live="polite" className="flex-1">
        {!posts
          ? [0, 1].map((k) => (
              <div key={k} aria-hidden className="grid gap-2.5 border-b border-border px-4 py-3.5">
                <div className="flex items-center gap-2.5">
                  <span className={bone + " size-8 rounded-full"} />
                  <div className="grid flex-1 gap-1.5"><span className={bone + " h-[11px] w-[46%]"} /><span className={bone + " h-[9px] w-[28%]"} /></div>
                </div>
                <span className={bone + " h-[11px] w-full"} />
                <span className={bone + " h-[11px] w-[72%]"} />
                {k === 0 && <span className={bone + " h-[116px] rounded-lg"} />}
                <span className={bone + " h-[11px] w-[44%]"} />
              </div>
            ))
          : posts.map((p) => (
              <article key={p.id} className="grid gap-2.5 border-b border-border px-4 py-3.5">
                <div {...rise()}>
                  <div className="flex items-center gap-2.5">
                    <span className="grid size-8 place-items-center rounded-full bg-primary-soft text-[11.5px] font-semibold text-primary-soft-fg">{p.initials}</span>
                    <div><p className="text-[13.5px] font-semibold leading-[18px]">{p.name}</p><p className="text-xs text-fg-muted">{p.meta}</p></div>
                  </div>
                </div>
                {p.event && <p {...rise()}><span className="inline-flex h-6 items-center gap-1.5 rounded-full bg-info-soft px-2 text-[11.5px] font-medium text-info-fg"><CalendarDays className="size-3.5" />{p.event}</span></p>}
                <p {...rise()}><span className="text-[13.5px] leading-[19px]">{p.body}</span></p>
                {p.photo && <div {...rise()}><div className="h-[116px] rounded-lg bg-gradient-to-br from-neutral-300 to-neutral-600" /></div>}
                <div {...rise()}>
                  <div className="flex gap-4 text-[12.5px] text-fg-muted">
                    <button type="button" className="inline-flex items-center gap-1.5"><Heart className="size-4" />{p.likes}</button>
                    <button type="button" className="inline-flex items-center gap-1.5"><MessageCircle className="size-4" />{p.comments}</button>
                    <button type="button" className="inline-flex items-center gap-1.5"><Share className="size-4" />Share</button>
                  </div>
                </div>
              </article>
            ))}
      </section>
    </main>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.