# Components & Modules

# Class Patterns

You must follow a few coding guidelines depending on whether your component is a Top-Level component or a reusable/nestable one.

# Top-Level Components

For modules and top-level components that won't be nested inside other components:

  • ✅ Pick a unique top-level class name for the component for e.g. hero, collage, complexlist, timeline, etc.
  • ❌ Don't use hyphens - in the top level classes, if you need a separator use underscores _ instead
  • ✅ Inside the component's markup, use "free classes" like title, copy wrap, etc
  • ✅ When selecting elements for styling, keep the nesting level to maximum of (3) nested classes, preferably only 2. Media queries, pseudo-classes and pseudo-elements doesn't count towards this limit.
  • ❌ Don't try to mimic the HTML structure in SCSS, its rigid and only makes the nesting situation worse
  • ✅ Use -mycustom-variant at the component root for component-wide variant styles that affect multiple elements

Example:

// a top level unique name
.hero {
  // free...
  .wrap {

  }

  // classes...
  .slider {

  }

  // everywhere.
  .copy-content {
  }


  // override styles for nested components, in this case a CTA 
  .cta {
    &:hover {

    }
  }

  // max nesting of 3 classes
  .super .deep {
    &::after {
      @media (min-width: 600px) {
        // this is fine
      }
    }
  }

  &-light-variant {
    .overlay {
      // overlay overrides for light variant
    }

    .copy {
      // copy overrides for light variant
    }
  }
}

# Nestable Components (A.K.A. Widgets or Shared components)

Components that are meant to be reused in many areas, such as:

  • CTAs
  • Text styles
  • Booking/Reservation Widget
  • Forms
  • Form inputs
  • UI Widgets: Comboboxes, Sliders, Accordions, Tabs

Must use an adapted BEM (opens new window)-like naming scheme and rules:

  • ✅ Pick a unique top-level class name for the component for e.g. cta, tabs, booking_widget, etc.
  • ❌ Don't use hyphens - in the top level classes, if you need a separator use underscores _ instead
  • ✅ All classes used in the markup must be unique
  • ✅ Use hyphens - for sub elements and variants
  • ✅ Use SCSS &-sub-element syntax for easier coding

Example:


<button class="cta cta-style-a"></button>

<div class="tabs">
    <div class="tabs-header"></div>
    <div class="tabs-body">
        <div class="tabs-panel"></div>
        <div class="tabs-panel"></div>
    </div>
</div>
.cta {
  &-style-a {
  }

  &-style-b {
  }
}

.tabs {
  &-body {
  }

  &-panel {
  }

  &-header {
    // even though it looks nested, the CSS output of this is: .tabs-header.is-selected
    &.is-selected {
    }
  }
}

# State classes

Whenever you have different component states that depend on user ACF settings or that are activated at runtime via JavaScript, use state classes to facilitate communication and avoid CSS clashes:

.custom_select {
  .custom_select-item {
    &.is-selected { // is-selected, is-empty, is-open, is-closed etc
    }
  }
}

.mycomponent {
  &.has-items { // .has-thing, .has-mobile-version, .has-extended-description etc
  }
}

<div class="mycomponent {{ !empty($items) ? 'has-items' : '' }}">
    @foreach($items as $item) ...
</div>

# Reusable Components

# CTAs & Actions

Call-to-Actions are implemented in a way that allows the developer to focus on the design and placement of the CTA's and let the user define which action she wants to trigger when the CTA is clicked/pressed.

  • Use the Component: CTAs ACF Field Group and the partials.ctas Blade Template in places where you need one or two CTAs, or the Component: Single CTA/partials.cta for exactly one CTA
  • Use Component: Actions to add or remove actions, you will need to update the partials.cta blade template and add logic to support this new action

Add you CTA styles in theme/assets/scss/partials/ctas.scss.

# Image and Video

There are two included components for handling media:

  • Component: Image - used for displaying images only, it allows CMS authors to do art direction by selecting a different image for mobile devices
  • Component: Media - extends Component: Image to support video formats (HLS, Vimeo, YouTUbe)

Both are rendered using the partials.media blade template, so you can interchangeably use both fields for the most part.

Choosing between the two:

  • For scenarios where media is not going to be rendered as part of an interactive widget (e.g. carousels, tabs, accordions, etc), and where there is enough space for a Video to make sense (e.g. not a logo) use "Component: Media".

  • Otherwise, default to use "Component: Image" unless the requirements say otherwise.

In some scenarios you might want to use a vanilla Image field from ACF, these are only intended for cases where you are certain a mobile image will never be provided, for example, Instagram galleries.

# Media Inside Interactive Widgets

Sometimes the requirements will ask to support videos on interactive areas, more commonly in carousels or the Hero. When this is the case you must ensure that Videos are well-supported in the context of the interactive widget, for example, videos must

  • fully play before changing slides on an autorotating carousel
  • not start unless their slide is activated
  • pause if the user manually changes the slide. Test both autoplay and non-autoplay videos as they behave differently

# Galleries

Media Galleries such as those used for Collage modules should also use Component: Media or Component: Image as described above. The same rules apply: if the gallery has any interactivity that might conflict with vide content, use Component: Image in a Repeater field by default, unless someone asked for video support.

# Styling & CSS API

See theme/views/partials/media.blade.php and theme/assets/scss/partials/media.scss for information of the classes that are applied by default. You should create styles in such a way that allows for both Videos and Images without too much hassle. Follow these guidelines:

  • To apply styles to all media regardless of type, use .swmedia
  • To target only images in both desktop and mobile use .swmedia-img
  • To target only the user-defined mobile images use .swmedia-sm. Note that this should be rarely used if ever, since its possible for the desktop version to render on mobile if the user didn't select a mobile image.
    • It is best to just use media queries and target .swmedia-img, for e.g.:
    .swmedia-img {
      // mobile styles
      @include media-breakpoint-up($swmedia-lg-brekpoint) {
        // desktop styles
      }
    }
    
  • To target only videos, use .swmedia-video. Note that this won't target a video tag, but a div containing a video tag, or an iframe (if using YouTube/Vimeo embeds). Use .swmedia-video-native to only select the container div of videos using the native tag.
  • To apply object-fit and/or object-position use .swmedia-object. This targets the underlying img or video tag (if present), for .e.g:
      .some-outer-container .swmedia-object {
        object-fit: contain;
      }
    
    The outer container is "needed" for specificity if you want to target a single media in a scenario where you have many of them, DON'T do .mycustommedia.swmedia-object since depending on if a Video, or an Image is rendered the swmedia-object class will be at the root or nested inside supporting marking.
  • You can use pass a custom class to partials.media, which allows you to apply different styles to multiple images easily:
    .mycustommedia {
      // applies to mycustommedia on any media type
    }
    
    .mycustommedia.swmedia-img {
      // applies to mycustommedia only when its an image
    }
    
    .my_other_custommedia.swmedia-img {
      // applies to my_other_custommedia only when its an image
    }
    
    .mycustommedia.swmedia-video {
      // applies to mycustommedia only when it a video
    }
    
    .mycustommedia-container .swmedia-object {
      // applies to the media object (video or img tag) 
    }
    

# Forms & Validation

There are a few considerations when you are creating a form:

  • ✅ Wrap the form components inside <form class="needs-validation" novalidate></form> tags. Make sure to add a needs-validation class and a novalidate attribute. The novalidate attribute help to avoid the validation on submission disabling the browser default feedback tooltips.
  • ✅ Associate label (opens new window) correctly with the input via for and id to reap multiple benefits.
    <label for="first-name" class="form-label">First name</label>
    <input required
           type="text"
           value="Mark"
           class="form-control"
           id="first-name">
  • ✅ Use concatenation of the module id with the input/label id to make it uniq. e.g:
    <label for="{{ $id }}-name" class="form-label">First name</label>
    <input required
           type="text"
           value="Mark"
           class="form-control"
           id="{{ $id }}-name">
  • ✅ Add correct type depending on the data of the field text, password, email, etc.
  • ✅ Use autocomplete attribute taking in count the requirements, sometime it will necessary to use off for example in a "confirm new password" but other you will want to autofill inputs like address. More info (opens new window)
  • ✅ Add needed HTML5 validation attributes such as required, maxlength, etc.
  • ✅ For input validation add data-validatio-valid or data-validation-invalid to show feedback to the user when is valid or not, for e.g:
    <label for="{{ $id }}-validation-first-name" class="form-label">First name</label>
    <input required
           maxlength=""
           type="text"
           value="Mark"
           class="form-control"
           id="{{ $id }}-validation-first-name"
           data-validation-valid="Valid. Looks good!"
           data-validation-invalid="Please choose a valid first name.">
  • ❌ Avoid adding a click event handler to the submit button.
    <button class="btn btn-primary" onClick="submit()">Submit form</button>
  • ✅ Use a type submit button and init form validator e.g:
    <form class="needs-validation" novalidate>
        ... inputs
        <button class="btn btn-primary" type="submit">Submit form</button>
    </form>
    import formValidator from './shared/formValidator'
    
    const formEl = document.querySelector('.needs-validation')
    formValidator(formEl, () => { console.log('Make something when valid') }, () => { console.log('Make something when invalid') })

The formValidator comes from a script that helps to validate the form is an easy way. It is using part of the bootstrap logic(how it works (opens new window)) combined with a custom handler to avoid a11y issues. Where the goal is checking if the field is invalid, it appends an element and we associate this new element with the invalid field, letting the user know the reason of the invalidation(helped by the data-validation-invalid attribute and aria-describedby of the field).

You can take a look on file theme/assets/js/shared/formValidator.js which contains the init function and handler of individual form inputs.

# Modals

Use Bootstrap modals (opens new window) for all Modals.

Use swiper (opens new window) for all Carousels.