DEV Community

loading...
Cover image for Component Libraries with Stencil.js - Your First Component

Component Libraries with Stencil.js - Your First Component

John Woodruff
Senior Software Engineer at GoReact, previously Domo. Co-author of Your First Year in Code. Working on something new.
Updated on ・5 min read

This is the third in a series of posts about creating a web component library using Stencil.js - Check out the first post

We've done a lot of setup so far, now let's create our first component. This is the foundation of any component library: the button component. Let's rename the my-component folder to button, and all the files inside replacing my-component to button. We're now ready to build our button.

Component Decorator

The first thing we're going to do is change the contents of button.tsx. We're going to change the tag in the @Component decorator to our actual tag name, and the styleUrl to point to our newly renamed SCSS file, in my case the following:

@Component({
  tag: 'mtn-button',
  styleUrl: 'button.scss',
  shadow: true
})
Enter fullscreen mode Exit fullscreen mode

Note the shadow property. This is declaring that we will be using the Shadow DOM for this component. This has many benefits including an isolated DOM and scoped CSS, among others. We'll definitely want to be taking advantage of this, as it's one of the key parts of web components.

Render Method/JSX

Next we're going to change the class to render a plain button with a single prop.

export class Button {
  render() {
    return (
      <button>
        <slot />
      </button>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Looking at the render method, you'll notice we're not writing HTML. We're using a JavaScript syntax extension called JSX or JavaScript XML. Note with Stencil we're actually using TSX, a file with the ability to write JSX using TypeScript. Let's change the markup to render a standard HTML button. We're also placing a slot inside the button. This is a part of the suite of browser web component APIs; it allows us to fill that slot with markup defined by the consumer of the component. In our case, consuming this component would look like this:

<mtn-button>Click Me!</mtn-button>
Enter fullscreen mode Exit fullscreen mode

The text "Click Me!" is projected down to the slot within the component. The markup in the component will be the following:

<button>
  <slot>Click Me!</slot>
</button>
Enter fullscreen mode Exit fullscreen mode

This button component is currently incredibly simple. With more complex components we will obviously have more markup, and will occasionally use multiple named slots to project multiple bits of markup down to the component. For now, we'll stick with that for our button.

Component Props

You'll have noticed by now if you're familiar at all with React that these components look and function very similarly to React components. Along those lines, we're now going to define a prop. An important piece of functionality for a button is to disable that button. Let's use the @Prop() decorator to define a disabled property. We're also going to pass that disabled property down to our actual button. We'll update our class like so:

export class Button {
  @Prop({ reflectToAttr: true })
  disabled: boolean;

  render() {
    return (
      <button disabled={this.disabled}>
        <slot />
      </button>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We've defined a property on the class called disabled, and added a @Prop() decorator to it. We've also passed in an object with a reflectToAttr key we've defined to be true. According to the Stencil Prop Docs, using that makes sure our disabled prop stays in sync with an HTML attribute. In this case we definitely want that because we're using a disabled attribute.

We are also adding disabled={this.disabled} to our button in the component. This will conditionally apply the disabled attribute depending on whether or not the disabled prop is defined.

Styling Our Button

Currently this is an ugly HTML button. We're obviously going to change that. I encourage you to come up with your own style guide and design for your components, but you're of course welcome to copy what I'm doing. First off I'm going to create a file at src/styles/variables.scss for my color variables. If I have to change colors, I want it to apply to all the colors rather than change them one by one, so I'm using SASS variables to do that. I'm going to start with my font, a couple basic colors, and my primary color shades which is all I'm focusing on right now.

// Font Family
$font-family: 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif;

// Basic Colors
$white: #ffffff;
$black: #000000;

// Brand Colors
$blue-steel: #4571c4;
$blue-steel-dark: #315db0;
Enter fullscreen mode Exit fullscreen mode

Now that I've got my font and colors established, I'm going to give my button some classy styling.

@import '../../styles/variables.scss';

:host {
  box-sizing: border-box;
}

:host([disabled]) {
  pointer-events: none;
}

button {
  font-family: $font-family;
  cursor: pointer;
  border: none;
  background-color: $blue-steel;
  color: $white;
  line-height: 20px;
  font-size: 14px;
  padding: 4px 12px;
  border-radius: 3px;

  &:hover {
    background-color: $blue-steel-dark;
  }
  &:active {
    background-color: darken($blue-steel-dark, 5%);
  }
  &:disabled {
    opacity: 0.4;
  }
}
Enter fullscreen mode Exit fullscreen mode

First I'm importing my variables for use. I'm then using the :host selector, which allows us to select the shadow host of the Shadow DOM, for a couple different things. First of all I'm setting box-sizing: border-box to the host element. I personally prefer using the border-box sizing to not take into account borders for the height and width. It makes more sense to me personally. Next I'm using the host selector to only select the host when it has a disabled attribute applied to it. In that case I'm applying pointer-events: none to the element. This makes it so setting a click handler on the element will not fire when the button is disabled.

Next I'm styling my button itself. You'll notice and possibly worry about me globally styling the button element. This is not a problem because we're using the Shadow DOM, and this is one of its best benefits. All of your styling is scoped to the component's Shadow DOM, no styles from outside can penetrate it, and no styles from inside can mess up anything outside of it. It's pretty awesome.

The rest of the styles are pretty straightforward. I have the button styles, the background color darkens on hover, and darkens even more on click. When it's disabled, I apply opacity: 0.4 to make it look disabled.

If you haven't already, make sure you run npm start to start up your dev server and it'll automatically open in your browser. Go ahead and add your button to your index.html page so you can test your component. I added the following markup:

<mtn-button>Button</mtn-button>
<mtn-button disabled>Button</mtn-button>
Enter fullscreen mode Exit fullscreen mode

When your page refreshes automatically you should see your beautiful new button component in both its disabled and non-disabled states!

new buttons

Next Steps

There you have it! A classy button component that looks great and has basic functionality. There's obviously a lot more to a button (and most components) that will implement, but we'll do that in our next post. See you there!

Simply want to see the end result repo? Check it out here

Discussion (1)

Collapse
rezarahmati profile image
Reza Rahmati

Thank you for your post, if you add that button to a page it doesn't get focused by tab, how can we fix this globally?