Skip to content

Web components

Prod:

The @zurich/web-components NPM package is a package that uses JavaScript and CSS. The components are developed using Lit and then wrapped to be easily used with the main frameworks React and Vue making sure that the capabilities and features of each framework are fully functional. Angular is done in @zurich/angular-components.

The library provide those ready-to-use wrapper components under the corresponding sub-reference (like @zurich/web-components/vue or @zurich/web-components/react).

Everything is adapted for the use of TypeScript (even for the frameworks) and the common development setups.

Disclaimer

Read first the @zurich/design-tokens installation guide since this package depends on that one.

Safari <16.4 support

Do NOT use this approach if you want to give support to versions of Safari before 16.4. The absence of support for adoptedStyleSheets before this patch makes the WebComponents approach extremely difficult to setup, requiring CSP special configs. Use @zurich/css-components instead.

Install

...

We can use two different approaches for the <SRC> value:

Via CDN

The fastest way to get going with @zurich/web-components is to load it via a CDN. Just import the https://zds.zurich.com/0.5.18/@zurich/web-components/styles.css file to the <head> of your HTML and start using the CSS components.

We can import all the component using https://zds.zurich.com/0.5.18/@zurich/web-components, but it is recommended to import then individually for performance.

In order to import anything via CDN, you need use the file inside the CDN/ folder to avoiding the call for local dependencies and breaking the modules' chain.

Here's the code example on how to import the inside the <head>:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://zds.zurich.com/0.5.18/@zurich/web-components/styles.css" />
    <script type="module" src="https://zds.zurich.com/0.5.18/@zurich/web-components/index.js"></script>
  </head>
</html>

Here's an example on how we use the components in a vanilla HTML file.

You can also import individual components using the name of the component within inside the subfolder CDN/. Here's an example with the icon component, but remember that this one requires also the used Icon:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="https://zds.zurich.com/0.5.18/@zurich/web-components/styles.css" />
    <script type="module" src="https://zds.zurich.com/0.5.18/@zurich/web-components/icon.js"></script>
  </head>
</html>

Here's an example on how we use the icon component in a vanilla HTML file.

html

<body z-theme="dark">
  <link rel="stylesheet" href="../@zurich/web-components/styles.css" />
  <script type="module" src="../@zurich/web-components/index.js"></script>

  <z-logo inline></z-logo>
</body>

Local installation

For NPM-style build systems or/and local development, you can install @zurich/web-components via NPM. Just use the installation command of you favorite package manager (NPM, Yarn, PNPM, Bun, etc.). The @zurich/design-tokens packages is set as a dependency of @zurich/web-components, so we don't require to explicitly install the package.

bash
npm i @zurich/web-components

Then access the CSS files inside the distribution folder and import the CSS in your JS/TS. Remember that the @zurich/design-tokens import needs to be done also first for the components to utilize the tokens:

ts
import '@zurich/web-components/styles.css';
import '@zurich/web-components';

Individual components can be also imported for better WPO, keeping the @zurich/web-components/styles.css import:

ts
import '@zurich/web-components/styles.css';
import '@zurich/web-components/safe-space.js';
import '@zurich/web-components/profile.js';
import '@zurich/web-components/button.js';

Imports

Tha main imports of the packages are:

  • 📄 ./index: the main file that contains all the components.

The export per framework are under the corresponding framework name:

  • 📄 ./react: the main file that contains all the components prepared for tree-shaking and the utilities for React. The components are wrapped in a React component function to allow React to use the Web component the right way.

  • 📄 ./vue: the main file that contains all the components prepared for tree-shaking and the utilities for Vue.

The styles:

  • 📄 ./styles.css: a CSS stylesheet file the contains the all the necessary CSS styles for components to be seen properly and with optimization for shared styles. This export already includes the ./index.css export of the @zurich/design-tokens package.

  • 📄 ./a11y.css: visual helpers for accessibility.

The types:

  • 📄 ./react-types: type utils for React.

  • 📄 ./vue-types: type utils for Vue.

We can import single components by name using their standalone JS file.

Examples:

  • Logo (local): @zurich/web-components/dist/logo.js
ts
import "@zurich/web-components/dist/logo.js"
html
<head>
  <script type="module" src="`https://zds.zurich.com/https://zds.zurich.com/0.5.18/@zurich/web-components/logo.js`"></script>
<head>

Complex attributes

If you're using the web component in plain HTML and you want to pass an Object or Array, make sure to parse it as JSON or using JS to inject the property. When you parse it as JSON, you won't be able to properly scape the keys and values quotations with ", so our recommendation is to use single quotes (') for the whole attribute and then set the whole object inside in JSON format.

An example:

There are other options of use like the slots or the slotted configurations in some occasions

Or we can directly use JavaScript to pass them as properties of the instance:

html
<body>
  <z-avatar-list id="my-avatar-list"></z-avatar-list>
  
  <script type="module">
    const avatarList = document.getElementById('my-avatar-list');
    if (avatarList) avatarList.profiles = [
      { initials: 'AZ', color: 2 },
      { initials: 'AZ', color: 4 },
      { initials: 'AZ', color: 3, 'image-src': `/sample.webp` }
    ]
  </script>
</body>

Check this documentation to know more about this options.

Accessibility

The WebComponents are handling the component level accessibility out of the box, but this might require in most cases to provide some attributes when the components is instantiated. Check the documentation of each component to check the corresponding requirements of each component.

For extra tooling, check this documentation.

Frameworks

In order to ease the use of the @zurich/web-components library, we have created a wrapper per component and per supported framework so they can be used as components natively developed for that framework, using directives, TypeScript, events, callbacks, slots, and all the frameworks's syntax.

The supported frameworks for the wrappers are React and Vue.

INFO

For the Angular framework, check the @zurich/angular-components package.

React

The WebComponent and React require some major changes to make a proper integration. Lit for React is one of the tools that allow so, but we've decided to make our own fine tunning. There's only the possibility of using the ZDS components inside the components files and not as a global component to avoid problems.

So we only need to import the necessary components from '@zurich/web-components/react' in the corresponding context.

This is the basic setup for React with ZrIcon as an example:

tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';

import '@zurich/web-components/styles.css';

const root = document.getElementById('root') as HTMLElement;

ReactDOM
  .createRoot(root)
  .render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  );
tsx

import { ZrIcon } from '@zurich/web-components/react';

export default function App () {
  return (
    <>
      <ZrIcon icon="close:line" /> 
    </>
  );
}
Using WebComponents directly

There's also a possibility for directly using the WebComponents. This requires some extra config for TypeScript:

tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';

import '@zurich/web-components/styles.css';

const root = document.getElementById('root') as HTMLElement;

ReactDOM
  .createRoot(root)
  .render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  );
tsx

import '@zurich/web-components/icon';

export default function App () {
  return (
    <>
      <z-icon icon="close:line" />
    </>
  );
}
json
{
  "compilerOptions": {
    "types": [
      "./react.d.ts",
    ]
  },
}
ts
import type { ZDSReactComponents } from '@zurich/web-components/react-types';

declare global {
  namespace JSX {
    interface IntrinsicElements extends ZDSReactComponents {
    }
  }
}

Vue

This is the basic setup for Vue with ZvIcon as an example:

ts
import { createApp } from 'vue';
import App from './App.vue';

import '@zurich/web-components/styles.css'; 

createApp(App).mount('#app');
html
<script lang="ts" setup>
  import { ZvIcon } from '@zurich/web-components/vue';  
<script>

<template>
  <zv-icon icon="close:line" />  
</template>
Custom elements compilation

In case of using Vite and you're receiving a message saying that z-* components are not recognized, make sure to add the necessary configuration in the vite.config.ts file for Vue 3 to recognize the custom elements instead of trying to render them as part of the @vitejs/plugin-vue.

If you use the directly the Vue wrappers (@zurich/web-components/vue), this warning should not appearing case of not setting the compilerOptions.isCustomElement, but might appear if you use the WebComponents directly.

ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('z-'), 
        },
      }
    }),
  ],
});
Global installation

It can be done importing @zurich/web-components in the main.ts file.

ts
import { createApp } from 'vue';
import App from './App.vue';

import '@zurich/web-components/styles.css'; 
import { ZvIcon } from '@zurich/web-components/vue';  

createApp(App)
  // A different Name can be set, but don't recommend so to keep consistency
  .component('ZvIcon', ZvIcon) 
  .mount('#app');

For TypeScript support:

ts
// vite-env.d.ts

import type { ZDSVueComponents } from '@zurich/web-components/vue';

declare module '@vue/runtime-core' {
  interface GlobalComponents extends ZDSVueComponents { }
}
Vue 2 compatibility

Attention!

This package is compatible with Vue 2 as it uses WebComponents, but you should not use the configuration for Vue since that's done for Vue 3.

Since Vue 2 reached the end of its life on on December 31st of 2023, the ZDS decided not to provide specific support for Vue 2 even when it's used in projects inside Zurich. This decision is based on several reasons like:

  • Having more time to better support other frameworks.
  • Embracing the migration to Vue 3 and not giving extra excuses for not doing that migration in old projects.
  • Solving lots of development issues caused by having to support two versions of the same dependency.
  • Ensuring a better developer experience when the development is under our control.

The use of the v-model directive won't work, since Vue 2 uses value and @input attributes and events and even when the value attribute will work, the @input event return a CustomEvent with the payload in the detail property and not the actual payload, so the assignation in not properly done. This is how it needs to be done:

vue
<script lang="ts">
import '@zurich/web-components/text-input.js';  

export default {
  data () {
    return {
      str: 'My value',
    };
  },
};
</script>

<template>
  <z-text-input
    :model="str"
    @change="(e) => { str = e.detail }"
  >
  </z-text-input>
</template>

Another option is to create your own wrappers. For that you can use this approach:

Attention!

Do not use the same name for the tag to avoid recursive calls! We recommend to change the prefix from z- to vue-.

Also, make sure to set the compilerOptions.isCustomElement to ignore tags stating with z- as explained in the Vue 3 section

vue
<script lang="ts">
import { VueTextInput } from './VueTextInput';  

export default {
  components: [ VueTextInput ],  
  data () {
    return {
      str: 'My value',
    };
  },
};
</script>

<template>
  <zv-text-input v-model="str" />  
</template>
ts

import { defineComponent, type PropType } from 'vue';
import '@zurich/web-components/text-input.js';

import type { ZTextInputEvents, ZTextInputProps } from '@zurich/dev-utils/code';
import type { Vue2Props } from './_shared';

/** ## `<VueTextInput />` */
export const VueTextInput = defineComponent({
  props: {
    /** ... */
    model: {
      type: String as PropType<ZTextInputProps['model']>,
      required: true,
    },
    /** ... */
    label: {
      type: String as PropType<ZTextInputProps['label']>,
      required: true,
    },
    /** ... */
    config: {
      type: String as PropType<ZTextInputProps['config']>
    },
    /** ... */
    name: {
      type: String as PropType<ZTextInputProps['name']>
    },
    /** ... */
    disabled: {
      type: Boolean as PropType<ZTextInputProps['disabled']>,
    },
    /** ... */
    required: {
      type: Boolean as PropType<ZTextInputProps['required']>
    },
    /** ... */
    'max-length': {
      type: Number as PropType<ZTextInputProps['max-length']>
    },
    /** ... */
    'data-list': {
      type: Array as PropType<ZTextInputProps['dataList']>
    },
  } satisfies Vue2Props<ZTextInputProps>,
  emits: {
    input: (e: ZTextInputProps['model']) => { },
  },
  render: function (h) {
    return h('z-text-input', {
      attrs: {
        ...this.$props,
      },
      on: {
        change: (e) => {
          e.stopPropagation();
        },
        input: (e) => {
          e.stopPropagation();
          this.$emit('input', e.detail);
        },
      } satisfies Required<ZTextInputEvents>
    });
  }
});
ts

import type { PropType } from 'vue';

type Vue2Prop<T, K extends keyof T> = 
  { type: PropType<T[K]>; } 
  & (undefined extends T[K] ? {} : { required: true; });

export type Vue2Props<T> =
  {
    [K in keyof Required<T> as K extends 'z-color' ? never : K]: Vue2Prop<T, K>
  }
  & {
    [K in keyof Required<T> as K extends 'z-color' ? 'zColor' : never]: Vue2Prop<T, K>
  };

Next.js

Since Next.js combines the Node runtime with browser components, it requires an specific compilation mode to make it 100% compatible with the approach: commonJS.

The component in this case is split into two parts:

  • React wrapper: the one build as CJS and that is interacted with by the Next.js. It's the faceplate that will be used in the code, being a contract for when it's running in the client. It's part of the bundle or optimized by Next.js as plain HTML to be delivered.

  • WebComponent: the actual code and styles of the component, with is directly loaded in the client via CDN and starts to be fetched at the very beginning of the page load for better performance independently of the content. This is done via Next's <Script> component and the URL of the specific component in the CDN.

This approach still requires to import styles.scss (once per page).

tsx
import Script from 'next/script';

import '@zurich/web-components/styles.css'; 

import { WC_CDN_ROOT } from '@zurich/web-components/info';
import { ZrIcon } from '@zurich/web-components/cjs/icon';

export default function Home () {

  return (
    <>
      <Script type="module" src={`${WC_CDN_ROOT}/text-input.js`} strategy='beforeInteractive' />
      <ZrIcon icon="close" />
    </>
  );
}

You can wrap everything in your own component too:

tsx
import Script from 'next/script';

import { WC_CDN_ROOT } from '@zurich/web-components/info';
import { ZrTextInput, type ZrTextInput_Props } from '@zurich/web-components/cjs/text-input';


/**  ## ZDS Text input
 * 
 * A lovely text input component 💚 */
export const ZDSTextInput = <T extends string = string> (props: ZrTextInput_Props<T>) => {
  return (
    <>
      <Script type="module" src={`${WC_CDN_ROOT}/text-input.js`} strategy='beforeInteractive' />
      <ZrTextInput {...props} />
    </>
  );
};

export default ZDSTextInput;
tsx
import { useState } from 'react';

import '@zurich/web-components/styles.css';

import { ZDSTextInput } from '@/components/TextInput';

export default function Home () {
  const [value, setValue] = useState('');

  return (
    <ZDSTextInput label="My label" model={value} onChange={setValue}/>
  );
}