v-fit-children
v2.0.0
Published
Vue directive that auto-hides children that don't fit within a container's width. Useful for chips, badges, tags, and other inline elements in tight spaces.
Maintainers
Readme
v-fit-children
Auto-hide overflowing children, emit the hidden ones for "+N more" badges
Note: Watch the usage example video to see the directive in action (temporary link).
A Vue 3 directive that automatically hides child elements that don't fit within a container's width. Ideal for chips, badges, tags, or any inline elements in a tight space.
Features
- Hides children that overflow the container width
- Emits a custom event with hidden children count, references, and optional data mapping (for "+N more" indicators)
- Supports
gap/column-gapin parent container - Accounts for margins, padding, and borders on both container and children
- Accounts for content overflow (
overflow: visible) viascrollWidth - Pin specific children so they are never hidden (
keepVisibleElordata-v-fit-keep) - Pass your
v-forarray viadatato receive typedhiddenDataandhiddenIndices - Responds to container resizes via
ResizeObserver - Monitors individual child size changes via
ResizeObserver - Detects child additions/removals via
MutationObserver - Uses a ghost DOM +
IntersectionObserverfor accurate overflow detection - Batches recalculations with
requestAnimationFramefor performance - Written in TypeScript — ships with full type declarations
Install
npm install v-fit-childrenVue 3 is a peer dependency — it won't be bundled.
Register the directive
Local (per-component) — recommended:
Import the directive in any <script setup> component. Vue auto-registers it because the variable name starts with v:
<script setup lang="ts">
import { vFitChildren } from "v-fit-children";
</script>Global (app-wide):
Register once in your entry file so every component can use v-fit-children without importing:
import { createApp } from "vue";
import { vFitChildren } from "v-fit-children";
import App from "./App.vue";
const app = createApp(App);
app.directive("fit-children", vFitChildren);
app.mount("#app");Quick start
<script setup lang="ts">
import { ref } from "vue";
import { vFitChildren } from "v-fit-children";
const containerRef = ref<HTMLElement>();
const hiddenCount = ref(0);
function onUpdate(e: CustomEvent) {
hiddenCount.value = e.detail.hiddenChildrenCount;
}
</script>
<template>
<div ref="containerRef">
<div
v-fit-children="{
widthRestrictingContainer: containerRef, // Optional: defaults to this element
offsetNeededInPx: 50, // Optional: defaults to 50
}"
@fit-children-updated="onUpdate"
>
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
</div>
<span v-if="hiddenCount">+{{ hiddenCount }} more</span>
</div>
</template>The directive element and the width-restricting container can be the same element or different elements. When they differ, the directive element's own margin, border, and padding are subtracted from the available space.
Options
All options are passed as the directive value:
<div v-fit-children="{ widthRestrictingContainer: containerRef, offsetNeededInPx: 80 }">| Option | Type | Default | Description |
|---|---|---|---|
| widthRestrictingContainer | HTMLElement | Directive Element | The element whose width constrains the children. Defaults to the element the directive is on. |
| offsetNeededInPx | number | 50 | Reserved space in px (e.g. for a "+N more" badge). Set to 0 if you don't need reserved space. |
| gap | number | Computed gap | Manually specify the gap between items in pixels. Useful if gap CSS is not used (e.g. inline-block margins). |
| data | unknown[] | — | The same array used in v-for. When provided, the event includes hiddenData with the corresponding data objects for hidden children. |
| keepVisibleEl | HTMLElement | — | An element (or descendant of a child) that should never be hidden. Useful for inputs or interactive elements. |
Options are reactive — changing them via the directive value triggers a recalculation.
TypeScript
The package ships with full type declarations. Exported types:
import { vFitChildren } from "v-fit-children";
import type { FitChildrenOptions, FitChildrenEventDetail } from "v-fit-children";FitChildrenOptions
interface FitChildrenOptions<T = unknown> {
data?: T[];
gap?: number;
keepVisibleEl?: HTMLElement;
offsetNeededInPx?: number;
widthRestrictingContainer?: HTMLElement;
}FitChildrenEventDetail
type FitChildrenEventDetail<T = unknown> = {
hiddenChildren: HTMLElement[];
hiddenChildrenCount: number;
hiddenData?: T[];
hiddenIndices: number[];
isOverflowing: boolean;
};Typing the event handler
Vue's @fit-children-updated handler receives a CustomEvent. You can type it like this:
interface Tag {
id: number;
label: string;
}
function onUpdate(e: CustomEvent<FitChildrenEventDetail<Tag>>) {
console.log(e.detail.hiddenChildrenCount);
console.log(e.detail.hiddenChildren); // HTMLElement[]
console.log(e.detail.hiddenIndices); // number[]
console.log(e.detail.hiddenData); // Tag[] | undefined
console.log(e.detail.isOverflowing); // boolean
}Event
The directive dispatches a fit-children-updated custom event on the directive's element whenever visibility is recalculated.
<div
v-fit-children="{ widthRestrictingContainer: containerRef }"
@fit-children-updated="onUpdate"
>The event's detail contains:
| Property | Type | Description |
|---|---|---|
| hiddenChildrenCount | number | Number of children that were hidden |
| hiddenChildren | HTMLElement[] | Direct references to the hidden DOM elements |
| hiddenIndices | number[] | DOM indices of the hidden children |
| hiddenData | unknown[] | Data objects for hidden children (only present when data option is provided) |
| isOverflowing | boolean | true if any children were hidden, false if all fit |
When all children fit (including the offset), isOverflowing is false and no offset space is reserved — the "+N" badge is unnecessary.
Keeping elements visible
You can prevent specific children from being hidden. This is useful for inputs, buttons, or any interactive element that should always remain accessible.
Option A — via directive value (keepVisibleEl):
Pass a ref to the element (or a descendant of a child) that should stay visible:
<script setup lang="ts">
import { ref } from "vue";
import { vFitChildren } from "v-fit-children";
const containerRef = ref<HTMLElement>();
const inputRef = ref<HTMLElement>();
</script>
<template>
<div ref="containerRef">
<div v-fit-children="{ widthRestrictingContainer: containerRef, keepVisibleEl: inputRef }">
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
<div class="input-wrapper">
<input ref="inputRef" />
</div>
</div>
</div>
</template>The directive walks up from keepVisibleEl to find the matching immediate child. So if inputRef points to a nested <input>, the parent child that contains it stays visible.
Option B — via data attribute (data-v-fit-keep):
Add the data-v-fit-keep attribute directly on the child element — no ref needed:
<div v-fit-children="{ widthRestrictingContainer: containerRef }">
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
<div data-v-fit-keep>
<input />
</div>
</div>Both methods can be used together. If a kept element is wider than the available space, it stays visible anyway — better to overflow than to hide an input the user is typing in.
Data mapping
Pass your v-for array via the data option to receive the corresponding data objects for hidden children in the event:
<script setup lang="ts">
import { ref } from "vue";
import { vFitChildren, type FitChildrenEventDetail } from "v-fit-children";
interface Tag {
id: number;
label: string;
color: string;
}
const tags = ref<Tag[]>([
{ id: 1, label: "Vue", color: "green" },
{ id: 2, label: "React", color: "blue" },
{ id: 3, label: "Angular", color: "red" },
{ id: 4, label: "Svelte", color: "orange" },
]);
const hiddenTags = ref<Tag[]>([]);
function onUpdate(e: CustomEvent<FitChildrenEventDetail<Tag>>) {
hiddenTags.value = e.detail.hiddenData ?? [];
}
</script>
<template>
<div
v-fit-children="{ data: tags, offsetNeededInPx: 50 }"
@fit-children-updated="onUpdate"
>
<span v-for="tag in tags" :key="tag.id">{{ tag.label }}</span>
</div>
<select v-if="hiddenTags.length">
<option v-for="tag in hiddenTags" :key="tag.id">{{ tag.label }}</option>
</select>
</template>The data array must map 1:1 with the directive's immediate children. hiddenIndices is always provided regardless of the data option, so you can also map manually if needed.
Inline "+N" badge
To keep the badge inline with the chips (instead of below), wrap both in a flex container and give the directive element flex: 1:
<template>
<div ref="containerRef" style="display: flex; align-items: center; gap: 8px;">
<div
style="flex: 1; overflow: hidden;"
v-fit-children="{ widthRestrictingContainer: containerRef, offsetNeededInPx: 0 }"
@fit-children-updated="onUpdate"
>
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
</div>
<span v-if="hiddenCount">+{{ hiddenCount }}</span>
</div>
</template>Set offsetNeededInPx: 0 since the badge lives outside the directive element.
How it works
- On mount, the directive sets up
ResizeObserveron the container (and any parent elements between the wrapper and container), plus aMutationObserverfor child list changes. - When any observer fires, a recalculation is scheduled via
requestAnimationFrame(deduplicated — only one pending at a time). - A hidden ghost element (
display: flex; overflow: hidden) is appended todocument.body. Real children are cloned into it — kept children (keepVisibleEl/data-v-fit-keep) go first withflex-shrink: 0, then the rest in DOM order. - The ghost's width is set to
containerWidth - offsetNeededInPx. If all children fit without the offset (smart fit), the full container width is used instead. - An
IntersectionObserver(root = ghost, threshold = 1.0) determines which clones are fully visible vs. clipped. - Visibility results are mapped back to the real children: visible clones → show, clipped clones → hide.
- A
fit-children-updatedevent is dispatched with hidden children, indices, optional data mapping, and overflow status. - On unmount, all observers are disconnected, the ghost is removed, hidden children are restored, and internal state is cleaned up.
Known limitations
- The directive hides children using
display: none !important. If a child has criticaldisplaystyles set inline, they will be overridden while hidden. keepVisibleElaccepts a single element. To pin multiple children, usedata-v-fit-keepon each. Kept elements are never hidden, so if multiple pinned children exceed the container width, they will overflow.
Browser support
Requires browsers that support ResizeObserver, MutationObserver, and getBoundingClientRect. All modern browsers (Chrome, Firefox, Safari, Edge) are supported.
Changelog
2.0.0
Breaking changes:
- Removed
sortBySizeoption — children are now hidden purely by overflow in DOM order - Removed
rowCountoption — the directive operates on a single row
New features:
- Added
dataoption to pass yourv-forarray and receive typedhiddenDatain the event - Added
hiddenIndicesto the event detail — always contains DOM indices of hidden children FitChildrenOptionsandFitChildrenEventDetailare now generic (<T = unknown>)
Internal:
- Rewrote overflow detection to use a ghost DOM +
IntersectionObserverinstead of manual width calculation - Accounts for content overflow (
overflow: visible) viascrollWidth - Fixed post-unmount ghost DOM leak when a
requestAnimationFramecallback was pending - Fixed
gapoption being ignored (now correctly applied to the ghost element)
1.0.1
- Shortened README subtitle
- Added directive registration guide (local and global)
- Fixed incomplete sentence in known limitations
1.0.0
- Initial release with core features: auto-hide, smart fit, gap support,
keepVisibleEl,data-v-fit-keep, ResizeObserver/MutationObserver, and RAF batching
License
MIT
