diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss
index a7e94835bda63f3bc89f6653a31d72e6cb19b395..1e9c68d583488aa03d42fa725af54ca3c34060d0 100644
--- a/lms/static/sass/features/_course-experience.scss
+++ b/lms/static/sass/features/_course-experience.scss
@@ -301,6 +301,119 @@
   }
 }
 
+// Course outline for visual progress waffle switch
+.course-outline-visualprogress {
+  .block-tree {
+    margin: 0;
+    padding: 0;
+    list-style-type: none;
+
+    .section {
+      @include media-breakpoint-up(md) {
+        margin: 0;
+      }
+
+      margin: 0 (-1 * $baseline) 0 ($baseline);
+      width: calc(100% + (2));
+      padding: 0;
+
+      border-bottom: 1px solid $border-color;
+
+      .section-name {
+        padding: ($baseline / 2) 0 ($baseline / 2) 0;
+
+        .section-title {
+          font-weight: $font-bold;
+          font-size: 1.1rem;
+          margin: 0;
+          display: inline;
+          padding-left: $baseline;
+        }
+      }
+
+      .outline-item {
+        @include padding-left(0);
+      }
+
+      ol.outline-item {
+        margin: 0;
+
+        .subsection {
+          list-style-type: none;
+          border-top: 1px solid $border-color;
+          margin: 0 0 ($baseline / 4) 35px;
+
+          .subsection-title {
+            margin: 0;
+            font-weight: $font-bold;
+            margin-left: $baseline;
+          }
+
+          .subsection-text {
+            .details {
+              font-size: $body-font-size;
+              color: theme-color("secondary");
+              margin-left: 35px;
+            }
+            .prerequisite {
+              color: theme-color("secondary");
+              font-weight: $font-bold;
+            }
+          }
+
+          .vertical {
+            @include margin-left(10px);
+
+            list-style-type: none;
+            border: 1px solid transparent;
+            border-radius: 2px;
+
+            a.outline-item {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              padding: ($baseline / 2) 0 ($baseline / 2) 0;
+              margin: 0 0 0 ($baseline);
+              border-top: 1px solid $border-color;
+            }
+
+            &:hover,
+            &:focus {
+              background-color: palette(primary, x-back);
+              border-radius: $btn-border-radius;
+              text-decoration: none;
+            }
+
+            .vertical-actions {
+              .resume-right {
+                position: relative;
+                top: calc(50% - (#{$baseline} / 2));
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// Course outline accordion
+button.accordion-trigger {
+  margin: 2px;
+  padding: 10px 0 10px 0;
+  border: none;
+  width: 100%;
+  text-align: left;
+
+  .fa {
+    color: $blue;
+  }
+}
+
+.accordion-panel.is-hidden {
+  display: none;
+}
+
 // Course outline
 .course-outline {
   .block-tree {
diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
index 08c34978fbd2db8fdb528fb0585be82f3a85664d..9fa21293e14698378bc0c9710f9ab25f6f7299bf 100644
--- a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
+++ b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js
@@ -4,7 +4,7 @@ import { keys } from 'edx-ui-toolkit/js/utils/constants';
 
 // @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window'
 export class CourseOutline {  // eslint-disable-line import/prefer-default-export
-  constructor() {
+  constructor(newCourseOutlineEnabled) {
     const focusable = [...document.querySelectorAll('.outline-item.focusable')];
 
     focusable.forEach(el => el.addEventListener('keydown', (event) => {
@@ -33,5 +33,37 @@ export class CourseOutline {  // eslint-disable-line import/prefer-default-expor
         );
       }),
     );
+
+    // TODO: EDUCATOR-2283 Remove check for waffle flag after it is turned on.
+    if (newCourseOutlineEnabled) {
+      [...document.querySelectorAll(('.accordion'))]
+        .forEach((accordion) => {
+          const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger'));
+
+          sections.forEach(section => section.addEventListener('click', (event) => {
+            const sectionToggleButton = event.currentTarget;
+            const $toggleButtonChevron = $(sectionToggleButton).children('.fa-chevron-right');
+
+            if (sectionToggleButton.classList.contains('accordion-trigger')) {
+              const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true';
+              const $contentPanel = $(document.getElementById(sectionToggleButton.getAttribute('aria-controls')));
+
+              if (!isExpanded) {
+                $contentPanel.slideDown();
+                $contentPanel.removeClass('is-hidden');
+                $toggleButtonChevron.addClass('fa-rotate-90');
+                sectionToggleButton.setAttribute('aria-expanded', 'true');
+              } else if (isExpanded) {
+                $contentPanel.slideUp();
+                $contentPanel.addClass('is-hidden');
+                $toggleButtonChevron.removeClass('fa-rotate-90');
+                sectionToggleButton.setAttribute('aria-expanded', 'false');
+              }
+
+              event.stopImmediatePropagation();
+            }
+          }));
+        });
+    }
   }
 }
diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html
new file mode 100644
index 0000000000000000000000000000000000000000..b6cfa9ee922ad708fd20dbb57bb02c9f2083f19a
--- /dev/null
+++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html
@@ -0,0 +1,180 @@
+## mako
+
+<%page expression_filter="h"/>
+
+<%namespace name='static' file='../static_content.html'/>
+
+<%!
+from datetime import date
+
+from django.utils.translation import ugettext as _
+
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+<main role="main" class="course-outline-visualprogress" id="main" tabindex="-1">
+    % if blocks.get('children'):
+        <ol class="block-tree accordion" role="presentation">
+            % for section in blocks.get('children'):
+                <li
+                    class="outline-item section"
+                    role="heading"
+                >
+                    <button class="section-name accordion-trigger"
+                            aria-expanded="false"
+                            aria-controls="${ section['id'] }_contents"
+                            id="${ section['id'] }">
+                        <span class="fa fa-chevron-right" aria-hidden="true"></span>
+                        <h3 class="section-title">${ section['display_name'] }</h3>
+                    </button>
+                    <ol class="outline-item accordion-panel is-hidden"
+                        id="${ section['id'] }_contents"
+                        role="region"
+                        aria-labelledby="${ section['id'] }"
+                    >
+                        % for subsection in section.get('children', []):
+                            <%
+                                gated_subsection = subsection['id'] in gated_content
+                                completed_prereqs = gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False
+                            %>
+                            <li class="subsection accordion ${ 'current' if subsection['resume_block'] else '' }" role="heading">
+                                % if gated_subsection and not completed_prereqs:
+                                    <button class="subsection-text accordion-trigger"
+                                            id="${ subsection['id'] }"
+                                    >
+                                % else:
+                                    <button class="subsection-text accordion-trigger"
+                                            id="${ subsection['id'] }"
+                                            aria-expanded="false"
+                                            aria-controls="${ subsection['id'] }_contents"
+                                    >
+                                % endif
+                                    ## Subsection title
+                                    % if gated_subsection:
+                                        % if completed_prereqs:
+                                            <span class="menu-icon icon fa fa-unlock"
+                                                aria-hidden="true">
+                                            </span>
+                                            <span class="subsection-title">
+                                                ${ subsection['display_name'] }
+                                            </span>
+                                            <span class="sr">&nbsp;${_("Unlocked")}</span>
+                                        % else:
+                                            <span class="menu-icon icon fa fa-lock"
+                                            aria-hidden="true">
+                                            </span>
+                                            <span class="subsection-title">
+                                                ${ subsection['display_name'] }
+                                            </span>
+                                            <div class="details prerequisite">
+                                                ${ _("Prerequisite: ") }
+                                                    <%
+                                                        prerequisite_id = gated_content[subsection['id']]['prerequisite']
+                                                        prerequisite_name = xblock_display_names.get(prerequisite_id)
+                                                    %>
+                                                    ${ prerequisite_name }
+                                            </div>
+                                        % endif
+                                    % else:
+                                        <span class="fa fa-chevron-right" aria-hidden="true"></span>
+                                        <span class="subsection-title">
+                                            ${ subsection['display_name'] }
+                                        </span>
+                                    % endif
+                                    <div class="details">
+
+                                        ## There are behavior differences between rendering of subsections which have
+                                        ## exams (timed, graded, etc) and those that do not.
+                                        ##
+                                        ## Exam subsections expose exam status message field as well as a status icon
+                                        <%
+                                            if subsection.get('due') is None:
+                                                # examples: Homework, Lab, etc.
+                                                data_string = subsection.get('format')
+                                            else:
+                                                if 'special_exam_info' in subsection:
+                                                    data_string = _('due {date}')
+                                                else:
+                                                    data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
+                                        %>
+                                        % if subsection.get('format') or 'special_exam_info' in subsection:
+                                        <span class="subtitle">
+                                            % if 'special_exam' in subsection:
+                                                ## Display the exam status icon and status message
+                                                <span
+                                                    class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
+                                                    aria-hidden="true"
+                                                ></span>
+                                                <span class="subtitle-name">
+                                                    ${subsection['special_exam_info'].get('short_description', '')}
+                                                </span>
+
+                                                ## completed exam statuses should not show the due date
+                                                ## since the exam has already been submitted by the user
+                                                % if not subsection['special_exam_info'].get('in_completed_state', False):
+                                                    <span
+                                                        class="localized-datetime subtitle-name"
+                                                        data-datetime="${subsection.get('due')}"
+                                                        data-string="${data_string}"
+                                                        data-timezone="${user_timezone}"
+                                                        data-language="${user_language}"
+                                                    ></span>
+                                                % endif
+                                            % else:
+                                                ## non-graded section, we just show the exam format and the due date
+                                                ## this is the standard case in edx-platform
+                                                <span
+                                                    class="localized-datetime subtitle-name"
+                                                    data-datetime="${subsection.get('due')}"
+                                                    data-string="${data_string}"
+                                                    data-timezone="${user_timezone}"
+                                                    data-language="${user_language}"
+                                                ></span>
+
+                                                % if subsection.get('graded'):
+                                                    <span class="sr">&nbsp;${_("This content is graded")}</span>
+                                                % endif
+                                            % endif
+                                        </span>
+                                        % endif
+                                  </div> <!-- /details -->
+                                </button> <!-- /subsection-text -->
+                                % if not gated_subsection or (gated_subsection and completed_prereqs):
+                                    <ol class="outline-item accordion-panel is-hidden"
+                                             id="${ subsection['id'] }_contents"
+                                             role="region"
+                                             aria-labelledby="${ subsection['id'] }"
+                                    >
+                                    % for vertical in subsection.get('children', []):
+                                        <li class="vertical outline-item focusable">
+                                            <a
+                                              class="outline-item focusable"
+                                              href="${ vertical['lms_web_url'] }"
+                                              id="${ vertical['id'] }"
+                                            >
+                                                <div class="vertical-details">
+                                                    <span class="vertical-title">
+                                                        ${ vertical['display_name'] }
+                                                    </span>
+                                                </div>
+                                            </a>
+                                        </li>
+                                    % endfor
+                                    </ol>
+                                % endif
+                            </li>
+                        % endfor
+                    </ol>
+                </li>
+            % endfor
+        </ol>
+    % endif
+</main>
+
+<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
+    DateUtilFactory.transform('.localized-datetime');
+</%static:require_module_async>
+
+<%static:webpack entry="CourseOutline">
+    new CourseOutline('.block-tree', true);
+</%static:webpack>
diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html
similarity index 99%
rename from openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
rename to openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html
index 03a69f176952a4434efb17efe2b7b5db7049fbe4..0ec6d43a164b8f0dddb8982affd79db290417cb9 100644
--- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html
+++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html
@@ -46,7 +46,7 @@ from openedx.core.djangolib.markup import HTML, Text
                                                     </span>
                                                     <span class="sr">&nbsp;${_("Unlocked")}</span>
                                                 % else:
-                                                    <span class="menu-icon icon fa fa-lock" 
+                                                    <span class="menu-icon icon fa fa-lock"
                                                     aria-hidden="true">
                                                     </span>
                                                     <span class="subsection-title-name">
@@ -147,5 +147,5 @@ from openedx.core.djangolib.markup import HTML, Text
 </%static:require_module_async>
 
 <%static:webpack entry="CourseOutline">
-    new CourseOutline('.block-tree');
+    new CourseOutline('.block-tree', false);
 </%static:webpack>
diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py
index 59d1d2a0b1b52aedeb7026c9f55688e362fffc11..7822f6cd11bb6698fed561f414ed81c292ddae23 100644
--- a/openedx/features/course_experience/views/course_outline.py
+++ b/openedx/features/course_experience/views/course_outline.py
@@ -1,6 +1,8 @@
 """
 Views to show a course outline.
 """
+import re
+
 from django.template.context_processors import csrf
 from django.template.loader import render_to_string
 from opaque_keys.edx.keys import CourseKey
@@ -8,10 +10,10 @@ from web_fragments.fragment import Fragment
 
 from courseware.courses import get_course_overview_with_access
 from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
+from openedx.features.course_experience import waffle as waffle
 
 from ..utils import get_course_outline_block_tree
 from util.milestones_helpers import get_course_content_milestones
-from xmodule.modulestore.django import modulestore
 
 
 class CourseOutlineFragmentView(EdxFragmentView):
@@ -30,28 +32,85 @@ class CourseOutlineFragmentView(EdxFragmentView):
         if not course_block_tree:
             return None
 
-        content_milestones = self.get_content_milestones(request, course_key)
-
         context = {
             'csrf': csrf(request)['csrf_token'],
             'course': course_overview,
-            'blocks': course_block_tree,
-            'gated_content': content_milestones
+            'blocks': course_block_tree
         }
-        html = render_to_string('course_experience/course-outline-fragment.html', context)
-        return Fragment(html)
+
+        # TODO: EDUCATOR-2283 Remove this check when the waffle flag is turned on in production
+        if waffle.new_course_outline_enabled(course_key=course_key):
+            xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
+
+            gated_content = self.get_content_milestones(request, course_key)
+
+            context['gated_content'] = gated_content
+            context['xblock_display_names'] = xblock_display_names
+
+            # TODO: EDUCATOR-2283 Rename this file to course-outline-fragment.html
+            html = render_to_string('course_experience/course-outline-fragment-new.html', context)
+            return Fragment(html)
+        else:
+            content_milestones = self.get_content_milestones_old(request, course_key)
+
+            context['gated_content'] = content_milestones
+
+            # TODO: EDUCATOR-2283 Remove this file
+            html = render_to_string('course_experience/course-outline-fragment-old.html', context)
+            return Fragment(html)
+
+    def create_xblock_id_and_name_dict(self, course_block_tree, xblock_display_names=None):
+        """
+        Creates a dictionary mapping xblock IDs to their names, using a course block tree.
+        """
+        if xblock_display_names is None:
+            xblock_display_names = {}
+
+        if course_block_tree.get('id'):
+            xblock_display_names[course_block_tree['id']] = course_block_tree['display_name']
+
+        if course_block_tree.get('children'):
+            for child in course_block_tree['children']:
+                self.create_xblock_id_and_name_dict(child, xblock_display_names)
+
+        return xblock_display_names
 
     def get_content_milestones(self, request, course_key):
         """
         Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
         """
+        def _get_key_of_prerequisite(namespace):
+            return re.sub('.gating', '', namespace)
+
+        all_course_milestones = get_course_content_milestones(course_key)
+
+        uncompleted_prereqs = {
+            milestone['content_id']
+            for milestone in get_course_content_milestones(course_key, user_id=request.user.id)
+        }
+
+        gated_content = {
+            milestone['content_id']: {
+                'completed_prereqs': milestone['content_id'] not in uncompleted_prereqs,
+                'prerequisite': _get_key_of_prerequisite(milestone['namespace'])
+            }
+            for milestone in all_course_milestones
+        }
+
+        return gated_content
+
+    # TODO: EDUCATOR-2283 Remove this function when the visual progress waffle flag is turned on in production
+    def get_content_milestones_old(self, request, course_key):
+        """
+        Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
+        """
 
         all_course_prereqs = get_course_content_milestones(course_key)
 
-        content_ids_of_unfulfilled_prereqs = [
+        content_ids_of_unfulfilled_prereqs = {
             milestone['content_id']
             for milestone in get_course_content_milestones(course_key, user_id=request.user.id)
-        ]
+        }
 
         course_content_milestones = {
             milestone['content_id']: {
diff --git a/openedx/features/course_experience/waffle.py b/openedx/features/course_experience/waffle.py
new file mode 100644
index 0000000000000000000000000000000000000000..3de0b966414aaa54914855b031bd6b91882c7612
--- /dev/null
+++ b/openedx/features/course_experience/waffle.py
@@ -0,0 +1,63 @@
+"""
+This module contains various configuration settings via
+waffle switches for the course experience app.
+"""
+from __future__ import unicode_literals
+
+from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
+from openedx.core.djangoapps.theming.helpers import get_current_site
+from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace
+
+# Namespace
+WAFFLE_NAMESPACE = 'course_experience'
+
+# Switches
+# Full name course_experience.enable_new_course_outline
+# Enables the UI changes to the course outline for all courses
+ENABLE_NEW_COURSE_OUTLINE = 'enable_new_course_outline'
+
+# Full name course_experience.enable_new_course_outline_for_course
+# Enables the UI changes to the course outline for a course
+ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE = 'enable_new_course_outline_for_course'
+
+# Full name course_experience.enable_new_course_outline_for_site
+# Enables the UI changes to the course outline for a site configuration
+ENABLE_NEW_COURSE_OUTLINE_FOR_SITE = 'enable_new_course_outline_for_site'
+
+
+def waffle_switch():
+    """
+    Returns the namespaced, cached, audited Waffle class for course experience.
+    """
+    return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='course_experience: ')
+
+
+def waffle_flag():
+    """
+    Returns the namespaced, cached, audited Waffle flags dictionary for course experience.
+    """
+    namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'course_experience: ')
+    # By default, disable the new course outline. Can be enabled on a course-by-course basis.
+    # And overridden site-globally by ENABLE_SITE_NEW_COURSE_OUTLINE
+    return CourseWaffleFlag(
+        namespace,
+        ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE,
+        flag_undefined_default=False
+    )
+
+
+def new_course_outline_enabled(course_key):
+    """
+    Returns whether the new course outline is enabled.
+    """
+    try:
+        current_site = get_current_site()
+        if not current_site.configuration.get_value(ENABLE_NEW_COURSE_OUTLINE_FOR_SITE, False):
+            return
+    except SiteConfiguration.DoesNotExist:
+        return
+
+    if not waffle_switch().is_enabled(ENABLE_NEW_COURSE_OUTLINE):
+        return waffle_flag().is_enabled(course_key)
+
+    return True