Vue Directive: v-bind
v-bind connects template attributes or props to reactive data.
If the state changes, the attribute changes too.
In Vue, v-bind is one of the core pieces that turns static HTML into dynamic UI without manual DOM manipulation.
Why it matters
Without v-bind, you would end up writing imperative logic to:
- Enable/disable buttons.
- Switch classes based on state.
- Render dynamic links or images.
- Pass data from a parent component to a child component.
With v-bind, all of that becomes declarative.
You describe the relationship between state and UI, and Vue handles the updates.
Core concept
Basic form:
<a v-bind:href="url">Go to site</a>Recommended shorthand:
<a :href="url">Go to site</a>v-bind works with:
- HTML attributes (
href,src,disabled,id,title), - component props (
:user="currentUser"), - special bindings for
classandstyle, - dynamic arguments (
:[attrName]="value"), - spread-like bindings (
v-bind="attrsObject").
When to use it
Use v-bind when you need:
- Dynamic values in attributes.
- State-driven presentation (
class/style). - Reactive data passed to child components.
- Reusable components with configurable props.
When to avoid it
Avoid it when:
- The value is fixed and never changes (plain static HTML is enough).
- You are putting complex logic directly in the template (move it to
computed). - You are trying to use
v-bindas a security or validation layer (it is not).
Common mistakes
1) Forgetting that v-bind evaluates JavaScript
Incorrect:
<img :src="/images/avatar.png" />Correct:
<img src="/images/avatar.png" />
<!-- or -->
<img :src="avatarUrl" />If you use :src, Vue expects a valid JS expression.
2) Writing too much inline logic in :class or :style
Incorrect:
<button :class="isAdmin && isActive && !isLocked ? 'btn btn-primary' : isLocked ? 'btn btn-muted' : 'btn'">
Save
</button>Better: extract this into computed.
3) Mixing up an HTML attribute with a component prop
v-bind supports both, but in components you must pass the exact prop expected by the child:
<UserCard :user="user" />If the child expects profile and you pass user, the data will not match.
4) Assuming v-bind "protects" data
v-bind only reflects state in the UI.
It does not replace backend validation or authorization rules.
Practical examples
1) Basic: dynamic link (href)
<script setup>
import { ref } from "vue";
const docsUrl = ref("https://vuejs.org/");
</script>
<template>
<a :href="docsUrl" target="_blank" rel="noopener">Vue Docs</a>
</template><script>
export default {
data() {
return {
docsUrl: "https://vuejs.org/",
};
},
};
</script>
<template>
<a :href="docsUrl" target="_blank" rel="noopener">Vue Docs</a>
</template>2) Dynamic boolean attribute (disabled)
<script setup>
import { ref } from "vue";
const isSaving = ref(false);
</script>
<template>
<button :disabled="isSaving">
{{ isSaving ? "Saving..." : "Save changes" }}
</button>
</template><script>
export default {
data() {
return {
isSaving: false,
};
},
};
</script>
<template>
<button :disabled="isSaving">
{{ isSaving ? "Saving..." : "Save changes" }}
</button>
</template>3) v-bind:class with an object (recommended pattern)
<script setup>
import { ref } from "vue";
const isActive = ref(true);
const hasError = ref(false);
</script>
<template>
<p
:class="{
'text-success': isActive,
'text-danger': hasError,
'text-muted': !isActive && !hasError,
}"
>
System status
</p>
</template><script>
export default {
data() {
return {
isActive: true,
hasError: false,
};
},
};
</script>
<template>
<p
:class="{
'text-success': isActive,
'text-danger': hasError,
'text-muted': !isActive && !hasError,
}"
>
System status
</p>
</template>4) Dynamic v-bind:style with an object
<script setup>
import { ref } from "vue";
const fontSize = ref(16);
const textColor = ref("#1D5BA1");
</script>
<template>
<p :style="{ fontSize: `${fontSize}px`, color: textColor }">
Text with reactive style
</p>
</template><script>
export default {
data() {
return {
fontSize: 16,
textColor: "#1D5BA1",
};
},
};
</script>
<template>
<p :style="{ fontSize: `${fontSize}px`, color: textColor }">
Text with reactive style
</p>
</template>5) Dynamic argument: :[attrName]="value"
Useful when the attribute name itself depends on state.
<script setup>
import { ref } from "vue";
const attrName = ref("title");
const attrValue = ref("Dynamic tooltip");
</script>
<template>
<button :[attrName]="attrValue">Hover me</button>
</template><script>
export default {
data() {
return {
attrName: "title",
attrValue: "Dynamic tooltip",
};
},
};
</script>
<template>
<button :[attrName]="attrValue">Hover me</button>
</template>6) v-bind="obj" to pass multiple attributes or props
<script setup>
import { ref } from "vue";
const inputAttrs = ref({
id: "email",
type: "email",
placeholder: "you@email.com",
autocomplete: "email",
required: true,
});
</script>
<template>
<input v-bind="inputAttrs" />
</template><script>
export default {
data() {
return {
inputAttrs: {
id: "email",
type: "email",
placeholder: "you@email.com",
autocomplete: "email",
required: true,
},
};
},
};
</script>
<template>
<input v-bind="inputAttrs" />
</template>
v-bind="obj"is also common for prop forwarding in base components.
Integrated example
Simple form using v-bind for attributes, classes, and state.
<script setup>
import { computed, ref } from "vue";
const email = ref("");
const isSubmitting = ref(false);
const hasError = ref(false);
const inputAttrs = computed(() => ({
type: "email",
placeholder: "Enter your email",
autocomplete: "email",
required: true,
}));
const buttonClass = computed(() => ({
"btn-primary": !isSubmitting.value,
"btn-disabled": isSubmitting.value,
}));
function submit() {
hasError.value = email.value.trim() === "";
if (hasError.value) return;
isSubmitting.value = true;
setTimeout(() => {
isSubmitting.value = false;
}, 1000);
}
</script>
<template>
<form @submit.prevent="submit">
<input v-bind="inputAttrs" v-model="email" :class="{ 'input-error': hasError }" />
<button :disabled="isSubmitting" :class="buttonClass">
{{ isSubmitting ? "Submitting..." : "Submit" }}
</button>
<p :style="{ color: hasError ? '#E74C3C' : '#2ECC71' }">
{{ hasError ? "Email is required." : "Ready to submit." }}
</p>
</form>
</template><script>
export default {
data() {
return {
email: "",
isSubmitting: false,
hasError: false,
};
},
computed: {
inputAttrs() {
return {
type: "email",
placeholder: "Enter your email",
autocomplete: "email",
required: true,
};
},
buttonClass() {
return {
"btn-primary": !this.isSubmitting,
"btn-disabled": this.isSubmitting,
};
},
},
methods: {
submit() {
this.hasError = this.email.trim() === "";
if (this.hasError) return;
this.isSubmitting = true;
setTimeout(() => {
this.isSubmitting = false;
}, 1000);
},
},
};
</script>
<template>
<form @submit.prevent="submit">
<input v-bind="inputAttrs" v-model="email" :class="{ 'input-error': hasError }" />
<button :disabled="isSubmitting" :class="buttonClass">
{{ isSubmitting ? "Submitting..." : "Submit" }}
</button>
<p :style="{ color: hasError ? '#E74C3C' : '#2ECC71' }">
{{ hasError ? "Email is required." : "Ready to submit." }}
</p>
</form>
</template>Summary
v-bind is the declarative way to connect state and attributes in Vue.
Mastering it helps you build dynamic interfaces that stay clean and maintainable.
Key takeaways:
:is shorthand forv-bind.- Use it for attributes, props,
class, andstyle. - Avoid complex inline logic: move rules into
computed. v-bind="obj"is ideal for grouping attributes/props.
If you understand v-bind well, you understand a core part of how Vue turns reactivity into real UI.
