Custom Field Overview
When to Use
You need to store multiple related values together in a single field without creating entity references or separate content types -- for example, an address with street/city/state/zip, a product with SKU/price/weight/dimensions, or a person with first/last/email/phone.
Decision
| If you need... | Use... | Why |
|---|---|---|
| 3-10 related simple values (text, numbers, dates) in one field | Custom Field | Single table storage, best performance, simpler queries, no entity overhead |
| Complex nested structures with their own fields and displays | Paragraphs | Full entity features, but significant performance cost from joins |
| Reusable components across content types with revisions | Paragraphs or Field Collection | Entity reference provides shared data, revision tracking |
| Simple multi-value field (one type repeated) | Core multi-value field | Simplest solution, no module needed |
| 50+ properties describing one thing | Custom field type plugin | Avoid field sprawl, keep related data together |
Performance comparison: Custom Field stores all sub-fields in one table row (one JOIN). Paragraphs creates separate entities (N+1 queries for N paragraphs). For 10 paragraph items, expect 20+ queries vs 1 query with Custom Field.
Two Triggers for a custom Compound Field
A compound field (type: custom) is the right tool for two distinct reasons -- reach for it when EITHER applies:
- Grouping trigger -- 3-10 related values that always belong together and render together (address parts; SKU/price/weight/dimensions). This is the performance play above: one table row instead of entity overhead.
- Polymorphic trigger -- a value has >=2 mutually-exclusive sub-shapes, only one of which renders per instance, and no single core field type expresses all of them. Examples: a partner logo that is an image OR a text fallback; a contact method that is an email OR a phone OR a URL. Model each shape as its own column on one
customfield and populate only the relevant one per instance.
Overengineering guard (counter-example): an optional single thing is NOT polymorphic. An optional link is just a single-value link field with required: false at the instance level -- there is no "cardinality 0..1" in Drupal (cardinality counts values; "optional" is the required setting). Do not reach for custom for the mere presence/absence of one shape.
A custom compound field CAN hold an entity_reference column -- the module ships an entity_reference sub-field type (type: entity_reference with a target_type). So "route to a wrapper entity because a column would be a reference" is wrong as a technical claim. The real rule is a judgment call: give a child its own wrapper entity when it needs its own view modes, cross-parent reuse, or an independent lifecycle -- NOT because a compound "can't hold a reference."
Pattern
Creating via UI (config-first approach):
- Structure > Content types > [Type] > Manage fields > Add field
- Select "Custom field" type
- Define sub-fields (columns): name, type, length/size/precision
- Configure widgets and formatters per sub-field
- Save -- single table created with all columns
Accessing in code:
// Load entity and access sub-fields
$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
$street = $node->field_address->value; // Main property
$city = $node->field_address->city; // Named sub-field
$state = $node->field_address->state;
// Multi-value access
foreach ($node->field_address as $delta => $item) {
$line1 = $item->line1;
$city = $item->city;
}
Common Mistakes
- Creating entity references when Custom Field would work -- Use Custom Field for data that always belongs together (address parts), use entity references for independent reusable entities (taxonomy terms, referenced content). Entity references add query overhead
- Not planning column schema before adding data -- Column types (string length, decimal precision, datetime type) are locked after data exists. Plan schema carefully or use update service
- Checking isEmpty() on individual sub-fields -- Custom Field's isEmpty() checks all sub-fields together. Some fields marked
never_check_empty(like UUIDs) don't affect empty state - Forgetting the double-underscore separator for extended properties -- Image fields use
field__alt,field__title,field__width,field__height. Link fields usefield__title,field__options. Access via$item->{'field__alt'}syntax - Using Custom Field for complex editorial workflows -- Paragraphs better for content that needs independent revisions, moderation, translations, or reuse across entities
See Also
- Reference: Custom Field project page
- Reference: Official documentation