API Keys

API keys

Keys give full access to your account. Keep them secret and rotate them regularly.

Create key
NameKeyLast usedActions
Production Live
spr_live_••••••••••••••••Yd4N
Staging Test
spr_test_••••••••••••••••p7dc
Old CI token Revokedspr_live_••••••••••••••••Q2aKNever

@use 'sprucecss/scss/spruce' as *;

.api-keys {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  padding: spacer('m');
  @include layout-stack('m');

  &__header {
    align-items: flex-start;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    justify-content: space-between;
  }

  &__heading {
    @include layout-stack('xxs');
  }

  &__title {
    font-size: font-size('h5');
  }

  &__text {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    max-inline-size: 36rem;
  }

  &__table {
    font-size: config('font-size-sm', $typography);

    th,
    td {
      vertical-align: middle;
      white-space: nowrap;
    }

    th[scope='row'] {
      font-weight: 400;
    }

    td:last-child {
      text-align: end;
    }

    tr[data-revoked] {
      opacity: 0.6;
    }
  }

  &__name {
    color: color('heading');
    font-weight: 600;
    margin-inline-end: spacer('xxs');
  }

  &__key {
    align-items: center;
    display: flex;
    gap: spacer('xxs');

    code {
      font-variant-numeric: tabular-nums;
      min-inline-size: 18ch;
    }

    .btn[hidden] {
      display: none;
    }
  }
}
<section class="api-keys" aria-labelledby="api-keys-title" data-api-keys>
    <header class="api-keys__header">
        <div class="api-keys__heading">
            <h2 class="api-keys__title" id="api-keys-title">API keys</h2>
            <p class="api-keys__text">Keys give full access to your account. Keep them secret and rotate them regularly.</p>
        </div>
        <a class="btn btn--primary btn--sm" href="#">Create key</a>
    </header>
    <div class="table-responsive">
        <table class="table api-keys__table">
            <thead>
                <tr>
                    <th scope="col">Name</th>
                    <th scope="col">Key</th>
                    <th scope="col">Last used</th>
                    <th scope="col"><span class="sr-only">Actions</span></th>
                </tr>
            </thead>
            <tbody>
                <tr data-api-key="spr_live_51Hx9QeK2vT8mLw3pZr7Yd4N">
                    <th scope="row">
                        <span class="api-keys__name">Production</span>
                        <span class="badge badge--success">Live</span>
                    </th>
                    <td>
                        <div class="api-keys__key">
                            <code data-api-key-value>spr_live_••••••••••••••••Yd4N</code>
                            <button class="btn btn--ghost-primary btn--icon btn--sm" type="button" aria-pressed="false" hidden data-api-key-reveal><svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><span class="sr-only">Show the Production key</span></button>
                            <button class="btn btn--ghost-primary btn--icon btn--sm" type="button" hidden data-api-key-copy><svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span class="sr-only">Copy the Production key</span></button>
                        </div>
                    </td>
                    <td><time datetime="2026-10-04T09:30">Today, 09:30</time></td>
                    <td><button class="btn btn--outline-danger btn--sm" type="button" data-api-key-revoke>Revoke<span class="sr-only"> the Production key</span></button></td>
                </tr>
                <tr data-api-key="spr_test_4eC39HqLyjWDarjtT1zdp7dc">
                    <th scope="row">
                        <span class="api-keys__name">Staging</span>
                        <span class="badge">Test</span>
                    </th>
                    <td>
                        <div class="api-keys__key">
                            <code data-api-key-value>spr_test_••••••••••••••••p7dc</code>
                            <button class="btn btn--ghost-primary btn--icon btn--sm" type="button" aria-pressed="false" hidden data-api-key-reveal><svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><span class="sr-only">Show the Staging key</span></button>
                            <button class="btn btn--ghost-primary btn--icon btn--sm" type="button" hidden data-api-key-copy><svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span class="sr-only">Copy the Staging key</span></button>
                        </div>
                    </td>
                    <td><time datetime="2026-09-28T14:02">Sep 28</time></td>
                    <td><button class="btn btn--outline-danger btn--sm" type="button" data-api-key-revoke>Revoke<span class="sr-only"> the Staging key</span></button></td>
                </tr>
                <tr>
                    <th scope="row">
                        <span class="api-keys__name">Old CI token</span>
                        <span class="badge badge--danger">Revoked</span>
                    </th>
                    <td><code>spr_live_••••••••••••••••Q2aK</code></td>
                    <td>Never</td>
                    <td></td>
                </tr>
            </tbody>
        </table>
    </div>
    <p class="sr-only" aria-live="polite" data-api-keys-status></p>
</section>
(() => {
  document.querySelectorAll('[data-api-keys]').forEach((section) => {
    const status = section.querySelector('[data-api-keys-status]');

    section.querySelectorAll('[data-api-key]').forEach((row) => {
      const key = row.dataset.apiKey;
      const name = row.querySelector('.api-keys__name').textContent;
      const value = row.querySelector('[data-api-key-value]');
      const masked = value.textContent;
      const reveal = row.querySelector('[data-api-key-reveal]');
      const copy = row.querySelector('[data-api-key-copy]');

      // Reveal and copy need JavaScript, so the buttons are hidden without it.
      reveal.hidden = false;
      copy.hidden = !('clipboard' in navigator);

      reveal.addEventListener('click', () => {
        const isShown = reveal.getAttribute('aria-pressed') !== 'true';

        reveal.setAttribute('aria-pressed', String(isShown));
        value.textContent = isShown ? key : masked;
      });

      copy.addEventListener('click', async () => {
        try {
          await navigator.clipboard.writeText(key);
          status.textContent = `The ${name} key is copied to the clipboard.`;
        } catch (error) {
          status.textContent = 'Copying failed. Show the key and copy it manually.';
        }
      });

      row.querySelector('[data-api-key-revoke]').addEventListener('click', (event) => {
        // A real app confirms this in a dialog and calls the API.
        if (!window.confirm(`Revoke the ${name} key? Apps using it will stop working.`)) return;

        row.setAttribute('data-revoked', '');
        row.querySelector('.badge').className = 'badge badge--danger';
        row.querySelector('.badge').textContent = 'Revoked';
        value.textContent = masked;
        reveal.remove();
        copy.remove();
        event.currentTarget.replaceWith(document.createTextNode(''));
        status.textContent = `The ${name} key is revoked.`;
        section.querySelector('h2').setAttribute('tabindex', '-1');
        section.querySelector('h2').focus();
      });
    });
  });
})();

API Keys

A table of API keys with masked values, buttons to show and copy a key, the last use and a revoke action.

Anatomy

  • .api-keys__header: the title, a short warning and the "Create key" button.
  • .api-keys__table: a standard Spruce CSS .table in a .table-responsive wrapper; the key name is the row header (th scope="row") with an environment Badge.
  • .api-keys__key: the masked code and the show/copy icon buttons.

Accessibility and Usability

  • The show button is a toggle (aria-pressed), and every icon button has hidden text with the key's name ("Copy the Production key"), so the buttons are unique out of context.
  • Copying and revoking are confirmed in a polite live region.
  • The key is masked by default, showing only the prefix and the last four characters: enough to recognize it, not enough to use it.
  • Revoking is destructive, so it asks for confirmation first. After revoking, the row's buttons are removed and the focus moves to the section heading instead of the page.

Technical Details

  • The full key is in data-api-key here only for the demo. In a real app, never render a secret into the page after it's created: show it once on creation, then only the masked version, and let the server reveal it after a re-authentication.
  • Copying uses navigator.clipboard.writeText(); the copy button stays hidden where the Clipboard API isn't available (it needs a secure context).
  • Replace the window.confirm() with a Modal for a consistent look.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.