File Manager

Files

Upload

6.4 of 10 GB6.4 GB of 10 GB used

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

.file-manager {
  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: center;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('s');
    justify-content: space-between;
  }

  &__path ol {
    display: flex;
    flex-wrap: wrap;
    font-weight: 600;
    gap: spacer('xxs');
    list-style: none;
    margin: 0;
    padding: 0;

    li {
      margin: 0;
    }

    li + li::before {
      color: color('muted');
      content: '/';
      margin-inline-end: spacer('xxs');
    }

    [aria-current] {
      color: color('heading');
    }
  }

  &__tools,
  &__view {
    align-items: center;
    display: flex;
    gap: spacer('xs');
  }

  &__view {
    gap: 0;

    &[hidden] {
      display: none;
    }

    [aria-pressed='true'] {
      background-color: color-alpha('primary', 0.12);
    }
  }

  &__items {
    display: grid;
    gap: spacer('s');
    grid-template-columns: repeat(auto-fill, minmax(min(9.5rem, 100%), 1fr));
    list-style: none;
    padding: 0;

    // The items sit side by side: clear the stack margins of the base list style.
    > li {
      margin-block: 0;
    }
  }

  &__item {
    @include a11y-card-link('.file-manager__name', true);
    align-items: center;
    border: config('border-width-base', $display) solid color('border');
    border-radius: radius('md');
    display: flex;
    flex-direction: column;
    gap: spacer('xxs');
    padding: spacer('s');
    text-align: center;

    &:hover {
      background-color: color('surface-muted');
    }

    &:focus-within {
      outline: 2px solid color('focus-ring');
      outline-offset: 2px;
    }
  }

  &__icon {
    --icon-color: #{color('muted')};
    color: var(--icon-color);
    display: grid;
    margin-block-end: spacer('xxs');
    place-items: center;

    > * {
      grid-area: 1 / 1;
    }

    svg {
      block-size: 3rem;
      inline-size: 3rem;
    }
  }

  &__item--folder &__icon { --icon-color: #{color('warning', 'alert')}; }
  &__item--image &__icon { --icon-color: #{color('info', 'alert')}; }
  &__item--pdf &__icon { --icon-color: #{color('danger', 'alert')}; }
  &__item--doc &__icon { --icon-color: #{color('primary')}; }
  &__item--sheet &__icon { --icon-color: #{color('success', 'alert')}; }
  &__item--video &__icon { --icon-color: #{color('secondary')}; }

  &__ext {
    font-size: 0.55rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    padding-block-start: 0.6rem;
  }

  &__name {
    color: color('heading');
    font-size: config('font-size-sm', $typography);
    font-weight: 600;
    inline-size: 100%;
    overflow: hidden;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  &__meta,
  &__modified {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
  }

  &__modified {
    display: none;
  }

  // The list view: one row per item with the details in columns.
  &__items--list {
    gap: 0;
    grid-template-columns: minmax(0, 1fr);

    .file-manager__item {
      border-radius: 0;
      border-width: 0 0 config('border-width-base', $display);
      display: grid;
      gap: spacer('s');
      grid-template-columns: 2rem minmax(0, 1fr) 6rem 6rem;
      padding: spacer('xs') spacer('xxs');
      text-align: start;
    }

    .file-manager__icon {
      margin: 0;

      svg {
        block-size: 1.75rem;
        inline-size: 1.75rem;
      }
    }

    .file-manager__ext {
      display: none;
    }

    .file-manager__modified {
      display: block;
    }
  }

  &__storage {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    font-size: config('font-size-sm', $typography);
    gap: spacer('xxs') spacer('s');

    label {
      color: color('heading');
      font-weight: 600;
    }

    meter {
      block-size: 0.5rem;
      flex: 1 1 8rem;
    }

    span {
      color: color('muted');
    }
  }
}
<section class="file-manager" aria-labelledby="file-manager-title" data-file-manager>
    <header class="file-manager__header">
        <h2 class="sr-only" id="file-manager-title">Files</h2>
        <nav class="file-manager__path" aria-label="Folder path">
            <ol role="list">
                <li><a href="#">Workspace</a></li>
                <li><span aria-current="location">Marketing</span></li>
            </ol>
        </nav>
        <div class="file-manager__tools">
            <div class="file-manager__view" role="group" aria-label="View" hidden data-file-manager-view>
                <button class="btn btn--sm btn--icon btn--ghost-primary" type="button" aria-pressed="true" data-view="grid"><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="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg><span class="sr-only">Grid</span></button>
                <button class="btn btn--sm btn--icon btn--ghost-primary" type="button" aria-pressed="false" data-view="list"><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="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg><span class="sr-only">List</span></button>
            </div>
            <a class="btn btn--primary btn--sm" href="#">Upload</a>
        </div>
    </header>
    <ul class="file-manager__items" role="list" data-file-manager-items>
        <li class="file-manager__item file-manager__item--folder">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" focusable="false"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4h3.6a2 2 0 0 1 1.5.7L12 6.2h6.5A2.5 2.5 0 0 1 21 8.7v8.8a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/></svg></span>
            <a class="file-manager__name" href="#">Brand assets</a>
            <span class="file-manager__meta">48 files</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Oct 2</span>
        </li>
        <li class="file-manager__item file-manager__item--folder">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" focusable="false"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4h3.6a2 2 0 0 1 1.5.7L12 6.2h6.5A2.5 2.5 0 0 1 21 8.7v8.8a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/></svg></span>
            <a class="file-manager__name" href="#">Campaigns</a>
            <span class="file-manager__meta">12 files</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Sep 30</span>
        </li>
        <li class="file-manager__item file-manager__item--image">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">JPG</span></span>
            <a class="file-manager__name" href="#">hero-autumn.jpg</a>
            <span class="file-manager__meta">2.4 MB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Today</span>
        </li>
        <li class="file-manager__item file-manager__item--pdf">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">PDF</span></span>
            <a class="file-manager__name" href="#">Q4 plan.pdf</a>
            <span class="file-manager__meta">860 KB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Yesterday</span>
        </li>
        <li class="file-manager__item file-manager__item--doc">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">DOCX</span></span>
            <a class="file-manager__name" href="#">Launch copy.docx</a>
            <span class="file-manager__meta">48 KB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Oct 1</span>
        </li>
        <li class="file-manager__item file-manager__item--sheet">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">XLSX</span></span>
            <a class="file-manager__name" href="#">Budget 2027.xlsx</a>
            <span class="file-manager__meta">112 KB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Sep 29</span>
        </li>
        <li class="file-manager__item file-manager__item--video">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">MP4</span></span>
            <a class="file-manager__name" href="#">Product tour.mp4</a>
            <span class="file-manager__meta">84 MB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Sep 21</span>
        </li>
        <li class="file-manager__item file-manager__item--zip">
            <span class="file-manager__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" focusable="false"><path d="M14 2.5H7A2.5 2.5 0 0 0 4.5 5v14A2.5 2.5 0 0 0 7 21.5h10a2.5 2.5 0 0 0 2.5-2.5V8z"/><path d="M14 2.5V8h5.5"/></svg><span class="file-manager__ext">ZIP</span></span>
            <a class="file-manager__name" href="#">Press kit.zip</a>
            <span class="file-manager__meta">16 MB</span>
            <span class="file-manager__modified"><span class="sr-only">Modified: </span>Sep 12</span>
        </li>
    </ul>
    <p class="file-manager__storage">
        <label for="file-manager-storage">Storage</label>
        <meter id="file-manager-storage" min="0" max="100" low="75" high="90" optimum="0" value="64">6.4 of 10 GB</meter>
        <span>6.4 GB of 10 GB used</span>
    </p>
</section>
(() => {
  const storageKey = 'file-manager-view';

  document.querySelectorAll('[data-file-manager]').forEach((manager) => {
    const view = manager.querySelector('[data-file-manager-view]');
    const items = manager.querySelector('[data-file-manager-items]');

    const setView = (value) => {
      items.classList.toggle('file-manager__items--list', value === 'list');
      view.querySelectorAll('[data-view]').forEach((button) => {
        button.setAttribute('aria-pressed', String(button.dataset.view === value));
      });

      try {
        localStorage.setItem(storageKey, value);
      } catch (error) {}
    };

    // The view switch needs JavaScript, so it's hidden without it.
    view.hidden = false;
    view.addEventListener('click', (event) => {
      const button = event.target.closest('[data-view]');

      if (button) setView(button.dataset.view);
    });

    try {
      setView(localStorage.getItem(storageKey) || 'grid');
    } catch (error) {
      setView('grid');
    }
  });
})();

File Manager

A file browser for a folder: the path, a grid or list view of the folders and files with type icons, an upload button and the storage usage.

Anatomy

  • .file-manager__header: the folder __path (a breadcrumb), the __view switch and the upload button.
  • .file-manager__items: every __item has a type modifier (--folder, --image, --pdf, --doc, --sheet, --video, --zip), an __icon with the file extension, the __name link, the __meta (size or file count) and the __modified date. The --list modifier switches to the list view.
  • .file-manager__storage: a meter of the used space.

Accessibility and Usability

  • The path is a nav with an ordered list; the current folder has aria-current="location".
  • The whole tile is clickable with the a11y-card-link mixin, but only the name is a link for assistive technology, so the files aren't announced twice.
  • The icons are decorative; the name has the extension and the type colors only repeat it.
  • The view switch is a group of toggle buttons (aria-pressed) with hidden text ("Grid", "List").
  • Long file names are truncated with an ellipsis in the grid; give the link a title with the full name if your names are often long.

Technical Details

  • The chosen view is saved in localStorage (in a try block) and restored on the next visit. The switch is hidden without JavaScript, and the grid view stays.
  • The grid fills the row with as many 9.5rem tiles as fit (auto-fill, so a folder with few files keeps the tile size).
  • The meter uses optimum="0", so it turns yellow and red as the storage fills up (see Usage Meter).

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.