Pro Pant 2.0 - Black - Taper Fit

Regular price $87.50 USD

/* eslint-disable max-len */ /** * Dependencies: * - Custom select component * - Fetch cache (theme.fetchCache) * - Quickbuy content transformer (theme.quickBuy.convertToQuickBuyContent) * * Required translation strings: * - noStock * - noVariant */ if (!customElements.get('variant-picker')) { class VariantPicker extends HTMLElement { constructor() { super(); this.quickBuyContainer = this.closest('.js-quickbuy'); this.featuredProductContainer = this.closest('.cc-featured-product'); this.section = this.quickBuyContainer || this.featuredProductContainer || this.closest('.shopify-section'); this.optionSelectors = this.querySelectorAll( '.option-selector:not(.option-selector--custom):not(.option-selector--composite-generated)' ); this.productAvailable = this.classList.contains('variant-picker--product-available'); this.variant = this.getVariantData(); this.compositeSelectionState = {}; if (this.dataset.compositeSelectionState) { try { this.compositeSelectionState = JSON.parse(this.dataset.compositeSelectionState) || {}; } catch (error) { this.compositeSelectionState = {}; } } // Track interaction independently for each composite option. this.compositeInteractionState = {}; if (this.dataset.compositeInteractionState) { try { this.compositeInteractionState = JSON.parse(this.dataset.compositeInteractionState) || {}; } catch (error) { this.compositeInteractionState = {}; } } this.compositeUserInteracted = Object.values(this.compositeInteractionState).some(Boolean); // A saved compositeSelectionState means Shopify has just replaced the // section. initCompositeSelectors() must not overwrite that state with // the blank controls it creates before syncCompositeSelectorsFromNative() // has a chance to restore them. this.restoringCompositeState = Object.keys(this.compositeSelectionState).length > 0; this.variantRequestId = 0; // High-variant support is only used when Shopify's Liquid // product.variants collection is over its 250-variant limit. this.highVariantDataLoaded = false; this.highVariantDataLoading = false; this.addListeners(); this.updateVariantContent(); if (!this.quickBuyContainer && !this.featuredProductContainer) { setTimeout(this.applySearchParams.bind(this), 0); } this.setAttribute('loaded', ''); } addListeners() { // The dynamic variant content used by the theme can be replaced without // constructing a new , so refresh this NodeList here. this.optionSelectors = this.querySelectorAll( '.option-selector:not(.option-selector--custom):not(.option-selector--composite-generated)' ); this.initCompositeSelectors(); // Do not change the normal v13 path for products with <=250 variants. // Larger products are augmented after the existing controls are built. this.loadHighVariantDataIfNeeded(); // Composite selectors are an alternate UI and must start unselected on // the initial page load. When Shopify replaces the picker after a real // composite selection, the selection state is carried in data attributes; // in that case we MUST NOT clear the newly inserted native selector. if (!this.compositeInitialised) { this.compositeInitialised = true; if (!Object.values(this.compositeInteractionState).some(Boolean)) { this.clearAllCompositeSelections(); } } this.boundHandleVariantChange = this.handleVariantChange.bind(this); this.addEventListener('change', this.boundHandleVariantChange); this.boundHandleLabelMouseEnter = this.handleLabelMouseEnter.bind(this); this.querySelectorAll('.opt-label, .custom-select__option').forEach((el) => { el.addEventListener('mouseenter', this.boundHandleLabelMouseEnter); el.addEventListener('touchstart', this.boundHandleLabelMouseEnter, { passive: true, once: true }); el.addEventListener('mouseleave', VariantPicker.handleLabelMouseLeave); }); } /** * Builds the additional human-friendly dropdowns for composite Shopify * option values. The real Shopify selector remains in the DOM but hidden. * * A composite option looks like: * Name: Make--Model--Bed Length * Value: Chevy--Silverado 1500--6' * * The generated controls are only a UI layer. When all component controls * are selected, the exact original Shopify value is selected on the * hidden custom-select, allowing the rest of the theme to work normally. */ /** * Augment the existing v13 variant matrix for products with more than * Shopify's 250-variant Liquid limit. * * The original Liquid JSON remains the initial data source. This method * only runs for >250 variants, fetches the complete variant connection, * appends missing variants, and then re-runs the existing v13 filtering. */ async loadHighVariantDataIfNeeded() { const variantCount = Number(this.dataset.variantCount || 0); if (!Number.isFinite(variantCount) || variantCount <= 250) return; if (this.highVariantDataLoaded || this.highVariantDataLoading) return; const handle = this.dataset.productHandle || ''; if (!handle) { console.warn('High-variant composite picker: product handle is missing.'); return; } this.highVariantDataLoading = true; try { const query = ` query ProductVariants($handle: String!, $cursor: String) { productByHandle(handle: $handle) { variants(first: 250, after: $cursor) { nodes { id availableForSale selectedOptions { name value } } pageInfo { hasNextPage endCursor } } } } `; const allVariants = []; let cursor = null; let hasNextPage = true; while (hasNextPage) { const response = await fetch('/api/2026-07/graphql.json', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ query, variables: { handle, cursor } }) }); if (!response.ok) { throw new Error(`Storefront API returned HTTP ${response.status}`); } const payload = await response.json(); if (payload.errors?.length) { throw new Error(payload.errors.map((error) => error.message).join('; ')); } const connection = payload.data?.productByHandle?.variants; if (!connection) { throw new Error('Storefront API did not return product variants.'); } allVariants.push(...(connection.nodes || [])); hasNextPage = Boolean(connection.pageInfo?.hasNextPage); cursor = connection.pageInfo?.endCursor || null; if (hasNextPage && !cursor) { throw new Error('Storefront API returned another page without a cursor.'); } } if (!allVariants.length) { throw new Error('Storefront API returned no variants.'); } const completeVariants = allVariants.map((variant) => ({ id: String(variant.id || '').split('/').pop(), available: Boolean(variant.availableForSale), options: (variant.selectedOptions || []).map((option) => option.value) })); const existingIds = new Set( (this.compositeVariants || []).map((variant) => String(variant.id)) ); const additionalVariants = completeVariants.filter( (variant) => variant.id && !existingIds.has(variant.id) ); this.compositeVariants = [ ...(this.compositeVariants || []), ...additionalVariants ]; this.highVariantDataLoaded = true; // Reuse the existing v13 filtering/selection logic. this.querySelectorAll( '.option-selector[data-composite-option="true"]' ).forEach((nativeSelector) => { this.updateCompositeSelectorsFor(nativeSelector); }); this.autoSelectForcedCompositeOptions(); } catch (error) { // Keep the original v13 first-250 behavior if the API is unavailable. console.warn( 'Unable to load variants beyond Shopify Liquid’s 250-variant limit for the composite picker.', error ); } finally { this.highVariantDataLoading = false; } } initCompositeSelectors() { const variantDataElement = this.querySelector('[data-composite-variant-data]'); if (!variantDataElement) return; let variants; try { variants = JSON.parse(variantDataElement.textContent); } catch (error) { console.warn('Unable to parse composite variant data.', error); return; } this.compositeVariants = Array.isArray(variants) ? variants : []; // Style invalid composite choices without affecting the theme's native // custom-select styling. The invalid choices remain in the list so the // customer can see the complete option set. if (!this.querySelector('style[data-composite-option-styles]')) { const style = document.createElement('style'); style.dataset.compositeOptionStyles = 'true'; style.textContent = ` .composite-option-select option.composite-option-invalid { color: #b85c5c; background-color: #fdeaea; } `; this.appendChild(style); } this.querySelectorAll('.option-selector[data-composite-option="true"]').forEach((nativeSelector) => { if (nativeSelector.dataset.compositeInitialized === 'true') return; const delimiter = nativeSelector.dataset.compositeDelimiter || '--'; const nativeOptionName = nativeSelector.dataset.option || ''; const firstNativeValue = nativeSelector.querySelector('.js-option[data-value]')?.dataset.value || ''; if (!firstNativeValue) return; // The Liquid template uses -- in values even when the abbreviated // Year-Make-Model name uses single hyphens. const partNames = nativeOptionName.includes('--') ? nativeOptionName.split('--') : nativeOptionName.split('-'); const nativeOptions = Array.from( nativeSelector.querySelectorAll('.js-option[data-value]:not([data-value=""])') ).map((option) => ({ value: option.dataset.value, valueId: option.dataset.valueId, parts: option.dataset.value.split('--') })); if (!partNames.length || nativeOptions.some((option) => option.parts.length !== partNames.length)) { return; } const compositeContainer = document.createElement('div'); compositeContainer.className = 'composite-option-pickers'; compositeContainer.dataset.compositeFor = nativeSelector.dataset.index; partNames.forEach((partName, partIndex) => { const wrapper = document.createElement('div'); wrapper.className = 'option-selector option-selector--composite-generated'; wrapper.dataset.compositeGenerated = 'true'; wrapper.dataset.compositeIndex = nativeSelector.dataset.index; wrapper.dataset.compositePartIndex = partIndex; const label = document.createElement('label'); label.className = 'label'; label.textContent = partName; const select = document.createElement('select'); select.className = 'select composite-option-select custom-select__btn input items-center'; select.dataset.compositeIndex = nativeSelector.dataset.index; select.dataset.compositePartIndex = partIndex; select.setAttribute('aria-label', partName); const placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = this.dataset.placeholderText || 'Please select'; select.appendChild(placeholder); wrapper.appendChild(label); wrapper.appendChild(select); compositeContainer.appendChild(wrapper); }); nativeSelector.after(compositeContainer); nativeSelector.classList.add('option-selector--composite-native'); nativeSelector.setAttribute('aria-hidden', 'true'); nativeSelector.style.display = 'none'; nativeSelector.dataset.compositeInitialized = 'true'; nativeSelector.querySelectorAll('.js-option').forEach((option) => { option.dataset.compositeOriginalValue = option.dataset.value || ''; }); this.updateCompositeSelectorsFor(nativeSelector); }); } /** * Reset every generated composite selector and its hidden Shopify selector. * This is deliberately used only during initial setup; after that, * selections are controlled by the customer. */ clearAllCompositeSelections() { this.querySelectorAll('.option-selector[data-composite-option="true"]').forEach((nativeSelector) => { const container = this.querySelector( `.composite-option-pickers[data-composite-for="${CSS.escape(nativeSelector.dataset.index)}"]` ); if (container) { container.querySelectorAll('.composite-option-select').forEach((select) => { select.value = ''; }); } this.compositeSelectionState[nativeSelector.dataset.index] = []; this.clearCompositeNativeSelection(nativeSelector); this.updateCompositeSelectorsFor(nativeSelector); }); } /** * Returns the component parts for a Shopify option value. */ getCompositeParts(nativeValue, delimiter) { // Shopify values always use the double-hyphen delimiter. The delimiter // argument is retained so the generated control can document which // name syntax activated the feature. return String(nativeValue || '').split('--'); } /** * Tests whether a generated component selection matches a native value. * Year is special: selecting 2005 matches a native value such as * "2000-2008". */ compositePartMatches(partName, selectedValue, nativePart) { if (!selectedValue) return true; if (partName.trim().toLowerCase() === 'year') { const rangeMatch = String(nativePart).trim().match(/^(\d{4})\s*-\s*(\d{4})$/); if (rangeMatch) { const year = Number(selectedValue); const start = Number(rangeMatch[1]); const end = Number(rangeMatch[2]); return Number.isInteger(year) && year >= Math.min(start, end) && year <= Math.max(start, end); } } return nativePart === selectedValue; } /** * Year dropdowns may temporarily display the exact Shopify range that was * selected by the variant refresh. This is presentation-only state; it is * not a real generated year selection. */ isCompositeYearRangePlaceholder(value) { return typeof value === 'string' && value.startsWith('__native_year_range__:'); } getCompositeYearRangeFromPlaceholder(value) { if (!this.isCompositeYearRangePlaceholder(value)) return ''; return value.slice('__native_year_range__:'.length); } /** * Gets the currently selected generated values for one composite option. */ getCompositeSelection(nativeSelector) { const container = this.querySelector( `.composite-option-pickers[data-composite-for="${CSS.escape(nativeSelector.dataset.index)}"]` ); if (!container) return []; return Array.from(container.querySelectorAll('.composite-option-select')).map((select) => select.value || null); } /** * Determines whether a Shopify variant is compatible with the selections * currently made in the generated controls. */ variantMatchesCompositeSelections(variant, selections, compositeSelectorIndex) { const nativeOptionIndex = Number(compositeSelectorIndex) - 1; const nativeValue = variant?.options?.[nativeOptionIndex]; if (nativeValue == null) return false; const nativeParts = this.getCompositeParts(nativeValue, ''); const container = this.querySelector( `.composite-option-pickers[data-composite-for="${CSS.escape(String(compositeSelectorIndex))}"]` ); const selects = container ? Array.from(container.querySelectorAll('.composite-option-select')) : []; return selections.every((selectedValue, partIndex) => { if (!selectedValue) return true; const partName = selects[partIndex]?.getAttribute('aria-label') || ''; return this.compositePartMatches(partName, selectedValue, nativeParts[partIndex]); }); } /** * Updates every generated dropdown so it contains only values that can * still lead to a real Shopify variant. */ updateCompositeSelectorsFor(nativeSelector) { const container = this.querySelector( `.composite-option-pickers[data-composite-for="${CSS.escape(nativeSelector.dataset.index)}"]` ); if (!container || !this.compositeVariants) return; const selects = Array.from(container.querySelectorAll('.composite-option-select')); const nativeOptions = Array.from( nativeSelector.querySelectorAll('.js-option[data-value]:not([data-value=""])') ).map((option) => ({ value: option.dataset.value, parts: this.getCompositeParts(option.dataset.value, nativeSelector.dataset.compositeDelimiter) })); const currentSelections = selects.map((select) => { const value = select.value || null; // A native year-range placeholder is informational only. It must not // constrain the sparse-tree filtering as though it were a real year. return this.isCompositeYearRangePlaceholder(value) ? null : value; }); selects.forEach((select, partIndex) => { const selectedOtherParts = currentSelections.slice(); selectedOtherParts[partIndex] = null; const nativeOptionIndex = Number(nativeSelector.dataset.index) - 1; const otherNativeSelections = Array.from(this.optionSelectors).map((selector, selectorIndex) => { if (selector === nativeSelector) return null; // Ignore hidden Shopify selections for other composite groups. Their // native values may be Shopify's automatic first variant. The // generated controls below are the authoritative composite state. if (selector.dataset.compositeOption === 'true') return null; if (selector.dataset.selectorType === 'dropdown') { const selected = selector.querySelector('.custom-select__option[aria-selected="true"]'); return selected && selected.dataset.value !== '' ? selected.dataset.value : null; } const selected = selector.querySelector('input:checked'); return selected ? selected.value : null; }); const possibleNativeValues = nativeOptions.filter((nativeOption) => { const hasVariant = this.compositeVariants.some((variant) => { const variantNativeValue = variant?.options?.[nativeOptionIndex]; if (variantNativeValue !== nativeOption.value) return false; // Respect any ordinary Shopify option selections. const otherNativeOptionsMatch = otherNativeSelections.every((selectedValue, selectorIndex) => { if (!selectedValue) return true; return variant?.options?.[selectorIndex] === selectedValue; }); if (!otherNativeOptionsMatch) return false; // Respect generated selections from every composite option, // including partially selected composites. This is what makes // the sparse tree work across multiple Shopify options. return Array.from( this.querySelectorAll('.option-selector[data-composite-option="true"]') ).every((otherNativeSelector) => { const otherIndex = Number(otherNativeSelector.dataset.index) - 1; const otherContainer = this.querySelector( `.composite-option-pickers[data-composite-for="${CSS.escape(otherNativeSelector.dataset.index)}"]` ); if (!otherContainer) return true; const otherSelects = Array.from( otherContainer.querySelectorAll('.composite-option-select') ); const selectionsForOtherComposite = otherNativeSelector === nativeSelector ? selectedOtherParts : this.getCompositeSelection(otherNativeSelector); const otherNativeValue = variant?.options?.[otherIndex]; if (otherNativeValue == null) return false; const otherParts = this.getCompositeParts( otherNativeValue, otherNativeSelector.dataset.compositeDelimiter ); return selectionsForOtherComposite.every((selectedValue, componentIndex) => { if (!selectedValue) return true; const componentName = otherSelects[componentIndex]?.getAttribute('aria-label') || ''; return this.compositePartMatches( componentName, selectedValue, otherParts[componentIndex] ); }); }); }); return hasVariant; }); // Build two complete option sets: // 1. valid values, based on the current selections // 2. all values that exist in the product, regardless of the current selections // // Valid values are displayed first. Invalid values remain visible in // the dropdown so the customer can see the complete option tree. const allValues = new Map(); const validValues = new Map(); const partName = select.getAttribute('aria-label') || ''; const isYear = partName.trim().toLowerCase() === 'year'; const addPartValue = (map, part) => { if (part == null || part === '') return; if (isYear) { const rangeMatch = String(part).trim().match(/^(\d{4})\s*-\s*(\d{4})$/); if (rangeMatch) { const start = Number(rangeMatch[1]); const end = Number(rangeMatch[2]); for (let year = Math.max(start, end); year >= Math.min(start, end); year -= 1) { map.set(String(year), String(year)); } return; } } map.set(part, part); }; nativeOptions.forEach((nativeOption) => { addPartValue(allValues, nativeOption.parts[partIndex]); }); possibleNativeValues.forEach((nativeOption) => { addPartValue(validValues, nativeOption.parts[partIndex]); }); const previousValue = select.value; const previousYearRange = this.isCompositeYearRangePlaceholder(previousValue) ? this.getCompositeYearRangeFromPlaceholder(previousValue) : ''; select.innerHTML = ''; const placeholder = document.createElement('option'); placeholder.value = ''; placeholder.textContent = this.dataset.placeholderText || 'Please select'; select.appendChild(placeholder); const validSortedValues = Array.from(validValues.entries()); const invalidSortedValues = Array.from(allValues.entries()) .filter(([value]) => !validValues.has(value)); const sortValues = (a, b) => { if (isYear) return Number(b[0]) - Number(a[0]); return a[1].localeCompare(b[1], undefined, { numeric: true, sensitivity: 'base' }); }; validSortedValues.sort(sortValues); invalidSortedValues.sort(sortValues); const appendOption = ([value, label], invalid) => { const option = document.createElement('option'); option.value = value; option.textContent = label; if (invalid) { option.classList.add('composite-option-invalid'); option.dataset.compositeInvalid = 'true'; // Inline styling provides a fallback for browsers that do not // apply stylesheet rules to native

Off The Grid Surplus  |  SKU: OMWP122003BL34

Limited Life Time Warranty

We stand behind our products. Because of this, we now offer a limited lifetime warranty against manufacturing defects, to give you peace of mind. *Check Terms and Conditions.

Third Party Lead Times

THIRD PARTY PRODUCTS

Xtrusion Overland started supplying various brands that go hand in hand with our own products, to provide a complete service and help us grow. This includes products that are drop shipped. Please be advised that availability and lead times of those product are solely impacted by the third-party vendor's schedule. Many products are sold under an open inventory method, as the technology to sync inventories is limited, therefore please reach out if a specific product is needed by a specific date for us to double check with the supplier for you!

Any potential delay that occurs in the sourcing, processing, or drop shipment at the third party level will be communicated to the best of our ability to the customer.


ESTIMATED DATE  |  LEAD TIMES

The estimated lead time provided when you placed your order is JUST AN ESTIMATE. It is not a guarantee that the product will be shipped on that date. The estimated lead time gives you some sense of what the current times to source, cut, pack and ship takes. Many times the lead time can be shorter, but likewise it can extend, depending on factors such as previous demand and current supply. We manufacture many components in house, but extenuating circumstances such as down time for maintenance or unscheduled downtimes for repairs might impact dates as well as delays related to our sourced components. There is a constant flow of parts and shipments, and sometimes the visibility becomes a challenge to pinpoint exactly when your product will ship.

Feel free to check on your order at any time. Be aware that our customer service team may not be able to give you an exact date for completion but rest assured we are tracking your order and it will be built in the order that it was received. 

Xtrusion Overland also reserves the right to cancel items from an order, offer an alternative substitution or issue refund accordingly when product is delayed at an unacceptable time frame or at the request of the customer.

Description

The TB Pro Pant 2.0 is a great evolution of a quality staple in your travel wardrobe. Comfortable, durable, sharp-looking, and lots of utility define these go-anywhere, do-anything pants.

FEATURES:

✔️ Water Resistant Coating
✔️ 4-Way Stretch Fabric
✔️ Quick Dry
✔️ Durable & Lightweight
✔️ Crotch Gusset
✔️ Knife Clip Patch
✔️ Upper Hip "Mag Pockets" (both sides)
✔️ Dual Zip-Closure Leg Pockets
✔️ Unique Partial Rear Seat Stitch = MORE STRETCH THAN BEFORE
✔️ Increased Belt Loops Height

94% Nylon | 6% Spandex

FIT:

Tapered Fit Pants are form-fitting and narrow from the thigh to the ankle leg opening. 

Payment & Security

Payment methods

  • American Express
  • Apple Pay
  • Diners Club
  • Discover
  • Google Pay
  • Mastercard
  • PayPal
  • Shop Pay
  • Venmo
  • Visa

Your payment information is processed securely. We do not store credit card details nor have access to your credit card information.