Justin Duke

Creating a reusable tabbed card component in Vue

I built a thing for Buttondown that lets you embed an iFrame to handle subscriptions really nicely:

This thing necessitated a change in the UI: The current share page was pretty gross (below), and this was going to make it grosser:

The old Share page. Not super elegant.

I am admittedly not a great designer, and whenever I need to change UI I go spelunking in Bootstrap to see if I can find a paradigm that I like.

And I found something great! Bootstrap cards with tabbed navigation ended up looking really great:

The Bootstrap component. So pretty!

I don’t actually include Bootstrap in Buttondown, though. A lot of its design cues are taken from it (and some things, like modals, are almost identically styled), but I didn’t want to deal with the additional CSS + JS overhead, especially because the jQuery-esque way of handling interactivity seems anti-Vue.

This seemed like a job for a Vue components: I really wanted to be able to just have a tabbed-card component that I could then add various dynamic tabs with transclusion.

Turns out, you can! The code isn’t even that complicated: the trick is using dynamic slot names to handle the work for you. You still have to pass in a list of tab names as a prop, but I think that’s totally reasonable.

Here’s what the TabbedCard component looks like:

<template>
  <div class="card">
    <div class="card-header">
      <ul class="card-nav nav-tabs card-header-tabs">
        <li class="nav-item" v-for="tab in tabs">
          <a
            :class="activeTab === tab ? 'nav-link active' : 'nav-link'"
            href="#"
            @click="(e) => activeTab = tab"
          >
            {{ tab }}
          </a>
        </li>
      </ul>
    </div>
    <div class="card-block">
      <slot :name="tab" v-for="tab in tabs" v-if="tab === activeTab" />
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      tabs: Array,
    },

    data() {
      return {
        activeTab: this.tabs[0],
      };
    },

  };
</script>

And here’s how I invoke it, in the Share screen:

<tabbed-card :tabs="['as a link', 'as a form', 'as an iFrame']">
  <div slot="as a link">
    <!-- Stuff. -->
  </div>

  <div slot="as a form">
    <!-- Stuff. -->
  </div>

  <div slot="as an iFrame">
    <!-- Stuff.-->
  </div>
</tabbed-card>

This ends up being simple, and just leverages props and Vue’s transclusion functionality: we provide a tabs array, which creates both the actual tabs and a bunch of slots for content to go into. This is nice separation of concerns: the screen declaring the tabbed card doesn’t know about how the tabbed card is formed, it just supplies a list of tabs and content for each tab.

And here’s the finished product:

Imgur

This is nothing groundbreaking, to be sure, but it’s really elegant and re-usable. It makes me happy when Vue makes it easy to create clean, maintainable components — it makes me feel like I’m doing the right thing, which can be an elusive feeling in software.

Liked this post? You should subscribe to my newsletter and follow me on Twitter.
© 2017 Justin Duke • All rights reserved • I hope you have a nice day.