The 8 States of a Hallmark Component: Complete CSS Implementation Guide
A Hallmark component must implement eight mandatory interaction states: default, hover, focus, active/pressed, disabled, loading, error, and success.
Every interactive element in the Hallmark design system—buttons, links, form controls, and more—must account for these eight states. This requirement is explicitly documented in the Interaction and states reference and enforced through the CSS architecture of the project. According to the source code in Nutlope/hallmark, omitting any state is considered an unfinished component.
Default State
The default state applies when an element is idle and unmodified.
In components.css, the .btn class establishes the baseline styling:
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-lg);
font-family: var(--font-label);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-ink);
background: transparent;
border: var(--rule-fine) solid var(--color-ink);
border-radius: var(--radius-pill, 0);
min-height: 44px;
transition: transform 100ms var(--ease-out),
background-color var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out),
border-color var(--dur-micro) var(--ease-out);
cursor: pointer;
}
Key characteristics include a minimum 44px touch target, smooth transitions, and theme-aware color variables.
Hover State
The hover state activates when a pointing device hovers over the element. Hallmark specifically targets true mouse devices to avoid triggering hover styles on touchscreens.
Implementation in components.css:
@media (hover: hover) and (pointer: fine) {
.btn:hover {
background: var(--color-ink);
color: var(--color-paper);
transform: translateY(-1px);
}
}
- Uses the
@media (hover: hover) and (pointer: fine)query - Provides subtle 1px upward translation
- Swaps foreground and background colors for inverted appearance
Focus State
The focus state indicates keyboard or programmatic focus. Hallmark implements this globally rather than per-component.
In base.css, the :focus-visible pseudo-class ensures the focus ring appears only for keyboard navigation:
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
This approach prevents focus rings from appearing on mouse clicks while maintaining full keyboard accessibility.
Active/Pressed State
The active state (also called pressed) occurs while the user is physically pressing the element.
Implementation in components.css:
.btn:active {
transform: translateY(1px);
transition-duration: 60ms;
}
- 1px downward translation creates tactile feedback
- Shortened transition duration (60ms) for snappier response
- Often paired with darker color values in production variants
Disabled State
The disabled state removes interactivity from an element.
.btn[disabled],
.btn[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}
Hallmark supports both the native disabled attribute and aria-disabled="true" for accessibility. The reduced opacity (0.5) and not-allowed cursor provide clear visual indication.
Loading State
The loading state indicates asynchronous processing. Hallmark uses data-state attributes to toggle this dynamically.
.btn[data-state="loading"] {
position: relative;
color: transparent; /* hide label */
}
.btn[data-state="loading"]::after {
content: "";
position: absolute;
inset: 0;
margin: auto;
width: 1rem;
height: 1rem;
border: 2px solid var(--color-muted);
border-top-color: var(--color-ink);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
The inline spinner preserves layout stability while keeping the button structurally intact for screen readers.
Error State
The error state communicates validation failures or processing errors.
.btn[data-state="error"] {
border-color: var(--color-error);
color: var(--color-error);
}
- Applies semantic error color from theme system
- Often accompanied by
aria-invalid="true"for accessibility - May include error icons or messages depending on component complexity
Success State
The success state confirms completed actions.
.btn[data-state="success"] {
border-color: var(--color-success);
color: var(--color-success);
}
Common patterns include:
- Green checkmark icons
- Confirmation message overlays
- Optional auto-dismiss after timeout
Complete Working Example
Here's a production-ready button demonstrating all eight states:
<button class="btn"
data-state="default"
aria-live="polite"
aria-label="Save"
id="saveBtn">
Save
</button>
// Example state transition in site/js/main.js pattern
const btn = document.getElementById('saveBtn');
// Trigger loading
btn.setAttribute('data-state', 'loading');
btn.setAttribute('aria-disabled', 'true');
// Simulate async operation
setTimeout(() => {
btn.setAttribute('data-state', 'success');
btn.textContent = 'Saved!';
// Auto-reset after delay
setTimeout(() => {
btn.setAttribute('data-state', 'default');
btn.textContent = 'Save';
btn.removeAttribute('aria-disabled');
}, 2000);
}, 1500);
Key Implementation Files
| File | Purpose |
|---|---|
skills/hallmark/references/interaction-and-states.md |
Authoritative documentation of the eight-state requirement (lines 9-16) |
site/css/components.css |
Component-specific state implementations |
site/css/base.css |
Global focus-visible and baseline styles |
site/js/main.js |
Runtime state management examples |
Summary
- Eight states are mandatory: default, hover, focus, active/pressed, disabled, loading, error, success
- State transitions use
data-stateattributes or modifier classes for JavaScript control - Hover detection requires
@media (hover: hover) and (pointer: fine)for proper device targeting - Focus management is global via
:focus-visibleinbase.css - Accessibility attributes (
aria-disabled,aria-invalid) must accompany visual state changes
Frequently Asked Questions
What happens if a Hallmark component omits one of the eight states?
A component missing any of the eight states is considered unfinished according to the Hallmark reference documentation. The interaction-and-states.md file explicitly requires all eight for production UI elements.
How does Hallmark prevent hover states on touch devices?
The components.css file wraps hover styles in @media (hover: hover) and (pointer: fine), which targets only devices with true hover capability and fine-grained pointing (typically mice). Touch screens fail this media query and receive no hover styles.
Why use data-state attributes instead of CSS classes for loading, error, and success?
data-state provides semantic clarity about the element's current condition and enables straightforward JavaScript toggling without class string manipulation. This approach also separates state logic from stylistic modifiers in the CSS architecture.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →