Pen Settings



CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource


Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource


Add Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import or require. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.


Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

              <h1>Responsively crop copy, restore onclick via typed animation</h1>

<p>Responsively crop content copy down to a user-defined number of lines.<br>Click to restore content via a typed animation.</p>
<p>Vanilla JavaScript running at 60fps.<br>1K approx minified &amp; gzipped.</p>

<p>UPDATED: Please take a loook at the smooth drop-down version: <a href="">Responsively crop copy, restore onclick via sliding drop-down animation</a></p>

<p>GitHub repo: <a href="" target=_blank title="[new window]">crop-copy-restore</a></p>

<h2>Single line crop</h2>
<div class=txt data-cropCopyRestore>My wife Maria, loves hot spicy food. I found this out on our first date. She said “I really fancy a curry”. Well, I was keen to impress so I said, “great, that’s my favourite too ”But it isn’t, never has been. We’ve been married 15 years and I still haven’t let on. Don’t know why. But whenever I cook her favourite chicken gujarati extra spicy, with green beans and fresh ginger, I do what any self-respecting chef would do. I cheat. I put a king size spoonful of natural yoghurt in mine. And I keep schtum.</div>

<h2>Three lines</h2>
<div class=txt data-cropCopyRestore=3>Sometimes, it's the undemandingly easy, everyday, recipes that deliver the most joy. For 'Iain' and his dad, from our Food Love Story, it's croque monsieur – they first had it on a joint trip to France, and since then it's become their favourite weekend lunch. Iain's made a few changes to it along the way (bonjour, wafer thin roast turkey) – but for him and his dad, it's most definitely 'proper'.</div>

<h2>Two lines</h2>
<div class=txt data-cropCopyRestore=2>Any parent will probably be able to relate to 'Sunita' in our Food Love Story. Kids can be champion fussy eaters, especially when it comes to vegetables. Here's a genius solution – meatballs packed with under-the-radar veg. Sunita's kids love them, and are none the wiser.</div>

<p>Considered too jarring visually.<br>Project to be redeveloped with smooth text expansion.</p>

<p class=smaller><a target=_blank title="[new window]" href="">Pens by Mike Foskett</a> &mdash; <a target=_blank title="[new window]" href="">webSemantics</a></p>
              .txt {
  margin: 2rem;
[data-cropCopyRestore][role="button"] {
  display: block;
  padding: .75rem 1rem;
  background-color: #000;
  transition: all .3s ease-out;
  -ms-touch-action: manipulation;
  touch-action: manipulation;
[data-cropCopyRestore][role="button"]:focus {
  outline: 0 dotted #fff;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .5);
[data-cropCopyRestore][role="button"]:hover {
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .5);
  cursor: pointer;

/* Required */
.-clone {
  visibility: hidden;
  opacity: 0;
  z-index: -1;

              // Crop copy responsively, to user-defined number of lines, then restore onclick - v1.0 - 08/01/2017 - M.J.Foskett -
var cropCopyRestore = function (window, document) {

  "use strict";

  var dataAttr = "data-cropCopyRestore";
  var buttonId = "cropCopyRestore_btn-";
  var ellipsis = "…"; // "\u2026"
  var clonedClass = "-clone";

  var debounce=function(e,t,n){var a;return function(){var r=this,i=arguments,o=function(){a=null,n||e.apply(r,i)},s=n&&!a;clearTimeout(a),a=setTimeout(o,t||200),s&&e.apply(r,i)}};

// String cropping functions

  function _removeLastOccur(str, removeStr) {
    return str.substring(0, str.lastIndexOf(removeStr));

  function _removeTrailingPunct(str) {
    return str.replace(/[ .,!?:;"“‘'\-]+$/, "");

// Display and animation functions

  function _animLoop(f,g){function b(c){if(!1!==d){window.requestAnimationFrame(b,g);var e=c-a;160>e&&(d=f(e));a=c}}var d,a=+new Date;b(a)}
  // Usage:
  //animLoop(function(deltaT) {
  // = (left += 10 * deltaT / 16) + "px";
  //  if (left > 400) {
  //    return false;
  //  }
  // optional 2nd arg: elem containing the animation
  //}, animationFunction);

  function _display(obj, str) {
    obj.textContent = str;

  function _displayAppend(obj, str) {
    _display(obj, obj.textContent + str);

  function _displayCroppedText(obj) {
    _display(obj, obj.croppedTxt + ellipsis)

  function _resetAttr(obj, bool) {
    obj.setAttribute("aria-expanded", bool);
    obj.transitioning = false;

  function _addRemainerText(obj) {
    var textArr = obj.remainerTxt.split(" ");
    var i = 0;

    window.requestAnimationFrame(function() {

      // remove ellipsis
      _display(obj, obj.croppedTxt);

      _animLoop(function() {
        _displayAppend(obj, textArr[i] + " ");
        if (i >= textArr.length - 1) {
          _resetAttr(obj, true);
          return false;
        i += 1;

  function _removeRemainerText(obj) {
    var textArr = obj.remainerTxt.split(" ");
    var i = textArr.length;

    _animLoop(function() {
      i -= 1;
      _display(obj, _removeLastOccur(obj.textContent, " " + textArr[i]));
      if (i <= 0) {
        _resetAttr(obj, false);
        return false;

// Set copy

  function _getRemainerText(obj) {
    return obj.fullTxt.replace(obj.croppedTxt, "");

  function _createClone(obj, str) {
    // create an invisible clone (used to get an objects height)
    var clone = obj.cloneNode(true);
    clone.setAttribute("class", obj.className + " " + clonedClass);
    clone.textContent = str;
    obj.parentNode.insertBefore(clone, obj.nextSibling);
    clone.initialHeight = clone.clientHeight;
    return clone;

  function _getCroppedText(obj) {
    var txtArr = obj.fullTxt.split(" ");
    var i = 0;
    var lines = 1;
    var clone = _createClone(obj, txtArr[i] + " ");

    for (i = 1; i < txtArr.length; i++) {
      _displayAppend(clone, txtArr[i] + ellipsis);
      if (clone.clientHeight !== clone.initialHeight) {

        if (lines + "" === obj.noOfLines) {
          _display(clone, _removeLastOccur(clone.textContent, txtArr[i] + ellipsis));
        clone.initialHeight = clone.clientHeight;

      // Bit of an assumption
      _display(clone, clone.textContent.replace(txtArr[i] + ellipsis, txtArr[i] + " "));


    _display(clone, _removeTrailingPunct(clone.textContent));
    return clone.textContent;

// Handle events

  function _clicked(event) {
    var obj =;
    if (!obj.transitioning) {
      obj.transitioning = true;
      if (obj.getAttribute("aria-expanded") === "true") {
      } else {

  function _keyPressed(event) {
    // Enter or space key
    if (event.which === 13 || event.which === 32) {

  function _addEvents(obj) {
    obj.addEventListener("click", _clicked, false);
    obj.addEventListener("keydown", _keyPressed, false);

  function _removeEvents(obj) {
    obj.removeEventListener("click", _clicked);
    obj.removeEventListener("keydown", _keyPressed);

// Initialisation

  function _initialiseAttributes(obj, i) {
    var str = obj.getAttribute(dataAttr);
    obj.noOfLines = (/^([1-9]\d*)$/.test(str)) ? str : "1"; // 1 - 9 only
    obj.fullTxt = obj.textContent;
    obj.setAttribute("id", || buttonId + i);
    obj.setAttribute("role", "button");
    obj.setAttribute("tabindex", "0");

  function start() {

    var objs = document.querySelectorAll("[" + dataAttr + "]");
    var i = objs.length;
    var obj;

    while (i--) {

      obj = objs[i];

      // In case it's a resize call rather than initialisation
      if (obj.fullTxt) {
      } else {
        _initialiseAttributes(obj, i);

      // Reset, or initialise, common attributes
      _resetAttr(obj, false);
      obj.croppedTxt = _getCroppedText(obj);
      obj.remainerTxt = _getRemainerText(obj);


  window.addEventListener("resize", debounce(start, 100, false), false);

}(window, document);

window.addEventListener("load", cropCopyRestore, false);