Skip to content

Button Layout Stability with Dynamic Content

When to use

Use this pattern when button text changes between states — like “Copy” to “Copied!” — and the button must not shift layout. This pattern overlays all states in a single CSS Grid cell.

The pattern

The HTML structure places all state labels inside the button as sibling spans. Each span carries a data-state attribute and an aria-hidden toggle — only the active state is visible, while hidden states still contribute to the button's size.

html
<button
  class="feedback-button"
  type="button"
  aria-live="polite"
>
  <span data-state="idle" aria-hidden="false">
    Copy
  </span>
  <span
    data-state="success"
    aria-hidden="true"
    role="status"
  >
    <span role="presentation">✓</span>
    Copied
  </span>
</button>

The CSS uses inline-grid so all state spans stack in the same cell. Every span is visibility: hidden by default, and the [aria-hidden="false"] selector reveals only the active one — keeping layout space for all states.

css
.feedback-button {
  display: inline-grid;
  place-items: center;
  padding: 0.6em 1.2em;
}

.feedback-button [data-state] {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  visibility: hidden;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}

.feedback-button [data-state][aria-hidden="false"] {
  visibility: visible;
}

State transitions swap aria-hidden between spans so the visible state changes without affecting layout. The aria-live="polite" on the button ensures screen readers announce the new state.

js
document.querySelectorAll(".feedback-button")
  .forEach((btn) => {
    const [idle, success] =
      btn.querySelectorAll("[data-state]");
    
    btn.addEventListener("click", async () => {
      idle.setAttribute("aria-hidden", "true");
      success.setAttribute("aria-hidden", "false");
      
      await delay(1500);
      
      success.setAttribute("aria-hidden", "true");
      idle.setAttribute("aria-hidden", "false");
    });
  });

How it works

  1. All states occupy the same grid cell
  2. Button auto-sizes to widest state content
  3. Toggle aria-hidden to switch visibility
  4. visibility: hidden preserves layout space
  5. Screen readers announce state via aria-live

Common use cases

  • Copy buttons: Copy → Copied!
  • Save buttons: Save → Saving... → Saved!
  • Submit buttons: Submit → Submitting... → Success!
  • Toggle actions: Follow → Following