Appearance
Setting Types
Settings are the fields the merchant fills in the editor's settings panel. You declare them as fields with @setting on the component's type in etc/components/<Template>.graphqls (Creating Components). Each setting becomes a prop of the same name, and the field's type picks the control.
graphql
type VendorPicks @component(…) {
"Shown above the list"
title: LocalizedText! @setting(label: "Title", default: "Our picks")
}Declare settings with !: a stored setting is never null.
@setting arguments
| Argument | What it does |
|---|---|
label | Field label in the settings panel (required) |
default | The value a new node starts with, and the value the server fills in when a node has none. See Defaults. |
min, max, step | Range of a number. See Int, Float. |
source | Options from code for a String field. See Select. |
labelField | The sub-field that titles each item of a list. See Array. |
dependsOn | Shows the setting only while other settings have given values. See Dependent settings. |
tab | The group the setting is shown in. See Grouping settings. |
control | An editor control the field type does not pick, one added to the settings panel by an editor extension |
The string above a field is its description: help text behind a ? icon next to the label.
Types
| Field type | Field in the editor | default as | Value in props |
|---|---|---|---|
String, Text | One-line input | "" | String |
Textarea | Multi-line input | "" | String |
RichText | Rich text editor (bold, links, lists) | "" | HTML string |
LocalizedText | Input with a Translations dialog | "" | Object of texts per locale; resolve it in a provider |
LocalizedTextarea | Multi-line input with Translations | "" | Same as LocalizedText |
LocalizedRichText | Rich text with Translations | "" | Same as LocalizedText, HTML texts |
Int, Float | Slider with a number box | number | Number |
Boolean | On/off switch | true / false | Boolean |
| enum | Dropdown | "value" | The chosen option's value |
String with source | Dropdown, options from code | "value" | The chosen option's value |
Color | Swatch with a color picker and the design's palette | "" | CSS color (#rrggbb or rgba(…)), or empty |
Image | Media gallery picker with upload and crop | "" | Path under pub/media; turn it into a URL in a provider |
Url | Link input | "" | Store path or absolute URL; turn it into a link in a provider |
[Input!]! (a list of an input type) | Sortable list of items, each with its own fields | [] | Array of objects |
A field type that no control handles is an error when the definitions are loaded.
String, Textarea
graphql
"Shown under the button"
note: Textarea! @setting(label: "Note", default: "")Use plain String / Textarea for values that are codes rather than shopper-facing text: SKUs, anchors, cookie names, CSS. For text the shopper reads, use the Localized… types.
RichText
The value is HTML. Render it with v-html.
graphql
content: RichText! @setting(label: "Content", default: "<p>Write something.</p>")LocalizedText, LocalizedTextarea, LocalizedRichText
Text that can be translated per store view without a separate design per language. The editor shows the base field (Text, Textarea or RichText) for the default text, and a Translations button where the merchant adds a text per locale code. The stored value is an object:
json
{ "default": "Mid-Season Sale", "de_DE": "Sale zur Saisonmitte", "fr_FR": "Soldes de mi-saison" }graphql
title: LocalizedText! @setting(label: "Title", default: "Mid-Season Sale")Declare the default as a plain string; the editor wraps it.
A component with a Localized… setting must have a provider that turns the object into a string with OOCS\LynxComponent\Helper\LocalizedText::get(). It picks the store view's locale, falls back to default, and passes the result through Magento's __(), so a default text that a language pack already translates ("Sign In", "Contact Us") is translated without the merchant entering anything.
php
$props['title'] = $this->localizedText->get($props['title']);The generated props type already declares title: string. A full provider is on Component Providers.
Int, Float
graphql
limit: Int! @setting(label: "Products", default: 8, min: 1, max: 24, step: 1)min, max and step default to 0, 500 and 1. The value is clamped to the range.
Boolean
graphql
show_title: Boolean! @setting(label: "Show Title", default: true)Every on/off setting is a Boolean, not a select with Yes and No.
Select
Static options: an enum. The options follow the order of its values, and @label gives each one its label. Value names are written in capitals and stored in lowercase (MD stores "md"); add @value when the stored value differs, for example when it starts with a digit.
graphql
enum VendorSize {
SM @label(text: "Small")
MD @label(text: "Medium")
}
enum VendorRatio {
AUTO @label(text: "Auto")
V16_9 @label(text: "16:9") @value(text: "16_9")
}
size: VendorSize! @setting(label: "Size", default: "md")
ratio: VendorRatio! @setting(label: "Ratio", default: "auto")Enum names are global: prefix yours, or reuse a shared one (Align, Justify, TextAlign, Rounded). Another module can add an option with extend enum VendorSize { LG @label(text: "Large") }, so a component maps values with a fallback.
Options from code: a String field with source, the class of an object implementing OOCS\LynxComponent\Api\OptionSourceInterface. The registry creates it and calls its toOptionArray() only when it builds the editor's configuration.
graphql
attribute: String! @setting(label: "Attribute", default: "sku", source: "Vendor\\Module\\Model\\Config\\Source\\ProductAttribute")php
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\Config\Source;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
use OOCS\LynxComponent\Api\OptionSourceInterface;
class ProductAttribute implements OptionSourceInterface
{
public function __construct(
private readonly CollectionFactory $collectionFactory,
) {
}
/**
* @return array<int, array{value: string, label: string}>
*/
public function toOptionArray(): array
{
$collection = $this->collectionFactory->create()
->addFieldToFilter('frontend_label', ['notnull' => true])
->setOrder('frontend_label', 'ASC');
$options = [];
foreach ($collection as $attribute) {
$options[] = [
'value' => (string) $attribute->getAttributeCode(),
'label' => (string) $attribute->getFrontendLabel(),
];
}
return $options;
}
}- Implement Lynx's
OptionSourceInterface, notMagento\Framework\Data\OptionSourceInterface. The registry ignores the Magento one. - Name the class itself, without
\Proxy: it is created only for the editor. - To reuse one source class with different arguments, declare a
virtualTypeof it inetc/di.xmland name the virtual type insource, asOOCS_LynxCatalogdoes for its image attribute list (ImageAttribute.graphqls). sourcealso works on a sub-field of anarraysetting.
Color
graphql
"Empty uses the default"
color: Color! @setting(label: "Text Color", default: "")The picker offers the colors of the current design first. Keep the default empty and fall back to a theme token, so the component follows the design until the merchant picks a color:
vue
<h2 :class="color ? '' : 'text-base-content'" :style="color ? { color } : undefined">{{ text }}</h2>Image
graphql
image: Image! @setting(label: "Image", default: "")The value is a path relative to pub/media, such as wysiwyg/sale/hero.jpg, so a layout works on every environment and store. Your provider turns it into a URL of the current store with OOCS\LynxThemeComponent\Helper\MediaUrl::url(), which leaves an absolute URL or a path starting with / unchanged:
php
$props['image'] = $this->mediaUrl->url((string) $props['image']);Url
graphql
cta_url: Url! @setting(label: "Button URL", default: "sale")The value is a store path (about-us, /checkout/cart) or an absolute URL. The editor reports an invalid value only when it has a scheme and cannot be parsed. Your provider turns a path into a link of the current store view with OOCS\LynxThemeComponent\Helper\StoreLink::path(). It returns a root-relative path that includes the store code when Add Store Code to URLs is on, and leaves #anchors, mailto:, tel: and absolute URLs unchanged:
php
$props['cta_url'] = $this->storeLink->path((string) $props['cta_url']);Render links with Inertia's Link. Lynx sends links to another origin (mailto:, another site) to the browser, so you do not need to tell internal and external links apart. Use a plain <a> only for an internal link that must reload the page.
MediaUrl and StoreLink live in OOCS_LynxThemeComponent; add it to your module's <sequence> and composer.json when you use them.
Array
A list the merchant can add to, remove from and reorder. The setting is a list of an input type, and each field of the input is a sub-field with its own @setting(label: …).
graphql
input VendorLinksItem {
label: LocalizedText! @setting(label: "Label")
url: Url! @setting(label: "URL")
new_tab: Boolean! @setting(label: "Open in New Tab")
}
links: [VendorLinksItem!]! @setting(label: "Links", default: [], labelField: "label")labelFieldnames the sub-field whose value titles each item in the list.- Sub-fields take any type above,
sourceincluded. A sub-field shows its label only: its description is not displayed, anddependsOndoes not apply. - Declared defaults are filled in for top-level settings only. Stored items are not completed with sub-field defaults, so read a sub-field with a fallback in the provider (
$item['url'] ?? ''). - A new item starts with
falsefor aBoolean,min(or 0) for a number, an empty list for a nested list, and the sub-field'sdefault(or an empty string) for the rest. - A non-empty default is a list of items:
graphql
links: [VendorLinksItem!]! @setting(label: "Links", default: [{label: "About Us", url: "about-us", new_tab: false}], labelField: "label")In the provider, loop over the items to resolve their Localized…, Image and Url fields:
php
foreach ($props['links'] as $i => $link) {
$props['links'][$i]['label'] = $this->localizedText->get($link['label'] ?? '');
$props['links'][$i]['url'] = $this->storeLink->path((string) ($link['url'] ?? ''));
}The component receives an array of objects shaped like the input, typed as Lynx.VendorLinksItem[].
Defaults
Give every setting a default. It is the single source of the default value:
- Editor: a new node is saved with every default. A node saved before you added a setting gets its default when the merchant opens it.
- Server: before your provider and component run, Lynx fills in the default of every setting the node lacks, including nodes your code inserts into a layout. A provider reads
$props['limit']directly, and settings are required in the generated props type.
Write the default as the value it stores: a number for Int and Float, true or false for Boolean, a list for a list ([] when empty), and a string for every other type, Localized… ones included.
An empty string default is fine for text, image, URL and color settings where empty has a meaning ("no image", "use the default color").
Dependent settings
dependsOn works like <depends> in Magento's system.xml: the setting is shown only while the listed settings have the given values.
graphql
# show_label is on, and display is card or list
label: LocalizedText! @setting(label: "Label", default: "", dependsOn: {show_label: "true", display: ["card", "list"]})- Several fields are combined with AND; an array of values for one field means any of them.
- Values compare as strings, so a
Booleanis written"true"or"false". - The panel updates as the merchant edits, before saving.
- When a setting is hidden, saving removes it from the node, so the server renders it with its
default. Your component always receives a value.
Grouping settings
A component with many settings can group them with tab. The settings panel then shows one row per group, as the Theme tab does: a click opens the group's settings underneath, and the first group is open already.
graphql
logo_size: VendorLogoSize! @setting(label: "Logo Size", default: "md", tab: "Header")- A setting without
tabgoes to General, shown first. The other groups follow in the order they first appear in the type. - When no setting names a
tab, the panel lists the settings without groups. - One Save button covers every group, and
dependsOnworks across groups. - A group whose settings are all hidden by
dependsOnis not shown.