Airframe GitHub Storybook

ba-tab-price-comparison

A tab component for comparing prices across multiple dates

Figma

GitHub

Storybook


<ba-tab-price-comparison></ba-tab-price-comparison>

<script>
  (() => {
    const priceDates = [
      {
        date: '2025-10-16',
        price: 'Sold out',
        state: 'unavailable'
      }, {
        date: '2025-10-17',
        price: 'Sold out',
        state: 'unavailable'
      }, {
        date: '2025-10-18',
        price: 'Sold out',
        state: 'unavailable'
      }, {
        date: '2025-10-19',
        price: '£144',
        state: ""
      }, {
        date: '2025-10-20',
        price: '£176',
        state: ""
      }, {
        date: '2025-10-21',
        price: '£176',
        state: ""
      }, {
        date: '2025-10-22',
        price: '£148',
        state: "sale",
        overline: "Special offer"
      }, {
        date: '2025-10-23',
        price: '£130',
        state: ""
      }, {
        date: '2025-10-24',
        price: '£189',
        state: ""
      }, {
        date: '2025-10-25',
        price: '£149',
        state: ""
      }, {
        date: '2025-10-26',
        price: '£108',
        state: ""
      }, {
        date: '2025-10-27',
        price: '£113',
        state: ""
      }, {
        date: '2025-10-28',
        price: '£107',
        state: ""
      }, {
        date: '2025-10-29',
        price: '£107',
        state: ""
      }, {
        date: '2025-10-30',
        price: '£93',
        state: ""
      }, {
        date: '2025-10-31',
        price: '£107',
        state: ""
      }, {
        date: '2025-11-01',
        price: '£107',
        state: ""
      }, {
        date: '2025-11-02',
        price: '£86',
        state: ""
      }, {
        date: '2025-11-03',
        price: '£86',
        state: ""
      }, {
        date: '2025-11-04',
        price: '£83',
        state: "success",
        overline: "Lowest fare"
      }, {
        date: '2025-11-05',
        price: '£95',
        state: ""
      }, {
        date: '2025-11-06',
        price: '£97',
        state: ""
      }, {
        date: '2025-11-07',
        price: 'Sold out',
        state: 'unavailable'
      }, {
        date: '2025-11-08',
        price: 'Sold out',
        state: 'unavailable'
      }, {
        date: '2025-11-09',
        price: 'Sold out',
        state: 'unavailable'
      }
    ];

    let showMoreDates = false;
    let selectedDate = '';
    let startIndex = 0
    let previousButtonHidden = false;
    let nextButtonHidden = false;
    const data = {
      data7: [],
      data2: []
    };

    const tabPrice = document.querySelector('ba-tab-price-comparison');

    if (!tabPrice)
      return;

    function setData() {
      if (selectedDate !== '') {
        const selectedIndex = priceDates.findIndex(item => item.date === selectedDate);
        let startIndex2 = selectedIndex % 2 == 0
          ? selectedIndex
          : selectedIndex - 1;
        data.data2 = structuredClone(priceDates.slice(startIndex2, startIndex2 + 2));
      } else {
        data.data2 = structuredClone(priceDates.slice(startIndex, startIndex + 2));
      }

      data.data7 = structuredClone(priceDates.slice(startIndex, startIndex + 7));

      setLowestFare()

      if (selectedDate !== '') {
        const data2SelectedIndex = data
          .data2
          .findIndex(item => item.date === selectedDate);
        if (data2SelectedIndex !== -1) {
          data
            .data2[data2SelectedIndex]
            .state = 'selected'
        }

        const data7SelectedIndex = data
          .data7
          .findIndex(item => item.date === selectedDate);
        if (data7SelectedIndex !== -1) {
          data
            .data7[data7SelectedIndex]
            .state = 'selected'
        }
      }

      tabPrice.setAttribute('data', JSON.stringify(data));
    }

    function setLowestFare() {
      let lowestFare2 = null;
      data
        .data2
        .forEach(item => {
          const price = parseInt(item.price.replace('£', ''));
          if (!isNaN(price)) {
            if (lowestFare2 === null || price < lowestFare2) {
              lowestFare2 = price;
            }
          }
        })

      if (lowestFare2 !== null) {
        const data2LowestFareIndex = data
          .data2
          .findIndex(item => item.price === '£' + lowestFare2);
        data
          .data2[data2LowestFareIndex]
          .state = 'success';
        data
          .data2[data2LowestFareIndex]
          .overline = 'Lowest fare';
      }

      let lowestFare7 = null;
      data
        .data7
        .forEach(item => {
          const price = parseInt(item.price.replace('£', ''));
          if (!isNaN(price)) {
            if (lowestFare7 === null || price < lowestFare7) {
              lowestFare7 = price;
            }
          }
        })

      if (lowestFare7 !== null) {
        const data7LowestFareIndex = data
          .data7
          .findIndex(item => item.price === '£' + lowestFare7);
        data
          .data7[data7LowestFareIndex]
          .state = 'success';
        data
          .data7[data7LowestFareIndex]
          .overline = 'Lowest fare';
      }
    }

    setData();

    document.addEventListener('baDateSelected', (event) => {
      selectedDate = event.detail.date;
      const state = event.detail.state;

      if (state == 'unavailable') {
        return;
      }

      setData()
    });

    document.addEventListener('baNextPage', (event) => {
      const dataType = event.detail.dataType;

      if (previousButtonHidden) {
        tabPrice.removeAttribute('hide-previous-button');
        previousButtonHidden = false;
      }

      if (dataType === 'data-7') {
        const index = startIndex + 7;
        startIndex = index > priceDates.length - 7
          ? priceDates.length - 7
          : index;

        if (index >= priceDates.length - 7) {
          tabPrice.setAttribute('hide-next-button', '');
          nextButtonHidden = true;
        }
      }

      if (dataType === 'data-2') {
        const index = startIndex + 2;
        startIndex = index >= priceDates.length - 1
          ? priceDates.length - 2
          : index;

        if (index >= priceDates.length - 1) {
          tabPrice.setAttribute('hide-next-button', '');
          nextButtonHidden = true;
        }
      }

      setData()
    })

    document.addEventListener('baPreviousPage', (event) => {
      const dataType = event.detail.dataType;

      if (nextButtonHidden) {
        tabPrice.removeAttribute('hide-next-button');
        nextButtonHidden = false;
      }

      if (dataType === 'data-7') {
        const index = startIndex - 7;
        startIndex = index <= 0
          ? 0
          : index;

        if (index <= 0) {
          tabPrice.setAttribute('hide-previous-button', '');
          previousButtonHidden = true;
        }
      }

      if (dataType === 'data-2') {
        const index = startIndex - 2;
        startIndex = index < 0
          ? 0
          : index;

        if (index <= 0) {
          tabPrice.setAttribute('hide-previous-button', '');
          hidePreviousButton = true;
        }
      }

      setData()
    })

    document.addEventListener('baMoreDatesToggled', (event) => {
      const dataType = event.detail.dataType;
      showMoreDates = dataType === 'data-7';
    })
  })()
</script>

The component accepts a data attribute which should contain the following properties within it.

  • data-2: Data to be shown when in mobile
  • data-7: Data to be shown in expanded mobile mode and desktop

As the component just ingests and displays the data, it is important to keep both sets of data up to date. This is incase the user switches between mobile and desktop views or toggles the view more date button.

The content below the component should update when a user selects a date. This content should have the following attributes added to it.

  • role="tabpanel"
  • aria-labelledby={the id of the selected tab}

This ensures that both the tabs and content are linked, which is useful for screen readers

Element State Key press Behaviour
Previous element in DOM Previous element in DOM has focus Tab <ba-tab-price-comparison> gets focus.
<ba-tab-price-comparison> Arrow left button has focus Tab. Moves focus to <ba-tab-price> element
<ba-tab-price> <ba-tab-price> has focus Right arrow Moves focus to next <ba-tab-price> element
<ba-tab-price> <ba-tab-price> has focus Left arrow Moves focus to previous <ba-tab-price> element
<ba-tab-price> <ba-tab-price> has focus Home. Moves focus to the first <ba-tab-price> element
<ba-tab-price> <ba-tab-price> has focus End. Moves focus to the last <ba-tab-price> element
<ba-tab-price> <ba-tab-price> has focus Tab. Moves focus to <ba-tab-price-comparison> next button element
<ba-tab-price-comparison> <ba-tab-price-comparison> has focus Tab. Moves focus to content below element.
Property Attribute Description Type Default
data data Data for the price comparison tab TabPriceData | string | undefined undefined
hideNextButton hide-next-button Whether to hide the next button boolean | undefined false
hidePreviousButton hide-previous-button Whether to hide the previous button boolean | undefined false
loading loading Whether to show the loading skeletons boolean | undefined false

No slotted content available for this component.

Basic usage showing how and which attributes to use. The `role="tabpanel"` attribute marks the content as a tabpanel and the aria-describedby attribute connects the content to the selected tab. This allows screen reader users to know which tab the content belongs to.
<ba-tab-price-comparison
  id="priceComparison"
  data="{
    'data-2': [
    {
        date: '2025-10-16',
        price: 'Sold out',
        state: 'unavailable',
        id: '2025-10-16'
      }, {
        date: '2025-10-17',
        price: 'Sold out',
        state: 'unavailable',
        id: '2025-10-17'
      }
    ],
    'data-7': [
    {
        date: '2025-10-16',
        price: 'Sold out',
        state: 'unavailable',
        id: '2025-10-16'
      }, {
        date: '2025-10-17',
        price: 'Sold out',
        state: 'unavailable',
        id: '2025-10-17'
      }, {
        date: '2025-10-18',
        price: 'Sold out',
        state: 'unavailable',
        id: '2025-10-18'
      }, {
        date: '2025-10-19',
        price: '£144',
        state: '',
        id: '2025-10-19'
      }, {
        date: '2025-10-20',
        price: '£176',
        state: '',
        id: '2025-10-20'
      }, {
        date: '2025-10-21',
        price: '£176',
        state: '',
        id: '2025-10-21'
      }, {
        date: '2025-10-22',
        price: '£148',
        state: 'sale',
        overline: 'Special offer',
        id: '2025-10-22'
      }]
  }"
></ba-tab-price-comparison>

<ba-page-segment>
  <div role="tabpanel" id="content" aria-labelledby="2025-10-16"></div>
</ba-page-segment>

<script>
  const priceComparison = document.getElementById('priceComparison')
  const content = document.getElementById('content')

  priceComparison.addEventListener('baDateSelected', (event) => {
    const tabId = event.detail.id
    content.setAttribute("aria-labelledby", tabId)
  })
</script>

The arrow buttons can be hidden. For example if the user is at the end of a date range.
<ba-tab-price-comparison
  hide-previous-button
  hide-next-button
></ba-tab-price-comparison>

Figma GitHub Storybook Version 3 release guide Release history BAgel helper QA process