The page builder can show every block the way it looks on the site, using the site's own CSS, fonts and scripts. This article explains how designers set that up and keep previews looking right.
How previews work
In the page builder's Preview view, each block is shown as it will appear on the page. There are two ways this can happen:
- Inline previews (the default). The block's HTML is placed inside the admin page, so it is styled by the admin's CSS. Site classes, fonts and scripts are missing, and blocks rarely look like they do on the site.
- Preview frames. Each block is shown in its own iframe. The iframe's document comes from a Blade view that belongs to the site, so it loads the site's own CSS, fonts and scripts. The block looks as it does on the site.
Preview frames switch on as soon as the site has the view resources/views/wrangler/preview-frame.blade.php, and off again if it is removed. Nothing else needs to change.
The frame view is the designer's to own: it decides which stylesheets and scripts load, the page background, the body classes and how wide content can grow. The block's own markup comes from the block's views, as on the site.
The same frame is used by the block editor: opening a block shows a Preview panel under its settings, which refreshes as the form changes and shows unsaved edits. Nothing is saved until Save.
Note - preview frames need a version of textstem-laravel that lists "Block previews in the site's look" in its CHANGELOG.
Creating the preview frame
A developer can create a starting copy of the view with:
php artisan vendor:publish --tag=textstem-preview-frame
This writes resources/views/wrangler/preview-frame.blade.php. It is an ordinary Blade view, so it can also be created by hand. The starting copy looks like this:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $page?->title ?? config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white antialiased">
<main>
{!! $content !!}
</main>
</body>
</html>
The view receives three variables:
| Variable | Contents |
|---|---|
$content |
the block's HTML, ready to print with {!! !!} |
$component |
the block as an array: type, title, content, data and so on |
$page |
the page the block belongs to, or null |
$component['type'] makes it possible to vary the frame by block, for example a dark background behind a block that is always placed on a dark band:
<body class="{{ in_array($component['type'], ['Hero', 'Banner']) ? 'bg-neutral-950' : 'bg-white' }}">
Textstem adds its own small script before </body> in every frame. It reports the frame's height to the page builder and stops links and forms inside the preview from navigating. The view doesn't need to include it.
Loading the site's styles
The frame should load what the site's public layout loads, and nothing that belongs to the page around the blocks. The simplest way to keep the two in step is to copy the <head> of the public layout (often resources/views/layouts/public.blade.php) and then remove what previews don't need.
Keep:
- the Vite entries (
@vite([...])), so Tailwind classes and the site's CSS apply - web fonts, from Google Fonts, Typekit or self-hosted
@font-facerules - third-party stylesheets blocks rely on, such as a slider's CSS
@livewireStyles, if blocks contain Livewire components- the site's JavaScript, so Alpine-driven blocks, carousels and anything hidden with
x-cloakshow as they do on the site
Leave out:
- the header, navigation and footer: a frame shows one block
- analytics, tag managers, chat widgets and cookie banners
- page transitions (Swup, Barba and similar) and smooth-scroll libraries
<meta>tags for search engines and social sharing
The <body> should carry the classes that set the site's base type and colour (for example font-sans text-gray-800 antialiased), but not layout classes that make the body fill the window.
Note - keep html and body at their natural height. Classes such as h-full, h-screen or min-h-screen on either element stop the frame from sizing itself to the block, because each frame takes its height from where the body ends.
Note - the site's assets must be built (npm run build) or the Vite dev server running (npm run dev), or frames show a Vite error instead of the block.
Making previews behave
A preview is a small, still picture of a block, shown many times on one screen. A few things that work well on a full page get in the way there. Every frame marks its root element with data-textstem-preview, so the site's CSS can adjust them for previews only.
Stop motion. Animations and transitions replay every time a preview reloads:
[data-textstem-preview] *,
[data-textstem-preview] *::before,
[data-textstem-preview] *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
Give full-height sections a real height. Inside a frame, 100vh is the height of the frame, which starts small and grows to fit its content. A hero set to h-screen or min-h-screen collapses. Give such sections a fixed height in previews:
[data-textstem-preview] .h-screen,
[data-textstem-preview] .min-h-screen {
height: auto;
min-height: 32rem;
}
Pin nothing. Elements with position: fixed or sticky attach to the frame, not the page, and can cover the block:
[data-textstem-preview] .fixed,
[data-textstem-preview] .sticky {
position: static;
}
Show content that waits for scrolling. Blocks that fade in when scrolled into view (AOS, x-intersect, and similar) may never be revealed:
[data-textstem-preview] [data-aos] {
opacity: 1 !important;
transform: none !important;
}
Links and forms inside a preview don't navigate. In the page builder, clicking a preview selects the block rather than the element clicked, so carousels and tabs can't be operated there; the block editor's preview panel does respond to clicks and hovers.
Note - a frame grows to fit its block, up to 4,000 pixels. Very long blocks are cut off at that height.
Preview views for individual blocks
What appears inside a frame is the block's preview: the HTML its preview() method returns, usually from the view resources/views/wrangler/components/<block>/preview.blade.php. A block with no preview of its own is shown by its real view instead, rendered as it would be on its page.
With frames on, a preview view is styled by the site, so it can use the same classes as the block's real view:
{{-- resources/views/wrangler/components/artist-grid/preview.blade.php --}}
<section class="mx-auto max-w-7xl px-6 py-12">
<h2 class="font-display text-3xl">{{ $title ?: 'Artists' }}</h2>
<div class="mt-6 grid grid-cols-4 gap-4">
@foreach ($sample as $artist)
<img class="aspect-square w-full rounded object-cover" src="/img/tile/{{ $artist->image }}" alt="">
@endforeach
</div>
</section>
Give a block its own preview view when its real view:
- loads a lot of data (a full listing, a calendar) and a sample reads just as well
- depends on the visitor (signed-in content, a basket) or on a search
- is interactive in a way a still picture can't show, and a summary helps editors more
Preview views written for inline previews often use inline style attributes, because the admin's CSS didn't have the site's classes. Once frames are on, those can be replaced with the site's classes so previews match the real block.
The short summary shown for each block in the page builder's List view is separate from this and is set with $preview_with, as described in Creating Page Components.
Width and breakpoints
A frame is as wide as the page builder's column, usually 700 to 900 pixels on a laptop. Breakpoints inside the frame respond to that width, not the editor's screen, so a block may show its tablet layout (md:) in the page builder and its desktop layout (lg:, xl:) on the site.
- Check that every block reads well at about 800 pixels; editors judge a block by its preview.
- Use the page's Preview button to see full-width layouts.
- To keep content from stretching in wide editor windows, limit the width in the frame view, as the site's layout does, for example
<main class="mx-auto max-w-7xl">.
Blocks that use container queries (@container) instead of viewport breakpoints show the same layout in the preview as on the site, at the same block width.
Checklist
- [ ]
resources/views/wrangler/preview-frame.blade.phpexists and loads the site's CSS, fonts and scripts - [ ] No header, footer, analytics, cookie banner or page transitions in the frame
- [ ]
htmlandbodyhave no full-height classes - [ ] Animations and transitions are off under
[data-textstem-preview] - [ ] Full-height heroes have a fixed height in previews; fixed and sticky elements are static
- [ ] Content revealed on scroll is visible in previews
- [ ] Preview views use the site's classes, not inline styles
- [ ] Every block reads well at about 800 pixels wide
- [ ] Blocks that load a lot of data or depend on the visitor have their own preview view
- [ ] The site's assets are built, or the Vite dev server is running