Skip to content

Slot Examples

This page shows concrete examples for every slot zone across all supported page types. Each example includes the manifest YAML and the matching primitive component.

The examples use a "products" entity throughout so you can see how __slotContext carries entity information from page to primitive.

Entity-list page

Available zones: header, toolbar, table, footer.

Prepend before the header

Add a dismissible banner above the page title.

yaml
pages:
  - key: products-list
    type: entity-list
    title: Products
    path: /products
    entity: product
    slotBindings:
      - slot: header.before
        primitive: beta-banner
        props:
          message: "Product management is in preview."
typescript
// primitives/beta-banner/BetaBanner.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface BetaBannerProps {
  message: string;
  __slotContext?: SlotContext;
}

export function BetaBanner({ message }: BetaBannerProps) {
  return (
    <div className="bg-yellow-50 border-b border-yellow-200 px-6 py-2 text-sm text-yellow-800">
      {message}
    </div>
  );
}

The banner renders above the page title row. The rest of the page is unchanged.


Append after the toolbar

Add an export button next to the search bar.

yaml
slotBindings:
  - slot: toolbar.after
    primitive: export-button
    props:
      format: csv
typescript
// primitives/export-button/ExportButton.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface ExportButtonProps {
  format: 'csv' | 'json';
  __slotContext?: SlotContext;
}

export function ExportButton({ format, __slotContext }: ExportButtonProps) {
  const label = `Export ${__slotContext?.entityPluralName ?? 'records'} as ${format.toUpperCase()}`;

  return (
    <button
      type="button"
      className="h-8 px-3 rounded-md border text-sm"
      onClick={() => console.log(`export ${format}`)}
    >
      {label}
    </button>
  );
}

__slotContext.entityPluralName resolves to "Products" at runtime, producing "Export Products as CSV".


Replace the toolbar

Swap out the default search input entirely with a custom filter panel.

yaml
slotBindings:
  - slot: toolbar
    primitive: advanced-filter-panel
typescript
// primitives/advanced-filter-panel/AdvancedFilterPanel.tsx
import { useState } from 'react';
import type { SlotContext } from '@ikary/cell-primitives';

interface AdvancedFilterPanelProps {
  __slotContext?: SlotContext;
}

export function AdvancedFilterPanel({ __slotContext }: AdvancedFilterPanelProps) {
  const [status, setStatus] = useState('');

  return (
    <div className="flex gap-2">
      <select
        value={status}
        onChange={(e) => setStatus(e.target.value)}
        className="h-8 rounded-md border px-3 text-sm"
      >
        <option value="">All statuses</option>
        <option value="active">Active</option>
        <option value="archived">Archived</option>
      </select>
      <span className="text-xs text-muted-foreground self-center">
        Filtering {__slotContext?.entityPluralName}
      </span>
    </div>
  );
}

The default search input no longer renders. Only advanced-filter-panel appears in the toolbar zone.


Append a summary card after the table

Show totals below the data grid.

yaml
slotBindings:
  - slot: table.after
    primitive: record-count-summary
typescript
// primitives/record-count-summary/RecordCountSummary.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface RecordCountSummaryProps {
  __slotContext?: SlotContext;
}

export function RecordCountSummary({ __slotContext }: RecordCountSummaryProps) {
  return (
    <div className="px-4 py-2 text-xs text-muted-foreground border-t">
      Showing {__slotContext?.entityPluralName ?? 'records'} from live data source.
    </div>
  );
}

Override the default pagination controls.

yaml
slotBindings:
  - slot: footer
    primitive: compact-pagination
typescript
// primitives/compact-pagination/CompactPagination.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface CompactPaginationProps {
  __slotContext?: SlotContext;
}

export function CompactPagination({ __slotContext }: CompactPaginationProps) {
  return (
    <div className="flex justify-end px-6 py-3 border-t text-sm">
      <span>{__slotContext?.pageTitle} — page controls</span>
    </div>
  );
}

Multiple bindings on one page

Bindings stack in declaration order. Prepend and append bindings accumulate; replace bindings follow last-wins.

yaml
slotBindings:
  - slot: header.before
    primitive: beta-banner
    props:
      message: "Preview mode."
  - slot: toolbar.after
    primitive: export-button
    props:
      format: csv
  - slot: toolbar.after
    primitive: bulk-actions
  - slot: table.after
    primitive: record-count-summary

The two toolbar.after bindings both render, in order: export-button appears first, then bulk-actions.


Entity-detail page

Available zones: header, navigation, content, footer.

Prepend a warning above the header

yaml
pages:
  - key: product-detail
    type: entity-detail
    title: Product
    path: /products/:id
    entity: product
    slotBindings:
      - slot: header.before
        primitive: archive-warning
typescript
// primitives/archive-warning/ArchiveWarning.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface ArchiveWarningProps {
  __slotContext?: SlotContext;
}

export function ArchiveWarning({ __slotContext }: ArchiveWarningProps) {
  return (
    <div className="bg-red-50 border-b border-red-200 px-6 py-2 text-sm text-red-800">
      This {__slotContext?.entityName ?? 'record'} is archived and read-only.
    </div>
  );
}

The warning appears above the entity name and action buttons in the header.


Replace the navigation tabs

Swap the standard Overview / History / Audit tabs with a custom set.

yaml
slotBindings:
  - slot: navigation
    primitive: product-tabs
typescript
// primitives/product-tabs/ProductTabs.tsx
import { NavLink } from 'react-router-dom';
import type { SlotContext } from '@ikary/cell-primitives';

interface ProductTabsProps {
  __slotContext?: SlotContext;
}

export function ProductTabs({ __slotContext }: ProductTabsProps) {
  const base = `/products/${window.location.pathname.split('/').at(-1)}`;

  return (
    <nav className="flex gap-4 px-6 border-b">
      <NavLink to={base} end className="py-2 text-sm border-b-2 border-transparent [&.active]:border-primary">
        Details
      </NavLink>
      <NavLink to={`${base}/inventory`} className="py-2 text-sm border-b-2 border-transparent [&.active]:border-primary">
        Inventory
      </NavLink>
      <NavLink to={`${base}/pricing`} className="py-2 text-sm border-b-2 border-transparent [&.active]:border-primary">
        Pricing
      </NavLink>
    </nav>
  );
}

yaml
slotBindings:
  - slot: content.after
    primitive: related-orders
    props:
      maxItems: 5
typescript
// primitives/related-orders/RelatedOrders.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface RelatedOrdersProps {
  maxItems?: number;
  __slotContext?: SlotContext;
}

export function RelatedOrders({ maxItems = 5, __slotContext }: RelatedOrdersProps) {
  return (
    <div className="border-t px-6 py-4">
      <h3 className="text-sm font-medium mb-3">
        Recent orders for this {__slotContext?.entityName ?? 'record'}
      </h3>
      <p className="text-xs text-muted-foreground">
        Showing up to {maxItems} orders.
      </p>
    </div>
  );
}

yaml
slotBindings:
  - slot: footer.before
    primitive: unsaved-changes-reminder
typescript
// primitives/unsaved-changes-reminder/UnsavedChangesReminder.tsx
export function UnsavedChangesReminder() {
  return (
    <div className="px-6 py-2 text-xs text-amber-700 bg-amber-50 border-t border-amber-200">
      You have unsaved changes. Submit the form to save.
    </div>
  );
}

Dashboard page

Available zones: header, content, footer.

Replace the header with a custom title and date picker

yaml
pages:
  - key: analytics-dashboard
    type: dashboard
    title: Analytics
    path: /analytics
    slotBindings:
      - slot: header
        primitive: dashboard-header
        props:
          subtitle: "Live data. Refreshes every 60 seconds."
typescript
// primitives/dashboard-header/DashboardHeader.tsx
import { useState } from 'react';
import type { SlotContext } from '@ikary/cell-primitives';

interface DashboardHeaderProps {
  subtitle?: string;
  __slotContext?: SlotContext;
}

export function DashboardHeader({ subtitle, __slotContext }: DashboardHeaderProps) {
  const [range, setRange] = useState('7d');

  return (
    <div className="flex items-center justify-between px-6 py-4 border-b">
      <div>
        <h1 className="text-xl font-semibold">{__slotContext?.pageTitle}</h1>
        {subtitle && <p className="text-sm text-muted-foreground mt-0.5">{subtitle}</p>}
      </div>
      <select
        value={range}
        onChange={(e) => setRange(e.target.value)}
        className="h-8 rounded-md border px-3 text-sm"
      >
        <option value="1d">Last 24h</option>
        <option value="7d">Last 7 days</option>
        <option value="30d">Last 30 days</option>
      </select>
    </div>
  );
}

Prepend an alert summary above dashboard content

yaml
slotBindings:
  - slot: content.before
    primitive: alert-summary
    props:
      severity: warning
typescript
// primitives/alert-summary/AlertSummary.tsx
interface AlertSummaryProps {
  severity: 'info' | 'warning' | 'error';
}

export function AlertSummary({ severity }: AlertSummaryProps) {
  const colors: Record<string, string> = {
    info: 'bg-blue-50 text-blue-800 border-blue-200',
    warning: 'bg-yellow-50 text-yellow-800 border-yellow-200',
    error: 'bg-red-50 text-red-800 border-red-200',
  };

  return (
    <div className={`px-6 py-3 border-b text-sm ${colors[severity]}`}>
      3 active alerts require attention.
    </div>
  );
}

Replace dashboard content entirely

yaml
slotBindings:
  - slot: content
    primitive: kpi-dashboard
typescript
// primitives/kpi-dashboard/KpiDashboard.tsx
export function KpiDashboard() {
  return (
    <div className="grid grid-cols-3 gap-6 p-6">
      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Total Revenue</p>
        <p className="text-2xl font-semibold mt-1">$84,200</p>
      </div>
      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Active Users</p>
        <p className="text-2xl font-semibold mt-1">1,204</p>
      </div>
      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Open Tickets</p>
        <p className="text-2xl font-semibold mt-1">17</p>
      </div>
    </div>
  );
}

The default content placeholder no longer renders. kpi-dashboard fills the entire content zone.


Custom page

Custom pages have a single content zone. Set primitive on the page to render a registered primitive as the page body.

Full page via blank-slot

Use blank-slot as the page primitive when you want to compose the content entirely from slot bindings.

yaml
pages:
  - key: announcements
    type: custom
    title: Announcements
    path: /announcements
    primitive: blank-slot
    slotBindings:
      - slot: content
        primitive: announcement-feed
typescript
// primitives/announcement-feed/AnnouncementFeed.tsx
export function AnnouncementFeed() {
  return (
    <div className="p-6 space-y-4">
      <h1 className="text-xl font-semibold">Announcements</h1>
      <ul className="space-y-2 text-sm text-muted-foreground">
        <li>v2.1 released — see release notes.</li>
        <li>Scheduled maintenance on Friday at 02:00 UTC.</li>
      </ul>
    </div>
  );
}

Stacking prepend and append around blank-slot

yaml
pages:
  - key: help-center
    type: custom
    title: Help Center
    path: /help
    primitive: blank-slot
    slotBindings:
      - slot: content.before
        primitive: help-search-bar
      - slot: content.before
        primitive: help-categories
      - slot: content.after
        primitive: help-footer-links

Both content.before bindings render in order: help-search-bar appears first, then help-categories. help-footer-links renders after the (empty) body.


Clearing a built-in zone with blank-slot

Use blank-slot to suppress an existing zone on an entity page.

yaml
pages:
  - key: products-list
    type: entity-list
    title: Products
    path: /products
    entity: product
    slotBindings:
      - slot: toolbar
        primitive: blank-slot
      - slot: toolbar.after
        primitive: custom-search-bar

blank-slot replaces the default toolbar content. custom-search-bar appends after the now-empty zone. The result is that only custom-search-bar renders in the toolbar area.


Using slotMode and slotZone in a primitive

A single primitive can adapt its appearance based on where it is placed.

typescript
// primitives/context-banner/ContextBanner.tsx
import type { SlotContext } from '@ikary/cell-primitives';

interface ContextBannerProps {
  message: string;
  __slotContext?: SlotContext;
}

export function ContextBanner({ message, __slotContext }: ContextBannerProps) {
  const zone = __slotContext?.slotZone ?? 'unknown';
  const mode = __slotContext?.slotMode ?? 'replace';

  const borderClass =
    mode === 'prepend' ? 'border-b' :
    mode === 'append'  ? 'border-t' :
                         'border-y';

  return (
    <div className={`px-6 py-2 text-sm bg-muted ${borderClass}`}>
      <span className="text-muted-foreground">[{zone}]</span> {message}
    </div>
  );
}

Bind the same primitive at different zones:

yaml
slotBindings:
  - slot: header.before
    primitive: context-banner
    props:
      message: "Shown above the header."
  - slot: footer.after
    primitive: context-banner
    props:
      message: "Shown below the footer."

When rendered in header.before, slotMode is "prepend" and slotZone is "header". When rendered in footer.after, they are "append" and "footer".