Search Snippet Preview
Write a page title, description and slug and see how the search result will look, with width-based truncation.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/seo-snippet.json'use client'
import { useState } from 'react'
import './seo-snippet.css'
const W: [RegExp, number][] = [[/[ilIj.,;:'!|]/, 0.3], [/[ftr ]/, 0.36], [/[mwMW]/, 0.88], [/[A-Z]/, 0.68], [/[0-9]/, 0.56]]
const px = (s: string, size: number) => [...s].reduce((a, c) => a + (W.find(([r]) => r.test(c))?.[1] ?? 0.53) * size, 0)
function cut(s: string, size: number, limit: number) { if (px(s, size) <= limit) return s; let out = ''; for (const c of s) { if (px(out + c + '…', size) > limit) break; out += c } return out.trimEnd() + ' …' }
const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9/]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
export interface SeoSnippetProps {
domain?: string
device?: 'desktop' | 'mobile'
fill?: 'dark' | 'light'
className?: string
}
/** Search result snippet editor with pixel-width limits. */
export function SeoSnippet({ domain = 'sundae.design', device = 'desktop', fill = 'dark', className }: SeoSnippetProps) {
const [title, setTitle] = useState('Card-sized UI components you can copy and paste')
const [desc, setDesc] = useState('Hundreds of interactive, accessible React components in plain CSS. Monochrome by default, tweakable in the browser, and free to use in your next product.')
const [slug, setSlug] = useState('components/interactive')
const [dev, setDev] = useState(device)
const tl = dev === 'mobile' ? 480 : 580, dl = dev === 'mobile' ? 680 : 920
const tw = px(title, 18), dw = px(desc, 13)
const meter = (w: number, l: number) => <div className="sd-seo__m" data-over={w > l || undefined}><i style={{ width: `${Math.min(100, (w / l) * 100)}%` }} /></div>
return (
<div className={['sd-seo', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-seo__seg" role="radiogroup" aria-label="Preview">{(['desktop', 'mobile'] as const).map((d) => <button key={d} type="button" role="radio" aria-checked={dev === d} onClick={() => setDev(d)}>{d === 'desktop' ? 'Desktop' : 'Mobile'}</button>)}</div>
<div className="sd-seo__pv" data-dev={dev} aria-label="Search result preview">
<div className="sd-seo__crumb"><i /><span>{domain} › {slug.split('/').filter(Boolean).join(' › ') || 'home'}</span></div>
<div className="sd-seo__t">{cut(title || 'Untitled page', 18, tl)}</div>
<div className="sd-seo__d">{cut(desc || 'Add a description so searchers know what the page offers.', 13, dl)}</div>
</div>
<label className="sd-seo__f"><span>Title<b data-over={tw > tl || undefined}>{title.length} chars · {Math.round(tw)}/{tl}px</b></span><input className="sd-seo__in" aria-label="Title" value={title} onChange={(e) => setTitle(e.target.value)} />{meter(tw, tl)}</label>
<label className="sd-seo__f"><span>Description<b data-over={dw > dl || undefined}>{desc.length} chars · {Math.round(dw)}/{dl}px</b></span><textarea className="sd-seo__in" aria-label="Description" value={desc} onChange={(e) => setDesc(e.target.value)} />{meter(dw, dl)}</label>
<label className="sd-seo__f"><span>Slug<b>/{slugify(slug)}</b></span><input className="sd-seo__in" aria-label="Slug" value={slug} onChange={(e) => setSlug(e.target.value)} onBlur={() => setSlug(slugify(slug))} /></label>
</div>
)
}.sd-seo { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-seo[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-seo :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-seo :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-seo .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-seo { width: min(100%, 350px); padding: 14px; grid-template-columns: minmax(0, 1fr); border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); display: grid; gap: 9px; }
.sd-seo__seg { display: flex; padding: 2px; border-radius: 9px; background: var(--soft); } .sd-seo__seg button { flex: 1; height: 30px; border-radius: 7px; font-size: 12px; color: var(--mute) !important; white-space: nowrap; padding: 0 4px; } .sd-seo__seg button[aria-checked='true'] { background: var(--bg) !important; color: var(--fg) !important; box-shadow: inset 0 0 0 1px var(--line); font-weight: 600; }
.sd-seo__rg { display: grid; grid-template-columns: 76px minmax(0, 1fr) 56px; align-items: center; gap: 8px; font-size: 12px; color: var(--mute); } .sd-seo__rg b { color: var(--fg); text-align: right; font-variant-numeric: tabular-nums; } .sd-seo__rg input { width: 100%; margin: 0; accent-color: var(--fg); }
.sd-seo__in { width: 100%; min-width: 0; height: 36px; padding: 0 10px; border-radius: 10px; border: 0; background: var(--soft); color: var(--fg); font: inherit; font-size: 13px; outline: 0; box-sizing: border-box; } .sd-seo__in:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-seo__pv { padding: 12px; border-radius: 12px; background: #fff; color: #202124; display: grid; gap: 3px; font-family: Arial, Helvetica, sans-serif; }
.sd-seo__pv[data-dev='mobile'] { max-width: 260px; justify-self: center; }
.sd-seo__crumb { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #4d5156; min-width: 0; }
.sd-seo__crumb i { flex: none; width: 18px; height: 18px; border-radius: 50%; background: #e8e8e8; box-shadow: inset 0 0 0 1px #d5d5d5; }
.sd-seo__crumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sd-seo__t { font-size: 18px; line-height: 1.3; color: #1f1f1f; text-decoration: underline; text-decoration-color: transparent; }
.sd-seo__d { font-size: 13px; line-height: 1.5; color: #4d5156; }
.sd-seo__f { display: grid; gap: 4px; }
.sd-seo__f > span { display: flex; justify-content: space-between; font-size: 11px; color: var(--mute); }
.sd-seo__f > span b { font-weight: 500; font-variant-numeric: tabular-nums; }
.sd-seo__f > span b[data-over] { color: var(--fg); font-weight: 700; }
.sd-seo textarea { height: 56px; resize: none; padding: 8px 10px; line-height: 1.4; }
.sd-seo__m { height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; }
.sd-seo__m i { display: block; height: 100%; background: var(--fg); transition: width 120ms; }
.sd-seo__m[data-over] { background: var(--fg); }
.sd-seo__m[data-over] i { background: var(--mute); }
@media (prefers-reduced-motion: reduce) { .sd-seo *, .sd-seo *::before, .sd-seo *::after { animation: none !important; transition: none !important; } }Overview
Truncation follows estimated pixel width rather than character count, since a title of wide letters cuts off sooner. Meters show how much room is left.
Desktop and mobile previews use different limits, and the slug field tidies itself into a readable path.
When to use
- CMS editors, blog publishing flows, marketing site settings.
When not to use
- Guaranteeing what search engines display: they may rewrite it.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/seo-snippet.json$
npx shadcn@latest add https://sundaeui.dev/r/seo-snippet.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/seo-snippet.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/seo-snippet.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy seo-snippet.tsx and seo-snippet.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { SeoSnippet } from '@/components/sundae/seo-snippet'
<SeoSnippet />Props
| Prop | Type | Default |
|---|---|---|
domainSite shown in the breadcrumb. | string | 'sundae.design' |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Each field has a counter read alongside it.
- The preview is plain text so screen readers get the truncated result.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-seo; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Search Snippet Preview free?
- Yes. Search Snippet Preview is free for personal and commercial projects, with full source on this page.
- How do I install Search Snippet Preview?
- Run npx shadcn@latest add https://sundaeui.dev/r/seo-snippet.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Search Snippet Preview support keyboard and screen readers?
- Each field has a counter read alongside it. The preview is plain text so screen readers get the truncated result.
- Does Search Snippet Preview respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Search Field
Forms · RefinedMarkdown Editor
Forms · RefinedOTP Field
Forms · Refined- Pro
Drop Zone
Forms · Refined - Pro
Onboarding Flow
Forms · Refined - Pro
Wheel Picker
Forms · Refined