The Shadower – Realistic Drop Shadows in Javascript

December 04, 2006 By: erik Category: Geeky, Programming, Wordpress 40,577 views

Rate this post:
1 Star2 Stars3 Stars4 Stars5 Stars (32 votes, average: 4.19 out of 5)
Loading ... Loading ...

This has been done before, but not in such a simple and elegant way, in my opinion. This implementation has four important features that separate it from the rest: no images, no messy nested divs in your html code, no CSS, and realistic fading shadows!

It is based on the Prototype Javascript Framework, and requires version 1.5, which you can get here.


Some Examples

Other drop shadow libraries that eschew images, provide their shadow by adding an absolutely positioned div just behind the object. Like so…

Example 1
(Drag Me Around)


I think you’ll agree that that looks pretty bad. But it’s a lot better with a little opacity tweak…

Example 2
(Drag Me Around)

But it still just looks like a gray box under there, doesn’t it? That’s where the magic of The Shadower comes in. Rather than just one gray box, it uses several successively smaller boxes stacked on top of each other to create a realistic drop shadow that really fades into the background.

Example 3
(Drag Me Around)


Now, doesn’t that look better? Drag it up next to the others if you aren’t convinced.

So how do I use it?

There are five parameters to play with in getting your drop shadow just right.

distance [default: 8]
The distance away from the shadowed element to draw the shadow. This can also be thought of as the height “above the page” of the shadowed object
angle [default: 130]
The direction away from the shadowed element to draw the shadow. It is measured in degrees from the vertical. If you think of it like the numbers on a clock, 12 is at 0°, 3 is at 90°, 6 is at 180°, 9 is at 270°, etc. The default angle of 130 would be between 3 and 4 on the clock face.
opacity [default: 0.7]
The opacity of the shadow, measured between 0.0 and 1.0.
nestedShadows [default: 4]
This is the number of gradually darker shadows to draw under the shadowed element.
color [default: #000000]
This is the color of the shadow. The default black color is good enough for most uses, but you can change it if you want.

Go ahead! Play with it!

Test Example
(Drag Me Around)

distance: angle:
opacity: nestedShadows:
color:

Presets: Example 1Example 2Example 3

Recommended Usage

Ideally, you should set a CSS class on any elements that you want shadowed, and then call Shadower.shadowWithClass() on page load. Like so…

<html>
  <head>
   <script type="text/javascript" src="prototype.js"></script>
   <script type="text/javascript" src="shadower.js"></script>
  </head>
  <body onload="Shadower.shadowWithClass('shadowed');">
   <div class="shadowed">This div is shadowed</div>
  </body>
</html>

Download

So what are you waiting for? Download it and start using it!

prototype.js

shadower.js

Feedback

I have not yet tested this on all browsers. If you have any problems, please either leave a comment on this blog entry or email me at erikwordpressplugins -at- gmail.

Enjoy!

 
  • Mikeytown2

    Cool JS! Two things though… First your shadowWithClass function seems to be broken. Example. One idea would be to use Nifty Corners getElementsBySelector function.
    Secondly the prototype js file is huge. If you remember lightbox the code is large. Here is an example of the same code but in a smaller package. If you use a smaller prototype file that would be helpful.

  • http://www.erik-rasmussen.com/ erik

    No, Mike, it does work. You have a javascript error on that page. You’re saying:

    Shadowed.shadowWithClass(‘shadowed’);

    when it should be

    Shadower.shadowWithClass(‘shadowed’);

    The reason I wrote it with Prototype is that I’m already using Prototype for a lot of other code (ajax calls, etc.) in my projects. Anyone is welcome to strip out the parts of prototype that I’m using and bundle it together with shadower.js to make a lighter download, but that wasn’t my intention.

    Thanks for your comment.

  • Mikeytown2

    Sweet! Thanks for the help. You might want to change the code under Recommended Usage; line 6 you have the shadowed/shadower typo.

  • http://www.erik-rasmussen.com/ erik

    Doh! Okay, we’ll split the blame then. :-)

    Usage code fixed.

  • Mikeytown2

    Hey i played around with this and a page i made uses position: fixed with great success. Your drop shadows fail when used in this kind of environment. Tested on XP SP2 IE6 & FFox 2.0. Example. This page btw only uses 2 pictures for the background. Also unlike other pages that used a fixed div in FFox 2.0 when you put your mouse over the fixed div and use a scroll wheel it will scroll. Example of problem. In short i used some minor css hacks to get IE & Firefox to work how i want, just letting you know that your JS solution isn’t perfect in all cases.

  • http://www.erik-rasmussen.com/ erik

    Yeah, I know it’s not perfect in all cases. I didn’t think it was. It uses absolute positioning to put the shadow divs behind the “shadowed” div. I would have been surprised if you had told me that it did worked with fixed-positioned divs. Thanks for your testing and posting the results.

  • mikeyroy

    Awesome, awesome, awesome, awesome!

    Only problem I’ve had is that in IE7 if a div I’m applying a shadow to contains javascript in there to display them (like google ads) the entire browser crashes. NBD though.

  • Darksaint

    Very nice little shadow script. Thanks

  • http://holytransfigurationorthodoxchurch.com Brett

    What is a CSS class? Isn’t Shadower.shadow a class? Or what about ‘testExample’ (or if it isn’t a class then what is it)?

    I don’t understand from your example what to put in my stylesheet and what to call it.

    If I wanted all right aligned images (in WordPress) to have shadows, would I use alignright as the class? What style elements would I add to that class? Would I still need the Shadower.shadow class?

    How would I call it in the body tag?

    I would like a plugin (like the lightbox plugins) that implements this with a rel tag! Better yet a plugin that automatically implements it on all images (or chosen image classes).

    Otherwise, where in the folder structure (of wordpress) would I put the .js files? Does prototype overwrite the prototype.js file already in the wordpress javascript directory?

    And how, then would I call the .js files in the header section (reletive paths or full path names)?

  • Guillaume

    Nice idea, Erik!

    However, I noticed a couple issues with the code.

    The script is currently making a clone of the element in case the position style applied to it is not an ‘absolute’ one.
    First, the style check doesn’t seems to work since even with absolutely positioned element a node cloning was happening (checked on FF/XP, FF/Mac, not too sure of other browsers).
    I would consider this a medium level issue since it doesn’t have side effects.

    Second, and this one can induce quite nasty bugs.
    When creating the place holder subtree, you definately take care to remove the id attribute of the top node but the subnodes ids do still exist, which causes access by getElementById() to fail since they access the hidden place holder copy elements.

    I tinkered a fix myself but you may be interested in double checking it for yourself :

    var placeHolder = element.cloneNode(true);
    placeHolder.id = null;

    =>
    var placeHolder = Shadower.idSafeClone(element);
    and the idSafeClone method would be the following

    idSafeClone: function(node)
    {
      var id = null;
      var clone = null;
      var children = null;
      if (node.nodeType == Node.ELEMENT_NODE) {
        id = node.getAttribute('id');
        if (id) {
          node.removeAttribute('id');
        }
      }
      clone = node.cloneNode(false);
      if (node.nodeType == Node.ELEMENT_NODE) {
        if (id) {
          node.setAttribute('id', id);
        }
        if (node.hasChildNodes()) {
          children = node.childNodes;
          for (i = 0; i < children.length; i++) {
            childClone = Shadower.idSafeClone(children[i]);
            clone.appendChild(childClone);
          }
        }
      }
      return clone;
    }

    Checked working on XP(IE6,IE7,FF), Mac(Safari, FF)

    There are probably ways to optimize it but I didn’t have the time for this.

  • http://www.erik-rasmussen.com/ Erik R.

    Excellent points, Guillaume! I’ve taken the liberty of formatting the code in your comment. I love how you initialize all your variables to null at the top of the function. Program in C much? :-)

    I was already aware of the style bug and had fixed it in my version that I’m using in production, but I had forgotten to update the one on this post.

    You’re absolutely right about the id collision bug. I might do the idSafeClone a little differently, more in the style of prototype.js.

    idSafeClone: function(node)
    {
      var clone = node.cloneNode(false);
      if (clone.hasAttribute && clone.hasAttribute('id'))
        clone.removeAttribute('id');
      var clonedChildren =
        $A(node.childNodes).collect(this.idSafeClone.bind(this));
      clonedChildren.each(function(child) {
        clone.appendChild(child);
      });
      return clone;
    }

    It was tempting to chain the collect() and each() calls together, but I went with the clarity of assigning a variable. And it can be even more elegant if you don’t mind traversing the tree twice, because then you can write only a function to strip ids and say stripIds(node.cloneNode(true)) and be done.

    Again, thank you for your insight into these problems. I’ll fix the file that’s hosted here with your ideas. I’ve also added some trickery to allow shadowing of fieldsets, since the legend screws with the height measurements.

  • ravi

    iam geeting error on the IE7 style is not an object.and also images is coming 2 times

  • http://www.ocsinet.com Francesco Mazzi

    Hi, excellent work!
    I needed a uniform shadowin all directions, so I added another function to Shadower:

    uniform_shadow: function(element)
      {
        element = $(element);
        var options = Object.extend(
        {
          distance: 8,
          opacity: 0.7,
          nestedShadows: 4,
          color: '#000000'
        }, arguments[1] || {});
        var positionStyle = Element.getStyle(element, 'position');
        var parent = element.parentNode;
        if (!element.shadowZIndex)
        {
          if (positionStyle != 'absolute' && positionStyle != 'fixed')
          {
            var placeHolder = this.idSafeClone(element);
            placeHolder.id = null;
            parent.insertBefore(placeHolder, element);
            Position.absolutize(element);
            Position.clone(placeHolder, element);
            element.style.margin = '0';
            placeHolder.style.visibility = 'hidden';
            positionStyle = 'absolute';
          }
          element.shadowZIndex = new Number(Element.getStyle(element, 'zIndex') ? Element.getStyle(element, 'zIndex') : 1);
          element.style.zIndex = element.shadowZIndex + options.nestedShadows;
        }
        if (arguments[2]) // force recreate
          this.deshadow(element);
        // create shadows
        if (!element.shadows)
        {
          element.shadows = new Array(options.nestedShadows);
          for (var i = 0; i < options.nestedShadows; i++)
          {
            var shadow = document.createElement('div');
            Element.hide(shadow);
            shadow.appendChild(document.createTextNode(' '));
            if (parent)
              parent.appendChild(shadow);
            shadow.style.position = positionStyle;
            shadow.style.backgroundColor = options.color;
            Element.setOpacity(shadow, options.opacity / options.nestedShadows);
            shadow.style.zIndex = element.shadowZIndex + i;
            element.shadows[i] = shadow;
          }
        }
        var legendHeight = this.getLegendHeight(element);
        // position shadows
        Position.prepare();
        var offsets = Position.positionedOffset(element);
        var topOffset = -options.distance;
        var leftOffset = -options.distance;
        element.shadows.each(function(shadow, i)
        {
          shadow.style.top = Math.ceil(offsets[1] + topOffset + i + (legendHeight / 2)) + 'px';
          shadow.style.left = (offsets[0] + leftOffset + i) + 'px';
          shadow.style.width = (element.offsetWidth - (2 * i)) + 2 * options.distance + 'px';
          shadow.style.height = (element.offsetHeight - (2 * i) - (legendHeight / 2)) + 2* options.distance + 'px';
          Element.show(shadow);
        });
      },

    I hope it will be useful…let me know!
    Bye

  • sparsh

    Nice idea, Erik!
    used it and div luks fine but i need to make dat div movable with shadow which i can’t.. only main div moves with my js script. please guide me.

  • http://www.erik-rasmussen.com/ Erik R.

    Sparsh, that’s because you have to call shadow() again when your div is dragged. See the third example above:

    var example3change = function() {
      Shadower.shadow('example3',{
        opacity:0.7,nestedShadows:4
        });
    }
    new Draggable('example3', { change: example3change });
    That way shadow() is called every time the Draggable “change” event fires.

  • sparsh

    hi Eric,

    i have used the same code u have given. But getting error like undefined draggable..

    have also used

    var example3change = function() {
    Shadower.shadow(’example3?,{
    opacity:0.7,nestedShadows:4
    });
    }
    new Draggable(’example3?, { change: example3change });

  • http://www.codestill.com Philip Snyder

    I got rid of the recursive calls & prototype’s extra overhead for you. :) Sorry if it doesn’t display nicely spaced — can’t figure it out on this forum. ??


    function stripIds(node) {
    var stack = new Array;
    stack[stack.length] = node;
    while (stack.length > 0) {
    var n = stack.shift();
    if (node.nodeType == Node.ELEMENT_NODE) {
    if (n.id) {
    n.removeAttribute('id');
    }
    if (n.hasChildNodes()) {
    var children = n.childNodes;
    for (var i=0; i<children.length; i++) {
    stack.push(children[i]);
    }
    }
    }
    }
    return node;
    }

  • Martin

    Hi, the script is excellent, but there is a little bug. If the element to be shadowed is more than half the height of the site, the site gets a vertical scrollbar when applying the function “parent.insertBefore” (shadower.js line 29). When the placeHolder is set to “visible=hidden” the scrollbar disappears, but the element stays at the left-position it had when the scrollbar was visible; the correct position of the element would be (element.left + 16px),
    for the scrollbars width is 16 px.

    Is there any solution for this problem?
    (Sorry for my bad english)

  • Alex_T

    To expand on ravi’s post. This error occurs in IE6 and IE7 (didn’t try it in IE8 though) when applying a drop-shadow to an image. These browsers report “‘style’ is null or not an object” in prototype.js, when the following line in shadower.js makes a call to it:

    Position.clone(placeHolder, element);

    Moreover, the image is doubled and placed on top of the original.

    The error is specific to a case, when the image has a position:static property. I haven’t found a workaround for this yet. This is very sad, because in every other browser that I used for testing (FF2.0/Opera9/Chrome/Safari) your script works perfectly.

  • Alex_T

    Ok, it appears to be a prototype.js bug.

    For those who are concerned, here is a possible solution. In order to apply it, modifications to prototype.js are in order.

    http://dev.rubyonrails.org/ticket/11473

  • zmove

    Hello,

    Pretty cool shadow, but it would be cool to be able to set a shadow of 180° that can make shadow in the left and right of the container. In the example, it just make a shadow in the bottom which is no reallistic.

    zmove

  • Kevin

    I implemented the shadow on my header div and now it is being pushed down just a small amount from the top of the page.

    Why is this happening / How can I fix it?

  • lastadm

    The are changes in prototype 1.7 – Position.positionedOffset now returns correct (?) offset, including element margins.

    So shadower code have to be changed like this:
    replace line 61:
    var offsets = Position.positionedOffset(element);
    with
    var layout = new Element.Layout(element);

    and then lines 67-68 change like this
    shadow.style.top = Math.ceil(layout.get(‘top’) + layout.get(‘margin-top’) + topOffset + i + (legendHeight / 2)) + ‘px’;
    shadow.style.left = (layout.get(‘left’) + layout.get(‘margin-left’) + leftOffset + i) + ‘px’;