ArtisanPack UI
DocsVisual EditorBlock Visibility

Block Visibility

Visual Editor v1.17.1

Runtime rules that decide whether an individual block renders. Ships in 1.4.0 across three issues: - #491 — Contextual rules: master Hide, screen-size...

Runtime rules that decide whether an individual block renders. Ships in 1.4.0 across three issues:

  • #491 — Contextual rules: master Hide, screen-size, query-string, referrer, browser / OS / device.
  • #492 — User & auth rules: login state, user role, specific user.
  • #493 — Scheduling rules: single date/time window, recurring weekly schedule, per-rule timezone override.

All rules evaluate server-side so hidden blocks never emit markup — there is no flash of content and no client-only privacy leak.


For editors

Every block that opts in surfaces a single Visibility panel in the inspector with a subsection per rule family. Only families you've enabled render — an untouched panel stays minimal. Combine any number of rules on a single block; the block renders only when every active rule passes.

Rule shape at a glance:

Rule What it checks
Hide block Master toggle — the block disappears from output entirely.
Screen size Emits @media rules per named breakpoint. Zero runtime JS.
Query string ?utm_source=newsletter, wildcard ?debug=*, Any / All.
Referrer Literal twitter.com, wildcard *.example.com, or (direct).
Browser / OS / Device Chrome / Firefox / Safari / …, iOS / Android / …, mobile / tablet.
Login state Logged in / Logged out / Either.
User role Any / All of a list of role slugs.
Specific user Match by email or id. Autocomplete backed by /users/search.
Date/time window Start and/or end datetime. Per-rule timezone override.
Recurring schedule Up to 14 weekly windows (day + HH:MM start/end).

Screen size

Since v1.15.2 (#974). The Screen size subsection offers:

Option Stored key(s) Hides the block at
Mobile (≤767px) mobile @media (max-width:767px)
Tablet (≤1023px) tablet @media (max-width:1023px) — phones too
Desktop (≥1024px) lg, xl, 2xl 1024px and up

mobile and tablet are the desktop-first device keys of the breakpoint registry, so a host that narrows them (e.g. 'mobile' => '599px') moves the query with them. Desktop is offered only when a legacy key starts one pixel above the widest device key.

The legacy mobile-first keys (sm, md, lg, xl, 2xl) still work for saved content: each is a width range from its min-width to one pixel below the next (sm = 640–767px, md = 768–1023px, 2xl = 1536px and up). The panel lists them under "Legacy width ranges" only when a block's saved rule uses one the options above don't cover. Keys of both families can be mixed:

{"artisanpackVisibility":{"screenSize":{"direction":"hide","breakpoints":["mobile"]}}}

direction: "show" (not offered in the panel, but honoured when saved) makes the block visible in the union of the listed ranges and hides it at every unlisted legacy range that starts above the widest listed device key. So "show" + ["mobile"] hides from 768px up (visible on phones only), ["tablet"] hides from 1024px up, and ["mobile", "xl"] is visible ≤767px and 1280–1535px. With only legacy keys listed, widths below 640px stay visible, as before. If a host narrows a device key off a legacy boundary, show rounds to the legacy boundaries: a legacy range starting inside the device's width stays visible in full.

In the editor

Since v1.13.0 (#842). The editor canvas previews visibility rules so authors see roughly what visitors will see:

  • Screen-size rules hide the block at the previewed viewport. Switch the viewport switcher to a device and blocks hidden at that width disappear from the canvas, using the same hidden width ranges as the front end. At the default (base) preview, the post editor's iframe canvas applies the front end's @media rules, so they test the canvas width. The site editor renders its canvas in the page rather than in an iframe, so at base it measures the canvas container's width (between the sidebars) and checks the ranges against that, instead of letting @media test the browser window.
  • A selected hidden block is shown dimmed. When a hidden block, or a block inside it, is selected (for example from List View), it is revealed at reduced opacity so its toolbar and inline editing keep working.
  • Rules the canvas can't evaluate dim the block at every width. The master Hide setting and the server-evaluated rules (login state, user role, specific user, date/time, schedule, query string, referrer, browser / OS / device) show the block dimmed rather than hidden.
  • List View labels say why. A block's List View label gets a suffix: "Hidden" (master Hide is on), "Hidden at some screen sizes" (a screen-size rule hides it at one or more breakpoints) or "Conditionally visible" (it has a server-evaluated rule). Custom block names keep the suffix.

The canvas styles for this live in a shared <style> in the canvas document's <head>, not next to each block, so they don't affect :first-child or block-gap selectors.


For developers

Configuration

// config/artisanpack/visual-editor.php
'visibility' => [
    'enabled'              => true,
    'user_model'           => null,        // defaults to auth.providers.users.model
    'user_search_columns'  => null,        // defaults to [ 'email', 'name' ] when present
],

Set enabled to false to bypass every visibility rule site-wide — useful during incident response if a mis-published rule accidentally hides critical content. When disabled the evaluator returns VisibilityDecision::visible() for every block without touching the rule registry.

Server-side integration

The Blade renderer calls VisibilityEvaluator::evaluate() for every block before rendering it. Blocks that return hidden() are dropped outright; blocks that return cssHidden() (only the screen-size rule today) are wrapped in a per-block <style> block emitting breakpoint-scoped display:none !important rules.

Most CSS-hidden blocks are wrapped in a <div class="ve-vis-N" data-ve-vis-scope> that carries the scope class. Blocks that render a single <li> root (core/navigation-link, core/navigation-submenu, core/list-item, and similar) sit directly inside a <ul> / <ol>, where a <div> is invalid HTML. For those, the scope class is added to the <li> itself and the <style> block is placed inside it, so list semantics stay intact for assistive technology. The React and Vue renderers do the same for core/navigation-link, core/navigation-submenu, and core/list-item.

For the React and Vue renderers, pipe your tree through \ArtisanPackUI\VisualEditor\Visibility\TreePruner::prune() before serialising it into the page payload. Hidden blocks are removed and CSS-hidden blocks receive a _veHiddenBreakpoints side-channel attribute the client picks up via stampVisibilityScopes().

use ArtisanPackUI\VisualEditor\Visibility\TreePruner;

$tree = $pruner->prune( $post->content );
return Inertia::render( 'PostShow', [ 'blocks' => $tree ] );
import { BlockTree, filterVisibleBlocks, stampVisibilityScopes } from '@artisanpack-ui/visual-editor-renderer-react';

const { tree, css } = stampVisibilityScopes( filterVisibleBlocks( blocks ) );

return (
    <>
        {css !== '' && <style>{css}</style>}
        <BlockTree tree={tree} />
    </>
);

Debug hook

Every evaluation fires ap.visualEditor.visibility.evaluated( decision, blockName, attributes, context ). Listen from a service provider to answer "why is this block hidden?":

addAction( 'ap.visualEditor.visibility.evaluated', function ( $decision, $name, $attrs, $ctx ) {
    if ( $decision->isHidden() ) {
        \Log::debug( sprintf(
            '%s hidden by %s (viewer: %s)',
            $name,
            implode( ', ', $decision->reasons ),
            $ctx->isAuthenticated ? $ctx->userEmail : 'anonymous',
        ) );
    }
} );

Adding a custom rule

Implement the VisibilityRule interface and register through the ap.visualEditor.visibility.registerRules filter:

use ArtisanPackUI\VisualEditor\Visibility\RuleRegistry;

addFilter( 'ap.visualEditor.visibility.registerRules', function ( RuleRegistry $registry ) {
    $registry->register( new \App\Visibility\CookieRule() );
    return $registry;
} );

Per-block opt-out

Every block ships opted in. Blocks that must not be conditionally hidden — e.g. a document title on a post editor — set supports.artisanpackVisibility: false in their block.json. Opt-out suppresses both the attribute injection and the inspector panel.

Audit command

php artisan visual-editor:audit-scheduled-blocks walks every resource registered under artisanpack.visual-editor.resources and prints a table of blocks with active dateTimeWindow or recurring rules. Filter to a single resource with --resource=pages.

Timezones

Both scheduling rules interpret their configured times in the timezone field. When omitted they fall back to config( 'app.timezone' ), which itself falls back to UTC. Wall-clock schedules ("10:00 America/Chicago every Sunday") stay correct across DST transitions — the rule matches at 10:30 whether that instant is CST or CDT.

Cache implications

User- and role-based rules make a page's rendered output dependent on the visitor. If a scheduled or auth-dependent block sits inside a cached template part or output cache, the cache TTL must be shorter than the window's precision (for a 5-minute recurring window, the cache TTL must be <5 minutes). A cache-tightening layer that auto-shortens TTLs when scheduled blocks are discovered is planned for a follow-up release; until then, hosts running the cache-heavy plans should either scope their cache keys to Auth::id() or set a short TTL for pages containing visibility-gated content.

Roles

The evaluator reads the viewer's role list from cms-framework's RoleManager when installed, falling back to Auth::user()->getRoleNames() (Spatie-compatible), ->roles (Eloquent relation with slug or name), or an empty list. Hosts on a non-standard role stack can override VisibilityEvaluator::resolveRoles() via container rebinding.

User-agent parsing

The Browser / OS / Device rule uses a bundled regex parser covering the top ~95% of real traffic. Hosts that need finer classification install jenssegers/agent and swap the bound UserAgentParser in their own service provider.