diff --git a/docs/.vitepress/sidebarGuide.js b/docs/.vitepress/sidebarGuide.js index af5a5a7f..3f09936d 100644 --- a/docs/.vitepress/sidebarGuide.js +++ b/docs/.vitepress/sidebarGuide.js @@ -96,6 +96,14 @@ const sidebarGuide = [ { text: 'Custom Nunjucks Tags', link: 'reference/template-tags.md' }, ] }, + { + text: 'Styling', + collapsed: true, + items: [ + { text: 'Global Styling', link: 'guide/global-styling.md' }, + { text: 'Widget Styles', link: 'guide/widget-styles.md'} + ] + }, { text: 'Headless', link: 'guide/headless-cms.md' }, { text: 'Users and Roles', link: 'guide/users.md' }, { text: 'Permissions and Workflow', link: 'guide/permissions-and-workflow.md' }, @@ -259,7 +267,6 @@ const sidebarGuide = [ { text: 'Advanced Permissions', link: 'https://apostrophecms.com/extensions/advanced-permission' }, { text: 'Document Versions', link: 'https://apostrophecms.com/extensions/document-version' }, { text: 'Template Library', link: 'https://apostrophecms.com/extensions/template-library' }, - { text: 'Palette Design Editor', link: 'https://apostrophecms.com/extensions/palette-3' }, { text: 'Apostrophe Basics', link: 'https://apostrophecms.com/extensions/apostrophe-basics' }, { text: 'Data Set', link: 'https://apostrophecms.com/extensions/data-set' }, { text: 'Account Signup', link: 'https://apostrophecms.com/extensions/account-signup' }, diff --git a/docs/guide/global-styling.md b/docs/guide/global-styling.md new file mode 100644 index 00000000..ffc01868 --- /dev/null +++ b/docs/guide/global-styling.md @@ -0,0 +1,1098 @@ +# Global Styles + +**Global styles** allow content creators to modify CSS properties through the ApostropheCMS admin interface. Developers define which properties can be modified using schema field configuration, and content creators adjust colors, spacing, typography, and other design elements without writing code. + +## Configuration + +Global styles are configured in a project-level `modules/@apostrophecms/styles/index.js` using a styles cascade that works much like the schema fields cascade. You define fields with types, labels, and properties—but instead of creating form inputs for content, these fields create controls for CSS properties. + + + +```javascript +export default { + styles: { + add: { + // Color field - uses color picker + backgroundColor: { + type: 'color', + label: 'Page Background', + selector: 'body', + property: 'background-color' + }, + // Range field - uses slider + maxWidth: { + type: 'range', + label: 'Content Width', + selector: '.container', + property: 'max-width', + min: 800, + max: 1400, + step: 50, + unit: 'px' + }, + // Preset - pre-configured multi-field control + containerPadding: { + preset: 'padding', + selector: '.container' + } + }, + group: { + layout: { + label: 'Layout', + fields: ['maxWidth', 'containerPadding'] + }, + colors: { + label: 'Colors', + fields: ['backgroundColor'] + } + } + } +}; +``` + + + + + +**Every field needs:** +- `type` (or `preset`) - The control type: `color`, `range`, `string`, `select`, `box`, or a preset name +- `label` - Display name in the interface +- `selector` - CSS selector(s) to target +- `property` - CSS property to modify + +**Optional properties** like `unit`, `mediaQuery`, `class`, and more give you fine-grained control. See [Field Properties](#field-properties) for details. + +**Presets** provide common multi-field controls like `padding`, `margin`, `border`. See [Using Presets](#using-presets). + +**Groups** organize fields into tabs and sections in the interface. See [Organizing the Interface](#organizing-the-interface). + +The admin interface and generated CSS are automatically injected into pages—no template modifications required. + +::: info Permissions +Global styles require editor-level permissions to modify. Contributors and guests cannot access the styles interface. This ensures site-wide design consistency and prevents unauthorized style changes. +::: + +## Field Properties + +Field properties control how styles are applied and what CSS is generated. + +### Required properties + +#### `type` or `preset` + +Every field must have either `type` (for direct field configuration) or `preset` (to use a pre-configured field). + + + +```javascript +// Using type +backgroundColor: { + type: 'color', + // ... +} + +// Using preset +padding: { + preset: 'padding', + // ... +} +``` + + + +#### `label` + +Display name shown in the admin interface. + + + +```javascript +label: 'Background Color' +label: 'Section Spacing' +``` + + + +#### `selector` + +CSS selector(s) that define which elements to target. Supports any valid CSS selector. + + + +```javascript +// Single selector +selector: '.header' + +// Multiple selectors +selector: ['.header', '.footer', '.sidebar'] + +// Complex selectors +selector: 'body' // Element +selector: '#main-content' // ID +selector: ':root' // Pseudo-class (for CSS custom properties) +selector: '[data-theme="dark"]' // Attribute +selector: '.btn:hover' // Pseudo-class with element +selector: '.container > .row' // Child combinator +``` + + + +#### `property` + +CSS property or properties to modify. + + + +```javascript +// Single property +property: 'color' +property: 'margin-top' +property: '--primary-color' // CSS custom property + +// Multiple properties +property: ['margin-top', 'margin-bottom'] +``` + + + +### Optional properties + +#### `class` + +Add CSS classes instead of inline styles. The `class` property has two modes: + +**For `select` and `checkboxes` fields** - Use `class: true` to add the field's value as a CSS class: + + + +```javascript +alignment: { + type: 'select', + label: 'Content Alignment', + selector: '.content', + class: true, // The selected value becomes a class + choices: [ + { label: 'Left', value: 'align-left' }, + { label: 'Center', value: 'align-center' }, + { label: 'Right', value: 'align-right' } + ] +} +// If user selects 'Center', the class 'align-center' is added to .content +``` + + + +**For `boolean` fields** - Use `class: 'class-name'` to add a specific class when true: + + + +```javascript +darkMode: { + type: 'boolean', + label: 'Enable Dark Mode', + selector: 'body', + class: 'dark-theme' // Adds 'dark-theme' class when checked +} +``` + + + +::: info +The `class` property is particularly useful for working with utility CSS frameworks or when you want to use predefined CSS classes instead of generating inline styles. +::: + +#### `unit` + +Append units to numeric values from `range` or `box` fields. + + + +```javascript +unit: 'px' // Results in "16px" +unit: 'rem' // Results in "1.5rem" +unit: '%' // Results in "75%" +unit: 'em' // Results in "2em" +``` + + + +#### `valueTemplate` + +Wrap values in CSS functions or add additional text. + + + +```javascript +// Create box shadow with color +valueTemplate: '0 4px 8px %VALUE%' +// Result: 0 4px 8px rgba(0,0,0,0.3) + +// URL function +valueTemplate: 'url(%VALUE%)' +// Result: url(image.jpg) +``` + + + +#### `mediaQuery` + +Apply styles only within specific media queries. + + + +```javascript +mobileFontSize: { + type: 'range', + label: 'Mobile Font Size', + selector: 'body', + property: 'font-size', + min: 14, + max: 18, + unit: 'px', + mediaQuery: '(max-width: 768px)' // Mobile only +} + +desktopSpacing: { + type: 'box', + label: 'Desktop Padding', + selector: '.container', + property: 'padding', + unit: 'px', + mediaQuery: '(min-width: 1200px)' // Desktop only +} +``` + + + +## Understanding Selectors +Global styles apply CSS to elements that already exist in your templates. The selector property targets your existing HTML markup—it doesn't create new elements. +When you configure a style field like this: + + + +```javascript +backgroundColor: { + type: 'color', + label: 'Page Background', + selector: 'body', + property: 'background-color' +} +``` + + +The styles module generates CSS that targets the `body` element in your templates. If the selector doesn't match any elements, the style will have no visible effect. + +**Finding good selectors** +Look at your existing template markup to identify selectors: + + + +```nunjucks +{# layout.html #} + + +
+
...
+
+ +``` + +
+Based on this markup, these selectors would work: + +- `body` - The body element +- `.site-header` - The header +- `.container` - Container divs (both instances) +- `.main-nav` - The navigation +- `.site-content` - The main content area + +These selectors would not work (they don't exist in the template): + +- `.hero-section` - No element has this class +- `#sidebar` - No element has this ID +- `.card-grid` - No element has this class + +::: tip +Use your browser's developer tools to inspect your site's HTML and identify existing classes and IDs that make good selector targets. +::: + +## Field Types + +The styles module officially supports these field types for design controls. Other ApostropheCMS field types may work but are not guaranteed to function properly. + +### `color` + +Color picker for backgrounds, text colors, borders, and shadows. + + + +```javascript +textColor: { + type: 'color', + label: 'Body Text Color', + selector: 'body', + property: 'color', + def: '#333333' +} +``` + + + +::: info +Color fields support additional configuration through the `options` property, including output format (hex, rgb, hsl), preset color swatches, and UI customization. See the [color field reference](/reference/field-types/color.md) for full details. +::: + +### `range` + +Slider control for spacing, sizes, and numeric values. + + + +```javascript +fontSize: { + type: 'range', + label: 'Base Font Size', + selector: 'body', + property: 'font-size', + min: 14, + max: 20, + step: 1, + def: 16, + unit: 'px' +} +``` + + + +### `string` + +Text input for font names, custom values, and CSS strings. + + + +```javascript +fontFamily: { + type: 'string', + label: 'Body Font', + selector: 'body', + property: 'font-family', + def: 'Arial, sans-serif' +} +``` + + + +### `select` + +Dropdown menu for predefined options. + + + +```javascript +fontWeight: { + type: 'select', + label: 'Heading Weight', + selector: 'h1, h2, h3', + property: 'font-weight', + choices: [ + { label: 'Normal', value: '400' }, + { label: 'Medium', value: '500' }, + { label: 'Bold', value: '700' } + ], + def: '700' +} +``` + + + +### `box` + +Four-sided spacing control for margins, padding, and border widths (top, right, bottom, left). + + + +```javascript +sectionPadding: { + type: 'box', + label: 'Section Padding', + selector: '.section', + property: 'padding', + unit: 'px', + def: { + top: 40, + right: 20, + bottom: 40, + left: 20 + } +} +``` + + + +::: info +Using other field types is not recommended and may result in unexpected behavior. +::: + +## Using Presets + +Presets are pre-configured field combinations that provide common styling controls. They save time and ensure consistency. Presets should be passed in place of a `type` field. Pass in values for preset fields to override the defaults. For example, you can could add a `unit: 'rem'` field to a `preset: 'margin'` to override the default `px` unit. + +### Built-in presets + +#### `width` + +Width control with percentage-based slider. + + + +```javascript +imageWidth: { + preset: 'width', + selector: '.featured-image' +} +``` + + + +- Type: `range` +- Range: 0-100%, step 10 +- Default: 100% +- Generates: `width: X%` + +#### `alignment` + +Content alignment using CSS classes. + + + +```javascript +contentAlign: { + preset: 'alignment', + selector: '.content-block' +} +``` + + + +- Type: `select` +- Options: left (`apos-left`), center (`apos-center`), right (`apos-right`) +- Uses `class: true` to add alignment classes + +::: info +The styles module includes built-in CSS for the alignment classes: +```css +.apos-left { margin-right: auto } +.apos-center { margin-left: auto; margin-right: auto } +.apos-right { margin-left: auto } +``` +These classes are available site-wide and can be overridden at project level. +::: + +#### `padding` + +Four-sided padding control. + + + +```javascript +sectionPadding: { + preset: 'padding', + selector: '.section' +} +``` + + + +- Type: `box` +- Unit: `px` +- Generates: `padding: Tpx Rpx Bpx Lpx` + +#### `margin` + +Four-sided margin control. + + + +```javascript +blockMargin: { + preset: 'margin', + selector: '.content-block' +} +``` + + + +- Type: `box` +- Unit: `px` +- Generates: `margin: Tpx Rpx Bpx Lpx` + +#### `border` + +Multi-field border controls including width, radius, color, and style. + + + +```javascript +cardBorder: { + preset: 'border', + selector: '.card' +} +``` + + + +- Type: `object` (multi-field preset) +- Fields: + - `active` (boolean) - Enable/disable border + - `width` (box) - Border width per side + - `radius` (range) - Border radius 0-32px + - `color` (color) - Border color + - `style` (select) - solid/dotted/dashed +- All fields except `active` use conditional display (`if: { active: true }`) + +#### `boxShadow` + +Multi-field drop shadow controls. + + + +```javascript +cardShadow: { + preset: 'boxShadow', + selector: '.card' +} +``` + + + +- Type: `object` (multi-field preset) +- Fields: + - `active` (boolean) - Enable/disable shadow + - `x` (range) - X offset -32 to 32px + - `y` (range) - Y offset -32 to 32px + - `blur` (range) - Blur 0-32px + - `color` (color) - Shadow color +- Uses `valueTemplate` to compose final CSS value +- Generates: `box-shadow: Xpx Ypx Blurpx Color` + +### Creating custom presets + +Create custom presets by extending the `registerPresets()` method: + + + +```javascript +export default { + extendMethods(self) { + return { + registerPresets(_super) { + _super(); + + // Define a custom preset + self.setPreset('gradient', { + type: 'object', + label: 'Gradient Background', + property: 'background-image', + valueTemplate: 'linear-gradient(%direction%, %startColor%, %endColor%)', + fields: { + add: { + active: { + type: 'boolean', + label: 'Enable Gradient', + def: false + }, + direction: { + type: 'select', + label: 'Direction', + def: 'to right', + if: { active: true }, + choices: [ + { label: 'Left to Right', value: 'to right' }, + { label: 'Top to Bottom', value: 'to bottom' }, + { label: 'Diagonal', value: 'to bottom right' } + ] + }, + startColor: { + type: 'color', + label: 'Start Color', + def: '#ffffff', + if: { active: true } + }, + endColor: { + type: 'color', + label: 'End Color', + def: '#000000', + if: { active: true } + } + } + } + }); + } + }; + }, + styles: { + add: { + heroGradient: { + preset: 'gradient', + selector: '.hero-section' + } + }, + group: { + backgrounds: { + label: 'Backgrounds', + fields: ['heroGradient'] + } + } + } +}; +``` + + + + + +### Extending built-in presets + +Modify existing presets without changing every usage: + + + +```javascript +extendMethods(self) { + return { + registerPresets(_super) { + _super(); + + // Get and modify an existing preset + const borderPreset = self.getPreset('border'); + borderPreset.fields.add.width.def = { + top: 2, + right: 2, + bottom: 2, + left: 2 + }; + self.setPreset('border', borderPreset); + } + }; +} +``` + + + + + +## Organizing the Interface + +The styles module uses **groups** to control how styling controls are organized in the admin interface. Grouping helps content creators work through large sets of design options by organizing related controls into sections. Groups affect **only the UI layout and navigation** — they do not change how CSS is generated. + +> **Note:** +> Unlike standard ApostropheCMS schemas, style controls are **not displayed automatically**. +> Every control must be included in a group (either directly in `fields` or within a nested group), or it will not appear in the interface. + +The styles UI is built around **drill-in navigation**: + +1. Editors first see a **section menu** (a list of top-level groups) +2. Selecting a section **drills in** to show that section’s controls +3. Within a drilled-in section, controls can be organized into **additional groups**, or shown directly + +Groups can also be marked as `inline: true`, which means they render directly in place (no drill-in navigation). + +--- + +### Top-level groups (sections) + +Top-level groups are defined under `styles.group`. + + + +```js +styles: { + add: { + primaryColor: { + type: 'color', + label: 'Primary Color', + selector: ':root', + property: '--primary-color' + }, + contentWidth: { + type: 'range', + label: 'Content Width', + selector: '.container', + property: 'max-width', + min: 800, + max: 1400, + step: 50, + unit: 'px' + } + }, + group: { + branding: { + label: 'Branding', + fields: ['primaryColor'] + }, + layout: { + label: 'Layout', + fields: ['contentWidth'] + } + } +} +``` + + + +In this example: + +* **Branding** and **Layout** appear in the top-level section menu +* selecting a section drills in and displays the fields listed in `fields` + +--- + +### Nested groups (drill-in sections within a section) + +Nested groups are defined using the `group` property inside a top-level group. + +By default, nested groups behave like **drill-in sections**: + +* the group label is shown with a caret +* selecting it drills into that subsection +* the label becomes the section title above the controls + +This is useful when a single section contains too many fields to show at once. + +--- + +### Mixing fields and nested groups in a section + +A top-level group can contain: + +* `fields` (controls shown directly in the drilled-in section), and +* `group` (additional groups shown within the drilled-in section) + +This lets you put the most important controls directly in the section while still breaking up the rest. + + + +```js +styles: { + add: { + primaryColor: { + type: 'color', + label: 'Primary Color', + selector: ':root', + property: '--primary-color' + }, + secondaryColor: { + type: 'color', + label: 'Secondary Color', + selector: ':root', + property: '--secondary-color' + }, + headingFont: { + type: 'string', + label: 'Heading Font Family', + selector: ':root', + property: '--heading-font-family' + }, + bodyFont: { + type: 'string', + label: 'Body Font Family', + selector: ':root', + property: '--body-font-family' + } + }, + group: { + branding: { + label: 'Branding', + + // fields shown immediately inside Branding + fields: ['primaryColor'], + + // additional groups within Branding + group: { + colors: { + label: 'More Colors', + fields: ['secondaryColor'] + }, + typography: { + label: 'Typography', + fields: ['headingFont', 'bodyFont'] + } + } + } + } +} + +``` + + + +In this example: + +* **Branding** appears in the section menu +* when drilled in, **Primary Color** appears immediately +* **More Colors** and **Fonts** appear as additional groups within the Branding section + +--- + +### Inline groups (no drill-in navigation) + +A group can be made *inline* by adding `inline: true`. + +Inline groups render their controls directly in the current UI view: + +* no caret +* no drill-in navigation +* just a labeled visual grouping + +Inline groups can be used: + +* as a **top-level group**, or +* as a **nested group** within a top-level group + + + +```js +styles: { + add: { + lineHeight: { + type: 'range', + label: 'Line Height', + selector: 'body', + property: 'line-height', + min: 1.0, + max: 2.0, + step: 0.05 + }, + letterSpacing: { + type: 'range', + label: 'Letter Spacing', + selector: 'body', + property: 'letter-spacing', + min: -0.05, + max: 0.2, + step: 0.01, + unit: 'em' + }, + paragraphSpacing: { + type: 'range', + label: 'Paragraph Spacing', + selector: 'p', + property: 'margin-bottom', + min: 0, + max: 2, + step: 0.1, + unit: 'rem' + } + }, + group: { + typography: { + label: 'Typography', + group: { + spacing: { + label: 'Text Spacing', + inline: true, + fields: ['lineHeight', 'letterSpacing', 'paragraphSpacing'] + } + } + } + } +} + +``` + + + +In this example: + +* **Typography** is selected from the top-level section menu +* **Text Spacing** renders inline as a titled grouping inside Typography +* all of its fields appear immediately + +--- + +## Nesting rules + +Groups support **one level of nesting only**: + +* Top-level groups may include nested groups via `group` +* Nested groups **cannot** contain additional groups + +This means you can have: + +* section menu → top-level section +* top-level section → nested group drill-in + +But not: + +* nested group → nested group → nested group + +--- + + +## Advanced Topics + +### CSS custom properties + +The styles module supports CSS custom properties (variables) for flexible theming: + + + +```javascript +accentColor: { + type: 'color', + label: 'Site Accent Color', + selector: ':root', + property: '--accent-color' +} +``` + + + + + +Then use the variable throughout your CSS: + +```css +.button { + background-color: var(--accent-color); +} + +.highlight { + border-left: 3px solid var(--accent-color); +} +``` + +### Object field limitations + +Object fields are supported in the styles module but cannot be nested within other object fields. This limitation exists because the styles module only iterates one level deep through object fields—it does not recursively process nested object structures. + +This means: + +- You **can** use object fields at the top level of your styles schema +- You **cannot** use presets within object fields (since presets may themselves be object fields) +- You **cannot** nest object fields within other object fields + +Object fields are primarily supported to enable the subfields used in multi-field presets like `border` and `boxShadow`. + +### Internationalization + +Localize field labels for global teams: + + + +```javascript +export default { + i18n: { + styleStrings: { + browser: true, + }, + }, + styles: { + add: { + primaryColor: { + type: "color", + label: "styleStrings:primaryColor", + selector: ":root", + property: "--primary-color", + }, + }, + group: { + colors: { + label: "styleStrings:colorGroup", + fields: ["primaryColor"], + }, + }, + }, +}; +``` + + + + + +Create translation files in `modules/@apostrophecms/styles/i18n/styleStrings/`: + + + +```json +// en.json +{ + "primaryColor": "Primary Brand Color", + "colorGroup": "Brand Colors" +} +``` + + + + + + + +```json +// fr.json +{ + "primaryColor": "Couleur Principale de Marque", + "colorGroup": "Couleurs de Marque" +} +``` + + + + + +### Custom CSS generation + +For advanced use cases requiring complex CSS transformations or integration with external styling systems, you can override the default CSS generation with custom logic. + +To enable custom rendering: + + + +```javascript +export default { + options: { + serverRendered: true, + }, + methods(self) { + return { + async getStylesheet(doc) { + // Custom CSS generation logic + // The doc parameter contains style field values + // The self.schema provides field configuration + + return css; + }, + }; + }, +}; +``` + + + + + +::: info +For detailed information on custom rendering methods, parameters, and best practices, see the [Styles Module Technical Reference](/reference/modules/styles.md). +::: \ No newline at end of file diff --git a/docs/guide/widget-styles.md b/docs/guide/widget-styles.md new file mode 100644 index 00000000..1fce7cc2 --- /dev/null +++ b/docs/guide/widget-styles.md @@ -0,0 +1,596 @@ +# Per-Widget Styling + +**Per-widget styling** allows content creators to customize individual widget instances through the widget editor modal. Unlike global styles that apply site-wide, widget styles are scoped to specific widget instances, enabling unique styling for each occurrence of a widget on your pages. + +## Configuration + +Widget styles are configured by adding a `styles` property to your widget's schema configuration, using the same `styles` cascade pattern as global styles. You define fields with types, labels, selectors, and properties—but these controls apply only to individual widget instances rather than site-wide. + +Add styles to any widget by including a `styles` property in the widget's `index.js`: + + + +```javascript +module.exports = { + extend: '@apostrophecms/widget-type', + options: { + label: 'Hero Widget' + }, + fields: { + add: { + title: { + type: 'string', + label: 'Title' + }, + image: { + type: 'area', + label: 'Image', + options: { + widgets: { + '@apostrophecms/image': {} + } + } + } + } + }, + styles: { + add: { + backgroundColor: { + type: 'color', + label: 'Background Color', + selector: '.hero-wrapper', + property: 'background-color', + def: '#ffffff' + }, + textColor: { + type: 'color', + label: 'Text Color', + selector: '.hero-title', + property: 'color', + def: '#000000' + }, + spacing: { + preset: 'padding', + selector: '.hero-wrapper' + } + } + } +}; +``` + + + + + +::: info Permissions +Widget styling permissions follow the widget's own edit permissions. If a user can edit a widget instance, they can modify its style settings. With the `@apostrophecms-pro/advanced-permissions` module installed you can add per-field permissions to limit styles to specific user groups. +::: + + +## Field types and properties + +Widget styles support the same field types and essential properties as global styles: + +### Supported field types + +- **`box`**: Four-sided spacing controls for margins, padding, and border widths (top, right, bottom, left) +- **`color`**: Color picker for backgrounds, text, borders +- **`range`**: Slider controls for spacing, sizes, numeric values +- **`string`**: Text input for font names, custom values +- **`select`**: Dropdown menus for predefined options + +::: info +Using other field types is not recommended and may result in unexpected behavior. +::: + +### Essential properties + +All the same properties from global styles are available: + +- **`selector`**: CSS selector(s) to target elements within the widget template (string or array) - **Optional for widget styles** +- **`property`**: CSS property or properties to modify (string or array) +- **`class`**: Add CSS classes instead of inline styles (see below) +- **`unit`**: Unit to append to numeric values (`px`, `rem`, `%`, etc.) +- **`valueTemplate`**: Template for wrapping values in CSS functions (e.g., `'rgb(%VALUE%)'`) +- **`mediaQuery`**: Apply styles only within specific media queries + +#### The `selector` property in widget styles + +Unlike global styles where `selector` is required, **`selector` is optional for widget styles**. Widget styles are automatically scoped to each widget instance, so: + +- **Without a selector**: The style applies directly to the widget wrapper element +- **With a selector**: The style targets nested elements within the widget template + + + +```javascript +styles: { + add: { + // No selector - applies to widget wrapper itself + wrapperBorder: { + type: 'color', + label: 'Border Color', + property: 'border-color' + }, + // With selector - targets nested element + titleColor: { + type: 'color', + label: 'Title Color', + selector: '.widget-title', + property: 'color' + } + } +} +``` + + + + + +#### The `class` property + +Add CSS classes instead of inline styles. The `class` property has two modes: + +**For `select` and `checkboxes` fields** - Use `class: true` to add the field's value as a CSS class: + + + +```javascript +alignment: { + type: 'select', + label: 'Alignment', + selector: '.content', + class: true, + choices: [ + { label: 'Left', value: 'align-left' }, + { label: 'Center', value: 'align-center' }, + { label: 'Right', value: 'align-right' } + ] +} +``` + + + +**For `boolean` fields** - Use `class: 'class-name'` to add a specific class when true: + + + +```javascript +featured: { + type: 'boolean', + label: 'Featured Style', + selector: '.card', + class: 'is-featured' +} +``` + + + + + +### Additional property examples + + + +```javascript +styles: { + add: { + // Multiple selectors and properties + spacing: { + type: 'range', + label: 'Vertical Spacing', + selector: ['.hero-header', '.hero-footer'], + property: ['padding-top', 'padding-bottom'], + min: 0, + max: 4, + step: 0.5, + unit: 'rem' + }, + // Value template + shadow: { + type: 'color', + label: 'Shadow Color', + selector: '.card', + property: 'box-shadow', + valueTemplate: '0 2px 8px %VALUE%' + }, + // Media query + mobileFont: { + type: 'range', + label: 'Mobile Font Size', + selector: '.widget-title', + property: 'font-size', + min: 14, + max: 24, + unit: 'px', + mediaQuery: '(max-width: 768px)' + } + } +} +``` + + + + + +For complete documentation on field types and properties, see the [Global Styling documentation](/guide/global-styling.md). + +## Using presets + +Widget styles support all the same built-in presets as global styles: + +- **`width`** - Width percentage slider +- **`alignment`** - Left/center/right alignment classes +- **`padding`** - Four-sided padding control +- **`margin`** - Four-sided margin control +- **`border`** - Multi-field border controls (width, radius, color, style) +- **`boxShadow`** - Multi-field drop shadow controls + +Use presets with shorthand or customization: + + + +```javascript +styles: { + add: { + // Shorthand + cardPadding: 'padding', + cardMargin: 'margin', + + // With customization + cardBorder: { + preset: 'border', + selector: '.card-container' + }, + dropShadow: { + preset: 'boxShadow', + selector: '.card-container' + }, + + // Alignment preset includes built-in CSS classes + contentAlign: { + preset: 'alignment', + selector: '.card-content' + } + } +} +``` + + + + + +::: info +The `alignment` preset uses built-in CSS classes (`.apos-left`, `.apos-center`, `.apos-right`) that ship with ApostropheCMS core. These classes are available site-wide and can be overridden at project level if needed. +::: + +For details on each preset's fields and configuration, see the [Global Styling documentation](/guide/global-styling.md). + +## Object field limitations + +Object fields are supported in widget styles but cannot be nested within other object fields. This limitation exists because the styles module only iterates one level deep through object fields—it does not recursively process nested object structures. + +This means: + +- You **can** use object fields at the top level of your widget's `styles` schema +- You **cannot** use presets within object fields (since presets may themselves be object fields) +- You **cannot** nest object fields within other object fields + +Object fields are primarily supported to enable the subfields used in multi-field presets like `border` and `boxShadow`. + +## Automatic styling (default) + +By default, widget styles are applied automatically with no template modifications required. The styles module generates scoped CSS for each widget instance and wraps the widget output with the necessary styling elements. + +**How it works:** +- Styles are automatically scoped to each widget instance (one widget's styles don't affect another) +- A unique ID is generated per instance +- CSS is injected via a `