Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pbakaus/impeccable/llms.txt

Use this file to discover all available pages before exploring further.

Make interactions feel fast. Use optimistic UI—update immediately, sync later.

The Eight Interactive States

Every interactive element needs these states designed:
StateWhenVisual Treatment
DefaultAt restBase styling
HoverPointer over (not touch)Subtle lift, color shift
FocusKeyboard/programmatic focusVisible ring (see below)
ActiveBeing pressedPressed in, darker
DisabledNot interactiveReduced opacity, no pointer
LoadingProcessingSpinner, skeleton
ErrorInvalid stateRed border, icon, message
SuccessCompletedGreen check, confirmation
The common miss: Designing hover without focus, or vice versa. They’re different. Keyboard users never see hover states.

Focus Rings: Do Them Right

Never outline: none without replacement. It’s an accessibility violation. Instead, use :focus-visible to show focus only for keyboard users:
/* Hide focus ring for mouse/touch */
button:focus {
  outline: none;
}

/* Show focus ring for keyboard */
button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
Focus ring design:
  • High contrast (3:1 minimum against adjacent colors)
  • 2-3px thick
  • Offset from element (not inside it)
  • Consistent across all interactive elements

Form Design: The Non-Obvious

Placeholders aren’t labels—they disappear on input. Always use visible <label> elements.
<!-- Good -->
<label for="email">Email address</label>
<input id="email" type="email" placeholder="you@example.com">

<!-- Bad - placeholder as label -->
<input type="email" placeholder="Email address">
Validate on blur, not on every keystroke (exception: password strength). Place errors below fields with aria-describedby connecting them.
<label for="username">Username</label>
<input 
  id="username" 
  type="text"
  aria-describedby="username-error"
  aria-invalid="true"
>
<p id="username-error" role="alert">
  Username must be at least 3 characters
</p>

Loading States

Optimistic Updates

Show success immediately, rollback on failure. Use for: Low-stakes actions (likes, follows) Avoid for: Payments or destructive actions
// Optimistic UI pattern
async function likePost(postId) {
  // Update UI immediately
  setLiked(true);
  setLikeCount(count + 1);
  
  try {
    await api.like(postId);
  } catch (error) {
    // Rollback on failure
    setLiked(false);
    setLikeCount(count);
    showError('Failed to like post');
  }
}

Skeleton Screens

Skeleton screens > spinners—they preview content shape and feel faster than generic spinners.
.skeleton {
  background: linear-gradient(
    90deg,
    var(--gray-200) 25%,
    var(--gray-100) 50%,
    var(--gray-200) 75%
  );
  background-size: 200% 100%;
  animation: loading 1.5s ease-in-out infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

Modals: The Inert Approach

Focus trapping in modals used to require complex JavaScript. Now use the inert attribute:
<!-- When modal is open -->
<main inert>
  <!-- Content behind modal can't be focused or clicked -->
</main>
<dialog open>
  <h2>Modal Title</h2>
  <!-- Focus stays inside modal -->
</dialog>
Or use the native <dialog> element:
const dialog = document.querySelector('dialog');
dialog.showModal();  // Opens with focus trap, closes on Escape

The Popover API

For tooltips, dropdowns, and non-modal overlays, use native popovers:
<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
  <button>Option 1</button>
  <button>Option 2</button>
</div>
Benefits: Light-dismiss (click outside closes), proper stacking, no z-index wars, accessible by default.

Destructive Actions: Undo > Confirm

Undo is better than confirmation dialogs—users click through confirmations mindlessly.
// Better pattern: Undo toast
function deleteItem(id) {
  const item = getItem(id);
  removeFromUI(id);
  
  showToast('Item deleted', {
    action: 'Undo',
    onAction: () => restoreItem(item),
    onExpire: () => api.delete(id)
  });
}
Use confirmation only for:
  • Truly irreversible actions (account deletion)
  • High-cost actions
  • Batch operations

Keyboard Navigation Patterns

Roving Tabindex

For component groups (tabs, menu items, radio groups), one item is tabbable; arrow keys move within:
<div role="tablist">
  <button role="tab" tabindex="0">Tab 1</button>
  <button role="tab" tabindex="-1">Tab 2</button>
  <button role="tab" tabindex="-1">Tab 3</button>
</div>
Arrow keys move tabindex="0" between items. Tab moves to the next component entirely. Provide skip links (<a href="#main-content">Skip to main content</a>) for keyboard users to jump past navigation. Hide off-screen, show on focus.
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--color-primary);
  color: white;
  padding: 8px;
}

.skip-link:focus {
  top: 0;
}

Gesture Discoverability

Swipe-to-delete and similar gestures are invisible. Hint at their existence:
  • Partially reveal: Show delete button peeking from edge
  • Onboarding: Coach marks on first use
  • Alternative: Always provide a visible fallback (menu with “Delete”)
Don’t rely on gestures as the only way to perform actions.

Guidelines

DO

  • Use progressive disclosure—start simple, reveal sophistication through interaction
  • Design empty states that teach the interface, not just say “nothing here”
  • Make every interactive surface feel intentional and responsive
  • Use optimistic UI for low-stakes actions
  • Provide keyboard alternatives for all mouse interactions
  • Use skeleton screens instead of generic spinners

DON’T

  • Repeat the same information—redundant headers, intros that restate the heading
  • Make every button primary—use ghost buttons, text links, secondary styles; hierarchy matters
  • Remove focus indicators without alternatives
  • Use placeholder text as labels
  • Create touch targets <44x44px
  • Use generic error messages
  • Build custom controls without ARIA/keyboard support

Build docs developers (and LLMs) love