Story

The story

The challenge

Fjord Bank’s app had 140 screens and five navigation patterns. Every team had added its feature where it fit at the time, and after ten years nobody could find anything.

The support team answered 4,200 questions a month, and most of them were the same: where is my card, how do I pay a bill, why can’t I see my savings?

The approach

We started with the people, not the screens. In two weeks we talked to 24 customers and listened to 60 support calls.

  • Mapped every task to its screen, and every screen to its traffic
  • Tested the navigation with paper prototypes in three branches
  • Agreed on one principle: one screen, one job

We didn’t need more features. We needed fewer, in the right place.

Jonas Berg, CPO

The solution

The new app has four tabs instead of eleven menus. The home screen shows the balance, the next payment and one clear action, and everything else is one tap away.

The spending overview with a chart of the monthly expenses.
The spending overview replaced three separate reports.

The design system

To keep the app calm after we left, we built a small design system with the engineers: tokens, 32 components and the rules for using them, in Figma and in code.

Every component was tested with VoiceOver and TalkBack before it shipped, which is why the screen reader issues stayed at zero.

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

.story-block {
  padding-block: spacer-clamp('xl', 'xxl');

  &__inner {
    align-items: start;
    display: grid;
    gap: spacer-clamp('l', 'xxl');

    @include breakpoint('lg') {
      grid-template-columns: minmax(0, 15rem) minmax(0, 44rem);
    }
  }

  // The chapter list follows the reader on wide screens.
  &__nav {
    @include layout-stack('s');

    @include breakpoint('lg') {
      inset-block-start: spacer('l');
      position: sticky;
    }
  }

  &__nav-title {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  &__toc {
    border-inline-start: config('border-width-base', $display) solid color('border');
    list-style: none;
    padding: 0;
    @include layout-stack('xxs');
  }

  &__toc-link {
    align-items: baseline;
    border-inline-start: 2px solid transparent;
    color: color('text');
    display: flex;
    gap: spacer('xs');
    margin-inline-start: calc(#{config('border-width-base', $display)} * -1);
    padding: spacer('xxs') spacer('s');
    text-decoration: none;

    &:hover {
      color: color('primary');
    }

    &[aria-current='true'] {
      border-inline-start-color: color('primary');
      color: color('heading');
      font-weight: 700;
    }
  }

  &__toc-number {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    font-variant-numeric: tabular-nums;
  }

  &__content {
    display: flex;
    flex-direction: column;
    gap: spacer-clamp('l', 'xl');
  }

  &__chapter {
    font-size: config('font-size-lead', $typography);
    scroll-margin-block-start: spacer('l');
    @include layout-stack('m');
  }

  &__eyebrow {
    color: color('primary');
    font-family: config('font-family-heading', $typography);
    font-size: font-size('h4');
    font-weight: config('font-weight-heading', $typography);
  }

  &__heading {
    @include layout-stack('xxs'); // number, title
  }

  &__title {
    font-size: responsive-font-size(2.25rem, 30, '2vw + 1rem');
  }

  &__list {
    @include layout-stack('xs');
  }

  &__quote {
    border-inline-start: 4px solid color('primary');
    margin-inline: 0;
    padding-inline-start: spacer('m');
    @include layout-stack('xs');

    blockquote {
      border: 0;
      color: color('heading');
      font-family: config('font-family-heading', $typography);
      font-size: font-size('h3');
      font-style: normal;
      line-height: 1.35;
      margin-inline: 0;
      padding: 0;
    }

    figcaption {
      color: color('muted');
      font-size: config('font-size-sm', $typography);
      text-align: start;
    }
  }

  &__figure {
    margin-inline: 0;
    @include layout-stack('xs');

    img {
      background-color: color('surface-muted');
      border-radius: radius('lg');
      inline-size: 100%;
    }

    figcaption {
      color: color('muted');
      font-size: config('font-size-sm', $typography);
    }
  }
}
<section class="story-block" aria-labelledby="story-title" data-story>
    <div class="container">
        <div class="story-block__inner">
            <h2 class="sr-only" id="story-title">The story</h2>
            <nav class="story-block__nav" aria-labelledby="story-nav-title">
                <p class="story-block__nav-title" id="story-nav-title">In this case study</p>
                <ol class="story-block__toc" role="list">
                    <li><a class="story-block__toc-link" href="#challenge" data-story-link><span class="story-block__toc-number" aria-hidden="true">01</span>The challenge</a></li>
                    <li><a class="story-block__toc-link" href="#approach" data-story-link><span class="story-block__toc-number" aria-hidden="true">02</span>The approach</a></li>
                    <li><a class="story-block__toc-link" href="#solution" data-story-link><span class="story-block__toc-number" aria-hidden="true">03</span>The solution</a></li>
                    <li><a class="story-block__toc-link" href="#system" data-story-link><span class="story-block__toc-number" aria-hidden="true">04</span>The design system</a></li>
                </ol>
            </nav>
            <div class="story-block__content">
                <article class="story-block__chapter" id="challenge" aria-labelledby="challenge-title" data-story-chapter>
                    <div class="story-block__heading">
                        <p class="story-block__eyebrow" aria-hidden="true">01</p>
                        <h3 class="story-block__title" id="challenge-title">The challenge</h3>
                    </div>
                    <p>Fjord Bank’s app had 140 screens and five navigation patterns. Every team had added its feature where it fit at the time, and after ten years nobody could find anything.</p>
                    <p>The support team answered 4,200 questions a month, and most of them were the same: where is my card, how do I pay a bill, why can’t I see my savings?</p>
                </article>
                <article class="story-block__chapter" id="approach" aria-labelledby="approach-title" data-story-chapter>
                    <div class="story-block__heading">
                        <p class="story-block__eyebrow" aria-hidden="true">02</p>
                        <h3 class="story-block__title" id="approach-title">The approach</h3>
                    </div>
                    <p>We started with the people, not the screens. In two weeks we talked to 24 customers and listened to 60 support calls.</p>
                    <ul class="story-block__list">
                        <li>Mapped every task to its screen, and every screen to its traffic</li>
                        <li>Tested the navigation with paper prototypes in three branches</li>
                        <li>Agreed on one principle: one screen, one job</li>
                    </ul>
                    <figure class="story-block__quote">
                        <blockquote>
                            <p>We didn’t need more features. We needed fewer, in the right place.</p>
                        </blockquote>
                        <figcaption>Jonas Berg, CPO</figcaption>
                    </figure>
                </article>
                <article class="story-block__chapter" id="solution" aria-labelledby="solution-title" data-story-chapter>
                    <div class="story-block__heading">
                        <p class="story-block__eyebrow" aria-hidden="true">03</p>
                        <h3 class="story-block__title" id="solution-title">The solution</h3>
                    </div>
                    <p>The new app has four tabs instead of eleven menus. The home screen shows the balance, the next payment and one clear action, and everything else is one tap away.</p>
                    <figure class="story-block__figure">
                        <img src="https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&amp;fit=crop&amp;w=1400&amp;q=70" alt="The spending overview with a chart of the monthly expenses." width="1400" height="900" loading="lazy">
                        <figcaption>The spending overview replaced three separate reports.</figcaption>
                    </figure>
                </article>
                <article class="story-block__chapter" id="system" aria-labelledby="system-title" data-story-chapter>
                    <div class="story-block__heading">
                        <p class="story-block__eyebrow" aria-hidden="true">04</p>
                        <h3 class="story-block__title" id="system-title">The design system</h3>
                    </div>
                    <p>To keep the app calm after we left, we built a small design system with the engineers: tokens, 32 components and the rules for using them, in Figma and in code.</p>
                    <p>Every component was tested with VoiceOver and TalkBack before it shipped, which is why the screen reader issues stayed at zero.</p>
                </article>
            </div>
        </div>
    </div>
</section>
(() => {
  document.querySelectorAll('[data-story]').forEach((block) => {
    const links = [...block.querySelectorAll('[data-story-link]')];
    const chapters = [...block.querySelectorAll('[data-story-chapter]')];

    if (!('IntersectionObserver' in window) || !links.length) {
      return;
    }

    // Mark the link of the chapter that is in the reading area (the upper part of the viewport).
    const setCurrent = (id) => {
      links.forEach((link) => {
        if (link.hash === `#${id}`) {
          link.setAttribute('aria-current', 'true');
        } else {
          link.removeAttribute('aria-current');
        }
      });
    };

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setCurrent(entry.target.id);
        }
      });
    }, { rootMargin: '-20% 0px -60% 0px' });

    chapters.forEach((chapter) => observer.observe(chapter));
  });
})();

Story

On this page

The long-form part of a case study or a report: chapters with text, lists, a pull quote and figures, next to a chapter list that follows the reader.

Technical Details

  • The block has a visually hidden h2; every chapter is an article with an h3 and an id, so the chapter list (a nav with an ordered list) links to them and they can be shared.
  • On wide screens the chapter list is sticky. The script marks the chapter in the reading area with aria-current="true" (an IntersectionObserver, no scroll listener); without JavaScript the links still work.
  • The chapter numbers are decorative (aria-hidden); the order of the list carries them.
  • The text column is limited to about 44rem for a comfortable line length; the chapters use the lead font size.
  • Pull quotes are a figure with a blockquote and a figcaption for the source; figures always have a caption.
  • Override the content with storyBlock: every chapter takes paragraphs, and optionally a list, a quote and a figure.

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.