joffreypersia's avatar

What is the best way to deal with Inertia and Checkboxes

Hi,

I'm working on a Laravel + Vue.js + Inertia v2 project and struggling to find the right approach for form handling with non-native form elements.

My setup:

The problem: Shadcn-vue components like Checkbox don't render native HTML inputs underneath, so Inertia's <Form> component doesn't detect changes for dirty state tracking. The checkboxes work, but the form never becomes "dirty" when I toggle them.

What I've tried:

  1. Adding hidden native checkboxes alongside shadcn checkboxes (complex and messy)
  2. Using v-model on hidden inputs (doesn't work with <Form> component)

My questions:

  1. Should I use the <Form> component or fall back to useForm() for better reactivity with shadcn components?
  2. Do I need to wrap shadcn components in a custom Field component to bridge them with Inertia?
  3. What's the recommended pattern for using non-native form components with Inertia v2?

I need basic features: dirty state detection, validation errors display, and proper form reactivity.

How do you handle this in your projects?

Thanks, Joffrey

1 like
7 replies
LaryAI's avatar
Level 58

Great question! You're not alone—integrating Inertia v2's <Form> component with non-native inputs like shadcn-vue's Checkbox requires some bridging since Inertia tracks input changes via native events, which shadcn components may not emit by default.

Here's how you can handle this pattern effectively:


1. Should I use <Form> or useForm()?

Both work, but when using non-native input components, you'll have to manually keep the form state in sync. The new <Form> is convenient for "native forms," but useForm() provides more granular control and might feel more natural when working with external UI component libraries (like shadcn).

tl;dr: Either works, but with custom components, useForm() is often clearer and less "magical."


2. Wrap shadcn components in a custom Field?

Recommended Pattern:
Yes, wrap your UI component (e.g., shadcn Checkbox) in a custom Field component that:

  • syncs its value with your Inertia form state
  • emits changes to Inertia so dirty state/validation remain accurate

3. The Example Pattern

Suppose you have this:

<template>
  <Form :initial-values="{ agree: false }" @submit="submit">
    <ShadcnCheckbox
      :checked="form.values.agree"
      @update:checked="form.setFieldValue('agree', $event)"
      :error="form.errors.agree"
      label="Accept terms"
    />
    <button type="submit">Submit</button>
  </Form>
</template>

<script setup>
import { Form } from '@inertiajs/vue3'
import { ShadcnCheckbox } from 'shadcn-vue'

function submit(values) {
  // submit logic
}
</script>

But the form isn't dirty because ShadcnCheckbox doesn't emit native input/change events.

Solution:
Wrap shadcn components in a custom field that directly sets the form state:

<!-- CheckboxField.vue -->
<template>
  <ShadcnCheckbox
    :checked="checked"
    @update:checked="updateValue"
    v-bind="$attrs"
  />
  <span v-if="error" class="error">{{ error }}</span>
</template>

<script setup>
import { computed } from 'vue'
import { ShadcnCheckbox } from 'shadcn-vue'
const props = defineProps({
  modelValue: Boolean,
  error: String,
})
const emits = defineEmits(['update:modelValue'])

const checked = computed({
  get: () => props.modelValue,
  set: (v) => emits('update:modelValue', v),
})

function updateValue(value) {
  emits('update:modelValue', value)
}
</script>

Then in your form:

<template>
  <Form :initial-values="{ agree: false }" v-slot="{ values, setFieldValue, errors }">
    <CheckboxField
      v-model="values.agree"
      :error="errors.agree"
      label="Accept terms"
    />
    <button type="submit">Submit</button>
  </Form>
</template>

Or, with useForm() (example):

<script setup>
import { useForm } from '@inertiajs/vue3'
import CheckboxField from './CheckboxField.vue'

const form = useForm({
  agree: false,
})

function submit() {
  form.post('/endpoint')
}
</script>

<template>
  <form @submit.prevent="submit">
    <CheckboxField
      v-model="form.agree"
      :error="form.errors.agree"
      label="Accept terms"
    />
    <button type="submit" :disabled="form.processing">Submit</button>
  </form>
</template>

Key Points

  • Encapsulate non-native logic in a custom Field that connects your UI component to the Inertia form state (v-model or setFieldValue is key).
  • Use v-model or manually sync values and events between your Field wrapper and the Inertia form object.
  • Dirty state, validation, and form processing will now work as expected, even with custom UI components.
  • Either <Form> or useForm works. With non-native UI, useForm may offer more transparency.

Summary

  • Create Field wrappers for external UI components
  • Sync values directly to your Inertia form state
  • Propagate validation errors into your wrappers

This ensures Inertia's reactivity stays in sync and you get dirty state tracking, error display, and proper form behavior.

Let me know if you want a more concrete example!

vincent15000's avatar

When I'm using VueJS, I create my own checkboxes, radio buttons, ... and generally I hide the native input element and show my custom one.

To display the validation errors, it depends if the errors come from the backend (just retrieve the errors) or if you also validate frontend side (you can use vuelidate).

1 like
joffreypersia's avatar

So you don't use the ShadCN component ? Or you customize the ones needed like Checkbox so it has an input hidden ?

Best

1 like
vincent15000's avatar

It seems to be like the headless ui.

https://headlessui.com/

You don't have anything more to do than adding v-model to the Checkbox component, you don't need to hide any native element.

Please or to participate in this conversation.