Dyrecteddyrected
Admin UI

Admin UI Configuration

Complete reference for all admin.* options on collections, globals, and fields that control the Dyrected Admin dashboard.

The admin object on a CollectionConfig, GlobalConfig, or Field configures how that item appears and behaves in the Admin UI. These options have no effect on the API or database — they are purely presentation and UX controls.


Collection Labels

The top-level labels object customizes the human-readable names Dyrected uses for this collection throughout the Admin UI — in buttons, breadcrumbs, and navigation.

export const Posts = defineCollection({
  slug: 'posts',
  labels: {
    singular: 'Blog Post',
    plural: 'Blog Posts',
  },
  admin: {
    icon: 'Newspaper',
    useAsTitle: 'title',
    ...
  },
})

Prop

Type


Collection admin Options

The admin object on a collection controls how it appears in the sidebar, what the list table looks like, and whether the edit page shows a live preview pane. These options have no effect on the API or database.

export const Posts = defineCollection({
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'status', 'author', 'publishedAt'],
    group: 'Blog',
    hidden: false,
    previewUrl: (doc) => `https://mysite.com/posts/${doc.slug}`,
    previewMode: 'postMessage',
  },
  fields: [...],
})

Prop

Type

useAsTitle

The field referenced by useAsTitle is used in:

  • The list table first column (document title link)
  • The breadcrumb in the edit page header
  • The relationship picker label when this collection is related to another
  • The Dashboard recent edits list
// Collection: products
admin: {
  useAsTitle: "name";
}

// In a relationship picker for products, each option shows item.name instead of item.id

defaultColumns

Controls exactly which fields appear as columns in the list view and in what order.

admin: {
  defaultColumns: ['title', 'status', 'author', 'publishedAt'],
}

Fields not in this list are still accessible in the Edit form — they simply don't appear as initial list columns. If a field name in defaultColumns does not exist in fields, it is silently ignored.

Editors can still reorder or toggle columns using the View button. Their choices are saved as personal preferences and can be overridden at the team level by an admin. See View Preferences for the full cascade and API reference.

filterable

Controls whether the list page exposes the filter builder for a collection.

admin: {
  filterable: false,
}

Set this to false for collections where filtering is not useful for editors, or where you want to keep the list toolbar simpler.

group

// These two collections will be grouped together under "Content" in the sidebar
export const Posts = defineCollection({ slug: 'posts', admin: { group: 'Content' }, ... })
export const Pages = defineCollection({ slug: 'pages', admin: { group: 'Content' }, ... })

// This collection appears under "Users"
export const Members = defineCollection({ slug: 'members', admin: { group: 'Users' }, ... })

Collections with no group are listed at the top of the sidebar ungrouped.

previewUrl

URL to open in the Live Preview pane when editing a document. There are two ways to define the preview URL: using a JavaScript function or a Jexl string expression.

When to use which:

  • Jexl String (Recommended): The safest and most robust approach. It works locally and is required if you use Dyrected Cloud Sync. The Admin automatically evaluates strings containing +, ?, ==, or siteUrl using Jexl, which is a safe, sandboxed expression language.
  • JavaScript Function: Works well for complex local-only configurations where you don't need to sync your schema to the cloud.

Dyrected Cloud Sync limitation If you use Dyrected Cloud Sync, your schema is serialized to JSON before being sent to the cloud. Because JSON cannot serialize JavaScript functions, any function passed to previewUrl will be stringified and break the Admin UI. Cloud users must use Jexl strings.

export default defineConfig({
  collections: [
    {
      slug: "posts",
      admin: {
        // Safely resolves slug and maps 'home' to '/'
        previewUrl: "slug ? (slug == 'home' ? '/' : '/' + slug) : null",
      },
    },
  ],
});

Example 2: JavaScript Function (Local only)

export default defineConfig({
  collections: [
    {
      slug: "posts",
      admin: {
        previewUrl: (doc, { locale }) => {
          if (!doc?.slug) return null;
          return `https://mysite.com/posts/${doc.slug}`;
        },
      },
    },
  ],
});

If previewUrl evaluates to null or undefined, the preview pane is hidden.


Global admin Options

Globals support a subset of the collection admin options. Use these to control where the global appears in the sidebar and whether it is visible at all.

export const SiteSettings = defineGlobal({
  slug: 'site-settings',
  admin: {
    icon: 'Settings2',
    group: 'Configuration',
    hidden: false,
  },
  fields: [...],
})

Prop

Type


Field admin Options

Every field accepts an admin object that controls how it renders inside the edit form — its placeholder, help text, visibility, and conditional display rules. None of these options affect validation or the API response.

{
  name: 'internalNotes',
  type: 'textarea',
  admin: {
    placeholder: 'Internal use only...',
    description: 'Never shown publicly. Only visible to admin users.',
    readOnly: false,
    hidden: false,
    condition: (data) => data.status === 'published',
  }
}

Prop

Type

Character and word counters are configured on the field itself with maxLength and maxWords, not through generic admin options. The Admin UI still reads admin.maxLength and admin.maxWords as compatibility aliases for supported text inputs, but new schemas should prefer the top-level field properties.

component — Custom Field UI

Replace the default input for any field with your own component by setting admin.component to a unique string key:

{
  name: 'brandColor',
  type: 'text',
  admin: {
    component: 'products.brandColor',
    description: 'Brand hex color, e.g. #6366f1',
  },
}

Then pass your component to <DyrectedAdmin> under the matching key:

<DyrectedAdmin
  :components="{
    fields: {
      'products.brandColor': BrandColorPicker,
    },
  }"
/>

Your component receives value, onChange, field, path, disabled, and collection as props. See the Vue integration guide for a full walkthrough including virtual fields and copyable inputs.

layout — Built-in Field Presentation

Use admin.layout when a field has more than one built-in Admin renderer.

{
  name: 'featured',
  type: 'boolean',
  admin: {
    layout: 'switch',
  },
}

Boolean fields render as a checkbox by default. Set layout: 'switch' when a switch better communicates an on/off setting.

{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    layout: 'radio',
    direction: 'horizontal',
  },
}

Select fields render as dropdowns by default. Set layout: 'radio' for short option lists where all choices should be visible at once.

layout is not a universal field option. Dyrected now types it only on field variants that actually support alternate built-in renderers.

admin.hooks.options — Client-side Dependent Options

This hook is only available on option-bearing fields:

  • select
  • radio
  • multiSelect

Use it when the available choices depend on other values already present in the form and you want the options to update instantly without a network request.

{
  name: 'state',
  type: 'select',
  admin: {
    hooks: {
      options: ({ siblingData }) => {
        if (siblingData.country === 'us') {
          return ['CA', 'NY', 'TX']
        }
        return []
      },
    },
  },
}

If the options require a database query, secret API key, or server-side cache, use the top-level field options resolver instead of admin.hooks.options.

condition — Conditional Fields

Use condition to show or hide a field based on the values of other fields in the same document. For maximum compatibility and security, Dyrected uses Jexl for conditions.

JEXL conditions are compiled and memoized in the Admin UI to ensure optimal typing performance. If a condition string contains syntax errors or throws evaluation errors, the Admin UI renders an inline red error boundary showing the field name and error traceback, while disabling the field.

String-based conditions are serializable and work across all environments (local, cloud, and production).

// Only show 'scheduledAt' when status is 'scheduled'
{
  name: 'scheduledAt',
  type: 'date',
  admin: {
    condition: 'status == "scheduled"',
  }
}

// Complex logic (Jexl supports logical operators)
{
  name: 'salePrice',
  type: 'number',
  admin: {
    condition: 'onSale == true && price > 0',
  }
}

Function Callbacks

Functions are supported in local/embedded mode but are stripped out when syncing your schema to the Dyrected Cloud dashboard. Use strings for cloud-compatible schemas.

{
  name: 'externalUrl',
  type: 'url',
  admin: {
    condition: (data) => data.contentType === 'link',
  }
}

The data argument is the current live values of the entire document as the editor is typing — not the saved version. The condition is re-evaluated on every keystroke.

condition only hides the field in the UI. The field is not removed from Zod validation or from the API. If you need the field to truly be absent from saves, combine condition with access.update.


Top-Level admin Config (on defineConfig)

In addition to per-collection and per-field options, you can configure global Admin UI branding at the top level:

export default defineConfig({
  collections: [...],
  globals: [...],
  admin: {
    branding: {
      logo: '/logo.svg',          // Path to your logo (shown in sidebar header)
      logoMark: '/logomark.svg',  // Compact mark shown when sidebar is collapsed
      primaryColor: '#6366f1',    // Accent color (overrides the default indigo)
      favicon: '/favicon.ico',
    },
    meta: {
      titleSuffix: '— My CMS',    // Appended to the browser tab title on every page
    },
  },
})

Prop

Type

Customizing the Admin UI with Component Slots

You can inject custom React or Vue components into specific areas of the Admin UI using Component Slots. First, declare the slot keys in your Dyrected configuration, then provide the matching components to the <DyrectedAdmin> wrapper.

Configure the Slot Keys

In your dyrected.config.ts, declare the component keys you want to render in the admin.components array for the dashboard or collections.

import { defineConfig, defineCollection } from '@dyrected/core'

export const Posts = defineCollection({
  slug: 'posts',
  admin: {
    components: {
      beforeList: ['posts-header'],
      afterListTable: ['custom-pagination'],
    }
  },
  fields: [...]
})

export default defineConfig({
  collections: [Posts],
  admin: {
    components: {
      beforeDashboard: ['welcome-banner'],
      afterDashboard: ['analytics-widget'],
    }
  }
})

Provide the Components

When mounting the Admin UI in your frontend, pass the components prop containing the component registries for dashboard and collectionList.

// app/admin/page.tsx
import { DyrectedAdmin } from "@dyrected/next/admin";

function WelcomeBanner(props) {
  return <div className="p-4 bg-blue-50">Welcome, {props.user.email}!</div>;
}

export default function AdminPage() {
  return (
    <DyrectedAdmin
      components={{
        dashboard: {
          "welcome-banner": WelcomeBanner,
          "analytics-widget": AnalyticsWidget,
        },
        collectionList: {
          "posts-header": PostsHeader,
          "custom-pagination": CustomPagination,
        },
      }}
    />
  );
}
import { DyrectedAdmin } from "@dyrected/react";
import "@dyrected/admin/styles";

function WelcomeBanner(props) {
  return <div className="p-4 bg-blue-50">Welcome, {props.user.email}!</div>;
}

export function AdminPage() {
  return (
    <DyrectedAdmin
      baseUrl="/dyrected"
      apiKey={import.meta.env.VITE_DYRECTED_API_KEY}
      components={{
        dashboard: {
          "welcome-banner": WelcomeBanner,
          "analytics-widget": AnalyticsWidget,
        },
        collectionList: {
          "posts-header": PostsHeader,
          "custom-pagination": CustomPagination,
        },
      }}
    />
  );
}

The @dyrected/nuxt module auto-imports <DyrectedAdmin /> — no explicit import needed.

<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({ layout: false });

import WelcomeBanner from "./WelcomeBanner.vue";
import AnalyticsWidget from "./AnalyticsWidget.vue";
import PostsHeader from "./PostsHeader.vue";
import CustomPagination from "./CustomPagination.vue";
</script>

<template>
  <ClientOnly>
    <DyrectedAdmin
      :components="{
        dashboard: {
          'welcome-banner': WelcomeBanner,
          'analytics-widget': AnalyticsWidget,
        },
        collectionList: {
          'posts-header': PostsHeader,
          'custom-pagination': CustomPagination,
        },
      }"
    />
  </ClientOnly>
</template>
<template>
  <DyrectedAdmin
    :components="{
      dashboard: {
        'welcome-banner': WelcomeBanner,
        'analytics-widget': AnalyticsWidget,
      },
      collectionList: {
        'posts-header': PostsHeader,
        'custom-pagination': CustomPagination,
      },
    }"
  />
</template>

<script setup>
import { DyrectedAdmin } from "@dyrected/vue";
import WelcomeBanner from "./WelcomeBanner.vue";
import AnalyticsWidget from "./AnalyticsWidget.vue";
import PostsHeader from "./PostsHeader.vue";
import CustomPagination from "./CustomPagination.vue";
</script>

Available Slots and Props

Dashboard Slots (beforeDashboard, afterDashboard): Components receive:

  • client: The initialized DyrectedClient for data fetching.
  • user: The currently authenticated admin user document.
  • schemas: The full loaded AdminSchemas object.

Collection List Slots (beforeList, beforeListTable, afterListTable, afterList): Components receive:

  • client: The initialized DyrectedClient for data fetching or mutations.
  • user: The currently authenticated admin user document.
  • collection: The configuration of the current collection.
  • collectionSlug: The current collection's slug.
  • response: The current paginated response, or undefined while loading.
  • documents: The documents currently rendered. Media collections include all pages loaded by infinite scroll.
  • isLoading: Whether the list's initial request is pending.
  • pagination: Read-only page, totalPages, total, hasNextPage, and hasPrevPage values.
  • permissions: Read-only canRead and canCreate results for the current user.
  • urls: The collection list and new-document URLs.

Use client for mutations and data fetching. Slot props intentionally do not expose setters for Dyrected's filters, pagination, or row selection.

If a configured key is not registered, or if a component throws during render, the Admin UI skips that component so the rest of the CMS remains usable. Missing-key warnings are deduplicated in development.


Theming & CSS Overrides

The Admin UI uses CSS custom properties for all design tokens. Override them on .dy-admin-ui from your host application's global stylesheet. Scoping overrides to .dy-admin-ui keeps the admin theme isolated from the rest of your app.

All color values use HSL without the hsl() wrapper (Tailwind CSS convention): H S% L%.

/* In your global.css — override any token you need */
.my-admin-page .dy-admin-ui {
  /* ── Fonts ─────────────────────────────────────────── */
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-serif: "Playfair Display", ui-serif, Georgia, serif;

  /* ── Page surface ───────────────────────────────────── */
  --background: 0 0% 100%; /* page background */
  --foreground: 240 10% 4%; /* default text */

  /* ── Card / panel surfaces ──────────────────────────── */
  --card: 0 0% 100%;
  --card-foreground: 240 10% 4%;

  /* ── Popover / dropdown surfaces ────────────────────── */
  --popover: 0 0% 100%;
  --popover-foreground: 240 10% 4%;

  /* ── Primary accent (buttons, active states, rings) ─── */
  --primary: 38 92% 50%; /* default: amber */
  --primary-foreground: 60 3% 6%; /* text on primary bg */
  --intelligence: 259 100% 62%; /* links, navigation accents */
  --intelligence-foreground: 0 0% 100%;

  /* ── Secondary (subtle buttons, tags) ───────────────── */
  --secondary: 60 10% 95%;
  --secondary-foreground: 60 3% 6%;

  /* ── Muted (placeholders, helper text, disabled) ─────── */
  --muted: 60 10% 95%;
  --muted-foreground: 60 4% 40%;

  /* ── Accent (hover tints) ───────────────────────────── */
  --accent: 60 10% 95%;
  --accent-foreground: 60 3% 6%;

  /* ── Destructive (delete buttons, error states) ──────── */
  --destructive: 0 84% 60%;
  --destructive-foreground: 60 17% 97%;

  /* ── Borders & inputs ───────────────────────────────── */
  --border: 60 7% 89%; /* field borders, dividers */
  --input: 60 7% 89%; /* input border color */
  --ring: 38 92% 50%; /* focus ring color */

  /* ── Border radius ──────────────────────────────────── */
  --radius: 0.5rem; /* applied to cards, inputs, buttons */

  /* ── Sidebar ────────────────────────────────────────── */
  --sidebar-background: 60 17% 97%;
  --sidebar-foreground: 60 3% 6%;
  --sidebar-primary: 38 92% 50%; /* active item accent */
  --sidebar-primary-foreground: 60 3% 6%;
  --sidebar-accent: 60 10% 95%; /* hover background */
  --sidebar-accent-foreground: 38 92% 50%; /* hover text */
  --sidebar-border: 60 7% 89%;
  --sidebar-ring: 38 92% 50%;
}

Common customisations

Change the accent color to blue:

.my-admin-page .dy-admin-ui {
  --primary: 217 91% 60%;
  --primary-foreground: 0 0% 100%;
  --intelligence: 217 91% 60%;
  --intelligence-foreground: 0 0% 100%;
  --ring: 217 91% 60%;
  --sidebar-primary: 217 91% 60%;
  --sidebar-primary-foreground: 0 0% 100%;
  --sidebar-accent-foreground: 217 91% 60%;
  --sidebar-ring: 217 91% 60%;
}

Use a neutral grey palette (no warm tint):

.my-admin-page .dy-admin-ui {
  --background: 0 0% 98%;
  --card: 0 0% 100%;
  --muted: 0 0% 94%;
  --muted-foreground: 0 0% 42%;
  --border: 0 0% 88%;
  --input: 0 0% 88%;
  --sidebar-background: 0 0% 97%;
  --sidebar-border: 0 0% 88%;
}

Use one brand color:

When you only have one brand color, use it for filled actions and active states, then derive lighter tints for secondary, muted, accent, and sidebar hover surfaces. Keep --primary-foreground high contrast against --primary.

.my-admin-page .dy-admin-ui {
  --background: 324 17% 97%;
  --foreground: 325 17% 16%;

  --primary: 321 17% 38%;
  --primary-foreground: 324 17% 97%;
  --intelligence: 321 17% 38%;
  --intelligence-foreground: 324 17% 97%;

  --secondary: 320 20% 95%;
  --secondary-foreground: 325 17% 16%;
  --muted: 320 20% 95%;
  --muted-foreground: 321 10% 42%;
  --accent: 320 20% 91%;
  --accent-foreground: 321 17% 34%;

  --border: 321 10% 86%;
  --input: 321 10% 86%;
  --ring: 321 17% 38% / 0.22;

  --sidebar-background: 320 20% 95%;
  --sidebar-foreground: 325 17% 16%;
  --sidebar-primary: 321 17% 38%;
  --sidebar-primary-foreground: 324 17% 97%;
  --sidebar-accent: 320 20% 89%;
  --sidebar-accent-foreground: 321 17% 34%;
  --sidebar-border: 321 10% 84%;
  --sidebar-ring: 321 17% 38%;
}

Use primary and accent colors:

When you have two brand colors, use --primary for committed actions like Save, Create, and Upload. Use --intelligence / accent tokens for links, active navigation details, hover text, and focus rings.

.my-admin-page .dy-admin-ui {
  --primary: 321 17% 38%;
  --primary-foreground: 324 17% 97%;

  --intelligence: 259 100% 62%;
  --intelligence-foreground: 0 0% 100%;

  --accent: 259 100% 96%;
  --accent-foreground: 259 100% 42%;
  --ring: 259 100% 62% / 0.24;

  --sidebar-primary: 321 17% 38%;
  --sidebar-primary-foreground: 324 17% 97%;
  --sidebar-accent: 259 100% 96%;
  --sidebar-accent-foreground: 259 100% 42%;
  --sidebar-ring: 259 100% 62%;
}

Swap fonts (using a Google Font):

<!-- In your <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Lora:wght@600;700&display=swap"
  rel="stylesheet"
/>
.my-admin-page .dy-admin-ui {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-serif: "Lora", ui-serif, Georgia, serif;
}

Scope: Prefer overriding variables on .dy-admin-ui, optionally nested under your admin page wrapper such as .my-admin-page .dy-admin-ui. This matches the Admin UI's scoped reset and prevents unrelated app styles from inheriting admin tokens.


The sidebar is built dynamically from your config at runtime, following this order:

  1. Grouped collections — Collections with an admin.group are listed inside a collapsible section labelled with the group name.
  2. Ungrouped collections — Collections without admin.group are listed at the top level.
  3. Media — Upload-enabled collections (upload: true) automatically get an image icon and appear in a dedicated "Media" section.
  4. Globals — Listed below collections, also respecting admin.group.
  5. Hidden items — Collections or globals with admin.hidden: true are excluded entirely.

The order within each group matches the order they appear in your collections / globals array in defineConfig.

Set admin.icon to override the contextual default. Dyrected accepts Lucide component names in PascalCase and provides type-safe autocomplete:

const Posts = defineCollection({
  slug: 'posts',
  admin: { icon: 'Newspaper' },
  fields: [...],
})

Configured icons take precedence over defaults. Without an override, upload collections use Image, auth collections use Users, regular collections use Database, and globals use Settings. Invalid values received from untyped JavaScript or a remote schema fall back to the relevant default.


Access Flags in the Admin UI

When a user is authenticated, the Admin UI resolves access functions against the current user and reflects the outcome:

Resolved AccessAdmin UI Behaviour
access.read === falseCollection is hidden from the sidebar and returns a 403 if navigated to directly
access.create === false"Create" button is hidden on the list page
access.update === falseEdit form renders in read-only mode; "Save" button is hidden
access.delete === false"Delete" action is hidden from the row action menu
field.access.read === falseField is removed from the form entirely
field.access.update === falseField is rendered as readOnly

Access is resolved server-side via the /api/schemas response, which includes computed access booleans for the authenticated user. The Admin UI never re-implements access logic — it only reads and reflects what the server returns.

On this page