{"id":8268,"date":"2026-01-15T16:30:23","date_gmt":"2026-01-15T21:30:23","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=8268"},"modified":"2026-01-15T16:30:24","modified_gmt":"2026-01-15T21:30:24","slug":"the-missing-link-for-web-components","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/the-missing-link-for-web-components\/","title":{"rendered":"The Missing Link for Web Components"},"content":{"rendered":"\n<p>Last year I was mainly working in a component library. I noticed that we had <em>a lot<\/em> of duplicated code.<\/p>\n\n\n\n<p>Every component defines its properties, events and slots, their types, whether they are required or not, plus a lot of <a href=\"https:\/\/jsdoc.app\/about-getting-started\">JSDoc annotations<\/a>. This is repeated for <a href=\"https:\/\/storybook.js.org\/\">Storybook<\/a>, to inform the controls of the Storybook UI, for example to switch from the primary to the secondary variant of a button. <\/p>\n\n\n\n<p>Then we need a template, where you wire these controls to the attributes of the component. A similar template is present in both unit and end-to-end tests. And another template for <a href=\"https:\/\/help.figma.com\/hc\/en-us\/articles\/23920389749655-Code-Connect\">Figma Code Connect<\/a>, which maps a component in Figma to the actual web component code. Naturally this can get out of sync far too easily. And since the codebase had already gone through a lot of hands for years, we had a lot of outdated Storybook files, properties that no longer existed but in test file templates etc. (For the particular Storybook sync problem, there was even an add-on, but since the <a href=\"https:\/\/stenciljs.com\/\">StencilJS<\/a> community is not huge, we wondered how well this would be maintained in the future).<\/p>\n\n\n\n<p>What if there was a solution to all of the problems above?<\/p>\n\n\n\n<p>Even better if it could be used across all web component frameworks, and thus maintained and improved by a much larger community. Enter the <strong><a href=\"https:\/\/github.com\/webcomponents\/custom-elements-manifest\">Custom Elements Manifest<\/a><\/strong>. This is the missing link to connect the tooling in a web components project, enable more automation and much improve the developer experience.<\/p>\n\n\n\n<p>I\u2019ll use the <a href=\"https:\/\/github.com\/fgeierst\/custom-elements-manifest-demo\">custom-elements-manifest-demo repository<\/a> to show how to set up the different parts and how to use them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Scaffold a New Lit project with a Button Component<\/h2>\n\n\n\n<p>First, we scaffold a new <a href=\"https:\/\/lit.dev\/\">Lit<\/a> project:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm create vite<\/pre>\n\n\n\n<p>In the CLI dialog, pick \u201cSelect a framework: Lit\u201d. Our example project will start with a single component:&nbsp;<code>touch src\/my-button\/my-button.ts<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { LitElement, css, html } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"lit\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { customElement, property } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"lit\/decorators.js\"<\/span>;\n\n<span class=\"hljs-meta\">@customElement<\/span>(<span class=\"hljs-string\">\"my-button\"<\/span>)\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">class<\/span> MyButton <span class=\"hljs-keyword\">extends<\/span> LitElement {\n  <span class=\"hljs-comment\">\/**\n   * The button variant style.\n   *\/<\/span>\n  <span class=\"hljs-meta\">@property<\/span>()\n  variant: <span class=\"hljs-string\">\"primary\"<\/span> | <span class=\"hljs-string\">\"secondary\"<\/span> = <span class=\"hljs-string\">\"primary\"<\/span>;\n\n  render() {\n    <span class=\"hljs-keyword\">return<\/span> html`<span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">part<\/span>=<span class=\"hljs-string\">\"button\"<\/span> <span class=\"hljs-attr\">class<\/span>=<\/span><\/span><span class=\"hljs-subst\">${<span class=\"hljs-keyword\">this<\/span>.variant}<\/span><span class=\"xml\"><span class=\"hljs-tag\">&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">slot<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">slot<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>`<\/span>;\n  }\n\n  <span class=\"hljs-keyword\">static<\/span> styles = css`<span class=\"css\">\n    <span class=\"hljs-selector-tag\">button<\/span> {\n    }\n  `<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The button has a default slot and a variant property that can be either primary or secondary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Create the Manifest JSON<\/h2>\n\n\n\n<p>You can imagine <a href=\"https:\/\/github.com\/webcomponents\/custom-elements-manifest\/blob\/main\/schema.d.ts#L264\">the Custom Elements Manifest like a detailed table of contents<\/a> for the entire project. Every component has its entry, complete with all attributes, and other meta data like default values, available options and textual descriptions.<\/p>\n\n\n\n<p>Here&#8217;s the beginning of a <code>custom-elements.json<\/code> file about our new button:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">{\n  <span class=\"hljs-attr\">\"schemaVersion\"<\/span>: <span class=\"hljs-string\">\"1.0.0\"<\/span>,\n  <span class=\"hljs-attr\">\"modules\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"path\"<\/span>: <span class=\"hljs-string\">\"src\/my-button\/my-button.ts\"<\/span>,\n      <span class=\"hljs-attr\">\"declarations\"<\/span>: &#91;\n        {\n          <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"MyButton\"<\/span>,\n          <span class=\"hljs-attr\">\"tagName\"<\/span>: <span class=\"hljs-string\">\"my-button\"<\/span>,\n          <span class=\"hljs-attr\">\"customElement\"<\/span>: <span class=\"hljs-literal\">true<\/span>\n          <span class=\"hljs-string\">\"attributes\"<\/span>: &#91;\n            {\n              <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"variant\"<\/span>,\n              <span class=\"hljs-attr\">\"type\"<\/span>: { <span class=\"hljs-attr\">\"text\"<\/span>: <span class=\"hljs-string\">\"\\\"primary\\\" | \\\"secondary\\\"\"<\/span> },\n              <span class=\"hljs-attr\">\"default\"<\/span>: <span class=\"hljs-string\">\"\\\"primary\\\"\"<\/span>,\n              <span class=\"hljs-attr\">\"description\"<\/span>: <span class=\"hljs-string\">\"The button variant style.\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Luckily we don\u2019t need to gather all of this information <em>by hand<\/em>. Instead, there is a tool that walks all files in a project, parses out the useful bits and compiles it to a JSON file.<\/p>\n\n\n\n<p>First we need to install the <a href=\"https:\/\/custom-elements-manifest.open-wc.org\/analyzer\/getting-started\/\">Custom Elements Analyzer<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm add -D @custom-elements-manifest\/analyzer<\/pre>\n\n\n\n<p>The analyzer comes with built-in configuration for common web component frameworks, like Lit, StencilJS or Fast, but you can also roll your own. Let\u2019s add an entry to the package.json scripts:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ package.json<\/span>\n  <span class=\"hljs-string\">\"scripts\"<\/span>: {\n+    <span class=\"hljs-string\">\"analyze\"<\/span>: <span class=\"hljs-string\">\"cem analyze --litelement --globs \\\"src\/**\/*.ts\\\"\"<\/span>,<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now lets run the analyzer&nbsp;<code>pnpm analyze<\/code>&nbsp;and it writes custom-elements.json to disk.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Auto-Generate the Storybook Stories File<\/h2>\n\n\n\n<p>Having the manifest, we can make our first use of it. As we will see, it can do most of the configuration part of a .stories file for us. Let\u2019s install Storybook first:&nbsp;<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm create storybook@latest<\/pre>\n\n\n\n<p>Then we need some initial wiring between Storybook and the manifest file. We will use a helper tool for that:&nbsp;<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm i -D @wc-toolkit\/storybook-helpers<\/pre>\n\n\n\n<p>Add the following to our existing .<code>storybook\/preview.ts<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { setCustomElementsManifest } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@storybook\/web-components-vite\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { setStorybookHelpersConfig } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@wc-toolkit\/storybook-helpers\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { withActions } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'storybook\/actions\/decorator'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> manifest <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"..\/custom-elements.json\"<\/span> <span class=\"hljs-keyword\">with<\/span> { <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"json\"<\/span> };\n\nsetCustomElementsManifest(manifest); <span class=\"hljs-comment\">\/\/ (1)<\/span>\nsetStorybookHelpersConfig({  hideArgRef: <span class=\"hljs-literal\">true<\/span>, }); <span class=\"hljs-comment\">\/\/ (2)<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> preview: Preview = {\n  <span class=\"hljs-comment\">\/\/ existing config here<\/span>\n  tags: &#91;<span class=\"hljs-string\">'autodocs'<\/span>], <span class=\"hljs-comment\">\/\/ (3)<\/span>\n  decorators: &#91;withActions], <span class=\"hljs-comment\">\/\/ (4)<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The first function call <strong>(1) <\/strong>makes our manifest available as a global in all stories files. The second <strong>(2) <\/strong>configures the helpers that we will use in the next step. Autodocs <strong>(3)<\/strong> adds a auto generated Docs page for each stories file, which includes a table with the component API definition and a preview for each story. The withActions decorator <strong>(4)<\/strong> is needed to display events that our component emits in Storybook\u2019s Actions panel.<\/p>\n\n\n\n<p>Now let\u2019s create a stories file for our button:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">touch src\/my-button\/my-button.stories.ts.<\/pre>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/my-button\/my-button.stories.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> { Meta, StoryObj } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@storybook\/web-components-vite\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { getStorybookHelpers } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@wc-toolkit\/storybook-helpers\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-string\">\".\/my-button\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> <span class=\"hljs-keyword\">type<\/span> { MyButton } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/my-button\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> { events, args, argTypes, template } =\n  getStorybookHelpers&lt;MyButton&gt;(<span class=\"hljs-string\">\"my-button\"<\/span>);\n\n<span class=\"hljs-keyword\">const<\/span> meta = {\n  title: <span class=\"hljs-string\">\"Components\/Button\"<\/span>,\n  component: <span class=\"hljs-string\">\"my-button\"<\/span>,\n  argTypes,\n  args: { ...args, <span class=\"hljs-string\">\"default-slot\"<\/span>: <span class=\"hljs-string\">\"Click me\"<\/span> },\n  render: <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">args<\/span><\/span>) =&gt;<\/span> template(args),\n  parameters: { actions: { handles: events } },\n} <span class=\"hljs-keyword\">as<\/span> Meta&lt;MyButton&gt;;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> meta;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> Primary: StoryObj&lt;MyButton&gt; = {};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you are familiar with typical stories files, you will notice that this one is very succinct. We can infer events, args, argTypes and even the template from the manifest data.<\/p>\n\n\n\n<p>Now lets run Storybook.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm storybook<\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"726\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?resize=1024%2C726&#038;ssl=1\" alt=\"Screenshot of a Storybook interface displaying documentation for a custom button component, including sections for attributes and slots.\" class=\"wp-image-8282\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?resize=1024%2C726&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?resize=300%2C213&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?resize=768%2C544&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?resize=1536%2C1089&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image6.png?w=1999&amp;ssl=1 1999w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Notice how this documents the interface of the component extensively. We see the attribute and the default slot, each with description and even the options \u201cprimary\u201d and \u201csecondary\u201d. Also the controls are fully functional. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Get Editor Support with the VSCode Language Server<\/h2>\n\n\n\n<p>Another application for the manifest file is editor support. First, we install <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=wc-toolkit.web-components-language-server\">the Web Components Language Server extension for VSCode<\/a>. To see it in action we add a new story, where we write the template ourselves:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ src\/my-button\/my-button.stories.ts<\/span>\n<span class=\"hljs-comment\">\/\/ ...existing code<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> Variants: StoryObj&lt;MyButton&gt; = {\n  render: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> <span class=\"hljs-string\">`\n    &lt;my-button variant=\"primary\"&gt;Primary&lt;\/my-button&gt;\n    &lt;my-button variant=\"\"&gt;Secondary&lt;\/my-button&gt;\n  `<\/span>,\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Hovering over&nbsp;reveals a lot of useful IntelliSense information.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"607\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image5.png?resize=1024%2C607&#038;ssl=1\" alt=\"A code snippet displaying the render function of a custom button component in a web development context, featuring two button instances with different variant attributes, and documentation about the component's attributes, events, and slots.\" class=\"wp-image-8283\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image5.png?resize=1024%2C607&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image5.png?resize=300%2C178&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image5.png?resize=768%2C455&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image5.png?w=1452&amp;ssl=1 1452w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Hitting&nbsp;<code>ctrl space<\/code>&nbsp;shows the available options for the variant attribute.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"203\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image3.png?resize=1024%2C203&#038;ssl=1\" alt=\"Code snippet demonstrating the render function for a button component in a web development context, showcasing options for the 'variant' attribute.\" class=\"wp-image-8284\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image3.png?resize=1024%2C203&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image3.png?resize=300%2C59&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image3.png?resize=768%2C152&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image3.png?w=1476&amp;ssl=1 1476w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Ground Copilot with the MCP server<\/h2>\n\n\n\n<p>Wouldn\u2019t it be nice if GitHub copilot had the same information? For this, we enable <a href=\"https:\/\/wc-toolkit.com\/integrations\/vscode\/#model-context-protocol-mcp-server\">the MCP server<\/a> that is part of the language server extension.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-comment\">\/\/ ~\/Library\/Application Support\/Code\/User\/settings.json<\/span>\n<span class=\"hljs-comment\">\/\/ ...existing configuration<\/span>\n\n\n  <span class=\"hljs-string\">\"wctools.mcp.enabled\"<\/span>: <span class=\"hljs-literal\">true<\/span>,\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can ask the AI agent about our web components by @-mentioning the MCP server.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"848\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image1.png?resize=848%2C1024&#038;ssl=1\" alt=\"A screenshot showing the description of a custom button component called 'my-button', including its attributes, properties, and events.\" class=\"wp-image-8287\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image1.png?resize=848%2C1024&amp;ssl=1 848w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image1.png?resize=248%2C300&amp;ssl=1 248w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image1.png?resize=768%2C927&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image1.png?w=1060&amp;ssl=1 1060w\" sizes=\"auto, (max-width: 848px) 100vw, 848px\" \/><\/figure>\n\n\n\n<p>While this seems trivial in this example, it helps to ground the LLM and makes it less likely to hallucinate components or properties.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Ensure Components are Used Correctly with the Web Components Linter<\/h2>\n\n\n\n<p>But what if you already have a lot of code and want to make sure the components are used correctly. This is where the <a href=\"https:\/\/wc-toolkit.com\/integrations\/wctools\/\">Web Component Linter<\/a> into play. Install it with:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm add @wc-toolkit\/wctools<\/pre>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ package.json<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-string\">\"scripts\"<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>\t<span class=\"hljs-comment\">\/\/ ...existing scripts<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>+      <span class=\"hljs-attr\">\"lint\"<\/span>: <span class=\"hljs-string\">\"wctools validate\"<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Running this&#8230;<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">pnpm lint<\/pre>\n\n\n\n<p>&#8230; checks the entire codebase against what is written in manifest. If my AI agent had invented a tertiary variant that does not exist&nbsp;<code>&lt;my-button variant=\"tertiary\"&gt;Tertiary&lt;\/my-button&gt;<\/code>&nbsp;the linter would catch it.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"273\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?resize=1024%2C273&#038;ssl=1\" alt=\"Error message displayed in a terminal window indicating a validation issue with a web component, specifically that 'tertiary' is not a valid value for the 'variant' attribute.\" class=\"wp-image-8286\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?resize=1024%2C273&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?resize=300%2C80&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?resize=768%2C205&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?resize=1536%2C410&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/image4.png?w=1544&amp;ssl=1 1544w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p class=\"learn-more\">Note that the naming here is a bit confusing, there is also the <a href=\"https:\/\/wc-toolkit.com\/cem-utilities\/cem-validator\/\">CEM Validator<\/a>, which looks at the manifest file itself.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Future Ideas<\/h2>\n\n\n\n<p>Looking at the future, I think there is a lot more that we could do with the Custom Elements Manifest! Component tests need a template, so why not use a helper that generates these, similar to the one for Storybook? Same for Figma Code Connect files.<\/p>\n\n\n\n<p>Recommended Podcasts: <\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.podcastawesome.com\/2092855\/episodes\/17732733-rage-coding-headless-web-components-and-the-future-of-dx-with-burton-smith\">Rage Coding, Headless Web Components, and the Future of DX with Burton Smith<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/shoptalkshow.com\/692\/\">Killer Feature of Web Components, Skills > MCP, and Streaming HTML?<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>If your project uses web components of your own making, you could be auto-generating a Custom Elements Manifest that can be ultra-helpful, like powering a VS Code language server. <\/p>\n","protected":false},"author":41,"featured_media":8271,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[438,3,439,415,36],"class_list":["post-8268","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-custom-elements-manifest","tag-javascript","tag-language-server","tag-mcp","tag-web-components"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/01\/cem-thumb.jpg?fit=2464%2C1728&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8268","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=8268"}],"version-history":[{"count":12,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8268\/revisions"}],"predecessor-version":[{"id":8303,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8268\/revisions\/8303"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/8271"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=8268"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=8268"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=8268"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}