Skip to main content

Documentation

No results found.
Features

Cascading Design Tabs

WebProCMS' page editor lets editors style every element of a row individually — drill into a card, into its heading, set the font size, drill back, repeat for the next card. That works fine for one-off edits, but the moment a row has six ma...

WebProCMS' page editor lets editors style every element of a row individually — drill into a card, into its heading, set the font size, drill back, repeat for the next card. That works fine for one-off edits, but the moment a row has six matching cards and "all titles need to be bigger," the per-element flow becomes the wrong tool: six identical edits, no consistency guarantees, no way to bulk-change later.

The cascading design tabs feature solves this by giving every container element three design tabs — Me, Direct Children, and Descendants — that group editable elements by their component type and apply one rule to every matching instance. Set padding once on the grid's "Card" entry → all six cards update. Set typography on the "Heading" descendant → every card title responds. Per-card customization still works through the regular drill-down editing; the cascade just means an editor doesn't have to use it for changes that apply uniformly.


The problem

Most useful page layouts repeat. A features grid has six cards with the same shape. A testimonial slider has four slides. A pricing table has three columns. The editor adds the row, fills in the per-card content (icon, title, description), and then wants the typography to feel consistent across the set — same title size, same description color, same padding inside each card.

In the old flow, those typography decisions live on each individual element. To change the title size, the editor drills into card 1 → into the title → opens the design tab → sets the size. Then card 2. Then card 3. Six edits, six chances to set a different value by accident, and any future "make all titles smaller" change repeats the same loop.

The cascade tabs are the bulk-style flow for this case.

The fix

When an editor opens design mode (the paintbrush button) on a container component — a grid, a wrapper, a section — the design sidebar shows a tab strip at the top:

Tab What it edits
Me This container's own design fields (alignment, container width, spacing, background, etc.)
Direct Children The component types nested one level deep inside this container — one entry per unique component type, not per instance
Descendants The component types nested anywhere inside this container at depth 2+ — same grouping by component type

The Direct Children and Descendants tabs surface the exact same design accordions an editor would see if they drilled into a single instance of that component type (Typography, Sizing, Spacing, Classes, etc.). The difference is that setting a value on the cascade tab writes to a single bulk rule on the container, which every matching instance inherits — instead of writing to one instance's individual classes.

Per-instance customization still works through the normal drill-down editing. If five out of six cards should look identical and one should be different, the editor uses the cascade tab to style the five, then drills into the sixth card individually to override. The override always wins.

A worked example

Take a features grid with six cards, each containing an icon, a heading, and a description.

Open design mode on the grid wrapper itself (paintbrush button on the "Features Grid" entry in the sidebar). The tab strip shows:

Tab Entries the editor sees
Me Alignment, Grid Layout, Spacing, Classes, Animation — the grid's own properties
Direct Children Grid Item (one entry — all 6 cards collapse here because they share the <x-dl.grid-item> component type)
Descendants Icon, Heading, Subheadline (one entry per nested component type — every card has one of each, so each shows once)

Click into Direct Children → Grid Item. Inside that collapsible the editor sees the same accordion structure as drilling into a single card: Item Span, Text Align, Spacing, Inner Layout. Setting padding to p-8 writes one cascade rule on the grid wrapper. The rule applies to all six cards through their shared component-type marker class — no per-card edits needed.

Click into Descendants → Heading. Inside, the Typography subgroup expands to Text Size, Font Weight, Text Align, Text Color, etc. Setting Text Size to text-xl writes one cascade rule. Every card title gets text-xl.

Want one specific card's title to be text-2xl instead? Drill into that card → into its heading → set the size on its own classes. That per-instance value wins automatically — the editor doesn't have to think about ordering or specificity.

How "all instances respond to one rule" actually works

The mechanism is built on component-type marker classes. Every <x-dl.*> rendered element emits two CSS classes:

Marker Scope Used for
dl-{kebab(prefix)} One specific instance (e.g. dl-feature-1-title) Per-instance lookups (the inspector, individual leaf overrides)
dl-c-{kebab(slug)} All instances of the same component (e.g. dl-c-heading) Bulk cascade target — one rule on a parent applies to all matching descendants

When an editor sets a cascade rule on the Direct Children or Descendants tab, it's stored as a rule keyed {parent_prefix}__{scope}__{component_slug}__classes. At render time, the parent's class string picks up an arbitrary-variant utility — e.g. [&_.dl-c-heading]:text-xl — that styles every descendant matching the component-type marker. Because the marker class is shared across all instances of <x-dl.heading> inside that container, one rule styles them all.

Per-instance overrides on a leaf get an automatic !important modifier at save time, so the leaf's own value beats the descendant selector even though descendant selectors normally have higher CSS specificity. The editor sees the ! in the textarea (and the cascade inspector explains where it came from), but it works the same whether the editor pays attention to it or not.

Per-instance customization is still first-class

The cascade tabs are bulk-only by design. They never list six individual cards on the Direct Children tab — that would just be a worse version of the per-card editing the editor already has via drill-down.

When an editor wants one specific card to look different, the path is unchanged: click into that card in the sidebar, click into its heading or icon or whatever needs adjusting, set the value on the Me tab. That value lands on the leaf's own classes and wins the cascade.

The model is "shared decisions live at the parent, individual exceptions live at the leaf." It maps directly onto how an editor thinks about a designed grid: most cards look the same, with one or two exceptions.

The cascade inspector

When an editor drills into a leaf that's currently inheriting cascade rules from above, the design tab surfaces an "Inherited from ancestors" collapsible at the top:

┌────────────────────────────────────────────────────┐
│ ⓘ Inherited from ancestors (1)               ▼    │
├────────────────────────────────────────────────────┤
│ These cascade rules are set on ancestors and       │
│ currently affect this element. Your own settings   │
│ can still override them.                           │
│                                                    │
│ ┌────────────────────────────────────────────────┐│
│ │ FEATURES GRID · DESCENDANTS                    ││
│ │ text-2xl font-black                            ││
│ └────────────────────────────────────────────────┘│
└────────────────────────────────────────────────────┘

Each entry shows which ancestor set the rule, which tab it was set on (Direct Children or Descendants), and the utility classes it applies. Editors who notice unexpected styling on an element can use this to track down "why is this red" without leaving the current item's panel.

Edge cases

  • Tabs only appear on containers. A leaf component with no nested <x-dl.*> (a single heading sitting alone in a row, an isolated button) shows only the Me tab — there's nothing below it to bulk-style.
  • Mixed children show multiple entries. A hero wrapper with one heading + one subheadline + one button group shows three entries on the Direct Children tab — Heading, Subheadline, Buttons — each with its own accordions. The "one rule per component type" model still holds; there just happen to be multiple types at the same depth.
  • Cascade compounds when you drill in. An editor styling "all cards in this grid" sets a rule on the grid. To then style "all cards in row 1 of this grid specifically," they drill into the row-1 wrapper and the cascade tabs reappear there — a new set of rules scoped to that wrapper only. Drill-in restarts the cycle.
  • The cascade survives row template updates. Cascade rules are stored in content_overrides keyed by the row slug, not in the blade template. A row that gets its design library template swapped out (browsing for a different layout) keeps its cascade rules — anything that still matches the new structure applies; anything that doesn't is harmless.

Why bulk-by-type and not bulk-by-instance

An earlier iteration of this feature grouped cascade rules by per-card prefix. A features grid with six cards (each with prefix feature_1, feature_2, etc.) showed six entries on the Direct Children tab — "Feature 1", "Feature 2", "Feature 3", and so on. Editors had to set padding on each entry individually. That's a different UI doing the same per-card work the drill-down already did — net zero value.

The current model groups by component slug (the kind of component, not its individual identity). A features grid has only one kind of card, so the Direct Children tab has one entry. Six cards, one rule. That's the actual win.

The trade-off: a rule on the Direct Children tab cannot target "only card 3" — it always targets all instances of the matching component. For per-instance edits the editor uses drill-down, where they always could. Cascade tabs and drill-down complement each other.

Where the feature lives in the codebase

For engineers wanting to extend or modify the system, the technical spec is at docs/cascading-design-tabs.md. Key entry points:

  • Marker class emission — DlMarker::withCascade() (every <x-dl.*> view calls this for its leading class string)
  • Cascade rule render — CascadeDescendants::for() (reads content_overrides, emits [&_.dl-c-X]: arbitrary variants)
  • Per-component descendant discovery — RowBladeSurgery::extractDescendantMap() (returns the children/descendants groupings the cascade tabs render)
  • Save-time conflict resolution — DescendantCascadeResolver (applies ! to leaf utilities that conflict with ancestor rules)
  • Editor UI — cascadeFields in resources/js/editor/editor-state.js + the tab strip in content-field-alpine-body.blade.php
  • Cascade inspector — inheritedRules in editor-state.js + the amber collapsible in content-field-alpine-body.blade.php