Vue Tutorial
A step-by-step guide to using NYS Design System components in a Vue 3 application.
Installation
To start from scratch, create a new Vue Vite app:
Install the two NYSDS packages in your app folder:
- @nysds/vue for the Vue components and
- @nysds/styles for the design tokens and global CSS.
Note: Both packages are versioned together. Always install matching versions to avoid token/component mismatches.
Project Setup
After installing, import the NYSDS stylesheet once, at the top of your entry file (src/main.ts):
Without it, components render unstyled. @nysds/styles ships the design tokens and global styles. Component-level styles live in each component's shadow DOM and need no extra setup.
Optional agency theme: set <html data-theme="health"> (or admin, business, environment, local, safety, transportation). Fonts aren't bundled, so load them the way your agency normally does.
Your First Component
<script setup lang="ts">
import { NysAlert, NysButton } from "@nysds/vue";
const start = () => console.log("started");
</script>
<template>
<NysAlert type="info" heading="Welcome" />
<NysButton label="Start" @nys-click="start" />
</template>
Props, Events, and Slots
Props are set as DOM properties. Use either camelCase or kebab-case in templates: appName and app-name are the same prop. Bind numbers and booleans with : so they keep their type, not a string:
<NysPagination :total-pages="5" :current-page="1" />
<NysTextinput label="Name" required :disabled="locked" />
Events keep their full NYSDS names. Listen with @nys-change, @nys-input, and so on. The handler receives the typed event, so e.detail autocompletes:
<NysTextinput
label="First name"
@nys-input="(e) => console.log(e.detail.value)"
/>
Use the nys-* events instead of native @input or @change. They cover cases native events don't, like a combobox selection or a date picked from the calendar.
Slots work with <template #name>. The default slot renders as direct children:
<NysTextinput label="Email">
<template #description>We'll never share it.</template>
</NysTextinput>
<NysTooltip text="Tooltip text">
<NysButton label="Hover me" />
</NysTooltip>
Forms
v-model
Every form control supports v-model, bound to the property the component's form contract defines:
| Component | v-model value |
|---|---|
NysTextinput, NysTextarea, NysSelect, NysCombobox, NysDatepicker, NysRadiogroup |
string |
NysCheckboxgroup |
string[] |
NysCheckbox, NysToggle |
boolean |
<script setup lang="ts">
import { reactive } from "vue";
import {
NysButton,
NysCheckbox,
NysCheckboxgroup,
NysRadiobutton,
NysRadiogroup,
NysTextinput,
} from "@nysds/vue";
const model = reactive({
firstName: "",
agree: false,
languages: [] as string[],
contact: "",
});
function onSubmit() {
console.log(model);
}
</script>
<template>
<form @submit.prevent="onSubmit">
<NysTextinput v-model="model.firstName" label="First name" required />
<NysCheckbox v-model="model.agree" label="I agree to the terms" />
<NysCheckboxgroup v-model="model.languages" label="Languages">
<NysCheckbox label="English" value="en" />
<NysCheckbox label="Spanish" value="es" />
</NysCheckboxgroup>
<NysRadiogroup v-model="model.contact" label="Preferred contact" name="contact">
<NysRadiobutton label="Email" name="contact" value="email" />
<NysRadiobutton label="Phone" name="contact" value="phone" />
</NysRadiogroup>
<NysButton type="submit" label="Submit" />
</form>
</template>
Bind checkbox and radio groups on NysCheckboxgroup and NysRadiogroup, not on each child. The model updates on the component's nys-input and nys-change events. Add .lazy (v-model.lazy) to update on nys-change alone, as with a native input.
File input has no v-model. Listen for the change event and read the files from its detail:
<NysFileinput
label="Resume"
@nys-change="(e) => (model.resume = e.detail.files[0]?.name ?? '')"
/>
Submit and Validate
NYSDS form components are form-associated custom elements. They submit with a plain <form> like native inputs, and required, pattern, and the rest drive the component's own validation and error display.
<form @submit.prevent="onSubmit">
<NysTextinput v-model="model.firstName" label="First name" required />
<NysButton type="submit" label="Submit" />
</form>
A NysButton with type="submit" submits through form.requestSubmit(), so an invalid field blocks @submit and the component shows its own error.
To drive errors from your own validation library instead, set the component's error props directly:
<NysTextinput
v-model="model.firstName"
label="First name"
:show-error="!!errors.firstName"
:error-message="errors.firstName"
/>
Resetting forms: setting the model back to its initial values resets every bound control. The file input has no bound value, so also call reset() on the form itself:
const form = ref<HTMLFormElement | null>(null);
function reset() {
Object.assign(model, initialModel());
form.value?.reset();
}
What's next
Explore the full component library on the official NYSDS reference site.
Edit this page on GitHub (Permissions required)
Last updated: October 5, 2026