# The 8 Essential States for Interactive Components in Hallmark

> Discover the 8 essential states for interactive components in Hallmark: Default, Hover, Focus Visible, Active Pressed, Disabled, Error, Loading, and Filled. Ensure accessible UIs.

- Repository: [Hassan El Mghari/hallmark](https://github.com/Nutlope/hallmark)
- Tags: deep-dive
- Published: 2026-08-01

---

**Hallmark requires every interactive element to implement eight distinct UI states—Default, Hover, Focus-Visible, Active/Pressed, Disabled, Error, Loading, and Filled/Success—to ensure consistent, accessible, and fully functional interfaces.**

Hallmark is an open-source design system created by [Nutlope](https://github.com/Nutlope) that enforces strict UI quality standards. According to the project's source files, particularly [`interaction-and-states.md`](https://github.com/Nutlope/hallmark/blob/main/interaction-and-states.md), **all eight states must be present on every interactive component**. Missing any state—especially `:focus-visible`, `:active`, or `:disabled`—is explicitly flagged as a failure mode that degrades user experience.

## The Eight Required States Explained

The following table breaks down each state as documented in Hallmark's reference files. These states apply universally to buttons, inputs, links, cards, and any element that users can interact with.

| State | Purpose | Key Implementation Details |
|-------|---------|---------------------------|
| **Default** | Baseline appearance | Normal look when no interaction occurs; defines the component's core visual identity |
| **Hover** | Pointer feedback | Background shift, elevation change, or subtle color transition on mouse hover |
| **Focus-Visible** | Keyboard navigation | Visible focus ring or outline for keyboard users; must never be suppressed |
| **Active/Pressed** | Press confirmation | Immediate feedback during click/tap—often inset shadows, scale reduction, or color darkening |
| **Disabled** | Non-interactive state | Dimmed opacity, removed pointer events, and clear visual indication of unavailability |
| **Error** | Failure indication | Red border, error icon, accompanying message, and `aria-invalid` attribute |
| **Loading** | Progress state | Inline spinner, label replacement, or reduced opacity during async operations |
| **Filled/Success** | Completion state | Checkmark, accent color, or stronger border indicating valid input or successful action |

## Why All Eight States Matter

Hallmark's eight-state rule addresses common failures in AI-generated and hand-coded interfaces. The documentation in [`skills/hallmark/references/interaction-and-states.md`](https://github.com/Nutlope/hallmark/blob/main/skills/hallmark/references/interaction-and-states.md) emphasizes four core benefits:

- **Accessibility compliance** — `:focus-visible` guarantees keyboard users can locate and operate elements without mouse dependency
- **Visual consistency** — Uniform border widths and spacing across states prevent layout shifts
- **Action feedback** — `:active` and loading states provide immediate confirmation that user input registered
- **Error clarity** — Dedicated error states eliminate ambiguous color-only signals that fail WCAG contrast requirements

## Implementing the 8 States in Code

The following examples demonstrate practical implementation using semantic HTML, CSS custom properties, and state-driven JavaScript. These patterns mirror approaches found in Hallmark's [`component-cookbook.md`](https://github.com/Nutlope/hallmark/blob/main/component-cookbook.md) and [`site/js/main.js`](https://github.com/Nutlope/hallmark/blob/main/site/js/main.js).

### Base HTML Structure

```html
<button class="hallmark-btn" data-state="">
  <span class="hallmark-btn__label">Submit</span>
</button>

```

### CSS State Definitions

```css
/* ---- Default ---- */
.hallmark-btn {
  --bg: var(--color-surface);
  --fg: var(--color-on-surface);
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--color-rule-2);
  border-radius: 0.5rem;
  transition: background 150ms ease, box-shadow 150ms ease, transform 100ms ease;
}

/* ---- Hover ---- */
.hallmark-btn:hover {
  background: var(--color-surface-hover);
  box-shadow: var(--elevation-1);
}

/* ---- Focus-Visible ---- */
.hallmark-btn:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* ---- Active / Pressed ---- */
.hallmark-btn:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 4px var(--color-shadow);
}

/* ---- Disabled ---- */
.hallmark-btn:disabled,
.hallmark-btn[data-state="disabled"] {
  opacity: 0.5;
  pointer-events: none;
  cursor: not-allowed;
}

/* ---- Error ---- */
.hallmark-btn[data-state="error"] {
  border-color: var(--color-error);
  background: var(--color-error-bg);
  color: var(--color-error);
}

/* ---- Loading ---- */
.hallmark-btn[data-state="loading"] .hallmark-btn__label {
  visibility: hidden;
}
.hallmark-btn[data-state="loading"]::after {
  content: "";
  position: absolute;
  width: 1rem;
  height: 1rem;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: hallmark-spin 0.8s linear infinite;
}

/* ---- Filled / Success ---- */
.hallmark-btn[data-state="filled"] {
  background: var(--color-success);
  border-color: var(--color-success);
  color: var(--color-on-success);
}

@keyframes hallmark-spin {
  to { transform: rotate(360deg); }
}

```

### JavaScript State Management

```javascript
const button = document.querySelector('.hallmark-btn');

button.addEventListener('click', async () => {
  // Prevent re-trigger during active operation
  if (button.dataset.state === 'loading') return;

  // 1. Enter loading state
  button.dataset.state = 'loading';
  button.disabled = true;

  try {
    await performAsyncAction();           // Replace with actual operation
    button.dataset.state = 'filled';      // Success / filled state
    
    // Auto-reset after delay
    setTimeout(() => {
      button.dataset.state = '';
      button.disabled = false;
    }, 2000);
    
  } catch (error) {
    button.dataset.state = 'error';       // Error state
    
    // Clear error after user acknowledgment delay
    setTimeout(() => {
      button.dataset.state = '';
      button.disabled = false;
    }, 3000);
  }
});

```

## Key Source Files in Hallmark

These files contain the authoritative specifications and implementation patterns for the eight essential states:

| File | Location | Contains |
|------|----------|----------|
| [`interaction-and-states.md`](https://github.com/Nutlope/hallmark/blob/main/interaction-and-states.md) | `skills/hallmark/references/` | Complete eight-state checklist and rationale |
| [`microinteractions.md`](https://github.com/Nutlope/hallmark/blob/main/microinteractions.md) | `skills/hallmark/references/` | Timing, easing curves, and `prefers-reduced-motion` handling |
| [`component-cookbook.md`](https://github.com/Nutlope/hallmark/blob/main/component-cookbook.md) | `skills/hallmark/references/` | Production-ready component examples |
| [`main.js`](https://github.com/Nutlope/hallmark/blob/main/main.js) | `site/js/` | Runtime state-toggle logic for the demo site |

## Verifying State Completeness

To audit a component against Hallmark's requirements, check each state sequentially:

1. Load page — confirm **Default** renders correctly
2. Hover with mouse — verify **Hover** feedback appears
3. Tab to element — **Focus-Visible** ring must be clearly visible
4. Click and hold — **Active/Pressed** state activates immediately
5. Set `disabled` attribute — **Disabled** styling applies and blocks interaction
6. Add `data-state="error"` — **Error** visual treatment displays
7. Add `data-state="loading"` — **Loading** indicator replaces content
8. Add `data-state="filled"` — **Filled/Success** completion state shows

All eight must function independently without visual regressions in other states.

## Summary

- Hallmark mandates **eight essential states** for every interactive component: **Default, Hover, Focus-Visible, Active/Pressed, Disabled, Error, Loading, and Filled/Success**
- These states are defined in [`interaction-and-states.md`](https://github.com/Nutlope/hallmark/blob/main/interaction-and-states.md) and enforced as a hard requirement, not a suggestion
- Implementation uses semantic HTML, CSS custom properties, and `data-state` attributes for JavaScript control
- Missing states—particularly `:focus-visible` or `:disabled`—constitute explicit failure modes per the Hallmark specification
- The pattern appears throughout Hallmark's reference files, cookbook examples, and demo site JavaScript

## Frequently Asked Questions

### What happens if a Hallmark component omits one of the eight states?

Omitting any state violates the design system's core contract. The documentation in [`interaction-and-states.md`](https://github.com/Nutlope/hallmark/blob/main/interaction-and-states.md) treats missing states as failure modes that produce "almost-right" interfaces—broken keyboard navigation, ambiguous feedback, or inaccessible error messaging. Automated or manual audits should flag incomplete state coverage.

### Are the eight states required for non-button elements like inputs and links?

Yes. The eight-state rule applies universally to **every interactive element**. Inputs require the same state coverage plus additional considerations for validation timing. Links implement a subset of visual states but still require `:focus-visible`, `:hover`, and `:active` at minimum.

### How does Hallmark handle the Loading state for accessibility?

The loading state must preserve focus management and announce status changes to screen readers. The reference implementation uses `aria-live` regions or `aria-busy` attributes, ensures the element remains focusable, and maintains sufficient color contrast for the loading indicator—never relying solely on animation.

### Can I customize the visual appearance of each state?

Customization is expected through CSS custom properties (`--color-*`, `--elevation-*`), but the **existence of each state** is non-negotiable. The [`microinteractions.md`](https://github.com/Nutlope/hallmark/blob/main/microinteractions.md) file specifies timing constraints (default 150ms transitions) and requires `prefers-reduced-motion` fallbacks for accessibility compliance.