Files
RenewCMS/docs/design/source/RenewCMS Components Primitives.dc.html
T

345 lines
77 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="icon" href="brand/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>body{margin:0;font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px}*{box-sizing:border-box}a{color:inherit;text-underline-offset:3px}a:hover{opacity:.8}code{font-family:'IBM Plex Mono',monospace;font-size:12px}</style>
</helmet>
<div ref="{{ rootRef }}" data-screen-label="Primitives" style="--ui-bg:#ffffff;--ui-bg-muted:#fafafa;--ui-bg-elevated:#f3f2f0;--ui-bg-accented:#e6e4e0;--ui-bg-inverted:#111111;--ui-text-dimmed:#999999;--ui-text-muted:#666666;--ui-text-toned:#444444;--ui-text:#222222;--ui-text-highlighted:#111111;--ui-text-inverted:#ffffff;--ui-border:#e6e4e0;--ui-border-muted:#f3f2f0;--ui-border-accented:#8a8783;--ui-primary:#111111;--ui-secondary:#635643;--ui-success:#315b33;--ui-info:#34536d;--ui-warning:#6c4700;--ui-error:#882c21;--rc-primary-hover:#333333;--rc-primary-active:#444444;--rc-error-hover:#6a1a11;--rc-sand:#e8dfd1;--rc-success-soft:#eaefea;--rc-info-soft:#ebeef0;--rc-warning-soft:#f0ede6;--rc-error-soft:#f3eae9;--rc-overlay:rgb(10 10 10 / 0.4);--rc-shadow-pop:0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06);--rc-shadow-modal:0 16px 48px rgb(17 17 17 / 0.18);background:var(--ui-bg-muted);color:var(--ui-text);min-height:100vh">
<div style="max-width:1200px;margin:0 auto;padding:48px 24px 96px;display:flex;flex-direction:column">
<div style="display:flex;flex-wrap:wrap;align-items:flex-end;gap:24px;padding-bottom:40px">
<div style="flex:1;min-width:280px;display:flex;flex-direction:column;gap:10px">
<h1 style="margin:0;font:600 32px/40px 'IBM Plex Sans',sans-serif;letter-spacing:-0.02em;color:var(--ui-text-highlighted)">Components: themed primitives</h1>
<p style="margin:0;max-width:720px;font-size:15px;line-height:24px;color:var(--ui-text-toned);text-wrap:pretty">These are Nuxt UI components with the Ledger config applied. Every specimen reads the real <code>--ui-*</code> variables, so the theme switch on the right changes the whole page. The product components are on <a href="RenewCMS Components Product.dc.html">a second page</a>.</p>
</div>
<div role="radiogroup" aria-label="Theme" style="display:flex;border:1px solid var(--ui-border-accented);background:var(--ui-bg)">
<sc-for list="{{ modes }}" as="m" hint-placeholder-count="3">
<button role="radio" aria-checked="{{ m.on }}" onClick="{{ m.pick }}" style="all:unset;cursor:pointer;height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;font-weight:500;background:{{ m.bg }};color:{{ m.fg }}" style-focus-visible="outline:2px solid var(--ui-primary);outline-offset:2px">{{ m.label }}</button>
</sc-for>
</div>
</div>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Button</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">There are five roles. A screen gets one primary button. Danger is used only for irreversible actions, and always inside a confirmation step.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-direction:column;gap:28px">
<div style="display:flex;flex-wrap:wrap;gap:24px;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat"></span>New article</span><code style="color:var(--ui-text-muted)">primary</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented);font-weight:500">Cancel</span><code style="color:var(--ui-text-muted)">secondary</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/book-open.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/book-open.svg') center / contain no-repeat"></span>Preview</span><code style="color:var(--ui-text-muted)">ghost</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-error);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>Delete article</span><code style="color:var(--ui-text-muted)">danger</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span><code style="color:var(--ui-text-muted)">icon-only</code></div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:24px;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:28px;padding:0 10px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:12px">Save</span><code style="color:var(--ui-text-muted)">sm 28</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">Save</span><code style="color:var(--ui-text-muted)">md 32</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:40px;padding:0 16px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:14px">Save</span><code style="color:var(--ui-text-muted)">lg 40</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:44px;padding:0 18px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:16px">Save</span><code style="color:var(--ui-text-muted)">xl 44, touch</code></div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:24px;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">Publish</span><code style="color:var(--ui-text-muted)">default</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--rc-primary-hover);color:var(--ui-bg);font-weight:500">Publish</span><code style="color:var(--ui-text-muted)">hover</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;outline:2px solid var(--ui-primary);outline-offset:2px">Publish</span><code style="color:var(--ui-text-muted)">focus-visible</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--rc-primary-active);color:var(--ui-bg);font-weight:500">Publish</span><code style="color:var(--ui-text-muted)">active</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;opacity:0.4">Publish</span><code style="color:var(--ui-text-muted)">disabled</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>Publishing…</span><code style="color:var(--ui-text-muted)">loading</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--rc-error-hover);color:var(--ui-bg);font-weight:500">Delete article</span><code style="color:var(--ui-text-muted)">danger hover</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented);font-weight:500">Cancel</span><code style="color:var(--ui-text-muted)">secondary hover</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/book-open.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/book-open.svg') center / contain no-repeat"></span>Preview</span><code style="color:var(--ui-text-muted)">ghost hover</code></div>
</div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UButton</code>. Primary <code>color="primary" variant="solid"</code> · secondary <code>color="neutral" variant="outline"</code> · ghost <code>color="neutral" variant="ghost"</code> · danger <code>color="error" variant="solid"</code> · icon-only <code>square icon="i-lucide-ellipsis" :aria-label</code> wrapped in <code>UTooltip</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Loading</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>:loading</code> swaps the leading icon for <code>i-lucide-loader-circle</code> and disables the button. The label changes to the -ing form ("Publishing…") and the button width stays fixed, so the layout doesn't jump. <code>loading-auto</code> is used on async <code>@click</code> handlers.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard, ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Native <code>&lt;button&gt;</code>, activated with Enter or Space. Icon-only buttons need an <code>aria-label</code>. Loading sets <code>aria-busy="true"</code>. Disabled buttons stay focusable only when a tooltip explains why they're disabled.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Responsive</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>md</code> on desktop, <code>xl</code> (44 px) below <code>md</code>. Primary page actions go full width in sticky footers on 390 px screens.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Bend</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Disabled opacity is 40% instead of Nuxt UI's 75%, because a 75% black button still looks clickable. Hover darkens the button one step instead of using Nuxt UI's <code>/75</code> alpha, so the label keeps its contrast.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Input, password input, textarea</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">One outline style for every field. Focus adds a 2 px ink ring inside the edge. Errors turn the edge brick red and always come with a message, never color alone.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,240px),1fr));gap:24px">
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-muted)">marta</span><code style="color:var(--ui-text-muted)">default, placeholder</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-text-muted);background:var(--ui-bg);color:var(--ui-text-muted)">marta</span><code style="color:var(--ui-text-muted)">hover</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-primary);box-shadow:inset 0 0 0 1px var(--ui-primary);background:var(--ui-bg);color:var(--ui-text-highlighted)">mar<span style="width:1px;height:16px;background:var(--ui-text-highlighted)"></span></span><code style="color:var(--ui-text-muted)">focus</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-error);box-shadow:inset 0 0 0 1px var(--ui-error);background:var(--ui-bg);color:var(--ui-text-highlighted)">ma</span><code style="color:var(--ui-text-muted)">error</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-border);background:var(--ui-bg-elevated);color:var(--ui-text-dimmed)">owner@renew.example</span><code style="color:var(--ui-text-muted)">disabled / read-only</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted)"><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat"></span></span><span style="flex:1">plaster</span><span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></span><code style="color:var(--ui-text-muted)">leading icon, clear</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted)"><span style="flex:1;letter-spacing:3px">••••••••••</span><span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/eye.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/eye.svg') center / contain no-repeat"></span></span></span><code style="color:var(--ui-text-muted)">password, hidden</code></div>
<div style="display:flex;flex-direction:column;gap:8px"><span style="height:32px;padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted)"><span style="flex:1">limewash2026</span><span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned);background:var(--ui-bg-elevated)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat"></span></span></span><code style="color:var(--ui-text-muted)">password, shown</code></div>
<div style="display:flex;flex-direction:column;gap:8px;grid-column:span 2"><span style="min-height:80px;padding:6px 10px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted)">A short first paragraph. You can keep writing in the editor.</span><code style="color:var(--ui-text-muted)">textarea, autoresize 3–8 rows</code></div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UInput variant="outline" color="primary"</code>, <code>:highlight</code> bound to the field error so the edge turns red. The search field is <code>icon="i-lucide-search"</code> with a <code>#trailing</code> clear <code>UButton</code>. <code>UTextarea autoresize :rows="3" :maxrows="8"</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Password</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UInput :type="shown ? 'text' : 'password'"</code> with a <code>#trailing</code> ghost <code>UButton</code>. <code>aria-label</code> reads "Show password" or "Hide password", with <code>aria-pressed</code> and <code>aria-controls</code>. <code>autocomplete</code> is <code>current-password</code> on log in and <code>new-password</code> on register and reset. Showing the password never clears or moves focus.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">States</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Default, hover (edge goes to muted), focus (2 px ink), filled, error, disabled, read-only (same look as disabled but selectable and focusable, used for the email on the reset screen). Fields are 16 px text below <code>md</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Placeholder</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Placeholders use <code>--ui-text-muted</code> (5.7:1) and only show an example. The label always carries the meaning.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Form field</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">A label on top, then the control, then the help text or the error in the same spot. The right-hand hint slot holds the character counter. Every field in the product is required, so none are marked.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:32px">
<div style="display:flex;flex-direction:column;gap:6px"><div style="display:flex;justify-content:space-between"><span style="font-weight:500;color:var(--ui-text-highlighted)">Username</span><span style="font:400 12px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">5 / 20</span></div><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted)">marta</span><span style="color:var(--ui-text-muted);font-size:12px;line-height:18px">3 to 20 characters. You'll use it to log in.</span></div>
<div style="display:flex;flex-direction:column;gap:6px"><div style="display:flex;justify-content:space-between"><span style="font-weight:500;color:var(--ui-text-highlighted)">Username</span><span style="font:400 12px 'IBM Plex Mono',monospace;color:var(--ui-error)">2 / 20</span></div><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-error);box-shadow:inset 0 0 0 1px var(--ui-error);background:var(--ui-bg);color:var(--ui-text-highlighted)">ma</span><span style="display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px;line-height:18px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat"></span>Use at least 3 characters.</span></div>
<div style="display:flex;flex-direction:column;gap:6px"><span style="font-weight:500;color:var(--ui-text-highlighted)">Email</span><span style="height:32px;padding:0 10px;display:flex;align-items:center;border:1px solid var(--ui-error);box-shadow:inset 0 0 0 1px var(--ui-error);background:var(--ui-bg);color:var(--ui-text-highlighted)">marta@renew</span><span style="display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px;line-height:18px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat"></span>Enter a full email address, like name@example.com.</span></div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UForm :schema :state @submit</code> + <code>UFormField name label help</code>, with the counter in the <code>#hint</code> slot. Validation runs on blur and on submit, then on every input once a field has an error, so the error clears as soon as it's fixed. The schema mirrors the API rules exactly, so a validator 400 shouldn't reach the UI.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Nuxt UI links the label and <code>aria-describedby</code> (help or error) and sets <code>aria-invalid</code>. On a failed submit, focus moves to the first invalid field. Error text sits in the help slot, so there is no layout shift.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Checkbox, switch, select</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">All three are square. The Preview toggle in the editor is the only switch. The select sets sort order in the mobile article list. No screen needs a checkbox yet, but it's themed so it matches if one gets added.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-wrap:wrap;gap:40px;align-items:flex-start">
<div style="display:flex;flex-direction:column;gap:12px">
<span style="display:flex;align-items:center;gap:8px"><span style="width:16px;height:16px;border:1px solid var(--ui-border-accented);background:var(--ui-bg)"></span>Off</span>
<span style="display:flex;align-items:center;gap:8px"><span style="width:16px;height:16px;background:var(--ui-primary);color:var(--ui-bg);display:flex;align-items:center;justify-content:center"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></span>On</span>
<span style="display:flex;align-items:center;gap:8px"><span style="width:16px;height:16px;background:var(--ui-primary);outline:2px solid var(--ui-primary);outline-offset:2px"></span>Focus</span>
<span style="display:flex;align-items:center;gap:8px;opacity:0.4"><span style="width:16px;height:16px;border:1px solid var(--ui-border-accented)"></span>Disabled</span>
</div>
<div style="display:flex;flex-direction:column;gap:12px">
<span style="display:flex;align-items:center;gap:10px"><span style="width:36px;height:20px;background:var(--ui-bg-accented);padding:2px;display:flex"><span style="width:16px;height:16px;background:var(--ui-bg);box-shadow:0 0 0 1px var(--ui-border-accented)"></span></span>Preview off</span>
<span style="display:flex;align-items:center;gap:10px"><span style="width:36px;height:20px;background:var(--ui-primary);padding:2px;display:flex;justify-content:flex-end"><span style="width:16px;height:16px;background:var(--ui-bg)"></span></span>Preview on</span>
<span style="display:flex;align-items:center;gap:10px"><span style="width:36px;height:20px;background:var(--ui-primary);padding:2px;display:flex;justify-content:flex-end;outline:2px solid var(--ui-primary);outline-offset:2px"><span style="width:16px;height:16px;background:var(--ui-bg)"></span></span>Focus</span>
<span style="display:flex;align-items:center;gap:10px;opacity:0.4"><span style="width:36px;height:20px;background:var(--ui-bg-accented);padding:2px;display:flex"><span style="width:16px;height:16px;background:var(--ui-bg)"></span></span>Disabled</span>
</div>
<div style="display:flex;flex-direction:column;gap:4px;width:240px">
<span style="height:32px;padding:0 8px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-primary);box-shadow:inset 0 0 0 1px var(--ui-primary);background:var(--ui-bg);color:var(--ui-text-highlighted)"><span style="flex:1">Newest updated first</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px">
<div style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span style="flex:1">Newest updated first</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></div>
<div style="height:32px;padding:0 8px;display:flex;align-items:center;color:var(--ui-text)">Oldest updated first</div>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UCheckbox</code>, <code>USwitch</code> (slots <code>base</code> and <code>thumb</code> set to <code>rounded-none</code>), <code>USelect :items value-key="value"</code>, whose content uses <code>--shadow-popover</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Space toggles a checkbox or switch. The select opens with Enter, Space or ↓, moves with the arrow keys and type-ahead, and closes with Esc, returning focus to the trigger. The switch uses <code>role="switch"</code> and <code>aria-checked</code>, and its visible label is its name.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Badge</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Badges show status or a count. They never replace a button. Solid ink is for the one state that matters most in a list. Soft semantic badges are for system messages.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-wrap:wrap;gap:20px;align-items:center">
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span>
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span>
<span style="height:20px;min-width:20px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned);font:500 11px 'IBM Plex Mono',monospace">12</span>
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;background:var(--rc-success-soft);color:var(--ui-success);font-size:12px;font-weight:500">success</span>
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;background:var(--rc-info-soft);color:var(--ui-info);font-size:12px;font-weight:500">info</span>
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;background:var(--rc-warning-soft);color:var(--ui-warning);font-size:12px;font-weight:500">warning</span>
<span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;background:var(--rc-error-soft);color:var(--ui-error);font-size:12px;font-weight:500">error</span>
<span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font:500 11px 'IBM Plex Mono',monospace">GET</span>
<span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online · md</span>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UBadge</code>. Solid ink <code>color="neutral" variant="solid"</code> · outline <code>color="neutral" variant="outline"</code> · count <code>color="neutral" variant="soft" size="sm"</code> · semantic <code>variant="soft"</code>. Sizes are <code>sm</code> (22 px, in tables) and <code>md</code> (26 px, in the editor header).</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Badges are text, not live regions. When a status changes, the announcement comes from the toast.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Alert</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Alerts are for messages that belong to a page and stay until the situation changes. Anything that confirms an action someone just took is a toast instead.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,460px),1fr));gap:16px">
<div role="status" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-info-soft);color:var(--ui-info)"><span style="padding-top:2px;display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Your session expired</span><span style="color:var(--ui-text-toned)">Log in again to pick up where you left off.</span></div></div>
<div role="status" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-success-soft);color:var(--ui-success)"><span style="padding-top:2px;display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-check.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Password changed</span><span style="color:var(--ui-text-toned)">Log in with your new password.</span></div><span style="display:flex;color:var(--ui-text-toned);height:20px;align-items:center"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
<div role="status" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-warning-soft);color:var(--ui-warning)"><span style="padding-top:2px;display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/triangle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/triangle-alert.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Almost at the length limit</span><span style="color:var(--ui-text-toned)">9 240 of 10 000 characters used. Images and formatting count too.</span></div></div>
<div role="alert" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="padding-top:2px;display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't load your articles</span><span style="color:var(--ui-text-toned)">Check your connection and try again.</span></div><span style="align-self:flex-start;height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Try again</span></div></div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UAlert variant="soft" :color :icon :title :description :actions</code>. <code>close</code> only on alerts that are safe to dismiss (password changed, session expired). The error alert has no close button because its retry is the way out.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Info, success and warning use <code>role="status"</code> and error uses <code>role="alert"</code>. Alerts that are already there when the page loads aren't announced again. The description text uses <code>--ui-text-toned</code> rather than the semantic color, so long text stays readable.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Toast</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Toasts confirm that an action finished or failed. Each one has a short title and, at most, one action.</p>
<div style="background:var(--ui-bg-muted);border:1px solid var(--ui-border);padding:24px;display:flex;flex-wrap:wrap;gap:24px;align-items:flex-start">
<div style="width:356px;max-width:100%;display:flex;flex-direction:column;gap:8px">
<div style="display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)"><span style="display:flex;color:var(--ui-success);padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-check.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:var(--ui-text-highlighted)">Article published</span><span style="color:var(--ui-text-muted)">"Winter checklist for timber windows" is now online.</span></div><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
<div style="display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)"><span style="display:flex;color:var(--ui-info);padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:var(--ui-text-highlighted)">That article no longer exists</span><span style="color:var(--ui-text-muted)">It was deleted somewhere else, so we removed it from the list.</span></div><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
<div style="display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)"><span style="display:flex;color:var(--ui-error);padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:var(--ui-text-highlighted)">Couldn't unpublish</span><span style="color:var(--ui-text-muted)">The server didn't respond.</span></div><span style="align-self:flex-start;height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Retry</span></div><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
</div>
<div style="flex:1;min-width:260px;display:grid;grid-template-columns:minmax(0,130px) minmax(0,1fr);font-size:13px;border-top:1px solid var(--ui-border)">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Placement</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Bottom right on desktop, 24 px from the edges. Bottom center at full width minus 16 px on mobile, above the sticky action bar.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Duration</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Success and info close after 5 s. Errors with retry stay until dismissed or retried. The timer pauses on hover and on focus.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Stack</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">At most 3 visible, newest at the bottom. A fourth pushes out the oldest non-error toast. Repeating the same action replaces its toast instead of adding a new one.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UApp :toaster="{ position: 'bottom-right', duration: 5000, max: 3 }"</code>. Toasts are raised with <code>useToast().add({ id, title, description, color, icon, actions, duration })</code>, where <code>id</code> is the action plus the article id so a repeat replaces the old toast. Errors use <code>duration: 0</code>. The progress bar is off.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">The Reka toast region announces politely, and errors assertively. F8 jumps to the region. Esc closes the focused toast.</span>
</div>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Modal and slideover</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Modals ask a question or hold a short form. Slideovers carry navigation and side panels on small screens. Both sit over a 40% ink overlay.</p>
<div style="background:var(--rc-overlay);padding:40px 24px;display:flex;flex-wrap:wrap;gap:32px;justify-content:center;align-items:flex-start">
<div role="dialog" aria-modal="true" style="width:440px;max-width:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal)">
<div style="padding:20px 24px 0;display:flex;align-items:flex-start;gap:12px"><div style="flex:1;display:flex;flex-direction:column;gap:4px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Leave without saving?</span><span style="color:var(--ui-text-muted)">You have changes to "How we price a bathroom renovation" that haven't been saved.</span></div><span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
<div style="padding:20px 24px;display:flex;justify-content:flex-end;gap:8px"><span style="height:32px;padding:0 12px;display:flex;align-items:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500">Discard changes</span><span style="height:32px;padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">Keep editing</span></div>
</div>
<div style="width:320px;max-width:100%;height:340px;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column">
<div style="height:52px;padding:0 16px;display:flex;align-items:center;border-bottom:1px solid var(--ui-border)"><span style="flex:1;font-weight:600;font-size:15px;color:var(--ui-text-highlighted)">Details</span><span style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div>
<div style="padding:16px;display:grid;grid-template-columns:auto 1fr;gap:10px 16px;font-size:14px"><span style="color:var(--ui-text-muted)">Created</span><span>12 Mar 2026, 09:14</span><span style="color:var(--ui-text-muted)">Updated</span><span>Today, 11:02</span><span style="color:var(--ui-text-muted)">ID</span><span style="font-family:'IBM Plex Mono',monospace">12</span><span style="color:var(--ui-text-muted)">Images</span><span>3</span></div>
</div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UModal :title :description</code> with the <code>#footer</code> slot for actions, width <code>sm:max-w-md</code>. Opened programmatically with <code>useOverlay().create(ConfirmDialog)</code>. <code>USlideover side="right"</code> for details, <code>side="left"</code> for mobile navigation (built into <code>UDashboardSidebar</code>).</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Button order</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">The safe action is primary and sits on the right, where the eye lands. In destructive confirmations, the danger button is on the right and Cancel on the left (see ConfirmDialog). On mobile the buttons stack full width, with the primary on top.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard, ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Focus is trapped and starts on the least destructive button. Esc and the overlay close it unless it's <code>:dismissible="false"</code>, which is used while an action is running. Focus returns to the trigger. The title is the <code>aria-labelledby</code> and the description is the <code>aria-describedby</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Responsive</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Below <code>sm</code>, modals sit 16 px from the screen edges and slideovers take the full width. Motion follows the foundations: 240 ms, and instant with reduced motion.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Dropdown menu and tooltip</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">The row-action menu is the main dropdown. Delete always comes last, below a separator, in brick red. Tooltips name icon-only buttons and show their keyboard shortcut.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-wrap:wrap;gap:48px;align-items:flex-start">
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;width:220px">
<span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span>
<div role="menu" style="width:220px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px">
<div style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat"></span>Edit</div>
<div style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;color:var(--ui-text)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe-lock.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe-lock.svg') center / contain no-repeat"></span>Unpublish</div>
<div style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;color:var(--ui-text)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat"></span>Copy API URL</div>
<div style="height:1px;background:var(--ui-border);margin:4px 0"></div>
<div style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>Delete…</div>
</div>
</div>
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;padding-top:4px">
<div style="background:var(--ui-bg-inverted);color:var(--ui-text-inverted);padding:4px 8px;display:flex;align-items:center;gap:8px;font-size:12px">Bold<span style="display:flex;gap:2px"><span style="height:18px;min-width:18px;padding:0 4px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-text-muted);font:500 11px 'IBM Plex Mono',monospace">Ctrl</span><span style="height:18px;min-width:18px;padding:0 4px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-text-muted);font:500 11px 'IBM Plex Mono',monospace">B</span></span></div>
<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/bold.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/bold.svg') center / contain no-repeat"></span></span>
</div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UDropdownMenu :items :content="{ align: 'end' }"</code>, with items grouped in nested arrays so separators appear automatically. The Delete item is <code>color: 'error'</code>, and its label ends in "…" because it opens a confirmation. <code>UTooltip :text :kbds="['ctrl', 'b']" :delay-duration="400"</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">The menu opens with Enter, Space or ↓. Arrow keys and type-ahead move through it, Home and End jump to the ends, and Esc closes it and returns focus. Tooltips appear on focus as well as hover and never hold information that isn't also in the <code>aria-label</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Touch</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Tooltips don't show on touch, so every icon-only control on mobile also appears in a labeled menu or has a visible label.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Tabs</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Tabs filter the article list. Each count comes from the full list, so it doesn't change while you search.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-direction:column;gap:24px">
<div role="tablist" style="display:flex;gap:20px;border-bottom:1px solid var(--ui-border)">
<span role="tab" aria-selected="true" style="height:36px;display:flex;align-items:center;gap:6px;border-bottom:2px solid var(--ui-primary);margin-bottom:-1px;font-weight:500;color:var(--ui-text-highlighted)">All<span style="font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">6</span></span>
<span role="tab" style="height:36px;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned)">Online<span style="font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">4</span></span>
<span role="tab" style="height:36px;display:flex;align-items:center;gap:6px;color:var(--ui-text-highlighted);border-bottom:2px solid var(--ui-border-accented);margin-bottom:-1px">Drafts<span style="font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">2</span></span>
<span role="tab" style="height:30px;align-self:center;padding:0 4px;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned);outline:2px solid var(--ui-primary);outline-offset:0">Focused</span>
</div>
<span style="color:var(--ui-text-muted);font-size:12px">From left: selected, default, hover, focus-visible.</span>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UTabs variant="link" color="neutral" :content="false" :items v-model</code>. The count goes in the <code>#trailing</code> slot as the mono <code>sm</code> count badge. The value syncs to <code>?status=online|draft</code> so the filtered view can be linked.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Roving tabindex: Tab enters the tab list on the selected tab, and ← and → move and activate. Each tab's accessible name includes its count, for example "Drafts, 2 articles".</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Responsive</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Tabs stay in one row at 360 px, which is why the labels are short. Each tab is 44 px tall on touch.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Table and pagination</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Rows are 44 px with hairline separators, and only the Updated column sorts. Pagination appears only when a filter shows more than 25 articles.</p>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border)">
<div style="display:grid;grid-template-columns:minmax(0,1fr) 110px 150px 44px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500"><span>Title</span><span>Status</span><span style="display:flex;align-items:center;gap:4px;color:var(--ui-text-highlighted)">Updated<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat"></span></span><span></span></div>
<div style="display:grid;grid-template-columns:minmax(0,1fr) 110px 150px 44px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)"><span style="font-weight:500;color:var(--ui-text-highlighted)">How we price a bathroom renovation</span><span>default</span><span style="color:var(--ui-text-muted)">2 hours ago</span><span style="display:flex;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span></div>
<div style="display:grid;grid-template-columns:minmax(0,1fr) 110px 150px 44px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);background:var(--ui-bg-elevated)"><span style="font-weight:500;color:var(--ui-text-highlighted)">Choosing lime plaster for old stone walls</span><span>hover</span><span style="color:var(--ui-text-muted)">Yesterday</span><span style="display:flex;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span></div>
<div style="display:grid;grid-template-columns:minmax(0,1fr) 110px 150px 44px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);outline:2px solid var(--ui-primary);outline-offset:-2px"><span style="font-weight:500;color:var(--ui-text-highlighted)">Before and after: the Rue Verte kitchen</span><span>focus-visible</span><span style="color:var(--ui-text-muted)">3 days ago</span><span style="display:flex;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span></div>
<div style="display:grid;grid-template-columns:minmax(0,1fr) 110px 150px 44px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);opacity:0.5"><span style="font-weight:500;color:var(--ui-text-highlighted)">Winter checklist for timber windows</span><span style="display:flex;align-items:center;gap:6px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>busy</span><span style="color:var(--ui-text-muted)">5 days ago</span><span></span></div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 12px;border-top:1px solid var(--ui-border);flex-wrap:wrap"><span style="color:var(--ui-text-muted)">26–50 of 142</span><div style="display:flex;gap:4px"><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat"></span></span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)">1</span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">2</span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)">3</span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)">…</span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)">6</span><span style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></span></div></div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UTable :data :columns v-model:sorting v-model:global-filter v-model:pagination :loading</code> with TanStack <code>getPaginationRowModel</code>, and <code>@select</code> for row click. <code>UPagination :total :items-per-page="25" :sibling-count="1" show-edges</code>, active item <code>color="primary" variant="solid"</code>.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">States</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Default, hover (<code>bg-elevated</code>), focus-visible (2 px inset ring), busy (a row action is running: 50% opacity, a spinner in the status cell, <code>aria-busy</code>, actions disabled), loading (skeleton rows), empty (EmptyState in the <code>#empty</code> slot).</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Keyboard, ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Each row's title is a real link to the editor, so Tab reaches it and Enter opens it. The rest of the row is a click target for the mouse only. The sort header is a button with <code>aria-sort</code>. Pagination is a <code>nav</code> labeled "Pages", and the current page has <code>aria-current</code>.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Skeleton, avatar, kbd, separator</h2>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:24px;display:flex;flex-wrap:wrap;gap:48px;align-items:flex-start">
<div aria-hidden="true" style="width:320px;max-width:100%;display:flex;flex-direction:column;gap:14px">
<div style="display:flex;gap:16px;align-items:center"><span style="flex:1;height:14px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:20px;background:var(--ui-bg-accented)"></span><span style="width:70px;height:14px;background:var(--ui-bg-accented)"></span></div>
<div style="display:flex;gap:16px;align-items:center"><span style="width:70%;height:14px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:20px;background:var(--ui-bg-accented);margin-left:auto"></span><span style="width:70px;height:14px;background:var(--ui-bg-accented)"></span></div>
<div style="display:flex;gap:16px;align-items:center"><span style="width:55%;height:14px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:20px;background:var(--ui-bg-accented);margin-left:auto"></span><span style="width:70px;height:14px;background:var(--ui-bg-accented)"></span></div>
</div>
<div style="display:flex;gap:16px;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:6px;align-items:center"><span style="width:24px;height:24px;background:#d4c5b0;color:#111;display:flex;align-items:center;justify-content:center;font:600 10px 'IBM Plex Sans',sans-serif">MA</span><code style="color:var(--ui-text-muted)">marta</code></div>
<div style="display:flex;flex-direction:column;gap:6px;align-items:center"><span style="width:32px;height:32px;background:#d4c5b0;color:#111;display:flex;align-items:center;justify-content:center;font:600 12px 'IBM Plex Sans',sans-serif">JV</span><code style="color:var(--ui-text-muted)">jan_vries</code></div>
<div style="display:flex;flex-direction:column;gap:6px;align-items:center"><span style="width:32px;height:32px;background:#d4c5b0;color:#111;display:flex;align-items:center;justify-content:center;font:600 12px 'IBM Plex Sans',sans-serif">BO</span><code style="color:var(--ui-text-muted)">bob</code></div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<span style="height:22px;min-width:22px;padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);border-bottom-width:2px;background:var(--ui-bg);font:500 12px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)">Ctrl</span>
<span style="height:22px;min-width:22px;padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);border-bottom-width:2px;background:var(--ui-bg);font:500 12px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)">S</span>
<span style="color:var(--ui-text-muted);margin-left:6px">Save</span>
</div>
<div style="width:200px;display:flex;flex-direction:column;gap:12px"><div style="height:1px;background:var(--ui-border)"></div><div style="height:1px;background:var(--ui-border-accented)"></div></div>
</div>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Skeleton</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>USkeleton</code> blocks shaped like the real layout: the same row heights, a badge-sized block and a date-sized block. The pulse runs at 1.6 s and stops with reduced motion. The container is <code>aria-busy="true"</code> and a visually hidden live region says "Loading articles". Skeletons appear only after 150 ms, so fast responses never flash.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Avatar</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UAvatar :alt="username" size="xs|sm"</code>. The initials come from the username: split on anything that isn't a letter or digit, take the first letter of the first two parts, or the first two letters if there is only one part, then uppercase. Sand <code>#d4c5b0</code> with ink text (11.2:1) in both themes. It's decorative (<code>aria-hidden</code>) because the username is always shown next to it.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Kbd</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>UKbd variant="outline"</code>, only inside tooltips and the editor's shortcut hints. The editor shortcuts are Ctrl/⌘ S to save, plus TipTap's defaults: B, I, U, Z and Shift Z.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Separator</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)"><code>USeparator</code> uses <code>--ui-border</code> by default and <code>--ui-border-accented</code> between groups in dense toolbars. It's decorative, with <code>role="none"</code>, except between menu groups.</span>
</div>
</section>
<section style="display:flex;flex-direction:column;gap:20px;padding:40px 0;border-top:1px solid var(--ui-border)">
<h2 style="margin:0;font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Color mode</h2>
<p style="margin:0;max-width:720px;color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">Three choices: System (the default), Light and Dark. The toggle at the top of this page is the component itself, so you can try it.</p>
<div style="display:grid;grid-template-columns:minmax(0,160px) minmax(0,1fr);border-top:1px solid var(--ui-border);font-size:13px">
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">Nuxt UI</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">In the user menu: a <code>UDropdownMenu</code> group of three <code>type: 'checkbox'</code> items bound to <code>useColorMode().preference</code>. On auth screens: <code>UColorModeSelect</code> in the footer. Nuxt UI's <code>UColorModeButton</code> isn't used because it only toggles between light and dark and hides the system option. <code>colorMode: true</code> in the plugin handles storage and applying the <code>.dark</code> class before first paint.</span>
<span style="padding:10px 0;border-bottom:1px solid var(--ui-border);font-weight:600">ARIA</span><span style="padding:10px 0;border-bottom:1px solid var(--ui-border)">Menu items use <code>menuitemcheckbox</code>, with <code>aria-checked</code> on the current choice. The segmented variant is a <code>radiogroup</code> named "Theme".</span>
</div>
</section>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;theme&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;system&quot;,&quot;light&quot;,&quot;dark&quot;],&quot;default&quot;:&quot;light&quot;,&quot;tsType&quot;:&quot;'system' | 'light' | 'dark'&quot;}}">
class Component extends DCLogic {
state = { mode: this.props.theme ?? 'light' }
rootRef = React.createRef()
light = { '--ui-bg': '#ffffff', '--ui-bg-muted': '#fafafa', '--ui-bg-elevated': '#f3f2f0', '--ui-bg-accented': '#e6e4e0', '--ui-bg-inverted': '#111111', '--ui-text-dimmed': '#999999', '--ui-text-muted': '#666666', '--ui-text-toned': '#444444', '--ui-text': '#222222', '--ui-text-highlighted': '#111111', '--ui-text-inverted': '#ffffff', '--ui-border': '#e6e4e0', '--ui-border-muted': '#f3f2f0', '--ui-border-accented': '#8a8783', '--ui-primary': '#111111', '--ui-secondary': '#635643', '--ui-success': '#315b33', '--ui-info': '#34536d', '--ui-warning': '#6c4700', '--ui-error': '#882c21', '--rc-primary-hover': '#333333', '--rc-primary-active': '#444444', '--rc-error-hover': '#6a1a11', '--rc-sand': '#e8dfd1', '--rc-success-soft': '#eaefea', '--rc-info-soft': '#ebeef0', '--rc-warning-soft': '#f0ede6', '--rc-error-soft': '#f3eae9', '--rc-overlay': 'rgb(10 10 10 / 0.4)', '--rc-shadow-pop': '0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06)', '--rc-shadow-modal': '0 16px 48px rgb(17 17 17 / 0.18)' }
dark = { '--ui-bg': '#111111', '--ui-bg-muted': '#0a0a0a', '--ui-bg-elevated': '#1a1a1a', '--ui-bg-accented': '#222222', '--ui-bg-inverted': '#fafafa', '--ui-text-dimmed': '#666666', '--ui-text-muted': '#999999', '--ui-text-toned': '#cfccc7', '--ui-text': '#e6e4e0', '--ui-text-highlighted': '#fafafa', '--ui-text-inverted': '#111111', '--ui-border': '#333333', '--ui-border-muted': '#222222', '--ui-border-accented': '#666666', '--ui-primary': '#fafafa', '--ui-secondary': '#d4c5b0', '--ui-success': '#a3caa3', '--ui-info': '#a5c2db', '--ui-warning': '#dfb67d', '--ui-error': '#fba394', '--rc-primary-hover': '#e5e5e5', '--rc-primary-active': '#cccccc', '--rc-error-hover': '#e37e6e', '--rc-sand': '#d4c5b0', '--rc-success-soft': '#1f241f', '--rc-info-soft': '#202428', '--rc-warning-soft': '#2a251e', '--rc-error-soft': '#2c201f', '--rc-overlay': 'rgb(0 0 0 / 0.6)', '--rc-shadow-pop': '0 4px 16px rgb(0 0 0 / 0.5)', '--rc-shadow-modal': '0 16px 48px rgb(0 0 0 / 0.6)' }
resolved() {
if (this.state.mode === 'system') return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
return this.state.mode
}
apply() {
const el = this.rootRef.current
if (!el) return
const vars = this.resolved() === 'dark' ? this.dark : this.light
for (const k in vars) el.style.setProperty(k, vars[k])
document.body.style.background = vars['--ui-bg-muted']
}
componentDidMount() { this.apply() }
componentDidUpdate(p) {
if (p.theme !== this.props.theme && this.props.theme) this.setState({ mode: this.props.theme })
this.apply()
}
renderVals() {
const modes = [['system', 'System'], ['light', 'Light'], ['dark', 'Dark']].map(([k, label]) => {
const on = this.state.mode === k
return { label, on, bg: on ? 'var(--ui-primary)' : 'transparent', fg: on ? 'var(--ui-bg)' : 'var(--ui-text-toned)', pick: () => this.setState({ mode: k }) }
})
return { rootRef: this.rootRef, modes }
}
}
</script>
</body>
</html>