diff --git a/exampleSite/content/shortcodes/tabs.en.md b/exampleSite/content/shortcodes/tabs.en.md
new file mode 100644
index 0000000000..d664eeacaf
--- /dev/null
+++ b/exampleSite/content/shortcodes/tabs.en.md
@@ -0,0 +1,119 @@
+---
+title: Tabbed views
+description : "Synchronize selection of content in different tabbed views"
+---
+
+Choose which content to see across the page. Very handy for providing code
+snippets for multiple languages or providing configuration in different formats.
+
+## Code example
+
+	{{</* tabs */>}}
+	{{%/* tab name="python" */%}}
+	```python
+	print("Hello World!")
+	```
+	{{%/* /tab */%}}
+	{{%/* tab name="R" */%}}
+	```R
+	> print("Hello World!")
+	```
+	{{%/* /tab */%}}
+	{{%/* tab name="Bash" */%}}
+	```Bash
+	echo "Hello World!"
+	```
+	{{%/* /tab */%}}
+	{{</* /tabs */>}}
+
+Renders as:
+
+{{< tabs >}}
+{{% tab name="python" %}}
+```python
+print("Hello World!")
+```
+{{% /tab %}}
+{{% tab name="R" %}}
+```R
+> print("Hello World!")
+```
+{{% /tab %}}
+{{% tab name="Bash" %}}
+```Bash
+echo "Hello World!"
+```
+{{% /tab %}}
+{{< /tabs >}}
+
+Tab views with the same tabs that belong to the same group sychronize their selection:
+
+{{< tabs >}}
+{{% tab name="python" %}}
+```python
+print("Hello World!")
+```
+{{% /tab %}}
+{{% tab name="R" %}}
+```R
+> print("Hello World!")
+```
+{{% /tab %}}
+{{% tab name="Bash" %}}
+```Bash
+echo "Hello World!"
+```
+{{% /tab %}}
+{{< /tabs >}}
+
+## Config example
+
+	{{</* tabs groupId="config" */>}}
+	{{%/* tab name="json" */%}}
+	```json
+	{
+	  "Hello": "World"
+	}
+	```
+	{{%/* /tab */%}}
+	{{%/* tab name="XML" */%}}
+	```xml
+	<Hello>World</Hello>
+	```
+	{{%/* /tab */%}}
+	{{%/* tab name="properties" */%}}
+	```properties
+	Hello = World
+	```
+	{{%/* /tab */%}}
+	{{</* /tabs */>}}
+
+Renders as:
+
+{{< tabs groupId="config" >}}
+{{% tab name="json" %}}
+```json
+{
+  "Hello": "World"
+}
+```
+{{% /tab %}}
+{{% tab name="XML" %}}
+```xml
+<Hello>World</Hello>
+```
+{{% /tab %}}
+{{% tab name="properties" %}}
+```properties
+Hello = World
+```
+{{% /tab %}}
+{{< /tabs >}}
+
+{{% notice warning %}}
+When using tab views with different content sets, make sure to use a common `groupId` for equal sets but distinct
+`groupId` for different sets. The `groupId` defaults to `'default'`.  
+**Take this into account across the whole site!**  
+The tab selection is restored automatically based on the `groupId` and if it cannot find a tab item because it came
+ from the `'default'` group on a different page then all tabs will be empty at first.
+{{% /notice %}}
diff --git a/layouts/partials/header.html b/layouts/partials/header.html
index 2f97f76d83..8ee0334bc0 100644
--- a/layouts/partials/header.html
+++ b/layouts/partials/header.html
@@ -17,6 +17,7 @@
     <link href="{{"css/auto-complete.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
     <link href="{{"css/atom-one-dark-reasonable.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
     <link href="{{"css/theme.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
+    <link href="{{"css/tabs.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
     <link href="{{"css/hugo-theme.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
     {{with .Site.Params.themeVariant}}
     <link href="{{(printf "css/theme-%s.css" .) | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
diff --git a/layouts/shortcodes/tab.html b/layouts/shortcodes/tab.html
new file mode 100644
index 0000000000..d258e00ac4
--- /dev/null
+++ b/layouts/shortcodes/tab.html
@@ -0,0 +1,12 @@
+{{ if .Parent }}
+	{{ $name := trim (.Get "name") " " }}
+	{{ if not (.Parent.Scratch.Get "tabs") }}
+	    {{ .Parent.Scratch.Set "tabs" slice }}
+	{{ end }}
+	{{ with .Inner }}
+        {{ $.Parent.Scratch.Add "tabs" (dict "name" $name "content" . ) }}
+	{{ end }}
+{{ else }}
+	{{- errorf "[%s] %q: tab shortcode missing its parent" site.Language.Lang .Page.Path -}}
+{{ end}}
+
diff --git a/layouts/shortcodes/tabs.html b/layouts/shortcodes/tabs.html
new file mode 100644
index 0000000000..417a808783
--- /dev/null
+++ b/layouts/shortcodes/tabs.html
@@ -0,0 +1,21 @@
+{{ with .Inner }}{{/* don't do anything, just call it */}}{{ end }}
+{{ $groupId := default "default" (.Get "groupId") }}
+<div class="tab-panel">
+    <div class="tab-nav">
+    {{ range $idx, $tab := .Scratch.Get "tabs" }}
+        <button
+          data-tab-item="{{ .name }}"
+          data-tab-group="{{ $groupId }}"
+          class="tab-nav-button btn {{ cond (eq $idx 0) "active" ""}}"
+          onclick="switchTab('{{ $groupId }}','{{ .name }}')"
+         >{{ .name }}</button>
+    {{ end }}
+    </div>
+    <div class="tab-content">
+        {{ range $idx, $tab := .Scratch.Get "tabs" }}
+        <div data-tab-item="{{ .name }}" data-tab-group="{{ $groupId }}" class="tab-item {{ cond (eq $idx 0) "active" ""}}">
+            {{ .content }}
+        </div>
+        {{ end }}
+    </div>
+</div>
diff --git a/static/css/tabs.css b/static/css/tabs.css
new file mode 100644
index 0000000000..2ad2728772
--- /dev/null
+++ b/static/css/tabs.css
@@ -0,0 +1,43 @@
+#body .tab-nav-button {
+    border-width: 1px 1px 1px 1px !important;
+    border-color: #ccc !important;
+    border-radius: 4px 4px 0 0 !important;
+    background-color: #ddd !important;
+    float: left;
+    display: block;
+    position: relative;
+    margin-left: 4px;
+    bottom: -1px;
+}
+#body .tab-nav-button:first-child {
+    margin-left: 0px;
+}
+#body .tab-nav-button.active {
+    background-color: #fff !important;
+    border-bottom-color: #fff !important;
+}
+
+#body .tab-panel {
+    margin-top: 32px;
+    margin-bottom: 32px;
+}
+#body .tab-content {
+    display: block;
+    clear: both;
+    padding: 8px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: #ccc;
+}
+#body .tab-content .tab-item{
+    display: none;
+}
+
+#body .tab-content .tab-item.active{
+    display: block;
+}
+
+#body .tab-item pre{
+    margin-bottom: 0;
+    margin-top: 0;
+}
diff --git a/static/js/learn.js b/static/js/learn.js
index b0d199da86..85e09d55b7 100644
--- a/static/js/learn.js
+++ b/static/js/learn.js
@@ -47,6 +47,57 @@ function fallbackMessage(action) {
     return actionMsg;
 }
 
+function switchTab(tabGroup, tabId) {
+    allTabItems = jQuery("[data-tab-group='"+tabGroup+"']");
+    targetTabItems = jQuery("[data-tab-group='"+tabGroup+"'][data-tab-item='"+tabId+"']");
+
+    // if event is undefined then switchTab was called from restoreTabSelection
+    // so it's not a button event and we don't need to safe the selction or
+    // prevent page jump
+    var isButtonEvent = event != undefined;
+
+    if(isButtonEvent){
+      // save button position relative to viewport
+      var yposButton = event.target.getBoundingClientRect().top;
+    }
+
+    allTabItems.removeClass("active");
+    targetTabItems.addClass("active");
+
+    if(isButtonEvent){
+      // reset screen to the same position relative to clicked button to prevent page jump
+      var yposButtonDiff = event.target.getBoundingClientRect().top - yposButton;
+      window.scrollTo(window.scrollX, window.scrollY+yposButtonDiff);
+
+      // Store the selection to make it persistent
+      if(window.localStorage){
+          var selectionsJSON = window.localStorage.getItem("tabSelections");
+          if(selectionsJSON){
+            var tabSelections = JSON.parse(selectionsJSON);
+          }else{
+            var tabSelections = {};
+          }
+          tabSelections[tabGroup] = tabId;
+          window.localStorage.setItem("tabSelections", JSON.stringify(tabSelections));
+      }
+    }
+}
+
+function restoreTabSelections() {
+    if(window.localStorage){
+        var selectionsJSON = window.localStorage.getItem("tabSelections");
+        if(selectionsJSON){
+          var tabSelections = JSON.parse(selectionsJSON);
+        }else{
+          var tabSelections = {};
+        }
+        Object.keys(tabSelections).forEach(function(tabGroup) {
+          var tabItem = tabSelections[tabGroup];
+          switchTab(tabGroup, tabItem);
+        });
+    }
+}
+
 // for the window resize
 $(window).resize(function() {
     setMenuHeight();
@@ -83,6 +134,8 @@ $(window).resize(function() {
 
 
 jQuery(document).ready(function() {
+    restoreTabSelections();
+
     jQuery('#sidebar .category-icon').on('click', function() {
         $( this ).toggleClass("fa-angle-down fa-angle-right") ;
         $( this ).parent().parent().children('ul').toggle() ;
@@ -229,7 +282,7 @@ jQuery(document).ready(function() {
              e.stopPropagation();
          }
      });
-    
+
     jQuery(document).keydown(function(e) {
       // prev links - left arrow key
       if(e.which == '37') {
@@ -264,7 +317,7 @@ jQuery(document).ready(function() {
         });
     }
 
-    /** 
+    /**
     * Fix anchor scrolling that hides behind top nav bar
     * Courtesy of https://stackoverflow.com/a/13067009/28106
     *
@@ -346,7 +399,7 @@ jQuery(document).ready(function() {
 
         $(document).ready($.proxy(anchorScrolls, 'init'));
     })(window.document, window.history, window.location);
-    
+
 });
 
 jQuery(window).on('load', function() {