# Cards Feed

Cards Feed — Legend List documentation.

Source: https://legend.so/list/v3/react/examples/cards-feed/

Legend List v3 documentation. Current documentation version.

The canonical example uses \`useWindowScroll\`. The embedded docs version is framed so it fits inside this page, but the source below shows the real example implementation.

Features used

-   `useWindowScroll` so the list uses the page scroll position instead of an internal scroller.
-   `useRecyclingState` to preserve poll selections, likes, and expansion state while interactive cards recycle.
-   `extraData.recycleState` to opt specific row-local state into recycling-aware storage for mixed interactive card types.

example-web/src/examples/CardsFeedExample.tsx

[View source in legend-list](https://github.com/LegendApp/legend-list/blob/a71f2cf68b30061f72bc0735b1326c6a21e2f762/example-web/src/examples/CardsFeedExample.tsx)

```tsx
import React, { memo } from "react";

import { LegendList, type LegendListRenderItemProps, useRecyclingState } from "@legendapp/list/react";
import { buildFeedCards, type FeedCard, type FeedPollOption } from "@examples/commerce";
import { buttonStyle, CARD_CLASS, Shell } from "./shared";

const feedCards = buildFeedCards();

function pollVotesForOption(optionId: string, option: FeedPollOption, selectedOptionId: string | null) {
    return option.votes + (selectedOptionId === optionId ? 1 : 0);
}

const FeedCardItem = memo(({ item, extraData }: LegendListRenderItemProps<FeedCard>) => {
    const [isExpandedValue, setExpanded] = extraData?.recycleState
        ? useRecyclingState(() => false)
        : React.useState(false);
    const [isLikedValue, setLiked] = extraData?.recycleState ? useRecyclingState(() => false) : React.useState(false);
    const [selectedOptionIdValue, setSelectedOptionId] = extraData?.recycleState
        ? useRecyclingState<string | null>(() => null)
        : React.useState<string | null>(null);

    const isExpanded = Boolean(isExpandedValue);
    const isLiked = Boolean(isLikedValue);
    const selectedOptionId = selectedOptionIdValue ?? null;

    return (
        <div className={`${CARD_CLASS} bg-white`}>
            <div className="mb-3 flex items-center gap-3">
                <div
                    className="flex h-10 w-10 items-center justify-center rounded-full font-extrabold text-[#0f172a]"
                    style={{
                        background: item.accentColor,
                    }}
                >
                    {item.author.slice(0, 1)}
                </div>
                <div className="flex-1">
                    <div className="font-extrabold">{item.author}</div>
                    <div className="text-[13px] text-[#64748b]">{item.timestampLabel}</div>
                </div>
                <div className="rounded-full bg-[#eef2ff] px-[10px] py-[6px] text-xs font-bold capitalize text-[#4338ca]">
                    {item.kind}
                </div>
            </div>

            {item.kind === "story" ? (
                <>
                    <div className="mb-[10px] inline-block rounded-full bg-[#f8fafc] px-[10px] py-[6px] text-xs font-bold text-[#334155]">
                        {item.categoryLabel}
                    </div>
                    <div className="text-lg font-extrabold">{item.title}</div>
                    <div className="mt-[10px] leading-[1.55] text-[#334155]">{item.body}</div>
                </>
            ) : null}

            {item.kind === "photo" ? (
                <>
                    <div
                        className="mb-3 flex flex-col justify-end rounded-[18px] p-[14px] text-[#0f172a]"
                        style={{
                            background: item.accentColor,
                            height: item.mediaHeight,
                        }}
                    >
                        <div className="text-xs font-extrabold uppercase opacity-[0.72]">{item.mediaLabel}</div>
                        <div className="mt-1.5 text-[20px] font-extrabold">{item.title}</div>
                        <div className="mt-1.5 max-w-[260px] opacity-[0.78]">{item.mediaSubtitle}</div>
                    </div>
                    <div className="leading-[1.55] text-[#334155]">{item.body}</div>
                </>
            ) : null}

            {item.kind === "poll" ? (
                <>
                    <div className="text-lg font-extrabold">{item.title}</div>
                    <div className="mt-[10px] leading-[1.55] text-[#334155]">{item.body}</div>
                    <div className="mt-[14px] grid gap-[10px]">
                        {item.pollOptions.map((option) => {
                            const votes = pollVotesForOption(option.id, option, selectedOptionId);
                            const totalVotes = item.totalVotes + (selectedOptionId ? 1 : 0);
                            const width = `${Math.max(18, Math.round((votes / totalVotes) * 100))}%`;
                            const isSelected = selectedOptionId === option.id;
                            return (
                                <button
                                    className="cursor-pointer overflow-hidden rounded-2xl p-0 text-left"
                                    key={option.id}
                                    onClick={() => {
                                        if (!isSelected) {
                                            setSelectedOptionId(option.id);
                                        }
                                    }}
                                    style={{
                                        background: isSelected ? "#dbeafe" : "#f8fafc",
                                        border: isSelected ? "1px solid #60a5fa" : "1px solid #e2e8f0",
                                    }}
                                    type="button"
                                >
                                    <div
                                        className="h-full px-[14px] py-3"
                                        style={{
                                            background: isSelected ? "#bfdbfe" : "#e2e8f0",
                                            height: "100%",
                                            minWidth: width,
                                        }}
                                    >
                                        <div className="font-bold">{option.label}</div>
                                        <div className="mt-1 text-xs text-[#64748b]">{votes} votes</div>
                                    </div>
                                </button>
                            );
                        })}
                    </div>
                </>
            ) : null}

            {item.kind === "quote" ? (
                <>
                    <div
                        className="rounded-2xl bg-[#f8fafc] p-4"
                        style={{
                            borderLeft: `4px solid ${item.accentColor}`,
                        }}
                    >
                        <div className="text-[20px] leading-[1.5] font-bold text-[#0f172a]">"{item.quote}"</div>
                        <div className="mt-[10px] text-[#64748b]">{item.source}</div>
                    </div>
                    <div className="mt-3 leading-[1.55] text-[#334155]">{item.body}</div>
                </>
            ) : null}

            {item.kind === "event" ? (
                <>
                    <div className="mb-3 flex gap-2">
                        <div className="rounded-full bg-[#dcfce7] px-[10px] py-[6px] text-xs font-bold text-[#166534]">
                            {item.highlight}
                        </div>
                        <div className="rounded-full bg-[#f8fafc] px-[10px] py-[6px] text-xs font-bold text-[#334155]">
                            {item.attendeesLabel}
                        </div>
                    </div>
                    <div className="text-lg font-extrabold">{item.title}</div>
                    <div className="mt-[10px] leading-[1.55] text-[#334155]">{item.body}</div>
                    <div className="mt-3 text-[#64748b]">{item.location}</div>
                </>
            ) : null}

            {item.kind !== "poll" && isExpanded ? (
                <div className="mt-[14px] leading-[1.6] text-[#334155]">{item.expandedBody}</div>
            ) : null}

            <div className="mt-4 flex gap-[10px]">
                <button className={buttonStyle(isLiked)} onClick={() => setLiked((current) => !current)} type="button">
                    {isLiked ? "Liked" : "Like"} · {item.reactionCount + (isLiked ? 1 : 0)}
                </button>
                <div className="self-center text-[13px] text-[#64748b]">{item.commentCount} comments</div>
                {item.kind !== "poll" ? (
                    <button className={buttonStyle()} onClick={() => setExpanded((current) => !current)} type="button">
                        {isExpanded ? "Collapse" : "Expand"}
                    </button>
                ) : null}
            </div>
        </div>
    );
});

export function CardsFeedExample() {
    return (
        <Shell title="Cards Feed" windowScroll>
            <div className="flex min-w-0 flex-1 flex-col gap-4">
                <div className="rounded-[20px] border border-slate-200 bg-white p-4 text-slate-600">
                    <div className="font-extrabold text-slate-950">Window scroll example</div>
                    <div className="mt-2 leading-[1.6]">
                        This example uses the browser window as the scroll container. There is normal page content above
                        and below the list so the page stays scrollable.
                    </div>
                </div>

                <LegendList
                    className="rounded-[26px] border border-slate-200 bg-slate-50"
                    contentContainerStyle={{ padding: 8 }}
                    data={feedCards}
                    estimatedItemSize={286}
                    extraData={{ recycleState: true }}
                    keyExtractor={(item) => item.id}
                    recycleItems
                    renderItem={(props) => <FeedCardItem {...props} />}
                    useWindowScroll
                />

                <div className="rounded-[20px] border border-slate-200 bg-white p-4 text-slate-600">
                    <div className="font-extrabold text-slate-950">Below the list</div>
                    <div className="mt-2 leading-[1.6]">
                        Keeping regular content after the list makes it clear that the document is scrolling instead of
                        an internal list viewport.
                    </div>
                </div>
            </div>
        </Shell>
    );
}
```

[View Markdown](https://legend.so/list/v3/react/examples/cards-feed.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Cards+Feed&body=Page%3A+https%3A%2F%2Flegend.so%2Flist%2Fv3%2Freact%2Fexamples%2Fcards-feed%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
