Browse all documentation

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

admin/src/components/post-cards.svelte
<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:

admin/src/components/post-cards.css
@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

admin/src/admin.config.ts
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.