Repository: bootstrap-vue-next/bootstrap-vue-next
Pull request #2716: Tabs/Accordion, modelValue is id now and we have v-model:index.
Author: login: xvaara
Created: 2025-05-27T19:41:24Z | Merged: 2025-05-31T06:29:12Z

fixes #2497
fixes #2705


# Describe the PR

A clear and concise description of what the pull request does.

## Small replication

A small replication or video walkthrough can help demonstrate the changes made. This is optional, but can help observe the intended changes. A mentioned issue that contains a replication also works.

## PR checklist

<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix :bug: - `fix(...)`
- [x] Feature - `feat(...)`
- [ ] ARIA accessibility - `fix(...)`
- [ ] Documentation update - `docs(...)`
- [ ] Other (please describe)

**The PR fulfills these requirements:**

- [x] Pull request title and all commits follow the [**Conventional Commits**](https://www.conventionalcommits.org/) convention or has an [**override**](https://github.com/googleapis/release-please#how-can-i-fix-release-notes) in this pull request body **This is very important, as the `CHANGELOG` is generated from these messages, and determines the next version type. Pull requests that do not follow conventional commits or do not have an override will be denied**


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Accordion components now support selecting multiple items at once, using either item index or id.
  - Tabs components introduce more flexible control with new props for active tab by index or id.
  - Tabs expose methods to programmatically activate or deactivate tabs.

- **Enhancements**
  - Accordion and Tabs APIs have updated props and events for improved two-way binding and synchronization.
  - Tabs now support unmounting inactive tab content for better performance via the new `unmountLazy` prop.
  - Improved synchronization and event handling in Tabs, including cancelable activation events with detailed parameters.
  - Accordion items register and unregister dynamically to maintain accurate state and order.
  - Updated demos to use explicit `v-model:index` bindings for clearer state management.
  - Improved accessibility and usability in tab and accordion demos.

- **Bug Fixes**
  - Improved synchronization between selected indices and ids in Tabs and Accordion components.

- **Documentation**
  - Updated documentation to reflect new event signatures and prop changes for Tabs and Accordion components.

- **Tests**
  - Added and updated tests to cover new multi-select and synchronization behaviors in Accordion and Tabs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #2497: BAccordion issue when using v-model 
### Describe the bug

Hi,

When using BAccordion with v-model, the last opened accordion item always gets selected on the model, even after closing the relevant accordion.

In my opinion, when closing an accordion item, the model should be updated to reflect that the accordion item is not active.

So, if we open an accordion item, the model gets updated to the correct accordionitem id, but when we close it (so that all accordion items are closed and none is visible) the model variable still states that the last item that was opened is active.

The reproduction link should help visualize this a bit better.

Cheers




### Reproduction

https://stackblitz.com/edit/nuxt-starter-evsrdkcg?file=pages%2Findex.vue

### Used Package Manager

yarn
Linked issue comments: user: login: bolt-new-by-stackblitz[bot]
created_at: 2025-01-07T13:19:01Z
body: <a href='https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2497?repros=nuxt-starter-evsrdkcg'><img src='https://developer.stackblitz.com/img/fix_this_issue_small.svg' alt='Fix this issue in StackBlitz Codeflow' align='left' width='117' height='20'></a> _Start a new pull request in [StackBlitz Codeflow](https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2497?repros=nuxt-starter-evsrdkcg)._

- [**nuxt-starter-evsrdkcg** (Open reproduction)](https://stackblitz.com/edit/nuxt-starter-evsrdkcg?issueRepo=bootstrap-vue-next/bootstrap-vue-next&issueNumber=2497)

author_association: NONE
user: login: VividLemon
created_at: 2025-01-08T21:01:01Z
body: Weirdly it's a bit complicated to fix as if the component coordinates with the parent about them being closed, it causes odd behavior when another component tells the parent to open, and then the other component tells itself to close, causing a race condition of "am I closed or open"? The only way I can tell to fix it is by eliminating the v-model on BAccordionItem, which, likely doesn't have much impact because, why are you using a v-model on BAccordionItem and not BAccordion
author_association: MEMBER
user: login: ptdev
created_at: 2025-01-08T21:49:26Z
body: Hey,

I don't think I managed to explain it correctly. I don't have a v-model on BAccordionItem.

This is the setup:

```vue
<script setup lang="ts">
const accordion = ref()
</script>

<template>
    <BAccordion v-model="accordion">

        <BAccordionItem title="Accordion Item #1" id="accordion1">
            <strong>This is the first item's accordion body.</strong>
        </BAccordionItem>

        <BAccordionItem title="Accordion Item #2" id="accordion2">
            <strong>This is the second item's accordion body.</strong>
        </BAccordionItem>

        <BAccordionItem title="Accordion Item #3" id="accordion3">
            <strong>This is the third item's accordion body.</strong> 
        </BAccordionItem>

    </BAccordion>
</template>
```

The v-model is on BAccordion.  
The documentation says that the model gets updated with the id of the BAaccordionItem that is open.

The issue is that initially, the accordion ref is empty so all accordion items are closed. 
Once we open an item, the ref gets updated with the id of the opened item.
But when closing it back again, the ref still maintains the id of the last opened item.

Cheers 👍 







author_association: NONE
user: login: VividLemon
created_at: 2025-01-08T22:01:46Z
body: No, its explained well enough. Just in trying to fix it causes other issues, which is kind of odd imo. 
author_association: MEMBER
user: login: ptdev
created_at: 2025-01-08T22:22:49Z
body: Ah, I see 😕 

It's not ideal as we can't just use the model reference to find out which item is showing.

This is the workaround we're using in case anybody else needs it.

```vue
<script setup lang="ts">
const accordion = ref()

function clearRef(e) {
    if(accordion.value === e.target.id) {
        accordion.value = null
    }
}
</script>

<template>
    <BAccordion v-model="accordion">

        <BAccordionItem title="Accordion Item #1" id="accordion1" @hide="clearRef">
            <strong>This is the first item's accordion body.</strong>
        </BAccordionItem>

        <BAccordionItem title="Accordion Item #2" id="accordion2" @hide="clearRef">
            <strong>This is the second item's accordion body.</strong>
        </BAccordionItem>

        <BAccordionItem title="Accordion Item #3" id="accordion3" @hide="clearRef">
            <strong>This is the third item's accordion body.</strong> 
        </BAccordionItem>

    </BAccordion>
</template>
```

Cheers




author_association: NONE
user: login: xvaara
created_at: 2025-01-10T15:51:07Z
body: also, in tabs v-model is index number of the tab, but on accordian it's the id? Should this be configurable?

 If accordion is "free" it doesn't update the modelValue at all.
author_association: CONTRIBUTOR
user: login: xvaara
created_at: 2025-01-19T13:21:20Z
body: @VividLemon @dwgray I was thinking.. we could change this to v-model -> id v-model:index -> index of the tab/accordion. Cases for both usage is there. so this could have both of them.
author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-01-23T16:49:30Z
body: @xvaara When using provide/inject and creating a psuedo-index array (keep in mind that BAccordion doesn't know about its children), you can't be certain that the order of the elements is going to be the same of the array.

Which, you don't exactly need to. Hypothetically 

```ts
{
  0: false,
  1: true,
  2: false
}
```

This could be how the order is generated (id is only representing the order that it was set in the above):

```html
<BAccordionItem :id="0" />
<BAccordionItem :id="2" />
<BAccordionItem :id="1" />
```

In which case the order of the "id" here doesn't matter. It's only that when you expose this ref (the modelValue), the output could be confusing, one could say "my last item is open, but it's index shown by the modelValue is '1'?" 

In this case, and possibly BTabs case, it may be best to make them some sort of stable id

Template refs have a limitation `It should be noted that the ref array does not guarantee the same order as the source array.`, which provide/inject should also share -- hypothetically some children could be async, or are simply batched in awkward edge-case ways, which could lead to confusion by the developer. 
author_association: MEMBER
user: login: xvaara
created_at: 2025-01-23T17:21:26Z
body: > [@xvaara](https://github.com/xvaara) When using provide/inject and creating a psuedo-index array (keep in mind that BAccordion doesn't know about its children), you can't be certain that the order of the elements is going to be the same of the array.
> 

That's why tabs check the dom for the order...

> Which, you don't exactly need to. Hypothetically
> 
> {
>   0: false,
>   1: true,
>   2: false
> }
> This could be how the order is generated (id is only representing the order that it was set in the above):
> 
> <BAccordionItem :id="0" />
> <BAccordionItem :id="2" />
> <BAccordionItem :id="1" />
> In which case the order of the "id" here doesn't matter. It's only that when you expose this ref (the modelValue), the output could be confusing, one could say "my last item is open, but it's index shown by the modelValue is '1'?"
> 
> In this case, and possibly BTabs case, it may be best to make them some sort of stable id
> 
> Template refs have a limitation `It should be noted that the ref array does not guarantee the same order as the source array.`, which provide/inject should also share -- hypothetically some children could be async, or are simply batched in awkward edge-case ways, which could lead to confusion by the developer.

I'm not sure what you think the problem is. Why would the order matter to the id? Or are you saying some user might be confused because using numbers as ids?

And what is your point about this? We must not use indexes? or we must not use ids?

author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-01-23T17:25:38Z
body: The only issue is what does the index "mean" if it doesn't represent the order of the components

Assuming we're not sorting the order
author_association: MEMBER
user: login: xvaara
created_at: 2025-01-23T17:26:25Z
body: why wouldn't we sort the order?

https://github.com/bootstrap-vue-next/bootstrap-vue-next/blob/d20348da91b20b36421fb35b51fb91bfbd788fd5/packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue#L273-L278

author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-01-23T17:31:28Z
body: We would only need to sort the order if we used ids. Which seems like unnecessary overhead when you can simply pass ids around. We also wouldn't need to keep an order registry at all for BAccordion.

Both have merits. If we use ids we don't need to store any additional information about the structure of the children, only trust that children pass correct values.

If we use index, we need to store additional information about the order, only so that one could pass in 1,2,3,etc. Which seems to provide no additional benefit over using ids. It makes me wonder if BTabs needs a lot of the manipulation it has
author_association: MEMBER
user: login: xvaara
created_at: 2025-01-23T17:42:40Z
body: Do we need index:ses in any of these as public api? Should we just convert  tabs to use id as v-model, and maybe expose v-modal:index if someone need the "old functionality"? There might be someone using button @click="tab++" in tabs for example.

Index is needed in tabs for the "next" functionality with keynav for example. The next could be searched from dom when needed also. It also searches for the next valid tab to open, if commanded to open a disabled tab.


author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-01-23T17:49:38Z
body: I would think the vast majority of people could just not even bind a modelValue in either BTabs or BAccordion. 

If we were to force ids in BTabs, and expose a duplicate v-modal:index, the work required for converting over would be somewhat equal to that of just converting to use the id schema

But that's if we really want to do it, I have no real issue with both things staying the same.

I'm only pointing this out because I plan on dropping v-model support for the BAccordionItem (so we can properly fix this bug, the v-model items doesn't serve a purpose that v-model on the accordion would) and making the component generic so that free -> modelValue type is array, and !free -> string | null
author_association: MEMBER
user: login: ptdev
created_at: 2025-01-23T21:43:45Z
body: > I would think the vast majority of people could just not even bind a modelValue in either BTabs or BAccordion.

Hey, keeping up with this issue, but I have to comment that I disagree with the above statement.

While it may be true that most people will just use the accordion and tabs without a v-model attached, it is still very useful to have some way to know what accordion or tab item is showing.

For all kinds of reasons, dashboards, content sites, etc. It's useful if we need our app to act on the visibility of the tab/accordion on some other part of the layout.

It's ok if you do end up removing this ability (we have our workaround as posted above), but it is a downgrade and regression IMHO.

Still, thanks for the work on this either way ❤ 
author_association: NONE
user: login: xvaara
created_at: 2025-01-24T09:41:26Z
body: @ptdev we are not removing it. Just talking about what we would like it to be. So we break the stuff before 1.0 😅

@VividLemon I'm thinking we move btabs id to v-model and add v-model:index, since that might be useful?
author_association: CONTRIBUTOR
user: login: xvaara
created_at: 2025-02-11T15:01:29Z
body: @VividLemon I was thinking that the v-model:index might be useful for accordion also, you could have a "wizard" and the accordionItem could have a button with `@click="index++"` to move to the next "step". But you could also name the Items `:id="'index'+index"`` and have `@click="value = 'index' + (index + 1) ` or something like that.
author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-02-11T17:37:10Z
body: Im torn between just using indexes. And now im leaning towards just indexes
author_association: MEMBER
user: login: xvaara
created_at: 2025-02-13T23:08:24Z
body: Index can change if rendered with v-for. So why not both?
author_association: CONTRIBUTOR
user: login: VividLemon
created_at: 2025-02-18T21:21:06Z
body: > Index can change if rendered with v-for. So why not both?

One simple api to use, not multiple different ways to do the same thing
author_association: MEMBER
user: login: VividLemon
created_at: 2025-02-27T23:44:49Z
body: But the issue with indexes is that we can't really know when the structure of the items have changed. 

Say if `v-for="el in items"` `items.sort()`. Our internal index could change. And I don't think I know of a reliable reactive way to get the indexes to update in this situation @xvaara Should we just emit the ids? Idk it simplifies a lot
author_association: MEMBER
user: login: xvaara
created_at: 2025-03-07T16:25:52Z
body: @VividLemon if the tab in v-for are changed and they unregister them selfs and register in the new order.. Doesn't it already work this way? 

I don't really see what the problem here is? basically rename modelValue to index and activeId to modelValue? And copypaste those to Accordion.

Or am I missing something here?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-07T02:52:46Z
body: This issue is stale because it has been open for 30 days with no activity. It will not be auto-closed
author_association: CONTRIBUTOR

Diff:
diff --git a/apps/docs/src/data/components/accordion.data.ts b/apps/docs/src/data/components/accordion.data.ts
index 54e497399..dc4898280 100644
--- a/apps/docs/src/data/components/accordion.data.ts
+++ b/apps/docs/src/data/components/accordion.data.ts
@@ -20,6 +20,16 @@ export default {
             default: false,
             description: 'Accordion items will stay open when another item is opened',
           },
+          index: {
+            type: 'number | number[]',
+            default: undefined,
+            description: 'Index of the accordion item that is open',
+          },
+          modelValue: {
+            type: 'string | string[]',
+            default: undefined,
+            description: 'Id of the accordion item that is open',
+          },
           // transProps: {
           //   type: 'TransitionProps',
           //   default: undefined,
@@ -35,7 +45,7 @@ export default {
           //   default: false,
           //   description: 'Alias for `noAnimation`',
           // },
-          ...pick(showHideProps, ['initialAnimation', 'modelValue', 'lazy', 'unmountLazy']),
+          ...pick(showHideProps, ['initialAnimation', 'lazy', 'unmountLazy']),
           ...pick(
             buildCommonProps({
               id: {
@@ -55,7 +65,18 @@ export default {
             {
               arg: 'value',
               description: 'Id of the accordion item that is open',
-              type: 'string',
+              type: 'string | string[]',
+            },
+          ],
+        },
+        {
+          event: 'update:index',
+          description: 'Update the index of the accordion item that is open',
+          args: [
+            {
+              arg: 'value',
+              description: 'Index of the accordion item that is open',
+              type: 'number | number[]',
             },
           ],
         },
diff --git a/apps/docs/src/data/components/tabs.data.ts b/apps/docs/src/data/components/tabs.data.ts
index dfe51a4a7..36c9e089e 100644
--- a/apps/docs/src/data/components/tabs.data.ts
+++ b/apps/docs/src/data/components/tabs.data.ts
@@ -16,11 +16,11 @@ export default {
             description:
               "Use a specific ID for this tab's tab control button. If not provided, one will automatically be generated",
           },
-          lazyOnce: {
+          unmountLazy: {
             type: 'boolean',
             default: undefined,
             description:
-              'When set and lazy is also set, the content will only be rendered once and then cached. Note: This will likely be deprecated in favor of unmount-lazy',
+              'When set and lazy is also set, the content will be unmounted when the tab is not active',
           },
           noBody: {
             type: 'boolean',
@@ -65,10 +65,11 @@ export default {
       sourcePath: '/BTabs/BTabs.vue',
       props: {
         '': {
-          activeId: {
-            type: 'string',
-            default: undefined,
-            description: 'The id of the currently active tab',
+          index: {
+            type: 'number',
+            default: -1,
+            description:
+              'The index (zero-based) of the currently active tab. Id (modelValue) takes priority over index.',
           },
           activeNavItemClass: {
             type: 'ClassValue',
@@ -131,9 +132,9 @@ export default {
             description: 'Lazily render the tab contents when shown',
           },
           modelValue: {
-            type: 'number',
-            default: -1,
-            description: 'Currently visible tab index (zero-based)',
+            type: 'string',
+            default: undefined,
+            description: 'The id of the currently active tab',
           },
           navClass: {
             type: 'ClassValue',
@@ -199,35 +200,51 @@ export default {
           args: [
             {
               arg: 'value',
-              type: 'number',
-              description: 'Tab being activated (0-based index)',
+              type: 'string',
+              description: 'The id of the currently active tab',
             },
+          ],
+          description: 'The active tab has changed. The id of the currently active tab',
+        },
+        {
+          event: 'update:index',
+          args: [
             {
-              arg: 'v2',
+              arg: 'value',
               type: 'number',
-              description:
-                'Tab that is currently active (0-based index). Will be -1 if no current active tab',
+              description: 'The index (zero-based) of the currently active tab',
             },
           ],
-          description: 'The active tab has changed',
+          description:
+            'The active tab has changed. The index (zero-based) of the currently active tab',
         },
         {
           description: 'Emitted just before a tab is shown/activated. Cancelable',
           event: 'activate-tab',
           args: [
             {
-              arg: 'v1',
+              arg: 'newTabId',
+              type: 'string',
+              description: 'Tab being activated (id)',
+            },
+            {
+              arg: 'prevTabId',
+              type: 'string',
+              description: 'Current active tab (id)',
+            },
+            {
+              arg: 'newTabIndex',
               type: 'number',
               description: 'Tab being activated (0-based index)',
             },
             {
-              arg: 'v2',
+              arg: 'prevTabIndex',
               type: 'number',
               description:
                 'Tab that is currently active (0-based index). Will be -1 if no current active tab',
             },
             {
-              arg: 'v3',
+              arg: 'event',
               type: 'BvEvent',
               description: 'BvEvent object. Call bvEvent.preventDefault() to cancel',
             },
diff --git a/apps/docs/src/docs/components/demo/TabsCustomLinks.vue b/apps/docs/src/docs/components/demo/TabsCustomLinks.vue
index 1727c9157..2761dee41 100644
--- a/apps/docs/src/docs/components/demo/TabsCustomLinks.vue
+++ b/apps/docs/src/docs/components/demo/TabsCustomLinks.vue
@@ -1,6 +1,6 @@
 <template>
   <BCard no-body>
-    <BTabs v-model="tabIndex" card>
+    <BTabs v-model:index="tabIndex" card>
       <BTab title="Tab 1" :title-link-class="linkClass[0]">Tab contents 1</BTab>
       <BTab title="Tab 2" :title-link-class="linkClass[1]">Tab contents 2</BTab>
       <BTab title="Tab 3" :title-link-class="linkClass[2]">Tab contents 3</BTab>
diff --git a/apps/docs/src/docs/components/demo/TabsDynamic.vue b/apps/docs/src/docs/components/demo/TabsDynamic.vue
index a73b39d3d..47dcb9708 100644
--- a/apps/docs/src/docs/components/demo/TabsDynamic.vue
+++ b/apps/docs/src/docs/components/demo/TabsDynamic.vue
@@ -4,15 +4,31 @@
       <BTabs card>
         <!-- Render Tabs, supply a unique `key` to each tab -->
         <BTab v-for="i in tabs" :key="'dyn-tab-' + i" :title="'Tab ' + i">
+          <template #title>
+            {{ 'Tab ' + i }}
+            <BButton
+              style="margin-right: -1rem"
+              variant="link"
+              class="link-secondary link-underline link-underline-opacity-0"
+              size="sm"
+              @click.prevent="closeTab(i)"
+              >&times;</BButton
+            >
+          </template>
           Tab contents {{ i }}
-          <BButton size="sm" variant="danger" class="float-right" @click="closeTab(i)">
-            Close tab
-          </BButton>
         </BTab>
 
         <!-- New Tab Button (Using tabs-end slot) -->
         <template #tabs-end>
-          <BNavItem role="presentation" @click.prevent="newTab"><b>+</b></BNavItem>
+          <li class="nav-item ms-auto mt-0 mb-2">
+            <BButton
+              variant="outline-secondary"
+              class="no-underline"
+              role="presentation"
+              @click.prevent="newTab"
+              ><b>+</b></BButton
+            >
+          </li>
         </template>
 
         <!-- Render this if no tabs -->
diff --git a/apps/docs/src/docs/components/demo/TabsExternalControls.vue b/apps/docs/src/docs/components/demo/TabsExternalControls.vue
index 04b7f2e03..c28c454be 100644
--- a/apps/docs/src/docs/components/demo/TabsExternalControls.vue
+++ b/apps/docs/src/docs/components/demo/TabsExternalControls.vue
@@ -2,7 +2,7 @@
   <div>
     <!-- Tabs with card integration -->
     <BCard no-body>
-      <BTabs v-model="tabIndex" small card>
+      <BTabs v-model:index="tabIndex" small card>
         <BTab title="General">I'm the first fading tab</BTab>
         <BTab title="Edit profile">
           I'm the second tab
diff --git a/apps/docs/src/docs/components/tabs.md b/apps/docs/src/docs/components/tabs.md
index e1fbd9c92..a84f6c3f8 100644
--- a/apps/docs/src/docs/components/tabs.md
+++ b/apps/docs/src/docs/components/tabs.md
@@ -236,11 +236,13 @@ order to use these methods.
 To prevent a tab from activating, simply set the `disabled` prop on the `<BTab>` component.
 
 Alternatively, you can listen for the `activate-tab` event, which provides an option to prevent the
-tab from activating. The `activate-tab` event is emitted with three arguments:
+tab from activating. The `activate-tab` event is emitted with five arguments:
 
+- `newTabId`: The id of the tab that is going to be activated
+- `prevTabId`: The id of the currently active tab
 - `newTabIndex`: The index of the tab that is going to be activated
 - `prevTabIndex`: The index of the currently active tab
-- `bvEvent`: The `BvEvent` object. Call `bvEvent.preventDefault()` to prevent `newTabIndex` from
+- `event`: The `BvEvent` object. Call `bvEvent.preventDefault()` to prevent `newTabIndex` from
   being activated
 
 For accessibility reasons, when using the `activate-tab` event to prevent a tab from activating, you
diff --git a/apps/playground/src/components/Comps/TTabs.vue b/apps/playground/src/components/Comps/TTabs.vue
index e9c8b46f4..49a62658d 100644
--- a/apps/playground/src/components/Comps/TTabs.vue
+++ b/apps/playground/src/components/Comps/TTabs.vue
@@ -2,7 +2,7 @@
   <BContainer fluid>
     <BRow>
       <BCol>
-        <BTabs v-model="tabIndex" small>
+        <BTabs v-model:index="tabIndex" small>
           <BTab title="First" disabled>
             <p>I'm the first tab</p>
           </BTab>
@@ -89,7 +89,7 @@
     </BRow>
     <BRow>
       <BCol>
-        <BTabs v-model:active-id="activeId" v-model="tabIndex2">
+        <BTabs v-model:active-id="activeId" v-model:index="tabIndex2">
           <BTab id="a1" title="a1">a1</BTab>
           <BTab id="a2" title="a2">a2</BTab>
           <BTab id="a3" title="a3">a3</BTab>
@@ -123,7 +123,7 @@
         </BTabs>
       </BCol>
       <BCol>
-        <BTabs v-model:active-id="tab3Id" v-model="tab3Index">
+        <BTabs v-model:active-id="tab3Id" v-model:index="tab3Index">
           <BTab v-for="t in tab3" :key="t.title" :title="t.title">{{ t.content }}</BTab>
         </BTabs>
         <BButton
diff --git a/packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue b/packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue
index bc296ac0d..54fd0926f 100644
--- a/packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue
+++ b/packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue
@@ -5,13 +5,27 @@
 </template>
 
 <script setup lang="ts">
-import {computed, provide, readonly, toRef} from 'vue'
+import {
+  computed,
+  nextTick,
+  provide,
+  ref,
+  type ShallowRef,
+  shallowRef,
+  toRef,
+  unref,
+  type VNode,
+  watch,
+} from 'vue'
 import {accordionInjectionKey} from '../../utils/keys'
 import {useId} from '../../composables/useId'
 import {useDefaults} from '../../composables/useDefaults'
 import type {BAccordionProps} from '../../types/ComponentProps'
+import {flattenFragments} from '../../utils/flattenFragments'
+import BAccordionItem from './BAccordionItem.vue'
+import {sortSlotElementsByPosition} from '../../utils/dom'
 
-const _props = withDefaults(defineProps<Omit<BAccordionProps, 'modelValue'>>(), {
+const _props = withDefaults(defineProps<Omit<BAccordionProps, 'modelValue' | 'index'>>(), {
   flush: false,
   free: false,
   initialAnimation: false,
@@ -20,7 +34,7 @@ const _props = withDefaults(defineProps<Omit<BAccordionProps, 'modelValue'>>(),
   unmountLazy: false,
 })
 
-defineSlots<{
+const slots = defineSlots<{
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   default?: (props: Record<string, never>) => any
 }>()
@@ -31,20 +45,229 @@ const modelValue = defineModel<BAccordionProps['modelValue']>({
   default: undefined,
 })
 
+const index = defineModel<BAccordionProps['index']>('index', {
+  default: undefined,
+})
+
+const itemElementsArray = ref<VNode[]>([])
+
+const updateItemElementsArray = () => {
+  const itemElements = flattenFragments(slots.default?.({}) ?? [])
+  itemElementsArray.value = (Array.isArray(itemElements) ? itemElements : [itemElements]).filter(
+    (item) => item.type === BAccordionItem
+  )
+}
+updateItemElementsArray()
+
+watch(
+  () => slots.default?.({}),
+  () => {
+    updateItemElementsArray()
+    nextTick(() => {
+      sortAccordionItems()
+    })
+  }
+)
+
+interface AccordionItem {
+  id: string
+  el: Readonly<ShallowRef<HTMLElement | null>>
+}
+
+const accordionItems = shallowRef<AccordionItem[]>([])
+
+const sortAccordionItems = () => {
+  accordionItems.value = accordionItems.value.sort((a, b) =>
+    sortSlotElementsByPosition(unref(a.el), unref(b.el))
+  )
+  if (modelValue.value) {
+    if (Array.isArray(modelValue.value)) {
+      const next = modelValue.value
+        .map((id) => accordionItems.value.findIndex((item) => item.id === id))
+        .filter((i) => i !== -1)
+
+      if (next.length !== modelValue.value.length) {
+        if (import.meta.env.DEV) {
+          console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
+        }
+      }
+      index.value = next
+    } else {
+      const idx = accordionItems.value.findIndex((item) => item.id === modelValue.value)
+      if (idx !== -1) index.value = idx
+    }
+  } else if (index.value !== undefined) {
+    modelValue.value = Array.isArray(index.value)
+      ? index.value.map((idx) => accordionItems.value[idx]?.id)
+      : accordionItems.value[index.value]?.id
+  }
+}
+
 const computedId = useId(() => props.id, 'accordion')
 
 const computedClasses = computed(() => ({
   'accordion-flush': props.flush,
 }))
 
+const areEqual = (a: unknown, b: unknown) => {
+  if (Array.isArray(a) && Array.isArray(b)) {
+    return a.length === b.length && a.every((item, index) => item === b[index])
+  }
+  return a === b
+}
+
+watch(
+  () => index.value,
+  (newValue, oldValue) => {
+    if (areEqual(newValue, oldValue)) return
+    if (index.value === undefined) {
+      modelValue.value = undefined
+      return
+    }
+
+    if (!props.free) {
+      const idx = !Array.isArray(index.value) ? index.value : index.value?.[0]
+      if (accordionItems.value[idx]?.id) {
+        if (modelValue.value !== accordionItems.value[idx]?.id) {
+          modelValue.value = accordionItems.value[idx]?.id
+        }
+      } else {
+        nextTick(() => {
+          index.value = undefined
+        })
+      }
+    } else {
+      // free mode
+      if (Array.isArray(index.value)) {
+        const newValue = index.value.map((item) => accordionItems.value[item]?.id)
+        if (!areEqual(newValue, modelValue.value)) {
+          modelValue.value = newValue
+        }
+      } else {
+        const newValue = accordionItems.value[index.value]?.id
+        if (newValue === undefined) {
+          nextTick(() => {
+            index.value = undefined
+          })
+          return
+        }
+        if (!areEqual(newValue, modelValue.value)) {
+          modelValue.value = newValue
+        }
+      }
+    }
+  }
+)
+
+watch(
+  () => modelValue.value,
+  (newValue, oldValue) => {
+    if (areEqual(newValue, oldValue)) return
+    if (modelValue.value === undefined) {
+      index.value = undefined
+      return
+    }
+
+    if (!props.free) {
+      const idx = !Array.isArray(modelValue.value)
+        ? accordionItems.value.findIndex((item) => item.id === modelValue.value)
+        : accordionItems.value.findIndex((item) => item.id === modelValue.value?.[0])
+      if (idx !== -1) {
+        if (index.value !== idx) {
+          index.value = idx
+        }
+      } else {
+        nextTick(() => {
+          modelValue.value = undefined
+        })
+      }
+    } else if (props.free) {
+      if (Array.isArray(modelValue.value)) {
+        const idxes = modelValue.value
+          .map((value) => accordionItems.value.findIndex((item) => item.id === value))
+          .filter((index) => index !== -1)
+        if (idxes.length > 0) {
+          if (!areEqual(idxes, index.value)) {
+            index.value = idxes
+          }
+        } else {
+          nextTick(() => {
+            modelValue.value = undefined
+          })
+        }
+      } else {
+        const newValue = accordionItems.value.findIndex((item) => item.id === modelValue.value)
+        if (!areEqual(newValue, index.value)) {
+          index.value = newValue
+        }
+      }
+    }
+  },
+  {deep: true}
+)
+
+watch(
+  () => props.free,
+  (free) => {
+    if (modelValue.value) {
+      if (!free && Array.isArray(modelValue.value)) {
+        modelValue.value = modelValue.value[0]
+      } else if (free && !Array.isArray(modelValue.value)) {
+        modelValue.value = [modelValue.value]
+      }
+    }
+  }
+)
+
 provide(accordionInjectionKey, {
-  openItem: readonly(modelValue),
+  openItem: toRef(() => modelValue.value),
   free: toRef(() => props.free),
   initialAnimation: toRef(() => props.initialAnimation),
   lazy: toRef(() => props.lazy),
   unmountLazy: toRef(() => props.unmountLazy),
   setOpenItem: (id: string) => {
-    modelValue.value = id
+    if (props.free && !Array.isArray(modelValue.value)) {
+      if (modelValue.value !== undefined) {
+        modelValue.value = [modelValue.value, id]
+      } else {
+        modelValue.value = [id]
+      }
+      return
+    }
+    if (!props.free && Array.isArray(modelValue.value)) {
+      modelValue.value = id
+      return
+    }
+    if (Array.isArray(modelValue.value)) {
+      if (!modelValue.value.includes(id)) {
+        modelValue.value = [...modelValue.value, id]
+      }
+    } else {
+      modelValue.value = id
+    }
+  },
+  setCloseItem: (id: string) => {
+    if (!props.free && Array.isArray(modelValue.value)) {
+      modelValue.value = undefined
+      return
+    }
+    if (Array.isArray(modelValue.value)) {
+      const next = modelValue.value.filter((item) => item !== id)
+      modelValue.value = next.length ? next : undefined
+    } else {
+      if (modelValue.value === id) {
+        modelValue.value = undefined
+      }
+    }
+  },
+  registerAccordionItem: (id: string, el: Readonly<ShallowRef<HTMLElement | null>>) => {
+    accordionItems.value = [...accordionItems.value, {id, el}]
+    if (accordionItems.value.length === itemElementsArray.value.length) {
+      sortAccordionItems()
+    }
+  },
+  unregisterAccordionItem: (id: string) => {
+    accordionItems.value = accordionItems.value.filter((item) => item.id !== id)
   },
 })
 </script>
diff --git a/packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue b/packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue
index a4f22808e..e9ce69300 100644
--- a/packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue
+++ b/packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue
@@ -1,5 +1,10 @@
 <template>
-  <div class="accordion-item" v-bind="props.wrapperAttrs" :class="processedAttrs.wrapperClass">
+  <div
+    ref="_el"
+    class="accordion-item"
+    v-bind="props.wrapperAttrs"
+    :class="processedAttrs.wrapperClass"
+  >
     <BCollapse
       :id="computedId"
       v-model="modelValue"
@@ -52,7 +57,16 @@
 </template>
 
 <script setup lang="ts">
-import {computed, inject, nextTick, onMounted, useAttrs, watch} from 'vue'
+import {
+  computed,
+  inject,
+  nextTick,
+  onMounted,
+  onUnmounted,
+  useAttrs,
+  useTemplateRef,
+  watch,
+} from 'vue'
 import BCollapse from '../BCollapse/BCollapse.vue'
 import {accordionInjectionKey} from '../../utils/keys'
 import {useDefaults} from '../../composables/useDefaults'
@@ -109,14 +123,21 @@ const modelValue = defineModel<Exclude<BAccordionItemProps['modelValue'], undefi
 })
 
 if (modelValue.value) {
-  if (!parentData?.free.value) {
-    parentData?.setOpenItem(computedId.value)
-  }
+  parentData?.setOpenItem(computedId.value)
 } else {
   modelValue.value =
-    parentData?.openItem.value === computedId.value && !parentData?.initialAnimation.value
+    (Array.isArray(parentData?.openItem.value)
+      ? parentData?.openItem.value.includes(computedId.value)
+      : parentData?.openItem.value === computedId.value) && !parentData?.initialAnimation.value
 }
 
+const el = useTemplateRef<HTMLElement>('_el')
+parentData?.registerAccordionItem(computedId.value, el)
+
+onUnmounted(() => {
+  parentData?.unregisterAccordionItem(computedId.value)
+})
+
 onMounted(() => {
   if (!modelValue.value && parentData?.openItem.value === computedId.value) {
     nextTick(() => {
@@ -125,12 +146,21 @@ onMounted(() => {
   }
 })
 
+const openInParent = computed(() =>
+  Array.isArray(parentData?.openItem.value)
+    ? parentData?.openItem.value.includes(computedId.value)
+    : parentData?.openItem.value === computedId.value
+)
+
 watch(
   () => parentData?.openItem.value,
-  () =>
-    (modelValue.value = parentData?.openItem.value === computedId.value && !parentData?.free.value)
+  () => (modelValue.value = openInParent.value)
 )
 watch(modelValue, () => {
-  if (modelValue.value && !parentData?.free.value) parentData?.setOpenItem(computedId.value)
+  if (modelValue.value && !openInParent.value) {
+    parentData?.setOpenItem(computedId.value)
+  } else if (!modelValue.value && openInParent.value) {
+    parentData?.setCloseItem(computedId.value)
+  }
 })
 </script>
diff --git a/packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts b/packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts
index 5e535cc5c..3fb4469ac 100644
--- a/packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts
+++ b/packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts
@@ -1,6 +1,7 @@
 import {enableAutoUnmount, mount} from '@vue/test-utils'
 import {afterEach, describe, expect, it} from 'vitest'
 import BAccordion from './BAccordion.vue'
+import BAccordionItem from './BAccordionItem.vue'
 
 describe('accordion', () => {
   enableAutoUnmount(afterEach)
@@ -37,4 +38,97 @@ describe('accordion', () => {
     })
     expect(wrapper.text()).toBe('foobar')
   })
+
+  it('has free prop', () => {
+    const wrapper = mount(BAccordion, {
+      props: {free: true},
+    })
+    expect(wrapper.props('free')).toBe(true)
+  })
+
+  const complexAccordion = {
+    template: `
+    <BAccordion v-model="modelValue" v-model:index="index" :free="free">
+      <BAccordionItem id="1">1</BAccordionItem>
+      <BAccordionItem id="2">2</BAccordionItem>
+    </BAccordion>
+    `,
+    data() {
+      return {
+        index: undefined,
+        modelValue: undefined,
+      }
+    },
+    props: {
+      free: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    components: {
+      BAccordion,
+      BAccordionItem,
+    },
+  }
+
+  it('renders in complex structure', async () => {
+    const wrapper = await mount(complexAccordion)
+    expect(wrapper.findComponent({name: 'b-accordion-item'}).text()).toBe('1')
+  })
+
+  it('has modelValue updates index when set', async () => {
+    const wrapper = await mount(complexAccordion, {
+      props: {free: true},
+    })
+    expect(wrapper.vm.modelValue).toEqual(undefined)
+    await wrapper.setData({modelValue: '1'})
+    expect(wrapper.vm.index).toEqual(0)
+    await wrapper.setData({modelValue: ['1', '2']})
+    expect(wrapper.vm.index).toEqual([0, 1])
+    await wrapper.setData({modelValue: '2'})
+    expect(wrapper.vm.index).toEqual(1)
+    await wrapper.setData({modelValue: ['1', '2']})
+    expect(wrapper.vm.index).toEqual([0, 1])
+  })
+
+  it('has index updates modelValue when set', async () => {
+    const wrapper = await mount(complexAccordion, {
+      props: {free: true},
+    })
+    expect(wrapper.vm.index).toEqual(undefined)
+    await wrapper.setData({index: 0})
+    expect(wrapper.vm.modelValue).toEqual('1')
+    await wrapper.setData({index: [0, 1]})
+    expect(wrapper.vm.modelValue).toEqual(['1', '2'])
+  })
+
+  it('has modelValue and index update as array when free is set to true', async () => {
+    const wrapper = await mount(complexAccordion, {
+      props: {free: true},
+    })
+    expect(wrapper.vm.modelValue).toEqual(undefined)
+    expect(wrapper.vm.index).toEqual(undefined)
+    const buttons = wrapper.findAll('button')
+    await buttons[0].trigger('click')
+    await new Promise((resolve) => setTimeout(resolve, 30))
+
+    await wrapper.vm.$nextTick()
+    expect(wrapper.vm.modelValue).toEqual(['1'])
+    expect(wrapper.vm.index).toEqual([0])
+
+    await buttons[1].trigger('click')
+    await new Promise((resolve) => setTimeout(resolve, 30))
+    expect(wrapper.vm.modelValue).toEqual(['1', '2'])
+    expect(wrapper.vm.index).toEqual([0, 1])
+
+    await buttons[0].trigger('click')
+    await new Promise((resolve) => setTimeout(resolve, 30))
+    expect(wrapper.vm.modelValue).toEqual(['2'])
+    expect(wrapper.vm.index).toEqual([1])
+
+    await buttons[1].trigger('click')
+    await new Promise((resolve) => setTimeout(resolve, 30))
+    expect(wrapper.vm.modelValue).toEqual(undefined)
+    expect(wrapper.vm.index).toEqual(undefined)
+  })
 })
diff --git a/packages/bootstrap-vue-next/src/components/BTabs/BTab.vue b/packages/bootstrap-vue-next/src/components/BTabs/BTab.vue
index 902f710e2..1df481035 100644
--- a/packages/bootstrap-vue-next/src/components/BTabs/BTab.vue
+++ b/packages/bootstrap-vue-next/src/components/BTabs/BTab.vue
@@ -14,7 +14,7 @@
 </template>
 
 <script setup lang="ts">
-import {computed, inject, onMounted, onUnmounted, ref, useAttrs, useTemplateRef, watch} from 'vue'
+import {computed, inject, onUnmounted, ref, useAttrs, useTemplateRef, watch} from 'vue'
 import {useId} from '../../composables/useId'
 import {useDefaults} from '../../composables/useDefaults'
 import type {TabType} from '../../types/Tab'
@@ -26,7 +26,7 @@ const _props = withDefaults(defineProps<Omit<BTabProps, 'active'>>(), {
   disabled: false,
   id: undefined,
   lazy: undefined,
-  lazyOnce: undefined,
+  unmountLazy: undefined,
   noBody: false,
   tag: 'div',
   title: undefined,
@@ -54,6 +54,7 @@ const activeModel = defineModel<Exclude<BTabProps['active'], undefined>>('active
 const parentData = inject(tabsInjectionKey, null)
 const computedId = useId(() => props.id, 'tabpane')
 const computedButtonId = useId(() => props.buttonId, 'tab')
+const internalId = useId('', 'tab-internal')
 
 const lazyRenderCompleted = ref(false)
 const el = useTemplateRef<HTMLElement>('_el')
@@ -65,53 +66,58 @@ const processedAttrs = computed(() => {
   return tabAttrs
 })
 
-const tab = computed(
-  () =>
-    ({
-      id: computedId.value,
-      buttonId: computedButtonId.value,
-      disabled: props.disabled,
-      title: props.title,
-      titleComponent: slots.title,
-      titleItemClass: () => props.titleItemClass,
-      titleLinkAttrs: () => props.titleLinkAttrs,
-      titleLinkClass: () => props.titleLinkClass,
-      onClick: () => attrs.onClick,
-      el: el.value,
-    }) as TabType
-)
+function updateTab() {
+  if (!parentData) return
+  parentData.registerTab(
+    computed(
+      () =>
+        ({
+          internalId: internalId.value,
+          id: computedId.value,
+          active: activeModel.value,
+          buttonId: computedButtonId.value,
+          disabled: props.disabled,
+          title: props.title,
+          titleComponent: slots.title,
+          titleItemClass: props.titleItemClass,
+          titleLinkAttrs: props.titleLinkAttrs,
+          titleLinkClass: props.titleLinkClass,
+          onClick: attrs.onClick,
+          el,
+        }) as TabType
+    )
+  )
+}
 
 if (parentData) {
-  parentData.registerTab(tab)
+  updateTab()
   if (activeModel.value) {
-    parentData.activateTab(computedId.value)
+    parentData.activateTab(internalId.value)
   }
 }
 
-onMounted(() => {
-  if (!parentData) return
-  parentData.sortTabs()
-})
-
 onUnmounted(() => {
   if (!parentData) return
-  parentData.unregisterTab(computedId.value)
+  parentData.unregisterTab(internalId.value)
 })
 
 const isActive = computed(() => parentData?.activeId.value === computedId.value)
 const show = ref(isActive.value)
 
-const computedLazy = computed(() => !!(parentData?.lazy.value || (props.lazyOnce ?? props.lazy)))
-const computedLazyOnce = computed(() => props.lazyOnce !== undefined)
+const computedLazy = computed(() => !!(parentData?.lazy.value || props.lazy))
 
 const computedActive = computed(() => isActive.value && !props.disabled)
 const showSlot = computed(
   () =>
     computedActive.value ||
     !computedLazy.value ||
-    (computedLazy.value && computedLazyOnce.value && lazyRenderCompleted.value)
+    (computedLazy.value && !props.unmountLazy && lazyRenderCompleted.value)
 )
 
+watch(showSlot, (shown) => {
+  if (shown && !lazyRenderCompleted.value) lazyRenderCompleted.value = true
+})
+
 watch(isActive, (active) => {
   if (active) {
     activeModel.value = true
@@ -123,7 +129,12 @@ watch(isActive, (active) => {
   show.value = false
   activeModel.value = false
 })
+
 watch(activeModel, (active) => {
+  if (props.disabled) {
+    activeModel.value = false
+    return
+  }
   if (!parentData) return
   if (!active) {
     if (isActive.value) {
@@ -131,7 +142,9 @@ watch(activeModel, (active) => {
     }
     return
   }
-  parentData.activateTab(computedId.value)
+  if (!isActive.value) {
+    parentData.activateTab(internalId.value)
+  }
 })
 
 const computedClasses = computed(() => [
@@ -145,7 +158,12 @@ const computedClasses = computed(() => [
   parentData?.tabClass.value,
 ])
 
-watch(showSlot, (shown) => {
-  if (shown && !lazyRenderCompleted.value) lazyRenderCompleted.value = true
+defineExpose({
+  activate: () => {
+    activeModel.value = true
+  },
+  deactivate: () => {
+    activeModel.value = false
+  },
 })
 </script>
diff --git a/packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue b/packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
index d218277de..62451772d 100644
--- a/packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
+++ b/packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
@@ -30,20 +30,20 @@
         <slot name="tabs-start" />
         <li
           v-for="(tab, idx) in tabs"
-          :key="tab.id"
+          :key="tab.id ?? tab.internalId"
           class="nav-item"
-          :class="tab.titleItemClass?.()"
+          :class="tab.titleItemClass"
           role="presentation"
         >
           <button
             :id="tab.buttonId"
             class="nav-link"
-            :class="[tab.navItemClasses, tab.titleLinkClass?.()]"
+            :class="[tab.navItemClasses, tab.titleLinkClass]"
             role="tab"
             :aria-controls="tab.id"
             :aria-selected="tab.active"
             :tabindex="props.noKeyNav ? undefined : tab.active ? undefined : -1"
-            v-bind="tab.titleLinkAttrs?.()"
+            v-bind="tab.titleLinkAttrs"
             @keydown.left.exact="!props.vertical && keynav($event, -1)"
             @keydown.left.shift="!props.vertical && keynav($event, -999)"
             @keydown.up.exact="props.vertical && keynav($event, -1)"
@@ -56,7 +56,7 @@
             @keydown.page-down="keynav($event, 999)"
             @keydown.home="keynav($event, -999)"
             @keydown.end="keynav($event, 999)"
-            @click.stop.prevent="(e) => handleClick(e, idx)"
+            @click.stop="(e) => handleClick(e, idx)"
           >
             <component :is="tab.titleComponent" v-if="tab.titleComponent" />
             <template v-else>
@@ -72,7 +72,7 @@
 </template>
 
 <script setup lang="ts">
-import {computed, nextTick, provide, type Ref, ref, toRef, unref, watch} from 'vue'
+import {computed, nextTick, provide, ref, type Ref, toRef, unref, type VNode, watch} from 'vue'
 import {BvEvent} from '../../utils/classes'
 import {useAlignment} from '../../composables/useAlignment'
 import {createReusableTemplate} from '@vueuse/core'
@@ -84,7 +84,7 @@ import {sortSlotElementsByPosition} from '../../utils/dom'
 import {flattenFragments} from '../../utils/flattenFragments'
 import BTab from './BTab.vue'
 
-const _props = withDefaults(defineProps<Omit<BTabsProps, 'modelValue' | 'activeId'>>(), {
+const _props = withDefaults(defineProps<Omit<BTabsProps, 'modelValue' | 'activeIndex'>>(), {
   activeNavItemClass: undefined,
   activeTabClass: undefined,
   align: undefined,
@@ -113,8 +113,13 @@ const _props = withDefaults(defineProps<Omit<BTabsProps, 'modelValue' | 'activeI
 const props = useDefaults(_props, 'BTabs')
 
 const emit = defineEmits<{
-  'activate-tab': [v1: number, v2: number, v3: BvEvent]
-  'click': [] // TODO click event is never used
+  'activate-tab': [
+    newTabId: string,
+    preTabId: string,
+    newTabIndex: number,
+    prevTabIndex: number,
+    event: BvEvent,
+  ]
 }>()
 
 const slots = defineSlots<{
@@ -128,10 +133,10 @@ const slots = defineSlots<{
   'tabs-start'?: (props: Record<string, never>) => any
 }>()
 
-const modelValue = defineModel<Exclude<BTabsProps['modelValue'], undefined>>({
+const activeIndex = defineModel<Exclude<BTabsProps['index'], undefined>>('index', {
   default: -1,
 })
-const activeId = defineModel<BTabsProps['activeId']>('activeId', {
+const activeId = defineModel<BTabsProps['modelValue']>({
   default: undefined,
 })
 
@@ -139,30 +144,52 @@ const ReusableEmptyTab = createReusableTemplate()
 
 const tabsInternal = ref<Ref<TabType>[]>([])
 
-const tabs = computed(() => {
+const tabElementsArray = ref<VNode[]>([])
+
+const updateTabElementsArray = () => {
   const tabElements = flattenFragments(slots.default?.({}) ?? [])
-  const tabElementsArray = (Array.isArray(tabElements) ? tabElements : [tabElements]).filter(
+  tabElementsArray.value = (Array.isArray(tabElements) ? tabElements : [tabElements]).filter(
     (tab) => tab.type === BTab
   )
+}
+updateTabElementsArray()
+
+watch(
+  () => slots.default?.({}),
+  () => {
+    updateTabElementsArray()
+    nextTick(() => {
+      sortTabs()
+    })
+  }
+)
+
+const tabs = computed(() => {
   if (tabsInternal.value.length === 0) {
     // fail back on the slot elements, the children haven't been registered yet
-    const activeIndex = tabElementsArray.findIndex(
+    const _activeIndex = tabElementsArray.value.findIndex(
       (tab) =>
         (tab.props?.active !== undefined &&
           (tab.props.disabled === false || tab.props.disabled === undefined)) ||
         (activeId.value && tab.props?.id === activeId.value)
     )
-    return tabElementsArray.map((tab, index) => {
-      const active = activeIndex !== -1 ? index === activeIndex : index === 0
+    return tabElementsArray.value.map((tab, index) => {
+      const active =
+        _activeIndex !== -1
+          ? index === _activeIndex
+          : activeIndex.value > -1
+            ? index === activeIndex.value
+            : index === 0
       return {
         id: tab.props?.id,
+        internalId: `premount-${index}`, // temporary id for the tab
         buttonId: tab.props?.buttonId,
         disabled: tab.props?.disabled,
         title: tab.props?.title,
         titleComponent: (tab.children as {title: unknown})?.title,
-        titleItemClass: () => tab.props?.titleItemClass,
-        titleLinkAttrs: () => tab.props?.titleLinkAttrs,
-        titleLinkClass: () => tab.props?.titleLinkClass,
+        titleItemClass: tab.props?.titleItemClass,
+        titleLinkAttrs: tab.props?.titleLinkAttrs,
+        titleLinkClass: tab.props?.titleLinkClass,
         onClick: tab.props?.onClick,
         active,
         navItemClasses: [
@@ -195,6 +222,49 @@ const tabs = computed(() => {
   })
 })
 
+let initialized = false
+let updateInitialActiveIndex = false
+let updateInitialActiveId = false
+if (activeIndex.value === -1 && activeId.value) {
+  if (tabs.value.findIndex((t) => t.id === activeId.value) !== -1) {
+    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
+  } else {
+    updateInitialActiveIndex = true
+  }
+} else if (activeIndex.value > -1 && !activeId.value) {
+  if (tabs.value[activeIndex.value]?.id) {
+    activeId.value = tabs.value[activeIndex.value]?.id
+  } else {
+    updateInitialActiveId = true
+  }
+} else if (activeIndex.value === -1 && !activeId.value) {
+  activeIndex.value = 0
+  updateInitialActiveId = true
+}
+
+function updateInitialIndexAndId() {
+  // we get the computedIds after registering the tabs
+  if (updateInitialActiveIndex) {
+    const index = tabs.value.findIndex((t) => t.id === activeId.value)
+    if (index !== -1) {
+      nextTick(() => {
+        activeIndex.value = index
+        updateInitialActiveIndex = false
+      })
+    }
+  }
+  if (updateInitialActiveId) {
+    if (activeIndex.value > -1 && tabs.value[activeIndex.value]?.id) {
+      nextTick(() => {
+        activeId.value = tabs.value[activeIndex.value]?.id
+        updateInitialActiveId = false
+      })
+    }
+  }
+}
+
+updateInitialIndexAndId()
+
 const showEmpty = computed(() => !(tabs?.value && tabs.value.length > 0))
 
 const computedClasses = computed(() => ({
@@ -217,44 +287,32 @@ const navTabsClasses = computed(() => ({
   'small': props.small,
 }))
 
-const activateTab = (index: number): void => {
-  if (index !== undefined) {
-    const id = tabs.value[index]?.id
-    if (
-      index > -1 &&
-      index < tabs.value.length &&
-      !tabs.value[index].disabled &&
-      (modelValue.value < 0 || activeId.value !== id || modelValue.value !== index)
-    ) {
-      const tabEvent = new BvEvent('activate-tab', {cancelable: true})
-      emit('activate-tab', index, modelValue.value, tabEvent)
-      if (!tabEvent.defaultPrevented) {
-        if (activeId.value !== id) activeId.value = id
-        if (modelValue.value !== index) modelValue.value = index
-      }
-    }
-  }
-}
-
 const handleClick = (event: Readonly<MouseEvent>, index: number) => {
-  activateTab(index)
   if (
     index >= 0 &&
     !tabs.value[index].disabled &&
     tabs.value[index]?.onClick &&
-    typeof tabs.value[index].onClick === 'function' &&
-    typeof tabs.value[index].onClick() === 'function'
+    typeof tabs.value[index].onClick === 'function'
   ) {
-    tabs.value[index].onClick?.()(event)
+    tabs.value[index].onClick?.(event)
+    if (event.defaultPrevented) {
+      document.getElementById(tabs.value[index].buttonId)?.blur()
+      return
+    }
   }
+  activeIndex.value = index
 }
 
 const keynav = (e: Event, direction: number) => {
   if (tabs.value.length <= 0 || props.noKeyNav) return
   e.preventDefault()
   e.stopPropagation()
-  modelValue.value = nextIndex(modelValue.value + direction, direction)
-  document.getElementById(tabs.value[modelValue.value]?.buttonId)?.focus()
+  activeIndex.value = nextIndex(activeIndex.value + direction, direction)
+  nextTick(() => {
+    if (activeIndex.value >= 0) {
+      document.getElementById(tabs.value[activeIndex.value]?.buttonId)?.focus()
+    }
+  })
 }
 
 const nextIndex = (start: number, direction: number) => {
@@ -279,72 +337,118 @@ const nextIndex = (start: number, direction: number) => {
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  // Early exit if there are no tabs or all tabs are disabled
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
     return
   }
 
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
+    return
+  }
+  // Calculate the next valid index
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    // If the index is not the same as the new value, set the previous index to the old value
+    // this is to prevent the event from being emitted twice
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  // Emit the activate-tab event
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  // If the event is prevented, revert to the previous index
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    // Update the active id this will also trigger the activeId watch which will update the activeIndex
+    // this is to make sure we handle case that starts with id change.
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  // Update the active id
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  // If the new tab is the same as the current tab, do nothing
+  if (index === activeIndex.value) return
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  // If the new tab is disabled, find the next enabled tab
+  if (tabs.value[index]?.disabled) {
+    // activeIndex watcher will update the activeId to the next enabled tab
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
+  // If the new tab is not found, find the first enabled tab
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    // activeIndex watcher will update the activeId to the first enabled tab
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  // change to the next tab
+  activeIndex.value = index
 })
 
 const registerTab = (tab: Ref<TabType>) => {
-  if (!tabsInternal.value.find((t) => t.value.id === tab.value.id)) {
+  const idx = tabsInternal.value.findIndex((t) => t.value.internalId === tab.value.internalId)
+  if (idx === -1) {
     tabsInternal.value.push(tab)
+    if (initialized) {
+      nextTick(() => {
+        sortTabs()
+      })
+    }
   } else {
-    tabsInternal.value[tabsInternal.value.findIndex((t) => t.value.id === tab.value.id)] = tab
+    tabsInternal.value[idx] = tab
+  }
+  if (tabsInternal.value.length === tabElementsArray.value.length && !initialized) {
+    sortTabs()
+    updateInitialIndexAndId()
+    initialized = true
   }
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
 }
 
 const sortTabs = () => {
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
+  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el.value, b.value.el.value))
+  if (activeIndex.value !== tabs.value.findIndex((t) => t.id === activeId.value)) {
+    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
+  }
 }
 
 const unregisterTab = (id: string) => {
-  tabsInternal.value = tabsInternal.value.filter((t) => t.value.id !== id)
-}
-
-watch(
-  tabsInternal,
-  () => {
-    findActive()
-  },
-  {deep: true}
-)
-
-const findActive = () => {
-  if (tabs.value.length === 0) {
-    modelValue.value = -1
-    activeId.value = undefined
-    return
-  }
-  if (modelValue.value >= 0 && !activeId.value) {
-    activeId.value = tabs.value[modelValue.value]?.id
-  }
-  if (tabs.value.find((t) => t.id === activeId.value)) {
-    activateTab(tabs.value.findIndex((t) => t.id === activeId.value))
-    return
-  }
-  activateTab(tabs.value.map((tab) => !tab.disabled).indexOf(true))
+  tabsInternal.value = tabsInternal.value.filter((t) => t.value.internalId !== id)
 }
 
 provide(tabsInjectionKey, {
@@ -357,14 +461,13 @@ provide(tabsInjectionKey, {
   registerTab,
   unregisterTab,
   activeId,
-  activateTab: (id) => {
-    const idx = tabs.value.findIndex((t) => t.id === id)
-    if (id === undefined || idx === -1) {
-      activateTab(nextIndex(0, 1))
+  activateTab: (internalId) => {
+    const idx = tabs.value.findIndex((t) => t.internalId === internalId)
+    if (internalId === undefined || idx === -1) {
+      activeIndex.value = nextIndex(0, 1)
       return
     }
-    activateTab(idx)
+    activeIndex.value = idx
   },
-  sortTabs,
 })
 </script>
diff --git a/packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts b/packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts
index 400af44ef..9fceb7bc9 100644
--- a/packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts
+++ b/packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts
@@ -528,7 +528,7 @@ describe('tabs', () => {
 
   const ComplexComponent = {
     template: `
-    <ParentComp v-model="index" v-model:active-id="id">
+    <ParentComp v-model:index="index" v-model="id">
       <ChildComp v-for="tab in tabs" :id="tab.id" :key="tab.title" :title="tab.title" :content="tab.content" />
       <BTab id="i3" :title="'t3'" >c3</BTab>
     </ParentComp>
diff --git a/packages/bootstrap-vue-next/src/types/ComponentProps.ts b/packages/bootstrap-vue-next/src/types/ComponentProps.ts
index c5b6f0a53..e4b20025d 100644
--- a/packages/bootstrap-vue-next/src/types/ComponentProps.ts
+++ b/packages/bootstrap-vue-next/src/types/ComponentProps.ts
@@ -117,9 +117,10 @@ export interface BAccordionProps {
   flush?: boolean
   free?: boolean
   id?: string
+  index?: number | number[]
   initialAnimation?: boolean
   lazy?: boolean
-  modelValue?: string
+  modelValue?: string | string[]
   unmountLazy?: boolean
 }
 
@@ -716,7 +717,7 @@ export interface BTabProps {
   disabled?: boolean
   id?: string
   lazy?: boolean
-  lazyOnce?: boolean
+  unmountLazy?: boolean
   noBody?: boolean
   tag?: string
   title?: string
@@ -726,7 +727,6 @@ export interface BTabProps {
 }
 
 export interface BTabsProps {
-  activeId?: string
   activeNavItemClass?: ClassValue
   activeTabClass?: ClassValue
   align?: AlignmentJustifyContent
@@ -735,11 +735,12 @@ export interface BTabsProps {
   end?: boolean
   fill?: boolean
   id?: string
+  index?: number
   inactiveNavItemClass?: ClassValue
   inactiveTabClass?: ClassValue
   justified?: boolean
   lazy?: boolean
-  modelValue?: number
+  modelValue?: string
   navClass?: ClassValue
   navItemClass?: ClassValue
   navWrapperClass?: ClassValue
diff --git a/packages/bootstrap-vue-next/src/types/Tab.ts b/packages/bootstrap-vue-next/src/types/Tab.ts
index d6ac310e0..eba7a96c8 100644
--- a/packages/bootstrap-vue-next/src/types/Tab.ts
+++ b/packages/bootstrap-vue-next/src/types/Tab.ts
@@ -1,17 +1,18 @@
-import type {Slot} from 'vue'
+import type {Ref, Slot} from 'vue'
 import type {ClassValue} from './AnyValuedAttributes'
 
 export interface TabType {
+  internalId: string
   id: string
   title?: string
   active: boolean
   buttonId: string
   disabled: boolean
   titleComponent?: Slot
-  titleItemClass?: () => ClassValue
-  titleLinkAttrs?: () => Record<string, unknown>
-  titleLinkClass?: () => ClassValue
-  onClick?: () => (event: Event) => void
+  titleItemClass?: ClassValue
+  titleLinkAttrs?: Record<string, unknown>
+  titleLinkClass?: ClassValue
+  onClick?: (event: Event) => void
   navItemClasses?: ClassValue
-  el: HTMLElement | null
+  el: Ref<HTMLElement | null>
 }
diff --git a/packages/bootstrap-vue-next/src/utils/keys.ts b/packages/bootstrap-vue-next/src/utils/keys.ts
index f9b5899c8..b6707877d 100644
--- a/packages/bootstrap-vue-next/src/utils/keys.ts
+++ b/packages/bootstrap-vue-next/src/utils/keys.ts
@@ -1,4 +1,4 @@
-import type {ComponentInternalInstance, ComputedRef, InjectionKey, Ref} from 'vue'
+import type {ComponentInternalInstance, ComputedRef, InjectionKey, Ref, ShallowRef} from 'vue'
 import type {TabType} from '../types/Tab'
 import type {ClassValue} from '../types/AnyValuedAttributes'
 import type {Numberish, ValidationState} from '../types/CommonTypes'
@@ -55,14 +55,13 @@ export const tabsInjectionKey: InjectionKey<{
   lazy: Readonly<Ref<boolean>>
   card: Readonly<Ref<boolean>>
   noFade: Readonly<Ref<boolean>>
-  registerTab: (tab: Readonly<Ref<TabType>>) => void
+  registerTab: (tab: ComputedRef<TabType>) => void
   unregisterTab: (id: string) => void
   activateTab: (id: string | undefined) => void
   activeTabClass: Readonly<Ref<ClassValue>>
   inactiveTabClass: Readonly<Ref<ClassValue>>
   tabClass: Readonly<Ref<ClassValue>>
   activeId: Readonly<Ref<string | undefined>>
-  sortTabs: () => void
 }> = createBvnInjectionKey('tabs')
 
 // BProgress
@@ -96,12 +95,15 @@ export const avatarGroupInjectionKey: InjectionKey<{
 
 // BAccordion
 export const accordionInjectionKey: InjectionKey<{
-  openItem: Readonly<Ref<string | undefined>>
+  openItem: Readonly<Ref<string | string[] | undefined>>
   free: Readonly<Ref<boolean>>
   initialAnimation: Readonly<Ref<boolean>>
   lazy: Readonly<Ref<boolean>>
   unmountLazy: Readonly<Ref<boolean>>
   setOpenItem: (id: string) => void
+  setCloseItem: (id: string) => void
+  registerAccordionItem: (id: string, el: Readonly<ShallowRef<HTMLElement | null>>) => void
+  unregisterAccordionItem: (id: string) => void
 }> = createBvnInjectionKey('accordion')
 
 // BFormCheckboxGroup


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request updates the Tabs and Accordion components to use IDs (modelValue) instead of indices for active items and introduces the new v-model:index syntax throughout the codebase. Key changes include:
- Updating TypeScript types and component APIs so that modelValue represents an id (or an array of ids) rather than an index.
- Refactoring internal tab registration to use a new internalId and adapting event signatures, particularly for the activate-tab event.
- Updating documentation, tests, and demo examples to reflect the new v-model:index usage.

### Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| packages/bootstrap-vue-next/src/utils/keys.ts | Added ShallowRef import to support new functionality. |
| packages/bootstrap-vue-next/src/types/Tab.ts | Introduced internalId and updated onClick, titleItemClass, titleLinkAttrs, and titleLinkClass definitions. |
| packages/bootstrap-vue-next/src/types/ComponentProps.ts | Updated props types for BTabs and accordion components, switching modelValue from a numeric index to a string id. |
| packages/bootstrap-vue-next/src/components/BTabs/* | Refactored tab registration, activation, and watcher logic to support the new v-model:index API, including changes in key binding and event signature. |
| packages/bootstrap-vue-next/src/components/BAccordion/* | Adjusted accordion item handling for free mode and synchronized index/modelValue, plus updated injection functions. |
| apps/playground/src/components/Comps/TTabs.vue, apps/docs/* | Updated examples and documentation to use the new v-model:index syntax. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384**
* [nitpick] The watcher on activeId contains logic that adjusts activeIndex based on tab order. It may benefit from being refactored into a helper function for improved clarity and maintainability.
```
watch(activeId, (newValue, oldValue) => {
```
**packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue:227**
* [nitpick] The branching logic in setOpenItem based on the free prop and modelValue type is somewhat convoluted; consider simplifying or adding detailed comments to clarify how modelValue is expected to behave in free vs. non-free modes.
```
setOpenItem: (id: string) => {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T19:45:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>apps/docs/src/docs/components/tabs.md (1)</summary><blockquote>

`239-246`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify consistency of the BTabs API migration across the codebase.**

The documentation update looks correct, but ensure all BTabs component usages have been migrated to the new API.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Verify all BTabs components use the new v-model:index syntax
# Expected: All BTabs should use v-model:index instead of plain v-model

echo "=== Checking for old v-model syntax on BTabs ==="
rg -A 2 '<BTabs.*v-model="' --type vue || echo "No old syntax found (good!)"

echo -e "\n=== Checking for new v-model:index syntax ==="
rg -A 2 '<BTabs.*v-model:index=' --type vue

echo -e "\n=== Checking for any BTabs without explicit model binding ==="
rg -A 2 '<BTabs(?![^>]*v-model)' --type vue
```

Length of output: 838

---

I’ll recheck all BTabs usages in `.vue` files with more reliable patterns:

---

🏁 Script executed:

```shell
#!/bin/bash
echo "=== All BTabs usages in .vue files ==="
rg -n -g '*.vue' '<BTabs'

echo -e "\n=== BTabs with old plain v-model syntax ==="
rg -n -g '*.vue' '<BTabs[^>]*v-model="' || echo "None found"

echo -e "\n=== BTabs with new v-model:index syntax ==="
rg -n -g '*.vue' '<BTabs[^>]*v-model:index=' || echo "None found"

echo -e "\n=== BTabs without any v-model binding ==="
rg -n -g '*.vue' '<BTabs' | grep -v 'v-model' | sed 's/$/  <-- missing binding/' \
  || echo "All BTabs have explicit binding"
```

Length of output: 6050

---



</details>

**Ensure BTabs usages are updated to the new v-model API**

Several demo and playground components still instantiate `<BTabs>` without any `v-model:index` (or `v-model:active-id`) binding. Please update each of these to use the new API—for example:
```vue
<BTabs v-model:index="tabIndex" …>
```
and define the corresponding `tabIndex` (e.g. `const tabIndex = ref(0)`) in their `<script setup>` blocks.

Files requiring fixes:
- apps/docs/src/docs/components/demo/TabsJustified.vue  
- apps/docs/src/docs/components/demo/TabsBottom.vue  
- apps/docs/src/docs/components/demo/TabsDynamic.vue  
- apps/docs/src/docs/components/demo/TabsVerticalWidth.vue  
- apps/docs/src/docs/components/demo/TabsVerticalEnd.vue  
- apps/docs/src/docs/components/demo/TabsVertical.vue  
- apps/docs/src/docs/components/demo/TabsPills.vue  
- apps/docs/src/docs/components/demo/TabsPictures.vue  
- apps/docs/src/docs/components/demo/TabsOverview.vue  
- apps/docs/src/docs/components/demo/TabsLazyAll.vue  
- apps/docs/src/docs/components/demo/TabsLazy.vue  
- apps/docs/src/docs/components/demo/TabsFill.vue  
- apps/docs/src/docs/components/demo/TabsEmpty.vue  
- apps/docs/src/docs/components/demo/TabsCustom.vue  
- apps/docs/src/docs/components/demo/TabsAlignment.vue  
- apps/docs/src/docs/components/demo/TabsActive.vue  
- apps/playground/src/components/Comps/TTabs.vue (lines 50 and 118)

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `preTabId`: The id of th...

(GOING_TO_WILL)

---

[style] ~243-~243: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...newTabIndex`: The index of the tab that is going to be activated - `prevTabIndex`: The inde...

(GOING_TO_WILL)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)</summary><blockquote>

`116-125`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider documenting the breaking changes and migration path.**

The changes to `BAccordionProps` introduce multi-selection support which is a nice enhancement. However, changing `modelValue` from `string` to `string | string[]` is a breaking change that will affect existing consumers.


Ensure that the migration guide documents these breaking changes. Run the following script to check if there's documentation for this change:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for migration guide or changelog entries about these breaking changes
fd -e md -e mdx | xargs rg -l "BAccordion.*modelValue.*breaking|migration.*accordion" -A 5
```

Length of output: 92

---

🏁 Script executed:

```shell
#!/bin/bash
# Search all markdown files for references to Accordion or migration guidance
fd -e md -e mdx | xargs rg -n "Accordion" -A2
fd -e md -e mdx | xargs rg -n "migration" -A2
```

Length of output: 20933

---



</details>

**Document BAccordion API Breaking Changes**

The migration guide and changelog don’t yet mention that:

- The `modelValue` prop on `BAccordion` has changed from `string` to `string | string[]` (enabling multiple selection).
- A new `index?: number | number[]` prop is available to control panels by index.

Please update:

• **CHANGELOG.md** (e.g. under “### Breaking Changes”):
  ```md
  - **BAccordionProps**: 
    - `modelValue` now accepts `string | string[]` (was `string`).
    - Added `index?: number | number[]` for index-based single or multiple panels.
  ```

• **apps/docs/src/docs/migration-guide.md** under the “### BAccordion” section:
  ```md
  #### BAccordion
  - `modelValue` now accepts both `string` and `string[]`. If you upgrade from single to multi-select, update your `v-model` usage to handle arrays.
  - Added `index?: number | number[]` to programmatically control active items by their index(es).
  ```

These additions will ensure consumers see the required steps when migrating.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>apps/docs/src/docs/components/tabs.md (1)</summary><blockquote>

`239-246`: **Documentation correctly updated, but address minor style issues.**

The documentation properly reflects the expanded `activate-tab` event signature with both ID and index parameters. However, there are minor style improvements needed:




Apply this diff to address the style issues:

```diff
-tab from activating. The `activate-tab` event is emitted with five arguments:
+tab from activating. The `activate-tab` event is emitted with five arguments:

-- `newTabId`: The id of the tab that is going to be activated
-- `preTabId`: The id of the currently active tab
+- `newTabId`: The id of the tab that will be activated
+- `preTabId`: The id of the currently active tab
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `preTabId`: The id of th...

(GOING_TO_WILL)

---

[style] ~243-~243: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...newTabIndex`: The index of the tab that is going to be activated - `prevTabIndex`: The inde...

(GOING_TO_WILL)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue (1)</summary><blockquote>

`141-147`: **Consider simplifying the mounted hook logic.**

The current logic in onMounted checks for a specific condition that might already be handled by the watchers. Since the `openInParent` computed property and watchers handle synchronization, this mounted hook might be redundant.


```diff
-onMounted(() => {
-  if (!modelValue.value && parentData?.openItem.value === computedId.value) {
-    nextTick(() => {
-      modelValue.value = true
-    })
-  }
-})
+// The watchers handle synchronization, so explicit mounted logic may not be needed
```

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)</summary><blockquote>

`714-756`: **Breaking API changes in tab components need clear documentation.**

The changes to `BTabProps` and `BTabsProps` represent significant breaking changes:
- `BTabProps`: `lazyOnce` → `unmountLazy` (semantic change)
- `BTabsProps`: `activeId` removed, `modelValue` type changed from `number` to `string`

These changes will require consumers to update their code.


Would you like me to help generate TypeScript migration examples or a codemod to help users migrate from the old API to the new one?

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTab.vue (2)</summary><blockquote>

`99-103`: **Remove commented code or implement the watcher if needed.**

This commented watcher suggests incomplete implementation. If prop changes need to trigger tab updates, implement the watcher. Otherwise, remove the commented code.

```diff
-// watch(() => props, () => {
-//   if (!parentData) return
-//   updateTab()
-// })
```

---

`69-90`: **Consider optimizing the tab registration approach.**

The current implementation creates a new computed ref on each `updateTab()` call. Since `updateTab()` is only called once during component initialization, this is acceptable. However, if you plan to make props reactive (as suggested by the commented watcher), consider creating the computed ref once and reusing it.


If you need reactive prop updates, consider this optimization:

```diff
+const tab = computed(
+  () =>
+    ({
+      internalId: internalId.value,
+      id: computedId.value,
+      active: activeModel.value,
+      buttonId: computedButtonId.value,
+      disabled: props.disabled,
+      title: props.title,
+      titleComponent: slots.title,
+      titleItemClass: props.titleItemClass,
+      titleLinkAttrs: props.titleLinkAttrs,
+      titleLinkClass: props.titleLinkClass,
+      onClick: attrs.onClick,
+      el,
+    }) as TabType
+)
+
 function updateTab() {
   if (!parentData) return
-  parentData.registerTab(
-    computed(
-      () =>
-        ({
-          internalId: internalId.value,
-          id: computedId.value,
-          active: activeModel.value,
-          buttonId: computedButtonId.value,
-          disabled: props.disabled,
-          title: props.title,
-          titleComponent: slots.title,
-          titleItemClass: props.titleItemClass,
-          titleLinkAttrs: props.titleLinkAttrs,
-          titleLinkClass: props.titleLinkClass,
-          onClick: attrs.onClick,
-          el,
-        }) as TabType
-    )
-  )
+  parentData.registerTab(tab)
 }
```

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue (1)</summary><blockquote>

`117-206`: **Consider extracting synchronization logic for better maintainability.**

The watchers for `index` and `modelValue` contain complex logic that could be extracted into helper functions for better readability and testability.


Would you like me to help refactor this synchronization logic into smaller, more focused helper functions? This would improve code maintainability and make unit testing easier.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 156-156: 
Unexpected console statement

</details>

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue (1)</summary><blockquote>

`290-292`: **Optional chaining is redundant with typeof check**

The optional chaining on line 292 is unnecessary since you already check that `onClick` is a function on line 290.



```diff
-    tabs.value[index].onClick?.(event)
+    tabs.value[index].onClick(event)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 790679d21ba648664e114ddd93907ffb5c7ba961 and 9de81a907ef3e19010ff5cd136e4870d5d4101b6.

</details>

<details>
<summary>📒 Files selected for processing (16)</summary>

* `apps/docs/src/data/components/accordion.data.ts` (3 hunks)
* `apps/docs/src/data/components/tabs.data.ts` (4 hunks)
* `apps/docs/src/docs/components/demo/TabsCustomLinks.vue` (1 hunks)
* `apps/docs/src/docs/components/demo/TabsDynamic.vue` (1 hunks)
* `apps/docs/src/docs/components/demo/TabsExternalControls.vue` (1 hunks)
* `apps/docs/src/docs/components/tabs.md` (1 hunks)
* `apps/playground/src/components/Comps/TTabs.vue` (3 hunks)
* `packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue` (3 hunks)
* `packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue` (4 hunks)
* `packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts` (2 hunks)
* `packages/bootstrap-vue-next/src/components/BTabs/BTab.vue` (6 hunks)
* `packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue` (10 hunks)
* `packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts` (1 hunks)
* `packages/bootstrap-vue-next/src/types/ComponentProps.ts` (3 hunks)
* `packages/bootstrap-vue-next/src/types/Tab.ts` (1 hunks)
* `packages/bootstrap-vue-next/src/utils/keys.ts` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>packages/bootstrap-vue-next/src/types/Tab.ts (2)</summary><blockquote>

<details>
<summary>packages/bootstrap-vue-next/src/types/index.ts (1)</summary>

* `TabType` (62-62)

</details>
<details>
<summary>packages/bootstrap-vue-next/src/types/AnyValuedAttributes.ts (1)</summary>

* `ClassValue` (2-2)

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/utils/keys.ts (1)</summary><blockquote>

<details>
<summary>packages/bootstrap-vue-next/src/types/Tab.ts (1)</summary>

* `TabType` (4-18)

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)</summary><blockquote>

<details>
<summary>packages/bootstrap-vue-next/src/types/AnyValuedAttributes.ts (1)</summary>

* `ClassValue` (2-2)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>apps/docs/src/docs/components/tabs.md</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `preTabId`: The id of th...

(GOING_TO_WILL)

</details>

</details>
<details>
<summary>🪛 GitHub Check: test-lint</summary>

<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue</summary>

[warning] 213-213: 
Use array destructuring     

---

[warning] 156-156: 
Unexpected console statement

---

[warning] 90-90: 
Unexpected console statement

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (26)</summary><blockquote>

<details>
<summary>apps/docs/src/docs/components/demo/TabsExternalControls.vue (1)</summary>

`5-5`: **LGTM! API migration correctly implemented.**

The change from `v-model="tabIndex"` to `v-model:index="tabIndex"` properly aligns with the new BTabs component API that separates index and ID models for better control.

</details>
<details>
<summary>apps/docs/src/docs/components/demo/TabsCustomLinks.vue (1)</summary>

`3-3`: **LGTM! Consistent API migration.**

The change to `v-model:index="tabIndex"` maintains consistency with the new BTabs API across demo components.

</details>
<details>
<summary>apps/playground/src/components/Comps/TTabs.vue (2)</summary>

`5-5`: **LGTM! API migration correctly implemented.**

The change to `v-model:index="tabIndex"` follows the consistent pattern for the new BTabs API.

---

`92-92`: **Excellent demonstration of enhanced dual binding capability.**

These lines showcase the new BTabs API's ability to simultaneously track both the active tab index and ID through dual binding (`v-model:index` and `v-model:active-id`). This provides more granular control over tab state management.




Also applies to: 126-126

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/tabs.spec.ts (1)</summary>

`531-531`: **Test binding syntax correctly updated for new BTabs API.**

The v-model binding syntax has been properly updated to reflect the new BTabs component API:
- `v-model:index="index"` for explicit index binding
- `v-model="id"` for the default model (active tab ID)

This change aligns with the broader refactor where `modelValue` now represents the active tab's ID as a string rather than an index number.

</details>
<details>
<summary>apps/docs/src/docs/components/demo/TabsDynamic.vue (2)</summary>

`7-17`: **Improved UX by moving close button to tab title.**

The close button placement in the tab title is more intuitive and follows common UI patterns. The styling with negative margin and link classes provides appropriate visual integration.

---

`23-31`: **Enhanced new tab button styling and accessibility.**

The change from `BNavItem` to `BButton` with proper Bootstrap utility classes improves the button appearance. The `role="presentation"` attribute is correctly applied for accessibility.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/accordion.spec.ts (4)</summary>

`4-4`: **Correct import addition for new test functionality.**

The import of `BAccordionItem` is properly added to support the new test cases.

---

`42-47`: **Good test coverage for the new free prop.**

Simple test verifies the `free` prop is correctly set and accessible.

---

`49-72`: **Well-structured complex test component for multi-selection testing.**

The test component properly demonstrates the new dual v-model binding pattern with both `modelValue` and `index` props, which aligns with the new accordion API.

---

`105-133`: **Comprehensive test for multi-selection behavior.**

The test thoroughly verifies the array-based state management when `free=true`, including:
- Initial state verification
- Adding selections
- Removing selections  
- Proper async handling with timeouts and `$nextTick`

The test logic correctly validates that both `modelValue` and `index` arrays stay synchronized.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/utils/keys.ts (4)</summary>

`1-1`: **Appropriate import addition for enhanced type safety.**

The `ComputedRef` import is correctly added to support the updated injection key interfaces.

---

`58-58`: **Enhanced type safety for tab registration.**

The change from `Readonly<Ref<TabType>>` to `ComputedRef<TabType>` provides better type safety and aligns with the component refactoring where tabs use computed properties for registration.

---

`98-98`: **Proper type expansion for multi-selection support.**

The `openItem` type correctly expanded from `string | undefined` to `string | string[] | undefined` to support multiple open accordion items when `free=true`.

---

`104-106`: **Well-designed new methods for accordion item management.**

The three new methods provide comprehensive accordion item lifecycle management:
- `setCloseItem`: For closing specific items
- `registerAccordionItem`: For registering items with DOM element reference
- `unregisterAccordionItem`: For cleanup when items are removed

The method signatures are appropriate and include necessary type constraints.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/types/Tab.ts (1)</summary>

`1-18`: **Breaking changes are properly aligned with the tabs refactoring goals.**

The interface changes are well-structured and improve type safety. The addition of `internalId` for robust internal identification and the simplification from function-returned values to direct values make the API cleaner.

Note for migration guide: These are breaking changes that will require updates to existing code:
- New required `internalId` property
- Properties changed from functions to direct values
- `onClick` handler signature change  
- `el` property now uses Vue's `Ref` wrapper

</details>
<details>
<summary>apps/docs/src/data/components/accordion.data.ts (1)</summary>

`23-82`: **Well-designed API for multi-selection accordion support.**

The addition of `index` and `modelValue` props with support for both single and array values provides excellent flexibility. The corresponding events are properly documented and align with Vue's v-model conventions.

</details>
<details>
<summary>apps/docs/src/data/components/tabs.data.ts (2)</summary>

`19-24`: **Good prop rename for clarity.**

The rename from `lazyOnce` to `unmountLazy` better describes the behavior - content is unmounted when inactive rather than cached.

---

`68-73`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify consistency between index and modelValue handling.**

The switch from `activeId` to `index` prop and changing `modelValue` from number to string represents a significant API change. The description notes that "Id (modelValue) takes priority over index" which is good for clarity.



Ensure examples and tests are updated to reflect these breaking changes:




Also applies to: 134-137

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check for outdated usage patterns that need updating

# Search for activeId prop usage that needs to be updated
rg -A 3 'activeId\s*[:=]' --type vue --type ts

# Search for v-model usage on BTabs that might expect numbers
ast-grep --pattern 'BTabs v-model=$_'
```

Length of output: 116

---

🏁 Script executed:

```shell
#!/bin/bash
# Retry searching for outdated activeId usage in .vue and .ts files
rg -A 3 'activeId\s*[:=]' -g '*.vue' -g '*.ts'

# Search for BTabs v-model usage in .vue files
rg -A 3 '<BTabs[^>]*v-model' -g '*.vue'
```

Length of output: 4331

---



</details>

**Update BTabs examples and tests for the new index/modelValue API**

We still have live usages of the old `activeId` binding alongside `v-model:index`. Please verify and update these examples and any tests to align with the switch from `activeId` → `index` (number) and the change of `modelValue` to a string ID:

• apps/playground/src/components/Comps/TTabs.vue  
  – Remove or adjust `v-model:active-id="activeId"` if you no longer intend to demonstrate ID-based activation, or confirm it now binds to a string.  
  – Ensure `v-model:index="tabIndex"` continues to work with numeric indexes.

• apps/docs/src/docs/components/demo/TabsExternalControls.vue  
  – Already uses `v-model:index`; confirm examples reflect the TypeScript change (`index: number`).

• apps/docs/src/docs/components/demo/TabsCustomLinks.vue  
  – Same as above: verify `v-model:index` examples and descriptions mention the index-based prop.

• packages/bootstrap-vue-next/src/components/BTabs  
  – Check any unit or integration tests under this folder to ensure they expect `modelValue` (string ID) and/or `index` (number) rather than the old `activeId` prop.

You can re-run these searches to catch any residual patterns:

```bash
rg -A3 'activeId' -g '*.vue' -g '*.ts'
rg -A3 '<BTabs[^>]*v-model' -g '*.vue'
```

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordionItem.vue (2)</summary>

`125-132`: **Excellent handling of single and multi-selection modes.**

The logic correctly checks whether the parent's `openItem` is an array to support both single and multiple open accordion items. Good defensive programming with the Array.isArray check.

---

`134-139`: **Good lifecycle management for accordion item registration.**

Registering and unregistering the item with the parent ensures proper state management and prevents memory leaks.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTab.vue (2)</summary>

`138-153`: **Good defensive programming with disabled state check!**

Excellent addition of the disabled state check to prevent activating disabled tabs. This improves the component's robustness.

---

`166-173`: **Well-designed public API exposure.**

The `activate()` and `deactivate()` methods provide a clean programmatic interface for external tab control, which enhances the component's flexibility.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue (2)</summary>

`213-213`: **Use array destructuring for cleaner code.**

Minor style improvement as suggested by the linter.

```diff
-        modelValue.value = modelValue.value[0]
+        modelValue.value = [modelValue.value][0]
```

Wait, this suggestion doesn't make sense. The current code is correct. The linter might be suggesting:
```diff
-        modelValue.value = modelValue.value[0]
+        const [firstValue] = modelValue.value
+        modelValue.value = firstValue
```

But the original code is cleaner. You can safely ignore this linting suggestion or disable it for this line.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 213-213: 
Use array destructuring     

</details>

</details>

---

`110-206`: **Well-implemented bidirectional synchronization logic.**

The synchronization between `index` and `modelValue` is comprehensive and handles edge cases well:
- Validates indices before updating
- Supports both single and multi-selection modes
- Uses `nextTick` appropriately for invalid values
- Implements `areEqual` for proper array comparison

This is a solid implementation of complex state management.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 156-156: 
Unexpected console statement

</details>

</details>

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue (1)</summary>

`200-200`: **Temporary IDs could cause issues with external references**

The `premount-${index}` temporary IDs might cause issues if external code tries to reference tabs before they're fully mounted.



Consider using a more robust ID generation strategy or document this behavior clearly.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T19:46:01Z
user: login: dwgray
body: I believe this PR addresses the issues I raised in #2383.  There is some cleanup remaining in the migration-guide and docs files, I am happy to handle that after this PR goes through.

Aside from the minor comments I added, it looks like the AI is raising flags on race conditions in a couple of places, I didn't take the time to get my head wrapped around them, but let me know if you'd like a second opinion on them and I'll try to grab some time to do that.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-27T22:15:47Z
user: login: xvaara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:01:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:01:56Z
user: login: xvaara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:32:05Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:32:46Z
user: login: xvaara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:56:36Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:57:21Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue (1)</summary><blockquote>

`90-93`: **Console statements should be replaced with proper error handling.**

This matches a previous review comment that hasn't been addressed. Console statements should not be left in production code.



Apply the suggested fix from the previous review:
```diff
-      if (next.length !== modelValue.value.length) {
-        if (import.meta.env.DEV) {
-          console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
-        }
-      }
+      if (next.length !== modelValue.value.length) {
+        if (import.meta.env.DEV) {
+          console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
+        }
+        // Optionally emit an event for production error tracking
+        // emit('error', { type: 'unknown-item-id', value: modelValue.value })
+      }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 91-91: 
Unexpected console statement

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/docs/src/docs/components/tabs.md (1)</summary><blockquote>

`241-243`: **Consider style improvements for better readability.**

Based on static analysis feedback, consider these minor style improvements:

```diff
- - `newTabId`: The id of the tab that is going to be activated
- - `prevTabId`: The id of the currently active tab
+ - `newTabId`: The id of the tab that will be activated
+ - `prevTabId`: The id of the currently active tab
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `prevTabId`: The id of t...

(GOING_TO_WILL)

---

[style] ~243-~243: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...newTabIndex`: The index of the tab that is going to be activated - `prevTabIndex`: The inde...

(GOING_TO_WILL)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue (1)</summary><blockquote>

`342-395`: **Consider minor optimization in nextIndex calculation.**

The activeIndex watcher logic is well-structured for handling complex activation scenarios. However, there's a small redundant calculation:

```diff
  const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
  if (index !== newValue) {
    previousIndex = oldValue
    activeIndex.value = index
    return
  }
  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
  emit(
    'activate-tab',
    tabs.value[index]?.id,
    tabs.value[previousIndex ?? oldValue]?.id,
    index,
    previousIndex ?? oldValue,
    tabEvent
  )
```

The logic is correct, but consider storing `previousIndex ?? oldValue` in a variable to avoid repetition.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9de81a907ef3e19010ff5cd136e4870d5d4101b6 and a72df00b9607b77b83f579aa23142b58b6a6edbc.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `apps/docs/src/data/components/tabs.data.ts` (4 hunks)
* `apps/docs/src/docs/components/tabs.md` (1 hunks)
* `packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue` (3 hunks)
* `packages/bootstrap-vue-next/src/components/BTabs/BTab.vue` (6 hunks)
* `packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue` (10 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/bootstrap-vue-next/src/components/BTabs/BTab.vue

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>apps/docs/src/data/components/tabs.data.ts (1)</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384-404
Timestamp: 2025-05-28T07:57:19.884Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex and activeId watchers are intentionally designed with separation of concerns: activeIndex watcher handles activate-tab event emission and complex validation logic, while activeId watcher is kept simple for synchronization between activeId and activeIndex values only.
```

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue (3)</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384-404
Timestamp: 2025-05-28T07:57:19.884Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex and activeId watchers are intentionally designed with separation of concerns: activeIndex watcher handles activate-tab event emission and complex validation logic, while activeId watcher is kept simple for synchronization between activeId and activeIndex values only.
```

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:425-430
Timestamp: 2025-05-28T07:32:45.637Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex synchronization within sortTabs() is necessary and correct. When sorting tabs by DOM position, indices change, so activeIndex must be updated immediately to maintain consistency with activeId. This is not an unwanted side effect but a required consequence of the sorting operation.
```

</details>

</details><details>
<summary>🪛 GitHub Check: test-lint</summary>

<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue</summary>

[warning] 214-214: 
Use array destructuring     

---

[warning] 91-91: 
Unexpected console statement

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>apps/docs/src/docs/components/tabs.md</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `prevTabId`: The id of t...

(GOING_TO_WILL)

---

[style] ~243-~243: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...newTabIndex`: The index of the tab that is going to be activated - `prevTabIndex`: The inde...

(GOING_TO_WILL)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue (5)</summary>

`52-70`: **VNode tracking implementation looks solid.**

The slot watching and VNode filtering logic is well-structured. Using `nextTick` for sorting after slot updates ensures DOM is ready.

---

`119-220`: **Review synchronization logic for potential race conditions.**

The dual-watcher setup between `index` and `modelValue` creates potential for infinite loops or race conditions, as mentioned in the PR comments. While the `areEqual` checks help prevent some redundant updates, the complex conditional logic could still cause issues.

Consider these improvements:
1. Use a single source of truth with computed values instead of dual watchers
2. Add debugging flags to track watcher execution order
3. Consider using `watchSyncEffect` or a state machine pattern



The array destructuring on line 214 should also be updated per the linter warning:

```diff
-        modelValue.value = modelValue.value[0]
+        [modelValue.value] = [modelValue.value[0]]
```

Would you like me to generate a verification script to test for race conditions or help refactor this synchronization logic?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 214-214: 
Use array destructuring     

</details>

</details>

---

`206-206`: **Deep watching is appropriate here.**

The deep watch on `modelValue` is necessary to detect changes in array values, and since the data is typically simple (strings/arrays of strings), the performance impact should be minimal.

---

`222-272`: **Injection implementation handles edge cases well.**

The `setOpenItem` and `setCloseItem` methods properly handle all combinations of free mode, array/single values, and state transitions. The registration system with automatic sorting when all items are registered is well-designed.

---

`112-117`: **Simple and effective equality utility.**

The `areEqual` function appropriately handles both array and primitive comparisons for preventing unnecessary watcher updates.

</details>
<details>
<summary>apps/docs/src/docs/components/tabs.md (1)</summary>

`239-246`: **Documentation accurately reflects the enhanced event signature.**

The updated documentation correctly describes the expanded `activate-tab` event with five arguments, providing clear descriptions for each parameter.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~241-~241: Loose punctuation mark.
Context: ...itted with five arguments:  - `newTabId`: The id of the tab that is going to be a...

(UNLIKELY_OPENING_PUNCTUATION)

---

[style] ~241-~241: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...:  - `newTabId`: The id of the tab that is going to be activated - `prevTabId`: The id of t...

(GOING_TO_WILL)

---

[style] ~243-~243: Use ‘will’ instead of ‘going to’ if the following action is certain.
Context: ...newTabIndex`: The index of the tab that is going to be activated - `prevTabIndex`: The inde...

(GOING_TO_WILL)

</details>

</details>

</details>
<details>
<summary>apps/docs/src/data/components/tabs.data.ts (5)</summary>

`19-24`: **LGTM! Property rename aligns with component implementation.**

The `lazyOnce` to `unmountLazy` rename with updated description correctly reflects the new lazy rendering behavior where content is unmounted when inactive.

---

`68-73`: **LGTM! Index prop introduction supports dual-model architecture.**

The new `index` prop with number type and -1 default provides explicit control over tab selection by index, complementing the ID-based `modelValue`.

---

`135-138`: **LGTM! ModelValue type change supports ID-based selection.**

Changing `modelValue` from number to string enables ID-based tab selection, which is more semantically meaningful and stable than index-based selection.

---

`209-220`: **LGTM! New index event enables dual binding support.**

The `update:index` event properly supports `v-model:index` binding for index-based control, complementing the ID-based `modelValue`.

---

`226-251`: **Enhanced event signature provides comprehensive context.**

The expanded `activate-tab` event with both ID and index parameters for new and previous tabs offers developers complete context for handling tab activation.

</details>
<details>
<summary>packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue (9)</summary>

`33-46`: **LGTM! Template updates align with component refactor.**

The template changes correctly use `internalId` for keys and remove function calls from prop bindings, improving performance by eliminating unnecessary computations on each render.

---

`87-142`: **Well-structured dual-model implementation.**

The separation of `activeIndex` and `activeId` models with proper defaults provides a clean API that supports both index-based and ID-based tab control.

---

`147-165`: **Reactive tab elements tracking improves state management.**

The `tabElementsArray` tracking with proper slot watching ensures the component responds correctly to dynamic tab changes, which is essential for proper DOM synchronization.

---

`225-264`: **Complex initialization logic necessary for SSR compatibility.**

Based on previous discussions, this initialization complexity with flags is required for SSR compatibility where tabs must be available before the mounted lifecycle. The logic properly handles the various initialization states.

---

`290-304`: **Simplified click handler improves maintainability.**

The updated click handler correctly processes the onClick callback and sets `activeIndex` directly, eliminating the need for the previous `activateTab` function complexity.

---

`397-422`: **ActiveId watcher maintains clean separation of concerns.**

Based on retrieved learnings, this watcher is intentionally kept simple for synchronization purposes while the activeIndex watcher handles complex event logic. The implementation correctly maintains this architectural separation.

---

`424-441`: **Tab registration handles dynamic updates effectively.**

The registration logic properly handles both initial registration and updates, with appropriate sorting triggered only when needed. The initialization flag prevents premature sorting.

---

`443-448`: **ActiveIndex synchronization in sortTabs is architecturally sound.**

Based on retrieved learnings, this synchronization is necessary and correct. When sorting changes tab positions, activeIndex must be updated immediately to maintain consistency with activeId.

---

`464-471`: **ActivateTab method provides clean programmatic interface.**

The method correctly uses `internalId` for tab lookup and delegates to the activeIndex model, maintaining consistency with the component's state management approach.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T08:20:44Z
user: login: dwgray
body: I believe you've addressed all of the code issues that coderabbitui brought up, so I'm good with this PR - great work on handling the multiple ways of addressing tabs, that's a hard problem.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T16:35:24Z

Review comments:
diff_hunk: @@ -279,72 +332,105 @@ const nextIndex = (start: number, direction: number) => {
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: Copilot
body: The logic managing previousIndex and isReverting in the activeIndex watcher is quite complex; consider refactoring this block or adding inline comments to clarify the intended flow.
```suggestion

// Watcher for activeIndex changes
watch(activeIndex, (newValue, oldValue) => {
  // Early exit if there are no tabs or all tabs are disabled
  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
    return
  }

  // Handle reverting due to a prevented event
  if (isReverting) {
    isReverting = false
    return
  }

  // Calculate the next valid index
  const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
  if (index !== newValue) {
    previousIndex = oldValue
    activeIndex.value = index
    return
  }

  // Emit the activate-tab event
```
created_at: 2025-05-27T19:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -236,8 +236,10 @@ order to use these methods.
 To prevent a tab from activating, simply set the `disabled` prop on the `<BTab>` component.
 
 Alternatively, you can listen for the `activate-tab` event, which provides an option to prevent the
-tab from activating. The `activate-tab` event is emitted with three arguments:
+tab from activating. The `activate-tab` event is emitted with five arguments:
path: apps/docs/src/docs/components/tabs.md
user: login: Copilot
body: [nitpick] Ensure that the documentation for the 'activate-tab' event correctly reflects the updated argument order and types as implemented in the BTabs component.
created_at: 2025-05-27T19:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,20 +45,228 @@
   default: undefined,
 })
 
+const index = defineModel<BAccordionProps['index']>('index', {
+  default: undefined,
+})
+
+const itemElementsArray = ref<VNode[]>([])
+
+const updateItemElementsArray = () => {
+  const itemElements = flattenFragments(slots.default?.({}) ?? [])
+  itemElementsArray.value = (Array.isArray(itemElements) ? itemElements : [itemElements]).filter(
+    (item) => item.type === BAccordionItem
+  )
+}
+updateItemElementsArray()
+
+watch(
+  () => slots.default?.({}),
+  () => {
+    updateItemElementsArray()
+    nextTick(() => {
+      sortAccordionItems()
+    })
+  }
+)
+
+interface AccordionItem {
+  id: string
+  el: Readonly<ShallowRef<HTMLElement | null>>
+}
+
+const accordionItems = shallowRef<AccordionItem[]>([])
+
+const sortAccordionItems = () => {
+  accordionItems.value = accordionItems.value.sort((a, b) =>
+    sortSlotElementsByPosition(unref(a.el), unref(b.el))
+  )
+  if (modelValue.value) {
+    if (Array.isArray(modelValue.value)) {
+      const next = modelValue.value
+        .map((id) => accordionItems.value.findIndex((item) => item.id === id))
+        .filter((i) => i !== -1)
+
+      if (next.length !== modelValue.value.length) {
+        console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
path: packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Replace console statements with proper error handling.**

Console statements should not be left in production code. Consider using Vue's warning system or emitting error events.

```diff
-      if (next.length !== modelValue.value.length) {
-        console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
-      }
+      if (next.length !== modelValue.value.length) {
+        if (import.meta.env.DEV) {
+          console.warn('[BAccordion] Unknown item id in v-model:', modelValue.value)
+        }
+        // Optionally emit an event for production error tracking
+        // emit('error', { type: 'unknown-item-id', value: modelValue.value })
+      }
```

And for line 156:
```diff
-    } else {
-      console.error('index.value is not a valid index', index.value)
-    }
+    } else {
+      if (import.meta.env.DEV) {
+        console.error('index.value is not a valid index', index.value)
+      }
+    }
```


Also applies to: 156-156

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: test-lint</summary>

[warning] 90-90: 
Unexpected console statement

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BAccordion/BAccordion.vue at lines
90 and 156, replace the console.warn statements with Vue's built-in warning
mechanism or emit error events instead. This involves using Vue's warn function
or emitting a custom event to notify the parent component about the issue,
ensuring proper error handling without relying on console output in production
code.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T19:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +237,29 @@ const tabs = computed(() => {
   })
 })
 
+function updateInitialIndexAndId() {
+  // we get the computedIds after registering the tabs
+  if (updateInitialActiveIndex) {
+    const index = tabs.value.findIndex((t) => t.id === activeId.value)
+    if (index !== -1) {
+      nextTick(() => {
+        activeIndex.value = index
+        updateInitialActiveIndex = false
+      })
+    }
+  }
+  if (updateInitialActiveId) {
+    if (activeIndex.value > -1 && tabs.value[activeIndex.value]?.id) {
+      nextTick(() => {
+        activeId.value = tabs.value[activeIndex.value]?.id
+        updateInitialActiveId = false
+      })
+    }
+  }
+}
+
+updateInitialIndexAndId()
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Complex initialization workaround indicates architectural issue**

The `updateInitialIndexAndId` function and the associated flags (`updateInitialActiveIndex`, `updateInitialActiveId`) suggest a timing issue in the component's initialization. This complexity could be avoided with a better initialization strategy.




Consider deferring the initial synchronization until after all tabs are registered, possibly using a `mounted` hook or a single initialization flag instead of multiple update flags.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue around lines 240
to 261, the function updateInitialIndexAndId uses multiple flags to handle
timing issues during initialization, causing unnecessary complexity. Refactor by
removing these flags and instead defer the initial synchronization of
activeIndex and activeId until after all tabs are registered, ideally within the
mounted lifecycle hook or by using a single initialization flag to ensure the
update happens once after the component is fully ready.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-27T19:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -128,41 +128,83 @@
   'tabs-start'?: (props: Record<string, never>) => any
 }>()
 
-const modelValue = defineModel<Exclude<BTabsProps['modelValue'], undefined>>({
+const activeIndex = defineModel<Exclude<BTabsProps['index'], undefined>>('index', {
   default: -1,
 })
-const activeId = defineModel<BTabsProps['activeId']>('activeId', {
+const activeId = defineModel<BTabsProps['modelValue']>({
   default: undefined,
 })
 
 const ReusableEmptyTab = createReusableTemplate()
 
 const tabsInternal = ref<Ref<TabType>[]>([])
 
-const tabs = computed(() => {
+const tabElementsArray = ref<VNode[]>([])
+
+const updateTabElementsArray = () => {
   const tabElements = flattenFragments(slots.default?.({}) ?? [])
-  const tabElementsArray = (Array.isArray(tabElements) ? tabElements : [tabElements]).filter(
+  tabElementsArray.value = (Array.isArray(tabElements) ? tabElements : [tabElements]).filter(
     (tab) => tab.type === BTab
   )
+}
+updateTabElementsArray()
+
+let initialized = false
+let updateInitialActiveIndex = false
+let updateInitialActiveId = false
+if (activeIndex.value === -1 && activeId.value) {
+  if (tabsInternal.value.findIndex((t) => t.value.id === activeId.value) !== -1) {
+    activeIndex.value = tabsInternal.value.findIndex((t) => t.value.id === activeId.value)
+  } else {
+    updateInitialActiveIndex = true
+  }
+} else if (activeIndex.value > -1 && !activeId.value) {
+  if (tabsInternal.value[activeIndex.value]?.value?.id) {
+    activeId.value = tabsInternal.value[activeIndex.value]?.value?.id
+  } else {
+    updateInitialActiveId = true
+  }
+} else if (activeIndex.value === -1 && !activeId.value) {
+  activeIndex.value = 0
+  updateInitialActiveId = true
+}
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential race condition in initialization logic**

The initialization logic attempts to synchronize `activeIndex` and `activeId` before tabs are registered. Since `tabsInternal` might be empty at this point, the `findIndex` operations on lines 156 and 157 will always return -1, causing `updateInitialActiveIndex` to be set to true unnecessarily.

Consider deferring this initialization until after tabs are registered, or use the `tabElementsArray` for initial lookups.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue around lines 155
to 170, the initialization logic runs before tabs are registered, causing
findIndex calls on an empty tabsInternal array and setting
updateInitialActiveIndex to true unnecessarily. To fix this, defer the
synchronization of activeIndex and activeId until after tabs are registered or
use the tabElementsArray for initial lookups to ensure the data exists before
performing these operations.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T19:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
 
 const registerTab = (tab: Ref<TabType>) => {
-  if (!tabsInternal.value.find((t) => t.value.id === tab.value.id)) {
+  const idx = tabsInternal.value.findIndex((t) => t.value.internalId === tab.value.internalId)
+  if (idx === -1) {
     tabsInternal.value.push(tab)
+    if (initialized) {
+      nextTick(() => {
+        sortTabs()
+      })
+    }
   } else {
-    tabsInternal.value[tabsInternal.value.findIndex((t) => t.value.id === tab.value.id)] = tab
+    tabsInternal.value[idx] = tab
+  }
+  if (tabsInternal.value.length === tabElementsArray.value.length && !initialized) {
+    sortTabs()
+    updateInitialIndexAndId()
+    initialized = true
   }
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
 }
 
 const sortTabs = () => {
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
+  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el.value, b.value.el.value))
+  if (activeIndex.value !== tabs.value.findIndex((t) => t.id === activeId.value)) {
+    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
+  }
 }
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Side effect in sortTabs could trigger unexpected watchers**

Updating `activeIndex` within `sortTabs` (lines 427-429) is a side effect that could trigger watchers unexpectedly. This violates the principle that sorting should be a pure operation.



Move the activeIndex synchronization outside of sortTabs:

```diff
 const sortTabs = () => {
   tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el.value, b.value.el.value))
-  if (activeIndex.value !== tabs.value.findIndex((t) => t.id === activeId.value)) {
-    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
-  }
 }
```

Then handle synchronization where sortTabs is called.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
const sortTabs = () => {
  tabsInternal.value.sort((a, b) =>
    sortSlotElementsByPosition(a.value.el.value, b.value.el.value)
  )
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue around lines 425
to 430, the function sortTabs currently updates activeIndex inside it, causing
side effects that may trigger watchers unexpectedly. Refactor by removing the
activeIndex update from sortTabs to keep it a pure sorting function. Instead,
after calling sortTabs elsewhere, add separate logic to synchronize activeIndex
with the current activeId, ensuring side effects happen outside the sorting
function.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-27T19:46:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Redundant nextIndex calls and potential synchronization issues**

The `activeId` watcher has several issues:
1. Line 401-403: Calling `nextIndex` twice with the same parameters
2. Potential infinite loop with the `activeIndex` watcher
3. The asynchronous update on lines 396-399 could cause race conditions



Apply this fix:

```diff
 watch(activeId, (newValue, oldValue) => {
-  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
-    return
-  }
+  if (isSyncing || tabs.value.length === 0) return
+  
+  const enabledTabs = tabs.value.filter(t => !t.disabled)
+  if (enabledTabs.length === 0) return
+  
   const index = tabs.value.findIndex((t) => t.id === newValue)
   const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
   if (tabs.value[index]?.disabled) {
     activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
     activeIndex.value = nextIndex(0, 1)
-    nextTick(() => {
-      activeId.value = tabs.value[activeIndex.value]?.id
-    })
     return
   }
-  const realIndex = nextIndex(index, 1)
-  if (realIndex === activeIndex.value) return
-  activeIndex.value = nextIndex(index, 1)
+  if (index !== activeIndex.value) {
+    isSyncing = true
+    activeIndex.value = index
+    nextTick(() => { isSyncing = false })
+  }
 })
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue between lines 384
and 404, the watcher on activeId calls nextIndex twice with the same arguments,
which is redundant and inefficient. To fix this, store the result of nextIndex
in a variable and reuse it instead of calling nextIndex multiple times with
identical parameters. Also, to avoid potential infinite loops and race
conditions, ensure that updates to activeIndex and activeId are coordinated
properly, possibly by removing the asynchronous nextTick update or restructuring
the logic to prevent triggering watchers recursively.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-27T19:46:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Complex state management with potential infinite loops**

The `activeIndex` watcher has several issues:
1. The `isReverting` flag pattern is fragile and could lead to race conditions
2. The interaction with `previousIndex` is complex and error-prone
3. There's potential for infinite loops between `activeIndex` and `activeId` watchers




Consider simplifying the synchronization logic:

```diff
-let previousIndex: number | undefined
-let isReverting = false
-watch(activeIndex, (newValue, oldValue) => {
-  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
-    return
-  }
-
-  // If we're reverting due to a prevented event, don't process further
-  if (isReverting) {
-    isReverting = false
-    return
-  }
-
-  const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  if (index !== newValue) {
-    previousIndex = oldValue
-    activeIndex.value = index
-    return
-  }
-  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
-  emit(
-    'activate-tab',
-    tabs.value[index]?.id,
-    tabs.value[previousIndex ?? oldValue]?.id,
-    index,
-    previousIndex ?? oldValue,
-    tabEvent
-  )
-  if (tabEvent.defaultPrevented) {
-    isReverting = true
-    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
-    previousIndex = undefined
-    if (activeId.value !== tabs.value[prev]?.id) {
-      activeId.value = tabs.value[prev]?.id
-    }
-    nextTick(() => {
-      if (prev >= 0) {
-        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
-      }
-    })
-    return
-  }
-
-  if (activeId.value !== tabs.value[index]?.id) {
-    activeId.value = tabs.value[index]?.id
-  }
-  previousIndex = undefined
-})
+let isSyncing = false
+watch(activeIndex, async (newValue, oldValue) => {
+  if (isSyncing || tabs.value.length === 0) return
+  
+  const enabledTabs = tabs.value.filter(t => !t.disabled)
+  if (enabledTabs.length === 0) return
+  
+  // Find nearest enabled tab
+  const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
+  if (index !== newValue) {
+    activeIndex.value = index
+    return
+  }
+  
+  // Emit cancelable event
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[oldValue]?.id,
+    index,
+    oldValue,
+    tabEvent
+  )
+  
+  if (tabEvent.defaultPrevented) {
+    // Revert to previous tab
+    activeIndex.value = oldValue
+    await nextTick()
+    document.getElementById(tabs.value[oldValue]?.buttonId)?.focus()
+    return
+  }
+  
+  // Sync activeId
+  isSyncing = true
+  activeId.value = tabs.value[index]?.id
+  await nextTick()
+  isSyncing = false
+})
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue between lines 337
and 382, the watcher on activeIndex uses a fragile isReverting flag and complex
previousIndex handling that risks race conditions and infinite loops with
activeId synchronization. Simplify this by removing the isReverting flag and
instead use a single source of truth for the active tab state. Refactor the
watcher to avoid directly setting activeIndex within itself and coordinate
updates through a controlled method or computed property to prevent circular
updates and ensure consistent state transitions without redundant reassignments.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T19:46:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,53 +66,63 @@ const processedAttrs = computed(() => {
   return tabAttrs
 })
 
-const tab = computed(
-  () =>
-    ({
-      id: computedId.value,
-      buttonId: computedButtonId.value,
-      disabled: props.disabled,
-      title: props.title,
-      titleComponent: slots.title,
-      titleItemClass: () => props.titleItemClass,
-      titleLinkAttrs: () => props.titleLinkAttrs,
-      titleLinkClass: () => props.titleLinkClass,
-      onClick: () => attrs.onClick,
-      el: el.value,
-    }) as TabType
-)
+function updateTab() {
+  if (!parentData) return
+  parentData.registerTab(
+    computed(
+      () =>
+        ({
+          internalId: internalId.value,
+          id: computedId.value,
+          active: activeModel.value,
+          buttonId: computedButtonId.value,
+          disabled: props.disabled,
+          title: props.title,
+          titleComponent: slots.title,
+          titleItemClass: props.titleItemClass,
+          titleLinkAttrs: props.titleLinkAttrs,
+          titleLinkClass: props.titleLinkClass,
+          onClick: attrs.onClick,
+          el,
+        }) as TabType
+    )
+  )
+}
 
 if (parentData) {
-  parentData.registerTab(tab)
+  updateTab()
   if (activeModel.value) {
-    parentData.activateTab(computedId.value)
+    parentData.activateTab(internalId.value)
   }
 }
 
-onMounted(() => {
-  if (!parentData) return
-  parentData.sortTabs()
-})
+// watch(() => props, () => {
path: packages/bootstrap-vue-next/src/components/BTabs/BTab.vue
user: login: dwgray
body: If this isn't necessary, delete it.
created_at: 2025-05-27T21:58:47Z
author_association: MEMBER
diff_hunk: @@ -113,7 +113,7 @@ const _props = withDefaults(defineProps<Omit<BTabsProps, 'modelValue' | 'activeI
 const props = useDefaults(_props, 'BTabs')
 
 const emit = defineEmits<{
-  'activate-tab': [v1: number, v2: number, v3: BvEvent]
+  'activate-tab': [v1: string, v2: string, v3: number, v4: number, v5: BvEvent]
   'click': [] // TODO click event is never used
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: dwgray
body: I think this comment is outdated. From my reading of the code, the `click` event is implemented. Actually, I'm confused at this point - BSV had a click event on BTab and none on BTabs (which I believe just passed through the native event when the tab was clicked) - I think we should do the same, which I think means deleting the emit and letting the BTab component propagate the click (which I think it does).


created_at: 2025-05-27T22:03:03Z
author_association: MEMBER
diff_hunk: @@ -113,7 +113,7 @@ const _props = withDefaults(defineProps<Omit<BTabsProps, 'modelValue' | 'activeI
 const props = useDefaults(_props, 'BTabs')
 
 const emit = defineEmits<{
-  'activate-tab': [v1: number, v2: number, v3: BvEvent]
+  'activate-tab': [v1: string, v2: string, v3: number, v4: number, v5: BvEvent]
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: dwgray
body: Is there a reason that these argument names aren't more friendly? In the docs, you're calling them `newTabId`, `pevTabId`, `newTabIndex`, `prevTabIndex`, and `bvEvent` - it seems like it would be worth using those names here and in the component reference file.
created_at: 2025-05-27T22:05:49Z
author_association: MEMBER
diff_hunk: @@ -236,8 +236,10 @@ order to use these methods.
 To prevent a tab from activating, simply set the `disabled` prop on the `<BTab>` component.
 
 Alternatively, you can listen for the `activate-tab` event, which provides an option to prevent the
-tab from activating. The `activate-tab` event is emitted with three arguments:
+tab from activating. The `activate-tab` event is emitted with five arguments:
 
+- `newTabId`: The id of the tab that is going to be activated
+- `preTabId`: The id of the currently active tab
path: apps/docs/src/docs/components/tabs.md
user: login: dwgray
body: ```suggestion
- `prevTabId`: The id of the currently active tab
```
created_at: 2025-05-27T22:08:09Z
author_association: MEMBER
diff_hunk: @@ -195,6 +237,29 @@ const tabs = computed(() => {
   })
 })
 
+function updateInitialIndexAndId() {
+  // we get the computedIds after registering the tabs
+  if (updateInitialActiveIndex) {
+    const index = tabs.value.findIndex((t) => t.id === activeId.value)
+    if (index !== -1) {
+      nextTick(() => {
+        activeIndex.value = index
+        updateInitialActiveIndex = false
+      })
+    }
+  }
+  if (updateInitialActiveId) {
+    if (activeIndex.value > -1 && tabs.value[activeIndex.value]?.id) {
+      nextTick(() => {
+        activeId.value = tabs.value[activeIndex.value]?.id
+        updateInitialActiveId = false
+      })
+    }
+  }
+}
+
+updateInitialIndexAndId()
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: xvaara
body: If we wait until they are mounted, we will not have the tabs registered in SSR context and no list of them for the buttons.
created_at: 2025-05-28T07:01:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,6 +237,29 @@ const tabs = computed(() => {
   })
 })
 
+function updateInitialIndexAndId() {
+  // we get the computedIds after registering the tabs
+  if (updateInitialActiveIndex) {
+    const index = tabs.value.findIndex((t) => t.id === activeId.value)
+    if (index !== -1) {
+      nextTick(() => {
+        activeIndex.value = index
+        updateInitialActiveIndex = false
+      })
+    }
+  }
+  if (updateInitialActiveId) {
+    if (activeIndex.value > -1 && tabs.value[activeIndex.value]?.id) {
+      nextTick(() => {
+        activeId.value = tabs.value[activeIndex.value]?.id
+        updateInitialActiveId = false
+      })
+    }
+  }
+}
+
+updateInitialIndexAndId()
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: @xvaara You're absolutely right about the SSR constraint. I didn't consider that the tabs need to be available for server-side rendering before the mounted lifecycle runs. The initialization complexity with those flags is actually necessary to ensure the component renders correctly in SSR context while still handling the asynchronous tab registration properly.

Thank you for the clarification - I'll withdraw that refactor suggestion as it would break SSR functionality.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@xvaara You're absolutely right about the SSR constraint. I didn't consider that the tabs need to be available for server-side rendering before the mounted lifecycle runs. The initialization complexity with those flags is actually necessary to ensure the component renders correctly in SSR context while still handling the asynchronous tab registration properly.

Thank you for the clarification - I'll withdraw that refactor suggestion as it would break SSR functionality.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-28T07:01:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
 
 const registerTab = (tab: Ref<TabType>) => {
-  if (!tabsInternal.value.find((t) => t.value.id === tab.value.id)) {
+  const idx = tabsInternal.value.findIndex((t) => t.value.internalId === tab.value.internalId)
+  if (idx === -1) {
     tabsInternal.value.push(tab)
+    if (initialized) {
+      nextTick(() => {
+        sortTabs()
+      })
+    }
   } else {
-    tabsInternal.value[tabsInternal.value.findIndex((t) => t.value.id === tab.value.id)] = tab
+    tabsInternal.value[idx] = tab
+  }
+  if (tabsInternal.value.length === tabElementsArray.value.length && !initialized) {
+    sortTabs()
+    updateInitialIndexAndId()
+    initialized = true
   }
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
 }
 
 const sortTabs = () => {
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
+  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el.value, b.value.el.value))
+  if (activeIndex.value !== tabs.value.findIndex((t) => t.id === activeId.value)) {
+    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
+  }
 }
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: xvaara
body: If we sort the tabs in tabsInternal, the indexes will change. Where should we then update the indexes if not next to the sort that always requires it?
created_at: 2025-05-28T07:32:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
 
 const registerTab = (tab: Ref<TabType>) => {
-  if (!tabsInternal.value.find((t) => t.value.id === tab.value.id)) {
+  const idx = tabsInternal.value.findIndex((t) => t.value.internalId === tab.value.internalId)
+  if (idx === -1) {
     tabsInternal.value.push(tab)
+    if (initialized) {
+      nextTick(() => {
+        sortTabs()
+      })
+    }
   } else {
-    tabsInternal.value[tabsInternal.value.findIndex((t) => t.value.id === tab.value.id)] = tab
+    tabsInternal.value[idx] = tab
+  }
+  if (tabsInternal.value.length === tabElementsArray.value.length && !initialized) {
+    sortTabs()
+    updateInitialIndexAndId()
+    initialized = true
   }
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
 }
 
 const sortTabs = () => {
-  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el, b.value.el))
-  findActive()
+  tabsInternal.value.sort((a, b) => sortSlotElementsByPosition(a.value.el.value, b.value.el.value))
+  if (activeIndex.value !== tabs.value.findIndex((t) => t.id === activeId.value)) {
+    activeIndex.value = tabs.value.findIndex((t) => t.id === activeId.value)
+  }
 }
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: @xvaara You're absolutely right, and I apologize for the poorly thought-out suggestion. 

When we sort the `tabsInternal` array, the indices inherently change, so the `activeIndex` synchronization is not an arbitrary side effect - it's a necessary consequence of maintaining consistency between `activeIndex` and `activeId` after the sort operation.

If we moved this synchronization elsewhere, we'd have to remember to call it after every `sortTabs()` invocation, which would be more error-prone and doesn't actually solve any real problem.

The current implementation is correct - the `activeIndex` update in `sortTabs` ensures that the active tab index remains consistent with the active tab ID after reordering. This is the right place for this logic.

Thank you for the correction. The current implementation should stay as is.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:425-430
Timestamp: 2025-05-28T07:32:45.637Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex synchronization within sortTabs() is necessary and correct. When sorting tabs by DOM position, indices change, so activeIndex must be updated immediately to maintain consistency with activeId. This is not an unwanted side effect but a required consequence of the sorting operation.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@xvaara You're absolutely right, and I apologize for the poorly thought-out suggestion. 

When we sort the `tabsInternal` array, the indices inherently change, so the `activeIndex` synchronization is not an arbitrary side effect - it's a necessary consequence of maintaining consistency between `activeIndex` and `activeId` after the sort operation.

If we moved this synchronization elsewhere, we'd have to remember to call it after every `sortTabs()` invocation, which would be more error-prone and doesn't actually solve any real problem.

The current implementation is correct - the `activeIndex` update in `sortTabs` ensures that the active tab index remains consistent with the active tab ID after reordering. This is the right place for this logic.

Thank you for the correction. The current implementation should stay as is.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:425-430
Timestamp: 2025-05-28T07:32:45.637Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex synchronization within sortTabs() is necessary and correct. When sorting tabs by DOM position, indices change, so activeIndex must be updated immediately to maintain consistency with activeId. This is not an unwanted side effect but a required consequence of the sorting operation.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-28T07:32:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: xvaara
body: This would prevent the activate-tab emit from firing. We are trying to seperate the logic of handling the event in the watch of the activeIndex and simplify the watch of activeId mainly to sync the index.
created_at: 2025-05-28T07:56:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -279,72 +332,105 @@
   return index
 }
 
-watch(modelValue, (newValue, oldValue) => {
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+let previousIndex: number | undefined
+let isReverting = false
+watch(activeIndex, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
+
+  // If we're reverting due to a prevented event, don't process further
+  if (isReverting) {
+    isReverting = false
     return
   }
 
   const index = nextIndex(newValue, newValue > oldValue ? 1 : -1)
-  nextTick(() => {
-    activateTab(index)
-  })
+  if (index !== newValue) {
+    previousIndex = oldValue
+    activeIndex.value = index
+    return
+  }
+  const tabEvent = new BvEvent('activate-tab', {cancelable: true})
+  emit(
+    'activate-tab',
+    tabs.value[index]?.id,
+    tabs.value[previousIndex ?? oldValue]?.id,
+    index,
+    previousIndex ?? oldValue,
+    tabEvent
+  )
+  if (tabEvent.defaultPrevented) {
+    isReverting = true
+    const prev = previousIndex ?? oldValue ?? nextIndex(0, 1)
+    previousIndex = undefined
+    if (activeId.value !== tabs.value[prev]?.id) {
+      activeId.value = tabs.value[prev]?.id
+    }
+    nextTick(() => {
+      if (prev >= 0) {
+        document.getElementById(tabs.value[prev]?.buttonId)?.focus()
+      }
+    })
+    return
+  }
+
+  if (activeId.value !== tabs.value[index]?.id) {
+    activeId.value = tabs.value[index]?.id
+  }
+  previousIndex = undefined
 })
 
 watch(activeId, (newValue, oldValue) => {
+  if (tabs.value.length <= 0 || tabs.value.filter((t) => !t.disabled).length <= 0) {
+    return
+  }
   const index = tabs.value.findIndex((t) => t.id === newValue)
-  if (newValue === oldValue) return
-  if (tabs.value.length <= 0) {
+  const oldIndex = tabs.value.findIndex((t) => t.id === oldValue)
+  if (tabs.value[index]?.disabled) {
+    activeIndex.value = nextIndex(index, index > oldIndex ? 1 : -1)
     return
   }
   if (index === -1) {
-    activateTab(nextIndex(0, 1))
+    activeIndex.value = nextIndex(0, 1)
+    nextTick(() => {
+      activeId.value = tabs.value[activeIndex.value]?.id
+    })
     return
   }
-  activateTab(index)
+  const realIndex = nextIndex(index, 1)
+  if (realIndex === activeIndex.value) return
+  activeIndex.value = nextIndex(index, 1)
 })
path: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue
user: login: coderabbitai[bot]
body: @xvaara Thank you for the clarification! You're absolutely right about the architectural separation of concerns. I can see that you've intentionally designed the watchers with distinct responsibilities:

- `activeIndex` watcher: Handles the `activate-tab` event emission and complex validation logic
- `activeId` watcher: Simple synchronization between activeId and activeIndex

My previous suggestion would indeed interfere with this clean separation. The design makes sense - keeping the `activeId` watcher focused solely on index synchronization while letting the `activeIndex` watcher handle all the event logic.

The only minor optimization I notice is the redundant `nextIndex(index, 1)` call on lines 401-403, but that doesn't affect the core architecture:

```javascript
const realIndex = nextIndex(index, 1)
if (realIndex === activeIndex.value) return
activeIndex.value = nextIndex(index, 1) // Could reuse `realIndex` here
```

But this is a minor style issue and doesn't impact the excellent separation of concerns you've achieved.



      (\   /)
     ( ._. )
    o_("_")("_")  *thoughtful rabbit*

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384-404
Timestamp: 2025-05-28T07:57:19.884Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex and activeId watchers are intentionally designed with separation of concerns: activeIndex watcher handles activate-tab event emission and complex validation logic, while activeId watcher is kept simple for synchronization between activeId and activeIndex values only.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:425-430
Timestamp: 2025-05-28T07:32:45.637Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex synchronization within sortTabs() is necessary and correct. When sorting tabs by DOM position, indices change, so activeIndex must be updated immediately to maintain consistency with activeId. This is not an unwanted side effect but a required consequence of the sorting operation.
```

---

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@xvaara Thank you for the clarification! You're absolutely right about the architectural separation of concerns. I can see that you've intentionally designed the watchers with distinct responsibilities:

- `activeIndex` watcher: Handles the `activate-tab` event emission and complex validation logic
- `activeId` watcher: Simple synchronization between activeId and activeIndex

My previous suggestion would indeed interfere with this clean separation. The design makes sense - keeping the `activeId` watcher focused solely on index synchronization while letting the `activeIndex` watcher handle all the event logic.

The only minor optimization I notice is the redundant `nextIndex(index, 1)` call on lines 401-403, but that doesn't affect the core architecture:

```javascript
const realIndex = nextIndex(index, 1)
if (realIndex === activeIndex.value) return
activeIndex.value = nextIndex(index, 1) // Could reuse `realIndex` here
```

But this is a minor style issue and doesn't impact the excellent separation of concerns you've achieved.



      (\   /)
     ( ._. )
    o_("_")("_")  *thoughtful rabbit*

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384-404
Timestamp: 2025-05-28T07:57:19.884Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex and activeId watchers are intentionally designed with separation of concerns: activeIndex watcher handles activate-tab event emission and complex validation logic, while activeId watcher is kept simple for synchronization between activeId and activeIndex values only.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:425-430
Timestamp: 2025-05-28T07:32:45.637Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex synchronization within sortTabs() is necessary and correct. When sorting tabs by DOM position, indices change, so activeIndex must be updated immediately to maintain consistency with activeId. This is not an unwanted side effect but a required consequence of the sorting operation.
```

---

```
Learnt from: xvaara
PR: bootstrap-vue-next/bootstrap-vue-next#2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.082Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-28T07:57:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: bolt-new-by-stackblitz[bot]
created_at: 2025-05-27T19:41:26Z
body: <a href="https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/pull/2716"><img src="https://developer.stackblitz.com/img/review_pr_small.svg" alt="Review PR in StackBlitz Codeflow" align="left" width="103" height="20" target="_blank"></a> _Run & review this pull request in [StackBlitz Codeflow](https://stackblitz.com/~/github.com/bootstrap-vue-next/bootstrap-vue-next/pull/2716)._

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-27T19:41:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change refactors and extends the APIs and internal logic of the `BAccordion` and `BTabs` components. It introduces index-based and multi-selection support for `BAccordion`, updates prop and event signatures for both components, synchronizes index and ID models, and revises related documentation, tests, and type definitions to align with the new behaviors.

## Changes

| Files/Groups                                                                                  | Change Summary                                                                                                  |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| `BAccordion.vue`, `BAccordionItem.vue`, `accordion.spec.ts`, types, utils/keys.ts            | Add index-based and multi-selection support, synchronize `modelValue`/`index`, update registration and events.  |
| `BTabs.vue`, `BTab.vue`, `tabs.spec.ts`, types, utils/keys.ts                                | Refactor tab activation logic, replace `modelValue` with `index`/`modelValue`, update events and registration.  |
| `ComponentProps.ts`, `Tab.ts`                                                                | Update prop and interface types for new API, add `internalId`, adjust function types to values, update refs.    |
| `accordion.data.ts`, `tabs.data.ts`                                                          | Document new/changed props and events for `BAccordion` and `BTabs`.                                             |
| `TabsCustomLinks.vue`, `TabsExternalControls.vue`, `TTabs.vue`, `TabsDynamic.vue`            | Update usage to bind `v-model:index`, adjust tab close/add button placement and styling.                        |
| `tabs.md`                                                                                    | Update documentation for `activate-tab` event argument changes.                                                 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Parent as Parent Component
    participant BAccordion as BAccordion
    participant BAccordionItem as BAccordionItem

    Parent->>BAccordion: Bind v-model and/or v-model:index
    BAccordion->>BAccordionItem: Provide openItem, setOpenItem, setCloseItem, registerAccordionItem
    BAccordionItem->>BAccordion: registerAccordionItem(id, el)
    BAccordionItem->>BAccordion: setOpenItem(id) / setCloseItem(id)
    BAccordion-->>Parent: Emit update:model-value / update:index
    BAccordionItem->>BAccordion: unregisterAccordionItem(id)
```

```mermaid
sequenceDiagram
    participant Parent as Parent Component
    participant BTabs as BTabs
    participant BTab as BTab

    Parent->>BTabs: Bind v-model:index and/or v-model
    BTabs->>BTab: Provide activeIndex, activeId, registerTab, activateTab
    BTab->>BTabs: registerTab(tab)
    BTab->>BTabs: activateTab(internalId)
    BTabs-->>Parent: Emit activate-tab(newTabId, prevTabId, newTabIndex, prevTabIndex, bvEvent)
```

## Assessment against linked issues

| Objective (Issue #)                                                      | Addressed | Explanation                                                                                       |
|--------------------------------------------------------------------------|-----------|---------------------------------------------------------------------------------------------------|
| Update BAccordion so closing all items sets model to undefined (#2497)   | ✅        |                                                                                                   |
| Allow BTabs to set and retain initial active tab via index (#2705)       | ✅        |                                                                                                   |

## Possibly related PRs

- [bootstrap-vue-next/bootstrap-vue-next#2663](https://github.com/bootstrap-vue-next/bootstrap-vue-next/pull/2663): Fixes initial `modelValue` assignment logic in `BAccordionItem`, related to synchronization of open state, but does not implement multi-selection or index-based control.

## Suggested reviewers

- VividLemon

## Poem

> In the garden of code where tabs and accordions play,  
> Now indices and IDs can both lead the way.  
> Multiple panels may open, or tabs jump to three—  
> With props and events in sweet harmony.  
> Bugs are now bunnies, hopping away!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=bootstrap-vue-next/bootstrap-vue-next&utm_content=2716):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: pkg-pr-new[bot]
created_at: 2025-05-27T19:43:47Z
body: 
[bsvn-vite-ts](https://pkg.pr.new/template/d27dfbcf-54e7-4ca5-8ea8-984448d9a6d9)


```
npm i https://pkg.pr.new/bootstrap-vue-next/bootstrap-vue-next@2716
```


```
npm i https://pkg.pr.new/bootstrap-vue-next/bootstrap-vue-next/@bootstrap-vue-next/nuxt@2716
```


_commit: <a href="https://github.com/bootstrap-vue-next/bootstrap-vue-next/runs/43022434749"><code>a72df00</code></a>_

author_association: NONE
user: login: xvaara
created_at: 2025-05-28T08:12:50Z
body: > Aside from the minor comments I added, it looks like the AI is raising flags on race conditions in a couple of places, I didn't take the time to get my head wrapped around them, but let me know if you'd like a second opinion on them and I'll try to grab some time to do that.

@dwgray @VividLemon  I'd like your input on the race conditions. Especially I got some hangs with infinite loops when I prevented inside the activate-tab emit, but they should work in the committed version.
author_association: CONTRIBUTOR
user: login: dwgray
created_at: 2025-05-29T16:33:51Z
body: > > Aside from the minor comments I added, it looks like the AI is raising flags on race conditions in a couple of places, I didn't take the time to get my head wrapped around them, but let me know if you'd like a second opinion on them and I'll try to grab some time to do that.
> 
> @dwgray @VividLemon I'd like your input on the race conditions. Especially I got some hangs with infinite loops when I prevented inside the activate-tab emit, but they should work in the committed version.

I just spent some time getting my head wrapped around the watchers for `activeIndex` and `activeId`, which I believe was the remaining issue from coderabbitai. I don't see a cleaner way to do this, the fundamental issue of having two `names` for the same object, each of which can be set in multiple ways is challenging. But I think it's the right dev experience, so it's worth the code complexity in this case. I also did not see anything that I would expect to raise a race condition, but of course, those are awfully hard to find by observation.

I'm good with this going through.
author_association: MEMBER