Javascript UI Elements & Classes

Theme Redone is a modular framework that doesn’t include redundant and unused files. It also doesn’t include libraries such as jQuery, or Bootstrap, to name a few, as we want to ship fast and performant websites.

However, it comes with a few Javascript classes responsible for building and interacting with repeating UI elements such as Tabs, Dropdowns, Accordions, Sliders, etc.

All of them are optional and their compiled scripts won’t be loaded or bundled unless the elements they are working with are present on the current page.


The most useful JS Classes (UI Elements), that we love using:


  • Tabs

    Let’s start with one of the most used UI elements on the internet; Tabs;

    Tabs – is a small Class that’s living in [theme-root]/src/js/lazily-loaded/Tabs.js file. Compiled and minified, it takes only about 680 bytes.


    Usage: 

    As stated, this Class won’t be imported on the Front End, unless there is at least one instance of tabs present on the page (HTML).

    We have a latte partial (embed), that makes it easier to create tabs. Here’s how to use it:

    {var $test_tabs = [
      [
        'anchor' => 'Tab 1',
        'content' => [
          'title' => 'Tab Panel 1 Title',
          'text'  => 'Tab Panel 1 text that is very long',
        ]
      ],
      [
        'anchor' => 'Tab 2',
        'content' => [
          'text'  => 'Tab Panel 2 text that is very long',
        ]
      ],
      [
        'anchor' => 'Tab 3',
        'content' => [
          'title' => 'Tab Panel 3 Title',
          'text'  => 'Tab Panel 3 text that is very long',
        ]
      ]
    ]}
    
    
    {embed 
      tr_part('_tabs'), 
      tabs: $test_tabs, 
      class: 'optional-test-class-2'
    }
      {block tab_anchor}
        {$ta_content['anchor']}
      {/block}
      {block tab_panel}
        <h3 
          n:if="!empty($tp_content['content']['title'])"
        >
          {$tp_content['content']['title']}
        </h3>
        <p n:ifcontent>
          {$tp_content['content']['text']}
        </p>
      {/block}
    {/embed}
    

    In case the syntax looks a bit confusing, here’s what’s happening.

    1. On line 25, we are using the {embed} tag to embed the _tabs partial. embed is pretty much the same as latte’s {include} tag, but it differs in a way that we can pass children elements besides props (we can wrap elements, or in this case, we wrap looped elements).
      On the same line, we are passing the $test_tabs array to the tabs property, and an “optional-test-class-2” to the class optional property.
    2. Then we have 2 blocks (tab_anchor, and tab_panel). These behave like slots in Vue for example. Whatever we put inside the {block tab_anchor} will render inside each tab’s anchor button, and the HTML placed inside {block tab_content} will render inside each tab’s panel. $ta_content and $tp_content are variables that represent each tab inside the loop, and those are defined in the _tabs partial.

    Here’s what the code for the actual tabs component looks like:

    {varType string $class} {* optional class to pass to the tabs element *}
    {varType array $tabs} {* $tabs array. Required. *}
    
    <div
      class="tabs {$class ?? ''}"
      n:if="!empty($tabs)"
    >
    
      <div class="tabs__nav">
    
        <button
          n:foreach="$tabs as $ta_key => $ta_content"
          type="button"
          class="tab-anchor {$ta_key == 0 ? 'activeTab' : ''}"
          data-href="panel-{$ta_key}"
        >
          {block tab_anchor}{/block}
        </button>
    
      </div>{* nav *}
    
      <div class="tabs__content">
    
        <div
          class="tab-panel {$tp_key == 0 ? 'activeTab enter' : ''}"
          data-id="panel-{$tp_key}"
          n:foreach="$tabs as $tp_key => $tp_content"
        >
          <div class="tab-panel__content">
            {block tab_panel}{/block}
          </div>{* panel-content *}
        </div>{* panel *}
    
      </div>{* content *}
    
    </div>{* tabs *}
    

    The code should be self-explanatory, and now it should be easier to understand what happens behind the scenes. Even if it is perfectly clear, we suggest that you read latte’s docs about the {embed}, {block}, and {include} on this page in latte’s official docs.


    Slider

    We have a Slider class that’s very versatile and made on top of the awesome and lightweight Embla carousel. Embla is a very well-written, small, and extensible Carousel library, and you are in control of extending it and using it however you want, without a lot of redundant Kilobytes.

    Slider – lives in [theme-root]/src/js/classes/global/UI/Slider.js file. Compiled and minified, it takes only about 30 kilobytes.


    Usage: 

    As stated, this Class won’t be imported on the Front End, unless there is at least one instance of Embla slider present on the page (HTML).

    The Slider file mentioned above is responsible for its logic, but to define and use sliders, we would use the InitSliders class that lives in [theme-root]/src/js/lazily-loaded/InitSliders.js file.

    As opposed to how we use Tabs (where we only need to use and modify the HTML (latte embed)), in the case of sliders, apart from the HTML, we also need to define settings for each of the sliders and init them.

    Let’s first start with the HTML (latte embed) part.

    {var $test_slides= [
      [
        'anchor' => './path-to-image/1.png',
        'alt'    => 'Alt Text 1'
      ],
      [
        'anchor' => './path-to-image/2.png',
        'alt'    => 'Alt Text 2'
      ],
      [
        'anchor' => './path-to-image/3.png',
        'alt'    => 'Alt Text 3'
      ],  
      [
        'anchor' => './path-to-image/4.png',
        'alt'    => 'Alt Text 5'
      ],
      [
        'anchor' => './path-to-image/5.png',
        'alt'    => 'Alt Text 5'
      ],
      [
        'anchor' => './path-to-image/6.png',
        'alt'    => 'Alt Text 6'
      ]
    ]}
    
    {* First Slider Example*}
    {embed tr_part('_slider'), slides: $test_slides, class: 'slider--test'}
      {block slide}
        <div style="box-shadow: 0 0 0 1px red;">
          <h5 n:ifcontent>{$s_key} - {$s_content['alt']}</h5>
          <img src="{$s_content['src']}" alt="{$s_content['alt']}" />
        </div>
      {/block}
    {/embed}
    
    {* Second Slider Example*}
    {embed tr_part('_slider'), slides: $test_slides, class: 'slider--test2'}
      {block slide}
        <div style="box-shadow: 0 0 0 1px red;">
          <h5 n:ifcontent>{$s_key} - {$s_content['alt']}</h5>
          <img src="{$s_content['src']}" alt="{$s_content['alt']}" />
        </div>
      {/block}
    {/embed}
    

    From the code above we can see that the structure is similar to Tabs, but a bit simpler as we only have one block {block slide} as behind the scenes only one loop is used.
    We also have two sliders that we will define the logic for.

    And just for reference, here’s what the code for the _slider component looks like:

    {varType string $class} {* optional class to pass to the slider wrap element *}
    {varType array $slides} {* $slides array. Required. *}
    
    <div
      n:if="!empty($class) && !empty($slides)"
      class="slider-wrap"
    >
      <div class="embla {$class}">
        <div class="embla__container">
          <div
            class="embla__slide"
            n:foreach="$slides as $s_key => $s_content"
          >
            <div class="embla__slide__inner">
              {block slide}{/block}
            </div>{* slinner *}
          </div>{* slide *}
        </div>{* cont *}
      </div>{* embla *}
    
      <div class="embla__buttons">
        <button
          class="embla__btn embla__btn-prev"
          type="button"
          aria-label="Go to previous slide"
        ></button>
        <div class="embla__dots"></div>
        <button
          class="embla__btn embla__btn-next"
          type="button"
          aria-label="Go to next slide"
        ></button>
      </div>{* embla__buttons *}
    
    </div>{* wrap *}
    

    And to define how the sliders should look and behave and init them, this is the code we would use inside the mentioned InitSliders file:

    import { Slider } from '../classes/global/UI/Slider'
    
    class InitSliders {
      constructor() {
        this.init()
      }
    
      init() {
        new Slider('.slider--test', {
          arrowsDisplay: false,
          dotsDisplay: false,
          wrapSlides: true,
          slideWidth: `${100 / 3}%`,
          draggable: false,
          gap: 'clamp(20px, 5.21vw, 100px)',
          marginBottom: 'clamp(20px, 5.21vw, 100px)',
          responsive: {
            999: {
              slidesToScroll: 2,
              dotsDisplay: true,
              arrowsDisplay: true,
              wrapSlides: false,
              draggable: true,
              marginBottom: 0,
              slideWidth: '50%',
              speed: 3
            },
            767: {
              slidesToScroll: 1,
              slideWidth: '80%'
            }
          }
        })
    
        new Slider('.slider--test2', {
          slideWidth: '30%',
          autoPlayInterval: 2000,
          gap: '50px',
          dragFree: true,
          dotsDisplay: false,
          arrowsDisplay: false,
          loop: true,
          autoplay: true,
          responsive: {
            999: {
              gap: '10px',
              slideWidth: '50%',
              autoPlayInterval: 5000,
              dotsDisplay: false,
              arrowsDisplay: true,
              speed: 3
            },
            700: {
              gap: '20px',
              slideWidth: '100%',
              autoPlayInterval: 1000,
              marginBottom: 30
            }
          }
        })
      }
    }
    
    new InitSliders()
    

    The code should be self-explanatory, and it has a format like most of the popular Carousel libraries, however, there are a few things we would like to explain.

    • In case you have multiple same sliders (with the same class name) on the same page, you don’t need to loop them and call the new Slider(…) for each of them, that is handled internally.
    • Settings properties:
      • slidesToScroll – How many slides to scroll.  Type: number (can be 2, or 2.5 for example), Default: 1
      • align – Aligns the slides relative to the carousel viewport. Type: string (‘start’ | ‘center’ | ‘end’) or number (between 0 and 1, where 1 is 100%), Default: ‘start’
      • slideWidth – Defines the width of a single slide. Type: string (for example: ‘100%’, or ‘200px’, ’10vw’, ‘calc(100% / 3)’), Default: ‘50%’. TODO: check clamp and calc…
      • speed – Adjust scroll speed (higher numbers enable faster scrolling). Type: number, Default: 10
      • dotsDisplay – Whether or not to show dot navigation. Type: boolean, Default: true
      • dotsDisplay – Whether or not to show arrows navigation. Type: boolean, Default: true
      • draggable– Whether or not the slides should be draggable. Type: boolean, Default: true
      • wrapSlides – If set to true, will disable all the slider functionality, and render it as a flexbox grid with flex-wrap: wrap; instead. Useful when you want to show the grid on large screens, and slider on mobile phones. Will take into account slideWidth and gap properties. Type: boolean, Default: false
      • gap – Defines horizontal space between slides. Type: string (for example: ’20px’, ‘clamp(20px, 5.21vw, 100px)’, …), Default: 0. TODO: Check if numbers work
      • marginBottom – This is intended to be used with wrapSlides enabled.  Default: 0
      • loop – Enables/disables infinite looping…   Type: boolean, Default: false
      • containScroll – Clear leading and trailing empty space that causes excessive scrolling. Type: string (‘trimSnaps’ | ‘keepSnaps’), Default: ‘trimSnaps’
      • dragFree – Enables momentum scrolling. Type: boolean, Default: false
      • autoplay – Whether or not to autoplay. Type: boolean, Default: false
      • autoPlayInterval – If autoplay is set to true, this is used. Default is 2000
      • responsive – Accepts all of the above, as seen in the example code. (Optional).

    Collapsible

    Collapsible – is the most versatile Class that Theme Redone uses. It’s used to build Naw Walker Dropdowns, General Dropdowns, Dropdown Selects, and Accordions. It lives in [theme-root]/src/js/lazily-loaded/Collapsible.js file. Compiled and minified, it takes only about 7KB.

    The Collapsible class, behind the scenes, utilizes the Web Animation API. It makes it possible to create smoother and interruptible animations. (Behaves very similar to the Spring physics based animations). One thing worth noting is that this approach made it a breeze working with dynamic heights (Something that a lot of accordions examples online struggle with).

    This class is also used for the menus dropdowns in Theme Redone.


    Usage: 

    As stated, this Class won’t be imported on the Front End, unless there is at least one instance of it present on the page (HTML).

    We have a few latte partials (embeds), that make it easier to create collapsibles (dropdowns, accordions, and dropdown selects). Here’s what these components look like and how to use them:


    Dropdown

    Latte embed looks like this:

    {varType string $class} {* optional class to pass to the collapsible element *}
    {varType string $aria_label} {* optional aria label*}
    {varType string $duration} {* optional duration - default is 300 *}
    {varType bool $close_outside} {* if trigger mode is set to click (default), enabling this will make it close on click outside *}
    {varType bool $is_absolute} {* by default, .collapsible__content is relative. This makes it absolute if set to true *}
    {varType bool $on_hover} {* default trigger mode is on click. If this is set to true, hover will be used instead (on devices that support hover, others will fallback to click) *}
    {varType array $custom_keyframes} {* by default only height is animated. This makes it possible to animate more properties *}
    {varType string $easing} {* default easing is 'ease-in-out'. This can be used to overwrite it *}
    
    <div
      class="collapsible {$class ?? ''} {if !empty($is_absolute)}collapsible--absolute{/if}"
      data-duration="{!empty($duration) ? $duration : '300'}"
      {if $close_outside ?? false}
        data-close-on-outside-click
      {/if}
      {if $on_hover ?? false}
        data-hover-trigger
      {/if}
      {if !empty($custom_keyframes)}
        data-keyframes="{json_encode($custom_keyframes)}"
      {/if}
      {if !empty($easing)}
        data-easing="{$easing}"
      {/if}
    >
      <button
        class="collapsible__trigger"
        type="button"
        aria-label="{!empty($aria_label) ? $aria_label : 'Toggle Dropdown'}"
      >
        {block collapsible_trigger}{/block}
        <span class="chevron"></span>
      </button>
      <div class="collapsible__content">
        <div class="collapsible__content__inner">
          {block collapsible_content}{/block}
        </div>{* inner *}
      </div>{* content *}
    </div>{* collapsible *}
    

    We would use this component like this for example

    {embed
      tr_part('_collapsible'),
      close_outside: true
    }
      {block collapsible_trigger}
        Regular Click Dropdown
      {/block}
      {block collapsible_content}
        <h5>Will close on outside click</h5>
        <p>
          This is the content of the dropdown
        </p>
      {/block}
    {/embed}
    

    Accordion

    The accordion component is a wrapper (nesting multiple accordions is supported) for multiple Dropdowns with some of the Dropdown’s properties stripped.

    Here’s how it looks:

    {varType string $class} {* optional class to pass to the accordion element *}
    {varType string $aria_label} {* optional aria-label to apply to each accordion item (dropdown) *}
    {varType string $duration} {* optional duration - default is 300 *}
    {varType string $easing} {* default easing is 'ease-in-out'. This can be used to overwrite it *}
    {varType array $items} {* Accordion $items array. Required. *}
    {varType bool $collapse_siblings} {* Whether or not opening one accordion items should close siblings. Default is falses *}
    {varType int $initially_open_item} {* By default all accordion items are closed. This can be used, and index passed to make one open by default *}
    
    <div
      class="accordion {$class ?? ''}"
      {if $collapse_siblings ?? false}
        data-collapse-siblings
      {/if}
      data-duration="{!empty($duration) ? $duration : '300'}"
      n:if="!empty($items)"
      {if !empty($easing)}
        data-easing="{$easing}"
      {/if}
    >
      {foreach $items as $index => $item}
        {var $is_initially_open = isset($initially_open_item) && $index === $initially_open_item}
        {var $aria_label_text = !empty($aria_label) ? $aria_label : 'Toggle Accordion Item'}
    
        <div
          class="collapsible"
          {if $is_initially_open}
            data-initially-open
          {/if}
        >
          <button
            class="collapsible__trigger"
            type="button"
            aria-label="{$aria_label_text}"
          >
            {block acc_trigger}{/block}
            <span class="chevron"></span>
          </button>
          <div class="collapsible__content">
            <div class="collapsible__content__inner">
              {block acc_content}{/block}
            </div>{* inner *}
          </div>{* content *}
        </div>{* collapsible *}
      {/foreach}
    </div>{* accordion *}
    

    And here’s how to use it:

    {var $acc_items = [
      [
        'anchor' => 'Accordion Item 1',
        'content' => [
          'title' => 'Accordion Item Content 1 Title',
          'text'  => 'Accordion Item Content 1 text that is very long',
        ]
      ],
      [
        'anchor' => 'Accordion Item 2',
        'content' => [
          'text'  => 'Accordion Item Content 2 text that is very long',
        ]
      ],
      [
        'anchor' => 'Accordion Item 3',
        'content' => [
          'title' => 'Accordion Item Content 3 Title',
          'text'  => 'Accordion Item Content 3 text that is very long',
        ]
      ]
    ]}
    
    {embed
      tr_part('_accordion'),
      items: $test_acc_items,
      initially_open_item: 0,
      collapse_siblings: true
    }
      {block acc_trigger}
        <h5>
          <strong>{$item['anchor']}</strong> ✅
        </h5>
      {/block}
      {block acc_content}
        <span>
          <div class="test-custom-class">
            <h4 n:if="!empty($item['content']['title'])">{$item['content']['title']}</h4>
            <p n:ifcontent>{$item['content']['text']}</p>
          </div>
        </span>
      {/block}
    {/embed}
    

    Dropdown Select

    Can be used as a replacement for the browser’s default <select> element.

    Here’s what the component’s code looks like:

    {varType string $class} {* optional class to pass to the dropdown-select element *}
    {varType string $aria_label} {* optional aria label*}
    {varType string $duration} {* optional duration - default is 300 *}
    {varType bool $close_outside} {* if trigger mode is set to click (default), enabling this will make it close on click outside *}
    {varType bool $is_absolute} {* by default, .collapsible__content is relative. This makes it absolute if set to true *}
    {varType bool $on_hover} {* default trigger mode is on click. If this is set to true, hover will be used instead (on devices that support hover, others will fallback to click) *}
    {varType string $easing} {* default easing is 'ease-in-out'. This can be used to overwrite it *}
    
    {varType array $options} 
    {* 
    options associative array. 
    Must be in this format: 
    [
      [
        'value' => 'value_here',
        'label' => 'Label Here'
      ],
      ...
    ] 
    *}
    
    {varType int $default_selected_key} {* by default, first item (index 0 is selected). This can be used to set a different one *}
    
    
    <div
      n:if="!empty($options)"
      class="collapsible {$class ?? ''} {if !empty($is_absolute)}collapsible--absolute{/if}"
      data-select
      data-duration="{!empty($duration) ? $duration : '300'}"
      {if $close_outside ?? false}
        data-close-on-outside-click
      {/if}
      {if $on_hover ?? false}
        data-hover-trigger
      {/if}
      {if !empty($easing)}
        data-easing="{$easing}"
      {/if}
    >
      <button
        class="collapsible__trigger"
        type="button"
        aria-label="{!empty($aria_label) ? $aria_label : 'Toggle Options'}"
      >
        <span class="collapsible__select-current"></span>
        <span class="chevron"></span>
      </button>
      <div class="collapsible__content">
        <div class="collapsible__content__inner">
          {spaceless}
          <button
            n:foreach="$options as $option"
            type="button"
            class=
              "collapsible__option
              {if
                (isset($default_selected_key) && $default_selected_key === $iterator->counter0)
              }
                picked
              {elseif !isset($default_selected_key) && $iterator->first}
                picked
              {/if}"
              data-value="{$option['value']}"
            >
              {$option['label']}
            </button>
            {/spaceless}
        </div>{* inner *}
      </div>{* content *}
    </div>{* collapsible *}
    

    And here’s how to use it:

    {var $dd_options = [
      [
        'value' => 'none',
        'label' => 'Select Option',
      ],
      [
        'value' => 'option-1',
        'label' => 'Option 1',
      ],
      [
        'value' => 'option-2',
        'label' => 'Option 2',
      ],
      [
        'value' => 'option-3',
        'label' => 'Option 3',
      ],
      [
        'value' => 'option-4',
        'label' => 'Option 4',
      ],
    ]}
    
    {include 
      tr_part('dropdown-select'), 
      options: $dd_options
    }
    
    
    {* Or, with a few customizations *}
    {include 
      tr_part('dropdown-select'), 
      options: $dd_options,
      class: 'custom-class',
      duration: 200,
      close_outside: true,
      default_selected_key: 3
    }
    

    This page is not 100% finished. Theme Redone has a few more useful JS Classes that we will write about soon.

Join Our Newsletter

To get updates about Theme Redone