Skip to content
ilinxa/pro-ui

Magazine Layout

alphav0.3.0

Slot-based magazine layout — hero, filter bar, sidebar, and a mixed-size article grid with infinite scroll and a filter hook.

Category: LayoutUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

The layout assembly for the news-domain family. Generic over `T`; renders cards via a `renderItem(item, slot)` callback so the layout itself imports nothing from sibling registry components — composition happens at the consumer level. Pair with news-card (renderItem), filter-bar (filterBar slot), category-cloud + newsletter-signup (sidebar slot), and page-hero (hero slot) for the full kasder magazine experience. The companion `useMagazineFilter` hook gives the simple consumer one-line filter+page state; sophisticated consumers skip the hook and drive props from React Query / their router. Migration origin: kasder kas-social-front-v0 NewsMagazineGrid.tsx (~320-line component) → distilled to this slot-based shell + a 60-line filter hook. The original's filter logic / chip row / search / date picker / sidebar contents all moved into the dedicated sibling components.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/magazine-layout

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/magazine-layout-fixtures

Preview

Urban

Featured: Editorial team announces 2026 redesign

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Sustainability

Article 2: Urban planning trends shaping the year ahead

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 11, 2026

Tech

Article 3: Sustainability moves to center stage

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 10, 2026

Events

Article 4: Smart city tech rolls out across metros

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 9, 2026

Research

Article 5: Public transit goes electric

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 8, 2026

Urban

Article 6: Permitting goes digital, finally

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 7, 2026

Sustainability

Article 7: Award-winning planners share their process

A short summary of the article — used to give the reader a taste of the content before they click through to the full piece.

Aug 6, 2026

Demo source

demo.tsxtsx

Usage

When to use

Reach for MagazineLayout when you need a magazine- style layout: optional hero band, optional filter row, descending- density card tower in a main column, optional sticky sidebar, infinite scroll. The layout is generic over your item type and slot-based — bring your own cards, hero, filter bar, sidebar.

Minimal example

import { MagazineLayout } from "@/components/magazine-layout";

<MagazineLayout<Article>
  displayedItems={articles}
  renderItem={(article, slot) => (
    <ArticleCard article={article} variant={slot} />
  )}
/>;

slot is either "large" (lead article) or "medium" (the rest of the tower). Your renderer maps slot to a card variant.

Full magazine composition (the canonical use case)

Combines all four sibling components from the news family. The layout itself imports nothing from them — composition happens at the consumer level.

import { MagazineLayout, useMagazineFilter } from "@/components/magazine-layout";
import { NewsCard } from "@/components/news-card";
import { FilterBar } from "@/components/filter-bar";
import { CategoryCloud } from "@/components/category-cloud";
import { NewsletterSignup } from "@/components/newsletter-signup";
import { PageHero } from "@/components/page-hero";

const filtered = useMagazineFilter<Article>({
  items: articles,
  pageSize: 6,
  isFeatured: (a) => a.featured,
  filterPredicate: (a) => a.title.includes(search) && (cat ? a.category === cat : true),
  sortComparator: (a, b) => +new Date(b.date) - +new Date(a.date),
});

<MagazineLayout<Article>
  hero={<PageHero badge="News" title="Latest Stories" description="..." />}
  filterBar={<FilterBar categories={categories} onChange={...} />}
  sidebar={
    <>
      <CategoryCloud items={categories} value={cat} onChange={setCat} title="Categories" />
      <NewsletterSignup onSubmit={subscribe} />
    </>
  }
  displayedItems={filtered.displayedItems}
  featuredItem={filtered.featuredItem}
  hasMore={filtered.hasMore}
  isLoading={filtered.isLoading}
  onLoadMore={filtered.loadMore}
  renderItem={(article, slot) => (
    <NewsCard item={article} variant={slot} href={`/news/${article.id}`} />
  )}
/>;

Companion hook: useMagazineFilter

For consumers who don't need server-driven filtering / pagination, useMagazineFilter derives all the props the layout expects from your full items array + filter / sort / feature predicates.

const {
  displayedItems,
  featuredItem,
  hasMore,
  isLoading,
  filteredCount,
  loadMore,
  reset,
} = useMagazineFilter<Article>({
  items,
  pageSize: 6,
  isFeatured: (a) => a.featured,
  filterPredicate: (a) => /* filter */,
  sortComparator: (a, b) => /* sort */,
  simulatedLoadingMs: 500, // optional artificial delay for demos
});

Slot-only patterns

  • Omit sidebar — main column expands to full width.
  • Omit hero and filterBar — bare layout for documentation indexes or simple lists.
  • Provide emptyState for a custom empty fallback; otherwise the default labels.emptyStateText renders.
  • Use renderFeatured for a different visual on the featured item (e.g., a hero card variant) vs. the in-tower large.

Server-driven filtering / pagination

Skip the companion hook. Drive displayedItems / hasMore / isLoading / onLoadMore from your data layer (React Query, Tanstack Router, etc.) directly:

const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery(...);

<MagazineLayout
  displayedItems={data?.pages.flatMap(p => p.items) ?? []}
  hasMore={!!hasNextPage}
  isLoading={isFetching}
  onLoadMore={() => fetchNextPage()}
  renderItem={...}
/>;

Accessibility

  • Sidebar uses <aside> for landmark semantics.
  • Loader region has aria-live="polite" with a visually-hidden text label.
  • End-of-list announcement also uses aria-live="polite".
  • Tower is plain CSS grid — natural Tab order through items.

Features

  • Slot-based layout — `hero` / `filterBar` / `sidebar` / `renderItem` / `renderFeatured` / `emptyState`
  • Generic over item type via `<MagazineLayout<T>>`
  • Magazine-tower main column — 1 large + 2-up medium row + 3-up medium grid
  • Persistent featured slot via `featuredItem` prop — the companion hook keeps it out of the paged regular list, so it renders once and survives load-more
  • Sticky sidebar (`sticky top-24`) with `<aside>` landmark
  • IntersectionObserver-driven infinite scroll via internal `useInfiniteScroll` hook
  • Bouncing-3-dots loader with `aria-live='polite'` and visually-hidden text label
  • End-of-list announcement also `aria-live='polite'`
  • Empty state slot or `labels.emptyStateText` fallback
  • Companion `useMagazineFilter` hook — in-memory filter + sort + page + simulated loading
  • Sealed-folder convention — layout imports zero sibling registry components at runtime

Tags

magazine-layoutlayoutmagazinefeedinfinite-scrollmigrationnews

Dependencies