Show collection results as cards
Replace a collection table with your own layout while keeping search, filters, pagination, and bulk actions.
Use listResultsRenderers when you want a different layout for a collection’s documents, such as
cards or a compact list. Ridu still handles search, filters, pagination, permissions,
and bulk actions. Your component displays the current results.
To replace the toolbar and the rest of the page too, use a whole-screen replacement instead.
1. Render the documents
<script lang="ts">
import type { AdminListResultsRendererProps } from '@riducms/plugin';
import { Link } from '@riducms/admin/routing';
import { Button } from '@riducms/ui';
import './post-cards.css';
let { list }: AdminListResultsRendererProps = $props();
</script>
<div class="grid gap-4 py-4 sm:grid-cols-2">
{#each list.documents as document (document.id)}
<article class="space-y-3 rounded-lg border p-5">
<Link
to={list.documentHref(document)}
class="block text-lg font-medium hover:underline"
>
{list.title(document)}
</Link>
{#if typeof document.readingMinutes === 'number'}
<p class="text-sm">{document.readingMinutes} min read</p>
{/if}
<Button
variant="outline"
class="post-cards__select"
aria-label={`Select ${list.title(document)}`}
aria-pressed={list.selectedIDs.has(document.id)}
disabled={!list.selectionEnabled}
onclick={() =>
list.toggleDocument(
document.id,
!list.selectedIDs.has(document.id)
)}
>
{list.selectedIDs.has(document.id) ? 'Selected' : 'Select'}
</Button>
</article>
{/each}
</div>AdminListResultsRendererProps supplies the
current list. Use it directly—do not fetch the documents again or copy them into local state.
Each document holds what the list shows: its ID, timestamps, title field and visible columns.
The list doesn’t return other fields, so name any other field the layout uses in fields when
you register it (step 2). Here, readingMinutes appears on every card even when its column is
hidden. A relationship or upload field named there arrives as the related document’s ID and
title.
list.title(document) uses the collection’s configured title field, falling back to its ID.
list.documentHref(document) builds the document destination with its encoded ID and content
locale. Link adds the configured admin base path and keeps normal admin navigation,
including unsaved-change blocking.
You own the markup. You can make an entire card the link, put an image inside it, or use a separate button-shaped link. Keep selection controls outside links so they remain separate keyboard and pointer targets.
The Select button updates Ridu’s existing selection. Selected records can then use the
normal bulk actions; selectionEnabled disables changes while the list cannot accept them.
Style the selection control
Button keeps its Ridu variant classes when you pass class. The example imports this
component’s stylesheet, which can set the selection button’s alignment:
@import '@riducms/ui/layers.css';
@layer app {
.post-cards__select {
justify-content: flex-start;
}
}The app layer follows the framework’s ridu layer. Use the
CSS customization guide for theme variables and other control
overrides.
2. Choose the collection
import { defineAdmin } from '@riducms/plugin/admin';
import { generatedAdminPlugins } from './ridu.plugins.generated';
import PostCards from './components/post-cards.svelte';
export default defineAdmin({
plugins: generatedAdminPlugins,
listResultsRenderers: [
{
key: 'post-cards',
collection: 'posts',
fields: ['readingMinutes'],
component: PostCards
}
]
});fields names the other top-level fields the cards read. Without it, a card loses
readingMinutes whenever an author hides that column. Ridu checks the names against the
collection when it builds the admin.
Add this entry to your existing admin.config.ts, keeping its other settings.
With bun run dev running, open Posts. Existing posts should appear as cards below
the normal search and filter controls. Select one to use the collection’s bulk actions,
or click its title to open it.
Create a post first if the collection is empty. Ridu supplies the empty, loading, and error messages around your results component. Trash continues to use the built-in restore/delete table.
Only one results component can target a collection. Do not register both listResultsRenderers and
a collectionList whole-screen replacement for the same collection.
To change only one cell in the normal table, use Table cells.