diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index cc6ddcad8..9533c07e8 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -133,6 +133,10 @@
.span-24.last{:style=> "#{yield(:break_the_mold)}"}
= yield
+ - unless @landing_page
+ %a{:id=>"back-to-top", :title=>"Back to top", :href=>"#"}
+ ⇧
+
%footer
.container
%ul#footer_nav
diff --git a/public/javascripts/widgets/back-to-top.js b/public/javascripts/widgets/back-to-top.js
new file mode 100644
index 000000000..a76c09439
--- /dev/null
+++ b/public/javascripts/widgets/back-to-top.js
@@ -0,0 +1,33 @@
+(function() {
+ var BackToTop = function() {
+ var self = this;
+
+ this.subscribe("widget/ready", function(evt, button) {
+ $.extend(self, {
+ button: button,
+ body: $("html, body"),
+ window: $(window)
+ });
+
+ self.button.click(self.backToTop);
+
+ var throttledScroll = _.throttle($.proxy(self.throttledScroll, self), 250);
+ self.window.scroll(throttledScroll);
+ });
+
+ this.backToTop = function(evt) {
+ evt.preventDefault();
+ self.body.animate({scrollTop: 0});
+ };
+
+ this.toggleVisibility = function() {
+ self.button[
+ (self.body.scrollTop() > 1000) ?
+ 'addClass' :
+ 'removeClass'
+ ]('visible')
+ };
+ };
+
+ Diaspora.Widgets.BackToTop = BackToTop;
+})();
diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass
index 34d27c8cc..500fb2b99 100644
--- a/public/stylesheets/sass/application.sass
+++ b/public/stylesheets/sass/application.sass
@@ -3506,3 +3506,20 @@ a.toggle_selector
:position relative
:margin
:bottom 15px
+
+#back-to-top
+ :display block
+ :color white
+ :position fixed
+ :z-index 49
+ :right 20px
+ :bottom 20px
+ :opacity 0
+ :font-size 3em
+ :padding 0 11px 0 12px
+ :border-radius 10px
+ :background-color #aaa
+ &:hover
+ :opacity 0.85 !important
+ &:visible
+ :opacity 0.5
\ No newline at end of file
diff --git a/spec/javascripts/widgets/back-to-top-spec.js b/spec/javascripts/widgets/back-to-top-spec.js
new file mode 100644
index 000000000..ea2e5337f
--- /dev/null
+++ b/spec/javascripts/widgets/back-to-top-spec.js
@@ -0,0 +1,53 @@
+describe("Diaspora.Widgets.BackToTop", function() {
+ var backToTop;
+ beforeEach(function() {
+ spec.loadFixture("aspects_index");
+ backToTop = Diaspora.BaseWidget.instantiate("BackToTop", $("#back-to-top"));
+ $.fx.off = true;
+ });
+
+ describe("integration", function() {
+ beforeEach(function() {
+ backToTop = new Diaspora.Widgets.BackToTop();
+
+ spyOn(backToTop, "backToTop");
+ spyOn(backToTop, "toggleVisibility");
+
+ backToTop.publish("widget/ready", [$("#back-to-top")]);
+ });
+
+ it("calls backToTop when the button is clicked", function() {
+ backToTop.button.click();
+
+ expect(backToTop.backToTop).toHaveBeenCalled();
+ });
+ });
+
+ describe("backToTop", function() {
+ it("animates scrollTop to 0", function() {
+ backToTop.backToTop($.Event());
+
+ expect($("body").scrollTop()).toEqual(0);
+ });
+ });
+
+ describe("toggleVisibility", function() {
+ it("adds a visibility class to the button", function() {
+ var spy = spyOn(backToTop.body, "scrollTop").andReturn(999);
+
+ backToTop.toggleVisibility();
+
+ expect(backToTop.button.hasClass("visible")).toBe(false);
+
+ spy.andReturn(1001);
+
+ backToTop.toggleVisibility();
+
+ expect(backToTop.button.hasClass("visible")).toBe(true);
+ });
+ });
+
+ afterEach(function() {
+ $.fx.off = false;
+ });
+});
\ No newline at end of file