DEV Community

Matt Swensen
Matt Swensen

Posted on • Originally published at mjswensen.com on

Building Stripe.com's Tabbed Preview Widget From Scratch in 30 Minutes

In this video I try to build a fully functional tabbed preview widget in HTML and CSS from scratch in 30 minutes—without looking at the original code. After the time’s up, I peek under the hood to see the approach of the original author and compare and contrast it to my approach.

The build process

Setting up a development environment

For this short project I used a very simple setup: an index.html file served by browser-sync for automatic reloads on save. It can be run without previous download or install via npx, which is included by default in node/npm installations.

npx browser-sync --server --files index.html
Enter fullscreen mode Exit fullscreen mode

Configuring the widget’s container

Since this widget doesn’t appear to respond to the window size, I used the macOS screenshot tool (command-shift-4) to measure the dimensions and simply hard-code them into the CSS.

Managing the active tab state

The original Stripe implementation uses JavaScript to maintain the state of the currently active tab, which is a perfectly reasonable approach. I thought it might be fun to see if we could do it without JavaScript. I landed on using radio-type HTML inputs and some CSS selector tricks to achieve the same effect. In a professional setting, I would have likely used JavaScript—one could argue that this is an inappropriate use of radio inputs since this is an informational widget and not part of a form with user-provided data.

The book I referenced in the video is called Resilient Web Design by Jeremy Keith and is freely available to read online.

Sliding the content left and right based on the active tab

I wrapped the code snippets in a container and positioned it absolutely, altering the left property based on the active tab. We discuss a better approach to this later on when we inspect Stripe’s solution.

Giving the widget a 3D appearance

To give the widget a 3D appearance, I rotated the widget around the X and Y axes, but it didn’t quite have the right effect. I should have used the rotate3d() function instead.

Adding a shine effect

For the shine effect, I added an ::after pseudo-element, positioned absolutely to stretch the width and height of the container, and added a background gradient. To keep the text beneath it selectable (and tabs clickable), pointer-events: none was required so that mouse events would fall through it.

My final code

Here is what I ended up with at the end of the session.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Code Preview Widget</title>
  <style>
    .container {
      --width: 490px;
      --bg: #31335B;
      --radius: 8px;
      --border-active: #596481;
      width: var(--width);
      height: 380px;
      background-color: var(--bg);
      border-radius: var(--radius);
      color: white;
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-template-rows: auto 1fr;
      transform: rotateX(-10deg) rotateY(10deg);
      position: relative;
    }

    .container::after {
      content: '';
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background-image: linear-gradient(to bottom right, hsla(0, 0%, 100%, 0.15), transparent);
      pointer-events: none;
    }

    .content {
      grid-column-start: 1;
      grid-column-end: 6;
      position: relative;
    }

    .slide-wrapper {
      display: flex;
      overflow-x: hidden;
      position: absolute;
      transition: left 400ms ease-in-out;
    }

    #js:checked ~ .content .slide-wrapper {
      left: 0;
    }

    #rb:checked ~ .content .slide-wrapper {
      left: calc(var(--width) * -1);
    }

    #py:checked ~ .content .slide-wrapper {
      left: calc(var(--width) * -2);
    }

    #go:checked ~ .content .slide-wrapper {
      left: calc(var(--width) * -3);
    }

    #other:checked ~ .content .slide-wrapper {
      left: calc(var(--width) * -4);
    }

    .content pre {
      width: var(--width);
      overflow: hidden;
    }

    input[type="radio"] {
      display: none;
    }

    #js:checked ~ label[for="js"],
    #rb:checked ~ label[for="rb"],
    #py:checked ~ label[for="py"],
    #go:checked ~ label[for="go"],
    #other:checked ~ label[for="other"] {
      background-color: var(--bg);
      border-left: 1px solid var(--border-active);
      border-right: 1px solid var(--border-active);
      border-bottom: 1px solid var(--bg);
    }

    label {
      background-color: #2D2F4A;
      border-top-left-radius: var(--radius);
      border-top-right-radius: var(--radius);
      text-align: center;
      padding: 0.25em;
      border-bottom: 1px solid var(--border-active);
    }
  </style>
</head>
<body>
  <div class="container">
    <input type="radio" name="tab" id="js" checked>
    <input type="radio" name="tab" id="rb">
    <input type="radio" name="tab" id="py">
    <input type="radio" name="tab" id="go">
    <input type="radio" name="tab" id="other">
    <label for="js">
      Node.js
    </label>
    <label for="rb">
      Ruby
    </label>
    <label for="py">
      Python
    </label>
    <label for="go">
      Go
    </label>
    <label for="other">
      ...
    </label>
    <section class="content">
      <div class="slide-wrapper">
        <pre><code>// Set your secret key
  const stripe = require('stripe')('sk_test_BQokikJOvBiI2HlWgH4olfQ2');

  // Get the payment token ID submitted by the form:
  const token = request.body.stripeToken;

  (async () => {
    const charge = await stripe.charges.create({
      amount: 999,
      currency: 'usd',
      description: 'Example charge',
      source: token,
    });
  })();</code></pre>
        <pre><code># Set your secret key
  Stripe.api_key = 'sk_test_BQokikJOvBiI2HlWgH4olfQ2'

  # Get the payment token ID submitted by the form:
  token = params[:stripeToken]

  charge = Stripe::Charge.create({
    amount: 999,
    currency: 'usd',
    description: 'Example charge',
    source: token,
  })</code></pre>
        <pre><code> Set your secret key
  stripe.api_key = 'sk_test_BQokikJOvBiI2HlWgH4olfQ2'

  # Get the payment token ID submitted by the form:
  token = request.form['stripeToken']

  charge = stripe.Charge.create(
    amount=999,
    currency='usd',
    description='Example charge',
    source=token,
  )</code></pre>
        <pre><code>// Set your secret key
  stripe.Key = 'sk_test_BQokikJOvBiI2HlWgH4olfQ2'

  // Get the payment token ID submitted by the form:
  token := r.FormValue('stripeToken')

  params := &stripe.ChargeParams{
    Amount: 999,
    Currency: 'usd',
    Description: 'Example charge',
  }
  params.SetSource(token)
  ch, _ := charge.New(params)</code></pre>
        <pre><code>TODO</code></pre>
      </div>
    </section>
  </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Inspecting the original Stripe code

Differences with my approach

Aside from the fact that the Stripe code was much more polished (with additional borders, typography, syntax highlighting, etc.), there were a number things about the original code that were much improved to my version. Here are a couple:

  • Rather than transitioning the left property to slide the code back and forth, the original author used translateX(), which is more performant.
  • The original author used the <figure> element to wrap this widget, which is much more semantically correct than the div I used.

Nice touches

Stripe is known for adding a level of polish and detail that most engineering teams can only dream of. Here are just a couple that I noticed:

  • Using ::before and ::after pseudo-elements with transparent-to-opaque background gradients, the text had an appearance of sliding “under” the edges of the widget as it moved back and forth.
  • A tasteful box-shadow was added to augment the 3D effect of the widget.

Conclusion

Any questions or suggestions about my tooling, approach, development style? What would you have done differently? I’d love to hear from you. Find me on Twitter or leave a comment on the video.

Tools

Here are the tools I used in this video:

Top comments (0)