Same UI component for all frameworks
Published: 2022-05-16 13:20:52

## Premise

"I love React😍", "I'm more of a vue person myself🤷🏽", "Angular all the way🙌🏽", "Solid js does it for me🙂". "Lol, ya'll just forgot Svelte uhn😏".

Five developers stating their preferences for whatever reasons. What do they have in common? They're all javascript frameworks. 

"I love chakra ui", "Vuetify is what's available to me", "I';; go with Angular Material UI", "Imma just stick to different packages", "Well flowbite I guess".

Now that's their respective component libraries of choice.

How could they contribute to each other's development across frameworks? Well it's everyone for himself.

Now that's where [Zag JS](https://zagjs.com) comes in.

Now react developers says "I found a bug in my Popover component", the rest say "shit, let's fix that".

Which of this conversation will be more beautiful to have? Instead of a cross framework community, they're now all one community, that can work together regardless of framework.

## History

[Chakra UI](https://chakra-ui.com/) is a component library for react. It's beautiful, and just makes development easier and easy going. [Chakra UI for vue](https://vue.chakra-ui.com/) came along and the experience wasn't really nice. There was a lot of logic used in react, and it had to be copied to the vue version, then reengineered to vue syntax, cause logic that depended on react hooks can't be used seamlessly in vue.

So the creator of Chakra UI. [Sage](https://twitter.com/thesegunadebayo) wanted a way to make this seamless, and forth came. [Zag JS](https://zagjs.com/).

## Zag JS

It's a collection of framework-agnostic UI component patterns like accordion, menu, and dialog that can be used to build design systems for React, Vue and Solid.js. And it's powered my state machines.

**How does it work?** Zag Js uses state machines to manage the state and logic of the components, so they're not reliant on the framework, they don't need `useState` or `useEffect` from react. Or `createSignal` from solid JS. 
The machine for the component you need is exported from zag Js and a `useMachine` method to interpret it and bring it t life.
Then there's a connect method, that accepts the state of the interpreted machine and constructs the attributes and events needed for your dom elements.
The connect method also accepts a `normalizeProps` method which is exported from your framework adapter in zagjs.
It's job is to resolve the the irregularities across framework attributes' naming.
Now all you need to do is spread the derived props on your html elements.
Well they say, code speaks louder than words, so let's get to it.

We'll practice with a number input component. The native number input is quite resistant to styling, so we'll create one that has all it's attributes, accessibility and functions. But easy to style.

First, you install the component. 
```sh
npm install @zag-js/number-input
```
One good thing about this, is you don't need to pull in a bunch of components you might not use. Just install the one you need. In this case we're only installing number input.

Then we install the adapter for our framework
```sh
npm @zag-js/react
//or
npm @zag-js/vue
//or
npm @zag-js/solid
```
Only three frameworks are available currently, more incoming.
But the [requirements](https://zagjs.com/overview/faq#what-would-it-take-to-support-other-frameworks) for a framework to be accepted is to support props spreading on elements `<element {...props}>`.

Now let's move on. We've installed the two things we need.
First we import our machine, then we import the `useMachine` and `useSetup` methods from our framework adapter
```js
import * as numberInput from "@zag-js/number-input"
import { useMachine, useSetup } from "@zag-js/react"
```
could be 
```js
import { useMachine, useSetup } from "@zag-js/vue"
//or
import { useMachine, useSetup } from "@zag-js/solid"
```
Next we interprete the machine, which returns the state of the machine and a method to send events to the machine.
```js
const [state, send] = useMachine(numberInput.machine)
```

Next we setup the machine, this helps zagJs have access to any kinds of DOM, whether an iframe, or an electron dom.
```js
const ref = useSetup({ send, id: "1" })
```
The ref is attached to the root of your component. If you have multiple instances of the component, give them different IDs

Then we pass the machine `state` and it's `send` method to the connect method, which as I said earlier constructs the pieces to bring the elements to life.
```js
const api = numberInput.connect(state, send)
```
Now all the attributes and events needed to make up our number input are available in the `api` variable. Let's see the full code.
**React:**
```tsx
import * as numberInput from "@zag-js/number-input"
import { useMachine, useSetup } from "@zag-js/react"

export function NumberInput() {
  const [state, send] = useMachine(numberInput.machine)
  const ref = useSetup({ send, id: "1" })
  const api = numberInput.connect(state, send)

  return (
    <div ref={ref} {...api.rootProps}>
      <label {...api.labelProps}>Enter number:</label>
      <div>
        <button {...api.decrementButtonProps}>DEC</button>
        <input {...api.inputProps} />
        <button {...api.incrementButtonProps}>INC</button>
      </div>
    </div>
  )
}
```

**Vue:**

```vue
<script setup>
import * as numberInput from "@zag-js/number-input";
import { normalizeProps, useMachine, useSetup } from "@zag-js/vue";
import { computed } from "vue";

const [state, send] = useMachine(numberInput.machine);
const ref = useSetup({ send, id: "1" });
const api = computed(() =>
  numberInput.connect(state.value, send, normalizeProps)
);
</script>

<template>
  <div ref="ref" v-bind="api.rootProps">
    <label v-bind="api.labelProps">Enter number</label>
    <div>
      <button v-bind="api.decrementButtonProps">DEC</button>
      <input v-bind="api.inputProps" />
      <button v-bind="api.incrementButtonProps">INC</button>
    </div>
  </div>
</template>
```

**Solid:**

```tsx
import * as numberInput from "@zag-js/number-input"
import { normalizeProps, useMachine, useSetup } from "@zag-js/solid"
import { createMemo, createUniqueId } from "solid-js"

export function NumberInput() {
  const [state, send] = useMachine(numberInput.machine)
  const ref = useSetup({ send, id: createUniqueId() })
  const api = createMemo(() => numberInput.connect(state, send, normalizeProps))

  return (
    <div ref={ref} {...api().rootProps}>
      <label {...api().labelProps}>Enter number:</label>
      <div>
        <button {...api().decrementButtonProps}>DEC</button>
        <input {...api().inputProps} />
        <button {...api().incrementButtonProps}>INC</button>
      </div>
    </div>
  )
}
```

See how in each of these frameworks, we just get what we need from `api`. Cool right?

Now `api` also provides some other methods we might need.
```js
api.value
// => "12"

api.valueAsNumber
// => 12

api.setValue("300")
api.clearValue() // => new value: ""
api.increment() // => new value: "1"
api.decrement() // => new value: "0"

api.setToMax() // => new value: "100"
api.setToMin() // => new value: "0"
api.focus()
```

Still cool right?

Now styling. Quite same across all frameworks. Every part of the component already has `data` attributes from the props `api` just provided them, which can be used for styling.

```css
[data-part="root"]{
  
}

[data-part="input"]{
  
}

[data-part="label"]{
  
}

[data-part="spin-button"]{
  // for decrement and increment buttons
}
```

You can also style based on states of the component.

**Disabled state:**
```css
[data-part="root"][data-disabled] {
  /* disabled styles for the input */
}

[data-part="input"][data-disabled] {
  /* disabled styles for the input */
}

[data-part="label"][data-disabled] {
  /* disabled styles for the label */
}

[data-part="spin-button"][data-disabled] {
  /* disabled styles for the spin buttons */
}
```

**Invalid state:**
```css
[data-part="root"][data-invalid] {
  /* disabled styles for the input */
}

[data-part="input"][data-invalid] {
  /* invalid styles for the input */
}

[data-part="label"][data-invalid] {
  /* invalid styles for the label */
}
```

You can head over to the docs if you want to find out more: https://zagjs.com/

And of course, it's open source 🤪
Repo: https://github.com/chakra-ui/zag
Docs Repo: https://github.com/chakra-ui/zag-docs

Also feel free to contact me if you have questions.😎

Thanks for reading. Have a nice day.🚀