Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/alineacms/alinea/llms.txt

Use this file to discover all available pages before exploring further.

Alinea’s field system is open for extension. When none of the built-in fields fit your use case you can define a custom field that integrates seamlessly with the dashboard editor and the query layer. Every custom field has two parts: a field definition that describes the TypeScript value type and its options, and a view component — a React component rendered in the dashboard when an editor opens an entry that uses the field.
The view component is only loaded in the dashboard bundle. It is never included in your Next.js application bundle or in the query layer. Alinea loads view modules lazily by their import path string, so only editors pay the cost of the dashboard UI code.

Architecture overview

PartWhere it livesWhat it does
Field definition (.ts)Anywhere in your projectCalls Field.create(...) and exports the factory function and type alias
View component (.view.tsx)Same directory, separate fileExports a named React component that Alinea mounts in the dashboard
The two files are kept separate so that the view component (and its dashboard dependencies) is never bundled into your production app.

Real-world example: PositionField

The following example is taken directly from the Alinea development workspace. It implements a position field that stores {x, y} coordinates as fractions of a 2D area — picked by clicking on an interactive map.

The field definition

PositionField.ts
import {Field} from 'alinea'

// 1. Declare the TypeScript type for your custom field
export type PositionField = Field.Create<{
  x: number | null
  y: number | null
}>

// 2. Export a factory function that calls Field.create
export function position(
  label: string,
  options: Field.Options<PositionField> = {}
): PositionField {
  return Field.create({
    label,
    options,
    // 3. Point to the view component using its module path + export name
    view: '@/field/PositionField.view#PositionInput'
  })
}
Three steps are required:
  1. Declare a type alias using Field.Create<YourValueType>. This is the TypeScript type that content queries will resolve to.
  2. Export a factory function that calls Field.create(config).
  3. Reference the view component by a module path string in the format "modulePath#ExportName". Alinea resolves this path at dashboard load time.

Field.create options

label
string
required
The label passed by the caller — forward it from the factory function’s first argument.
options
object
required
The options object passed by the caller. Use Field.Options<YourFieldType> to get the correct TypeScript type including all standard base options (required, readOnly, hidden, shared, initialValue, validate).
view
string
required
A module path and export name in the format "path/to/module#ExportedComponentName". The path is resolved relative to the root of your project (or as configured in your tsconfig paths or bundler aliases).

The view component

PositionField.view.tsx
import {InputLabel, useField} from 'alinea/dashboard'
import type {PositionField} from './PositionField.js'

interface PositionInputProps {
  field: PositionField
}

export function PositionInput({field}: PositionInputProps) {
  // useField gives you the label, options, current value, and a mutator function
  const {
    label,
    options,
    value = {x: null, y: null},
    mutator: setValue
  } = useField(field)

  return (
    <InputLabel label={label}>
      <div
        style={{position: 'relative', width: 300, height: 300, cursor: 'pointer'}}
        onClick={event => {
          const rect = event.currentTarget.getBoundingClientRect()
          const x = (event.clientX - rect.left) / rect.width
          const y = (event.clientY - rect.top) / rect.height
          // Write the new value via the mutator
          setValue({x, y})
        }}
      >
        {/* Render your custom UI here */}
        {value.x !== null && value.y !== null && (
          <div
            style={{
              position: 'absolute',
              left: `${(value.x * 100).toFixed(2)}%`,
              top: `${(value.y * 100).toFixed(2)}%`,
              transform: 'translate(-50%, -100%)'
            }}
          >
            📍
          </div>
        )}
      </div>
    </InputLabel>
  )
}

useField

useField(field) is imported from alinea/dashboard and is the primary hook for custom field views. It returns:
PropertyTypeDescription
labelstringThe field’s label, resolved from options
optionsOptionsThe full options object (including readOnly, required, etc.)
valueYourValueTypeThe current stored value
mutator(value: YourValueType) => voidCall this to update the field value
errorstring | undefinedValidation error message, if any
fieldKeystringThe key of this field in its parent form

InputLabel

Wrap your custom input in <InputLabel label={label}> (also from alinea/dashboard) to get consistent label rendering, error display, and help text that matches the built-in fields.

Using a custom field in a type definition

Once you have defined your field, use it exactly like any built-in field:
Using the custom field in a content type
import {Config} from 'alinea'
import {position} from './field/PositionField.js'

export const FeaturedImage = Config.document('Featured image', {
  fields: {
    image: /* Field.image(...) */,
    focalPoint: position('Focal point', {
      help: 'Click to set the focal point for cropping',
      required: true
    })
  }
})
The position field accepts all standard base options (required, readOnly, hidden, shared, initialValue, validate) in addition to any options you define in its options type.

Registering the view with bundler aliases

If your project uses TypeScript path aliases (e.g. @/ mapping to src/), make sure the view path in view: '@/field/PositionField.view#PositionInput' matches your tsconfig and/or bundler configuration. Alinea resolves view paths using the same module resolution as your dashboard bundle.

Summary

  1. Create a .ts file that calls Field.create({label, options, view}) and exports a factory function and type alias.
  2. Create a .view.tsx file that exports a named React component accepting {field: YourFieldType}.
  3. Use useField(field) inside the component to access the current value and write updates via the mutator.
  4. Reference the view with a "path#ExportName" string that your bundler can resolve.
  5. Use your factory function exactly like any built-in Field.* factory inside Config.document or Config.type.

Build docs developers (and LLMs) love