{"database": "24ways", "table": "articles", "is_view": false, "human_description_en": "where author = \"Jonathan Snook\", author_slug = \"jonathansnook\" and topic = \"code\"", "rows": [[64, "Being Responsive to the Small Things", "It\u2019s that time of the year again to trim the tree with decorations. Or maybe a DOM tree?\nAny web page is made of HTML elements that lay themselves out in a tree structure. We start at the top and then have multiple branches with branches that branch out from there. \n\nTo decorate our tree, we use CSS to specify which branches should receive the tinsel we wish to adorn upon it. It\u2019s all so lovely.\nIn years past, this was rather straightforward. But these days, our trees need to be versatile. They need to be responsive!\nResponsive web design is pretty wonderful, isn\u2019t it? Based on our viewport, we can decide how elements on the page should change their appearance to accommodate various constraints using media queries.\nClearleft have a delightfully clean and responsive site\nAlas, it\u2019s not all sunshine, lollipops, and rainbows. \nWith complex layouts, we may have design chunks \u2014 let\u2019s call them components \u2014 that appear in different contexts. Each context may end up providing its own constraints on the design, both in its default state and in its possibly various responsive states.\n\nMedia queries, however, limit us to the context of the entire viewport, not individual containers on the page. For every container our component lives in, we need to specify how to rearrange things in that context. The more complex the system, the more contexts we need to write code for.\n@media (min-width: 800px) {\n  .features > .component { }\n  .sidebar > .component {}\n  .grid > .component {}\n}\nEach new component and each new breakpoint just makes the entire system that much more difficult to maintain. \n@media (min-width: 600px) {\n  .features > .component { }\n  .grid > .component {}\n}\n\n@media (min-width: 800px) {\n  .features > .component { }\n  .sidebar > .component {}\n  .grid > .component {}\n}\n\n@media (min-width: 1024px) {\n  .features > .component { }\n}\nEnter container queries\nContainer queries, also known as element queries, allow you to specify conditional CSS based on the width (or maybe height) of the container that an element lives in. In doing so, you no longer have to consider the entire page and the interplay of all the elements within. \nWith container queries, you\u2019ll be able to consider the breakpoints of just the component you\u2019re designing. As a result, you end up specifying less code and the components you develop have fewer dependencies on the things around them. (I guess that makes your components more independent.)\nAwesome, right?\nThere\u2019s only one catch.\nBrowsers can\u2019t do container queries. There\u2019s not even an official specification for them yet. The Responsive Issues (n\u00e9e Images) Community Group is looking into solving how such a thing would actually work. \nSee, container queries are tricky from an implementation perspective. The contents of a container can affect the size of the container. Because of this, you end up with troublesome circular references. \nFor example, if the width of the container is under 500px then the width of the child element should be 600px, and if the width of the container is over 500px then the width of the child element should be 400px. \nCan you see the dilemma? When the container is under 500px, the child element resizes to 600px and suddenly the container is 600px. If the container is 600px, then the child element is 400px! And so on, forever. This is bad.\nI guess we should all just go home and sulk about how we just got a pile of socks when we really wanted the Millennium Falcon. \nOur saviour this Christmas: JavaScript\nThe three wise men \u2014 Tim Berners-Lee, H\u00e5kon Wium Lie, and Brendan Eich \u2014 brought us the gifts of HTML, CSS, and JavaScript. \nTo date, there are a handful of open source solutions to fill the gap until a browser implementation sees the light of day.\n\nElementary by Scott Jehl\nElementQuery by Tyson Matanich\nEQ.js by Sam Richards\nCSS Element Queries from Marcj\n\nUsing any of these can sometimes feel like your toy broke within ten minutes of unwrapping it.\nEach take their own approach on how to specify the query conditions. For example, Elementary, the smallest of the group, only supports min-width declarations made in a :before selector.\n.mod-foo:before {\n  content: \u201c300 410 500\u201d;\n}\nThe script loops through all the elements that you specify, reading the content property and then setting an attribute value on the HTML element, allowing you to use CSS to style that condition. \n.mod-foo[data-minwidth~=\"300\"] {\n  background: blue;\n}\nTo get the script to run, you\u2019ll need to set up event handlers for when the page loads and for when it resizes. \nwindow.addEventListener( \"load\", window.elementary, false );\nwindow.addEventListener( \"resize\", window.elementary, false );\nThis works okay for static sites but breaks down on pages where elements can expand or contract, or where new content is dynamically inserted.\nIn the case of EQ.js, the implementation requires the creation of the breakpoints in the HTML. That means that you have implementation details in HTML, JavaScript, and CSS. (Although, with the JavaScript, once it\u2019s in the build system, it shouldn\u2019t ever be much of a concern unless you\u2019re tracking down a bug.)\nAnother problem you may run into is the use of content delivery networks (CDNs) or cross-origin security issues. The ElementQuery and CSS Element Queries libraries need to be able to read the CSS file. If you are unable to set up proper cross-origin resource sharing (CORS) headers, these libraries won\u2019t help.\nAt Shopify, for example, we had all of these problems. The admin that store owners use is very dynamic and the CSS and JavaScript were being loaded from a CDN that prevented the JavaScript from reading the CSS. \nTo go responsive, the team built their own solution \u2014 one similar to the other scripts above, in that it loops through elements and adds or removes classes (instead of data attributes) based on minimum or maximum width.\nThe caveat to this particular approach is that the declaration of breakpoints had to be done in JavaScript. \n  elements = [\n    { \u2018module\u2019: \u201c.carousel\u201d, \u201cclassName\u201d:\u2019alpha\u2019, minWidth: 768, maxWidth: 1024 },\n    { \u2018module\u2019: \u201c.button\u201d, \u201cclassName\u201d:\u2019beta\u2019, minWidth: 768, maxWidth: 1024 } ,\n    { \u2018module\u2019: \u201c.grid\u201d, \u201cclassName\u201d:\u2019cappa\u2019, minWidth: 768, maxWidth: 1024 }\n  ]\nWith that done, the script then had to be set to run during various events such as inserting new content via Ajax calls. This sometimes reveals itself in flashes of unstyled breakpoints (FOUB). An unfortunate side effect but one largely imperceptible.\nUsing this approach, however, allowed the Shopify team to make the admin responsive really quickly. Each member of the team was able to tackle the responsive story for a particular component without much concern for how all the other components would react. \n\nEach element responds to its own breakpoint that would amount to dozens of breakpoints using traditional breakpoints. This approach allows for a truly fluid and adaptive interface for all screens.\nChristmas is over\nI wish I were the bearer of greater tidings and cheer. It\u2019s not all bad, though. We may one day see browsers implement container queries natively. At which point, we shall all rejoice!", "2015", "Jonathan Snook", "jonathansnook", "2015-12-19T00:00:00+00:00", "https://24ways.org/2015/being-responsive-to-the-small-things/", "code"], [147, "Christmas Is In The AIR", "That\u2019s right, Christmas is coming up fast and there\u2019s plenty of things to do. Get the tree and lights up, get the turkey, buy presents and who know what else. And what about Santa? He\u2019s got a list. I\u2019m pretty sure he\u2019s checking it twice.\n\nSure, we could use an existing list making web site or even a desktop widget. But we\u2019re geeks! What\u2019s the fun in that? Let\u2019s build our own to-do list application and do it with Adobe AIR!\n\nWhat\u2019s Adobe AIR?\n\nAdobe AIR, formerly codenamed Apollo, is a runtime environment that runs on both Windows and OSX (with Linux support to follow). This runtime environment lets you build desktop applications using Adobe technologies like Flash and Flex. Oh, and HTML. That\u2019s right, you web standards lovin\u2019 maniac. You can build desktop applications that can run cross-platform using the trio of technologies, HTML, CSS and JavaScript.\n\nIf you\u2019ve tried developing with AIR before, you\u2019ll need to get re-familiarized with the latest beta release as many things have changed since the last one (such as the API and restrictions within the sandbox.)\n\nTo get started\n\nTo get started in building an AIR application, you\u2019ll need two basic things:\n\n\n\tThe AIR runtime. The runtime is needed to run any AIR-based application.\n\tThe SDK. The software development kit gives you all the pieces to test your application. Unzip the SDK into any folder you wish.\n\n\nYou\u2019ll also want to get your hands on the JavaScript API documentation which you\u2019ll no doubt find yourself getting into before too long. (You can download it, too.)\n\nAlso of interest, some development environments have support for AIR built right in. Aptana doesn\u2019t have support for beta 3 yet but I suspect it\u2019ll be available shortly.\n\nWithin the SDK, there are two main tools that we\u2019ll use: one to test the application (ADL) and another to build a distributable package of our application (ADT). I\u2019ll get into this some more when we get to that stage of development.\n\nBuilding our To-do list application\n\nThe first step to building an application within AIR is to create an XML file that defines our default application settings. I call mine application.xml, mostly because Aptana does that by default when creating a new AIR project. It makes sense though and I\u2019ve stuck with it. Included in the templates folder of the SDK is an example XML file that you can use.\n\nThe first key part to this after specifying things like the application ID, version, and filename, is to specify what the default content should be within the content tags. Enter in the name of the HTML file you wish to load. Within this HTML file will be our application.\n\n<content>ui.html</content>\n\nCreate a new HTML document and name it ui.html and place it in the same directory as the application.xml file. The first thing you\u2019ll want to do is copy over the AIRAliases.js file from the frameworks folder of the SDK and add a link to it within your HTML document.\n\n<script type=\"text/javascript\" src=\"AIRAliases.js\"></script>\n\nThe aliases create shorthand links to all of the Flash-based APIs.\n\nNow is probably a good time to explain how to debug your application.\n\nDebugging our application\n\nSo, with our XML file created and HTML file started, let\u2019s try testing our \u2018application\u2019. We\u2019ll need the ADL application located in BIN folder of the SDK and tell it to run the application.xml file.\n\n/path/to/adl /path/to/application.xml\n\nYou can also just drag the XML file onto ADL and it\u2019ll accomplish the same thing. If you just did that and noticed that your blank application didn\u2019t load, you\u2019d be correct. It\u2019s running but isn\u2019t visible. Which at this point means you\u2019ll have to shut down the ADL process. Sorry about that!\n\nChanging the visibility\n\nYou have two ways to make your application visible. You can do it automatically by setting the placing true in the visible tag within the application.xml file.\n\n<visible>true</visible>\n\nThe other way is to do it programmatically from within your application. You\u2019d want to do it this way if you had other startup tasks to perform before showing the interface. To turn the UI on programmatically, simple set the visible property of nativeWindow to true.\n\n<script type=\"text/javascript\">\n\tnativeWindow.visible = true;\n</script>\n\nSandbox Security\n\nNow that we have an application that we can see when we start it, it\u2019s time to build the to-do list application. In doing so, you\u2019d probably think that using a JavaScript library is a really good idea \u2014 and it can be but there are some limitations within AIR that have to be considered.\n\nAn HTML document, by default, runs within the application sandbox. You have full access to the AIR APIs but once the onload event of the window has fired, you\u2019ll have a limited ability to make use of eval and other dynamic script injection approaches. This limits the ability of external sources from gaining access to everything the AIR API offers, such as database and local file system access. You\u2019ll still be able to make use of eval for evaluating JSON responses, which is probably the most important if you wish to consume JSON-based services.\n\nIf you wish to create a greater wall of security between AIR and your HTML document loading in external resources, you can create a child sandbox. We won\u2019t need to worry about it for our application so I won\u2019t go any further into it but definitely keep this in mind.\n\nFinally, our application\n\nGetting tired of all this preamble? Let\u2019s actually build our to-do list application. I\u2019ll use jQuery because it\u2019s small and should suit our needs nicely. Let\u2019s begin with some structure:\n\n<body>\n\t<input type=\"text\" id=\"text\" value=\"\">\n\t<input type=\"button\" id=\"add\" value=\"Add\">\n\t<ul id=\"list\"></ul>\n</body>\n\nNow we need to wire up that button to actually add a new item to our to-do list.\n\n<script type=\"text/javascript\">\n$(document).ready(function(){\n\t// make sure the application is visible\n\tnativeWindow.visible = true;\n\t$('#add').click(function(){\n\t\tvar t = $('#text').val();\n\t\tif(t)\n\t\t{\n\t\t\t// use DOM methods to create the new list item\n\t\t\tvar li = document.createElement('li');\n\t\t\t// the extra space at the end creates a buffer between the text\n\t\t\t// and the delete link we're about to add\n\t\t\tli.appendChild(document.createTextNode(t + ' '));\n\t\t\t// create the delete link\n\t\t\tvar del = document.createElement('a');\n\t\t\t// this makes it a true link. I feel dirty doing this.\n\t\t\tdel.setAttribute('href', '#');\n\t\t\tdel.addEventListener('click', function(evt){\n\t\t\t\tthis.parentNode.parentNode.removeChild(this.parentNode);\n\t\t\t});\n\t\t\tdel.appendChild(document.createTextNode('[del]'));\n\t\t\tli.appendChild(del);\n\t\t\t// append everything to the list\n\t\t\t$('#list').append(li);\n\t\t\t//reset the text box\n\t\t\t$('#text').val('');\n\t\t}\n\t})\n});\n</script>\n\nAnd just like that, we\u2019ve got a to-do list! That\u2019s it! Just never close your application and you\u2019ll remember everything. Okay, that\u2019s not very practical. You need to have some way of storing your to-do items until the next time you open up the application.\n\nStoring Data\n\nYou\u2019ve essentially got 4 different ways that you can store data:\n\n\n\tUsing the local database. AIR comes with SQLLite built in. That means you can create tables and insert, update and select data from that database just like on a web server.\n\tUsing the file system. You can also create files on the local machine. You have access to a few folders on the local system such as the documents folder and the desktop.\n\tUsing EcryptedLocalStore. I like using the EcryptedLocalStore because it allows you to easily save key/value pairs and have that information encrypted. All this within just a couple lines of code.\n\tSending the data to a remote API. Our to-do list could sync up with Remember the Milk, for example.\n\n\nTo demonstrate some persistence, we\u2019ll use the file system to store our files. In addition, we\u2019ll let the user specify where the file should be saved. This way, we can create multiple to-do lists, keeping them separate and organized.\n\nThe application is now broken down into 4 basic tasks:\n\n\n\tLoad data from the file system.\n\tPerform any interface bindings.\n\tManage creating and deleting items from the list.\n\tSave any changes to the list back to the file system.\n\n\nLoading in data from the file system\n\nWhen the application starts up, we\u2019ll prompt the user to select a file or specify a new to-do list. Within AIR, there are 3 main file objects: File, FileMode, and FileStream. File handles file and path names, FileMode is used as a parameter for the FileStream to specify whether the file should be read-only or for write access. The FileStream object handles all the read/write activity.\n\nThe File object has a number of shortcuts to default paths like the documents folder, the desktop, or even the application store. In this case, we\u2019ll specify the documents folder as the default location and then use the browseForSave method to prompt the user to specify a new or existing file. If the user specifies an existing file, they\u2019ll be asked whether they want to overwrite it.\n\nvar store = air.File.documentsDirectory;\nvar fileStream = new air.FileStream();\nstore.browseForSave(\"Choose To-do List\");\n\nThen we add an event listener for when the user has selected a file. When the file is selected, we check to see if the file exists and if it does, read in the contents, splitting the file on new lines and creating our list items within the interface.\n\nstore.addEventListener(air.Event.SELECT, fileSelected);\nfunction fileSelected()\n{\n\tair.trace(store.nativePath);\n\t// load in any stored data\n\tvar byteData = new air.ByteArray();\n\tif(store.exists)\n\t{\n\t\tfileStream.open(store, air.FileMode.READ);\n\t\tfileStream.readBytes(byteData, 0, store.size);\n\t\tfileStream.close();\n\n\t\tif(byteData.length > 0)\n\t\t{\n\t\t\tvar s = byteData.readUTFBytes(byteData.length);\n\t\t\toldlist = s.split(\u201c\\r\\n\u201d);\n\n\t\t\t// create todolist items\n\t\t\tfor(var i=0; i < oldlist.length; i++)\n\t\t\t{\n\t\t\t\tcreateItem(oldlist[i], (new Date()).getTime() + i );\n\t\t\t}\n\t\t}\n\t}\n}\n\nPerform Interface Bindings\n\nThis is similar to before where we set the click event on the Add button but we\u2019ve moved the code to save the list into a separate function.\n\n$('#add').click(function(){\n\t\tvar t = $('#text').val();\n\t\tif(t){\n\t\t\t// create an ID using the time\n\t\t\tcreateItem(t, (new Date()).getTime() );\n\t\t}\n})\n\nManage creating and deleting items from the list\n\nThe list management is now in its own function, similar to before but with some extra information to identify list items and with calls to save our list after each change.\n\nfunction createItem(t, id)\n{\n\tif(t.length == 0) return;\n\t// add it to the todo list\n\ttodolist[id] = t;\n\t// use DOM methods to create the new list item\n\tvar li = document.createElement('li');\n\t// the extra space at the end creates a buffer between the text\n\t// and the delete link we're about to add\n\tli.appendChild(document.createTextNode(t + ' '));\n\t// create the delete link\n\tvar del = document.createElement('a');\n\t// this makes it a true link. I feel dirty doing this.\n\tdel.setAttribute('href', '#');\n\tdel.addEventListener('click', function(evt){\n\t\tvar id = this.id.substr(1);\n\t\tdelete todolist[id]; // remove the item from the list\n\t\tthis.parentNode.parentNode.removeChild(this.parentNode);\n\t\tsaveList();\n\t});\n\tdel.appendChild(document.createTextNode('[del]'));\n\tdel.id = 'd' + id;\n\tli.appendChild(del);\n\t// append everything to the list\n\t$('#list').append(li);\n\t//reset the text box\n\t$('#text').val('');\n\tsaveList();\n}\n\nSave changes to the file system\n\nAny time a change is made to the list, we update the file. The file will always reflect the current state of the list and we\u2019ll never have to click a save button. It just iterates through the list, adding a new line to each one.\n\nfunction saveList(){\n\tif(store.isDirectory) return;\n\tvar packet = '';\n\tfor(var i in todolist)\n\t{\n\t\tpacket += todolist[i] + '\\r\\n';\n\t}\n\tvar bytes = new air.ByteArray();\n\tbytes.writeUTFBytes(packet);\n\tfileStream.open(store, air.FileMode.WRITE);\n\tfileStream.writeBytes(bytes, 0, bytes.length);\n\tfileStream.close();\n}\n\nOne important thing to mention here is that we check if the store is a directory first. The reason we do this goes back to our browseForSave call. If the user cancels the dialog without selecting a file first, then the store points to the documentsDirectory that we set it to initially. Since we haven\u2019t specified a file, there\u2019s no place to save the list.\n\nHopefully by this point, you\u2019ve been thinking of some cool ways to pimp out your list. Now we need to package this up so that we can let other people use it, too.\n\nCreating a Package\n\nNow that we\u2019ve created our application, we need to package it up so that we can distribute it. This is a two step process. The first step is to create a code signing certificate (or you can pay for one from Thawte which will help authenticate you as an AIR application developer).\n\nTo create a self-signed certificate, run the following command. This will create a PFX file that you\u2019ll use to sign your application.\n\nadt -certificate -cn todo24ways 1024-RSA todo24ways.pfx mypassword\n\nAfter you\u2019ve done that, you\u2019ll need to create the package with the certificate\n\nadt -package -storetype pkcs12 -keystore todo24ways.pfx todo24ways.air application.xml .\n\nThe important part to mention here is the period at the end of the command. We\u2019re telling it to package up all files in the current directory.\n\nAfter that, just run the AIR file, which will install your application and run it.\n\nImportant things to remember about AIR\n\nWhen developing an HTML application, the rendering engine is Webkit. You\u2019ll thank your lucky stars that you aren\u2019t struggling with cross-browser issues. (My personal favourites are multiple backgrounds and border radius!)\n\nBe mindful of memory leaks. Things like Ajax calls and event binding can cause applications to slowly leak memory over time. Web pages are normally short lived but desktop applications are often open for hours, if not days, and you may find your little desktop application taking up more memory than anything else on your machine!\n\nThe WebKit runtime itself can also be a memory hog, usually taking about 15MB just for itself. If you create multiple HTML windows, it\u2019ll add another 15MB to your memory footprint. Our little to-do list application shouldn\u2019t be much of a concern, though.\n\nThe other important thing to remember is that you\u2019re still essentially running within a Flash environment. While you probably won\u2019t notice this working in small applications, the moment you need to move to multiple windows or need to accomplish stuff beyond what HTML and JavaScript can give you, the need to understand some of the Flash-based elements will become more important.\n\nLastly, the other thing to remember is that HTML links will load within the AIR application. If you want a link to open in the users web browser, you\u2019ll need to capture that event and handle it on your own. The following code takes the HREF from a clicked link and opens it in the default web browser.\n\nair.navigateToURL(new air.URLRequest(this.href));\n\nOnly the beginning\n\nOf course, this is only the beginning of what you can do with Adobe AIR. You don\u2019t have the same level of control as building a native desktop application, such as being able to launch other applications, but you do have more control than what you could have within a web application. Check out the Adobe AIR Developer Center for HTML and Ajax for tutorials and other resources.\n\nNow, go forth and create your desktop applications and hopefully you finish all your shopping before Christmas!\n\nDownload the example files.", "2007", "Jonathan Snook", "jonathansnook", "2007-12-19T00:00:00+00:00", "https://24ways.org/2007/christmas-is-in-the-air/", "code"], [184, "Spruce It Up", "The landscape of web typography is changing quickly these days. We\u2019ve gone from the wild west days of sIFR to Cuf\u00f3n to finally seeing font embedding seeing wide spread adoption by browser developers (and soon web designers) with @font-face. For those who\u2019ve felt limited by the typographic possibilities before, this has been a good year.\n\nAs Mark Boulton has so eloquently elucidated, @font-face embedding doesn\u2019t come without its drawbacks. Font files can be quite large and FOUT\u2014that nasty flash of unstyled text\u2014can be a distraction for users.\n\nData URIs\n\nWe can battle FOUT by using Data URIs. A Data URI allows the font to be encoded right into the CSS file. When the font comes with the CSS, the flash of unstyled text is mitigated. No extra HTTP requests are required. \n\nDon\u2019t be a grinch, though. Sending hundreds of kilobytes down the pipe still isn\u2019t great. Sometimes, all we want to do is spruce up our site with a little typographic sugar. \n\nBe Selective\n\nDan Cederholm\u2019s SimpleBits is an attractive site. \n\n\n\nTake a look at the ampersand within the header of his site. It\u2019s the lovely (and free) Goudy Bookletter 1911 available from The League of Movable Type. The Opentype format is a respectable 28KB. Nothing too crazy but hold on here. Mr. Cederholm is only using the ampersand! Ouch. That\u2019s a lot of bandwidth just for one character.\n\nCan we optimize a font like we can an image? Yes. Image optimization essentially works by removing unnecessary image data such as colour data, hidden comments or using compression algorithms. How do you remove unnecessary information from a font? Subsetting. \n\nIf you\u2019re the adventurous type, grab a copy of FontForge, which is an open source font editing tool. You can open the font, view and edit any of the glyphs and then re-generate the font. The interface is a little clunky but you\u2019ll be able to select any character you don\u2019t want and then cut the glyphs. Re-generate your font and you\u2019ve now got a smaller file. \n\n\n\nThere are certainly more optimizations that can also be made such as removing hinting and kerning information. Keep in mind that removing this information may affect how well the type renders.\n\nAt this time of year, though, I\u2019m sure you\u2019re quite busy. Save yourself some time and head on over to the Font Squirrel Font Generator.\n\n\n\nThe Font Generator is extremely handy and allows for a number of optimizations and cross-platform options to be generated instantly. Select the font from your local system\u2014make sure that you are only using properly licensed fonts! \n\nIn this particular case, we only want the ampersand. Click on Subset Fonts which will open up a new menu. Unselect any preselected sets and enter the ampersand into the Single Characters text box. \n\nGenerate your font and what are you left with? 3KB. \n\n\n\nThe Font Generator even generates a base64 encoded data URI stylesheet to be imported easily into your project.\n\nCheck out the Demo page. (This demo won\u2019t work in Internet Explorer as we\u2019re only demonstrating the Data URI font embedding and not using the EOT file format that IE requires.) \n\nNo Unnecessary Additives\n\nIf you peeked under the hood of that demo, did you notice something interesting? There\u2019s no <span> around the ampersand. The great thing about this is that we can take advantage of the font stack\u2019s natural ability to switch to a fallback font when a character isn\u2019t available.\n\nJust like that, we\u2019ve managed to spruce up our page with a little typographic sugar without having to put on too much weight.", "2009", "Jonathan Snook", "jonathansnook", "2009-12-19T00:00:00+00:00", "https://24ways.org/2009/spruce-it-up/", "code"], [321, "Tables with Style", "It might not seem like it but styling tabular data can be a lot of fun. From a semantic point of view, there are plenty of elements to tie some style into. You have cells, rows, row groups and, of course, the table element itself. Adding CSS to a paragraph just isn\u2019t as exciting.\n\nWhere do I start?\n\nFirst, if you have some tabular data (you know, like a spreadsheet with rows and columns) that you\u2019d like to spiffy up, pop it into a table \u2014 it\u2019s rightful place!\n\nTo add more semantics to your table \u2014 and coincidentally to add more hooks for CSS \u2014 break up your table into row groups. There are three types of row groups: the header (thead), the body (tbody) and the footer (tfoot). You can only have one header and one footer but you can have as many table bodies as is appropriate.\n\nSample table example\n\nInspiration\n\nTable Striping\n\nTo improve scanning information within a table, a common technique is to style alternating rows. Also known as zebra tables. Whether you apply it using a class on every other row or turn to JavaScript to accomplish the task, a handy-dandy trick is to use a semi-transparent PNG as your background image. This is especially useful over patterned backgrounds. \n\ntbody tr.odd td {\n     background:transparent url(background.png) repeat top left;\n }\n\n * html tbody tr.odd td {\n     background:#C00;\n     filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(\n          src='background.png', sizingMethod='scale');\n }\n\nWe turn off the default background and apply our PNG hack to have this work in Internet Explorer. \n\nStyling Columns\n\nDid you know you could style a column? That\u2019s right. You can add special column (col) or column group (colgroup) elements. With that you can add border or background styles to the column.\n\n<table>\n     <col id=\"ingredients\">\n     <col id=\"serve12\">\n     <col id=\"serve24\">\n ...\n\nCheck out the example.\n\nFun with Backgrounds\n\nPop in a tiled background to give your table some character! Internet Explorer\u2019s PNG hack unfortunately only works well when applied to a cell.\n\nTo figure out which background will appear over another, just remember the hierarchy:\n\n (bottom) Table \u2192 Column \u2192 Row Group \u2192 Row \u2192 Cell (top)\n\nThe Future is Bright\n\nOnce browser-makers start implementing CSS3, we\u2019ll have more power at our disposal. Just with :first-child and :last-child, you can pull off a scalable version of our previous table with rounded corners and all \u2014 unfortunately, only Firefox manages to pull this one off successfully. And the selector the masses are clamouring for, nth-child, will make zebra tables easy as eggnog.", "2005", "Jonathan Snook", "jonathansnook", "2005-12-19T00:00:00+00:00", "https://24ways.org/2005/tables-with-style/", "code"]], "truncated": false, "table_rows_count": 336, "filtered_table_rows_count": 4, "expanded_columns": [], "expandable_columns": [], "columns": ["rowid", "title", "contents", "year", "author", "author_slug", "published", "url", "topic"], "primary_keys": [], "units": {}, "query": {"sql": "select rowid, * from articles where \"author\" = :p0 and \"author_slug\" = :p1 and \"topic\" = :p2 order by rowid limit 101", "params": {"p0": "Jonathan Snook", "p1": "jonathansnook", "p2": "code"}}, "facet_results": {}, "suggested_facets": [], "next": null, "next_url": null, "query_ms": 14.483213424682617}