Element Patterns
Native-first action, form, identity, navigation, feed, and application primitives.
These patterns fill the gaps identified in the Graffiti UI comparison. They
ship through index.css, use semantic state as their contract, and work
without JavaScript unless an enhancement is explicitly described.
Icon buttons and switches
Use a real labelled button for icon-only actions. A switch is a native checkbox
whose role="switch" provides the visual contract.
Compact native controls
<layout-cluster gap="var(--space-md)" align="center">
<button class="icon-button" aria-label="Close" type="button">×</button>
<button class="icon-button" data-size="sm" aria-label="Add item" type="button">+</button>
<label class="stack-intrinsic">
<span>Automatic updates</span>
<input type="checkbox" role="switch" checked/>
</label>
</layout-cluster> <button class="icon-button" aria-label="Close">×</button>
<label> <span>Automatic updates</span> <input type="checkbox" role="switch" checked></label>[data-icon-button] is equivalent to .icon-button. <icon-button> is only a
visual host and does not gain native button behavior, keyboard support, or form
semantics automatically. Floating actions use .fab or [data-fab], include
safe-area-aware logical offsets, and should represent the page’s primary action.
Text symbols and explicit svg, img, or [data-icon] children share the same
centered square. Icon controls intentionally use --text-box-icon instead of
label-oriented cap/alphabetic trimming.
Input group and search
Connected fields that adapt
<layout-stack container-query="true" gap="var(--space-lg)">
<div class="form-field">
<label for="project-slug">Project URL</label>
<input-group data-stack="auto">
<span class="input-affix">https://</span>
<input id="project-slug" class="form-input" value="atlas"/>
<span class="input-affix">.css-tags.dev</span>
<button class="form-button" type="button">Create</button>
</input-group>
<small class="form-help">Use a short, memorable project name.</small>
</div>
<form class="form-field" role="search">
<label for="project-search">Search projects</label>
<search-input>
<svg data-search-icon="true" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7">
</circle>
<path d="m20 20-4-4">
</path>
</svg>
<input id="project-search" type="search" placeholder="Try “grid” or “tabs”"/>
<kbd aria-hidden="true">⌘ K</kbd>
</search-input>
</form>
</layout-stack> <layout-stack container-query> <input-group data-stack="auto"> <span class="input-affix">https://</span> <input class="form-input" aria-label="Project name"> <button class="form-button">Create</button> </input-group>
<search-input> <svg data-search-icon aria-hidden="true"><!-- search icon --></svg> <input type="search" aria-label="Search projects"> <kbd aria-hidden="true">⌘ K</kbd> </search-input></layout-stack>Input groups connect adjacent controls. Search input provides leading and
trailing content slots around a native input type="search". With
data-stack="auto", a group stacks at narrow viewport widths or inside an
opted-in layout-container, as the resizable preview demonstrates.
File dropzone
Native file input drop target
<file-dropzone>
<strong>Drop files here or choose files</strong>
<small>PNG, JPG, or WebP up to 10 MB</small>
<input type="file" multiple="true" accept="image/png,image/jpeg,image/webp" aria-label="Upload images"/>
</file-dropzone> The transparent native file input covers the surface, preserving click,
keyboard, and browser drop behavior. JavaScript may set data-dragover="true"
and render selected filenames, but neither is required for the control to work.
Rating and reactions
Read-only and interactive ratings
<layout-stack gap="var(--space-md)">
<rating-output style="--rating-value: 80%" aria-label="4 out of 5 stars">★★★★★</rating-output>
<fieldset>
<legend>Rate this example</legend>
<rating-input>
<label aria-label="5 stars">
<input type="radio" name="docs-rating" value="5"/>★</label>
<label aria-label="4 stars">
<input type="radio" name="docs-rating" value="4" checked/>★</label>
<label aria-label="3 stars">
<input type="radio" name="docs-rating" value="3"/>★</label>
<label aria-label="2 stars">
<input type="radio" name="docs-rating" value="2"/>★</label>
<label aria-label="1 star">
<input type="radio" name="docs-rating" value="1"/>★</label>
</rating-input>
</fieldset>
<reaction-select>
<select class="form-select" aria-label="Add reaction">
<option value>Add reaction</option>
<option value="like">👍 Like</option>
<option value="love">❤️ Love</option>
<option value="celebrate">🎉 Celebrate</option>
</select>
</reaction-select>
</layout-stack> Interactive ratings use native radios. Put labels in descending value order,
as shown. Reactions remain an ordinary select and use customizable select only
where appearance: base-select is supported.
Avatars
Initial, image, and group avatars
<layout-cluster gap="var(--space-lg)" align="center">
<user-avatar aria-label="Ada Lovelace">AL</user-avatar>
<user-avatar shape="square" size="lg" aria-label="Grace Hopper">GH</user-avatar>
<avatar-group aria-label="Project members">
<user-avatar aria-label="Ada Lovelace">AL</user-avatar>
<user-avatar aria-label="Grace Hopper">GH</user-avatar>
<user-avatar aria-label="Katherine Johnson">KJ</user-avatar>
</avatar-group>
</layout-cluster> An img child fills the avatar when present; text initials remain the fallback.
Size values are sm, the default, and lg. Use shape="square" when needed.
Toolbar and navigation
Editor toolbar and rich navigation
Use variant="flush", data-variant="flush", or .is-flush for a quieter
sidebar or table of contents. It removes boxed rows while retaining hover,
focus, and an aria-current="page" indicator.
<ul data-list-navigation data-variant="flush"> <li><a href="/overview" aria-current="page">Overview</a></li> <li><a href="/settings">Settings</a></li></ul>Use role="toolbar" for a related control group. List navigation expects real
links or buttons and uses aria-current="page" for selection. icon-rail,
[data-icon-rail], and .icon-rail provide the compact vertical equivalent;
every icon-only destination still needs an accessible name.
Messages and logs
Application feedback surfaces
<layout-stack gap="var(--space-lg)">
<div class="chat" aria-label="Conversation">
<message-bubble data-sender="other">Can you review this deploy?</message-bubble>
<message-bubble data-sender="self">Yes. The checks are passing now.</message-bubble>
</div>
<log-card>
<div class="log-card-header">
<strong>Production deploy</strong>
<badge status="success">Passed</badge>
</div>
<div class="log-card-body" role="log" aria-live="polite">
<div class="log-row log-row-success">
<span>Dependencies installed</span>
<time>12:03</time>
</div>
<div class="log-row log-row-success">
<span>Site built</span>
<time>12:04</time>
</div>
<div class="log-row log-row-running">
<span>Publishing assets</span>
<time>Now</time>
</div>
</div>
</log-card>
</layout-stack> aria-busy="true" adds a visual thinking indicator to a bubble. It does not
announce updates by itself. Use role="log" and an appropriate live-region
strategy only when rows are appended dynamically.
Vertical snap feed
snap-feed, [data-snap-feed], and .snap-feed provide a vertical scroll-snap
container. Its direct children snap to the block-start edge. This is separate
from the existing horizontal layout-reel and from carousel.
<snap-feed aria-label="Updates"> <article>First full-height item</article> <article>Second full-height item</article></snap-feed>Set --snap-feed-height, --snap-feed-item-min-height, and
--snap-feed-strictness to adapt the viewport. Avoid mandatory snapping for
items taller than the feed viewport.
Public host forms
Surface and layout patterns support custom, data-*, and class hosts. Native
interaction remains canonical:
input-group,[data-input-group],.input-groupsearch-input,[data-search-input],.search-inputfile-dropzone,[data-file-dropzone],.file-dropzonerating-output,[data-rating-output],.rating-outputrating-input,[data-rating-input],.rating-inputreaction-select,[data-reaction-select],.reaction-selectuser-avatar,[data-avatar],.avatartool-bar,[data-toolbar],.toolbarnav-list,[data-list-navigation],.list-navigationicon-rail,[data-icon-rail],.icon-railmessage-bubble,[data-bubble],.bubblelog-card,[data-log-card],.log-cardsnap-feed,[data-snap-feed],.snap-feed