Not that I want to repeat all those desktop user interface patterns in the browser environment, but a titled border is simply a beautiful and elegant way to style a chapter in a web page.
WARNING: this article uses pure JavaScript (JS), no jQuery:-(, and ignores web-browsers that do not follow standards.
Here comes a CSS-only variant of "titled border", without JS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1"/> <style> .titled-border { border: 1px solid gray; border-radius: 0.5em; position: relative; /* be parent for absolutely positioned title */ padding-top: 0.5em; /* leave half of the title height space on top */ } .border-title { position: absolute; /* go out of the layout, let followers fill up */ top: -0.5em; /* slip upwards */ left: 0.4em; /* distance to border corner */ background-color: white; /* cover underlying border */ padding: 0 0.3em 0 0.3em; /* leave space between border and text, left and right */ } </style> </head> <body> <div class="titled-border"> <span class="border-title"><b>Titled Border</b></span> <div> Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. </div> </div> </body> </html> |
I created two CSS rules-sets, the selector .titled-border
is for the border
and .border-title
is for the text.
The .titled-border
carries the border specification,
and a relative positioning, which is for being the coordinate system to the absolutely positioned title element.
Further it specifies a padding-top
to make place for the title that will be lifted there.
The .border-title
is positioned absolutely, it goes upwards half a letter height,
and a little to the left to keep distance from the border corner. Then it needs a defined background-color
to not let strike through the border. The final paddings are the gaps between the corder and the title.
The HTML is simple, any border is an enclosing div
, and the title is its first child element.
Best are
inline-elements like span
,
as they do not expand across the whole width, like block-elements do.
Is it that easy?
I thought I was done, and tried this out in combination with different HTML. Here is a screenshot of the result.
As you see, there is a significant problem with positioning the title text,
and letting the border be visible where it should be.
Easy only for left-aligned titles, and the real problems start when using a <H1>
as title,
because this has margins
and is full-width.
Write CSS for each such box?
So I faced the necessity to either write new CSS for every bordered title I come across, or develop this further using JavaScript (JS). Of course I wanted to reuse a concept that makes it easy to style the title and the border independently of their CSS declarations.
Here comes my titled-border solution!
CSS
<style type="text/css"> .titled-border { border: 2px solid gray; border-radius: 0.5em; } </style>
Not much CSS is left. Positioning must be done dynamically, same for the background-color of the text.
You could even leave out this CSS and style each border individually via inline-styles.
But mind that the (now following) JS will search for the classes titled-border
and border-title
,
so you need at least to set titled-border
upon the border-element.
By default the first child could count as title, so this class can be ommitted, except on nested
titled borders, where the JS would fail when border-title
was not set upon the title element.
JS
Here is the scetch of the script that will evolve now.
You can find the complete source on bottom of this page, or on my
homepage.
<script type="text/javascript"> "use strict"; var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS) { var that = {}; // .... that.init = function(topElement) { topElement = topElement || document.body; // .... }; return that; }; titledBorderFactory("titled-border", "border-title").init(); </script>
This is the closure (or module) that encapsulates functions and variables working on titled borders.
As top-level factory-function parameters I want to pass the CSS class-names of the titled-border and the border-title,
so that this is not restricted to titled-border
and border-title
.
Then I would like to pass an optional top-level element where to search below for titled borders,
in case I want to have different areas on my page. The default for this would, of course, be
the document.body
element to process the whole page.
All the now following functions go into that closure.
Find Titled Border Elements
Here is the init()
implementation, which could be understood as constructor.
/** * Initializes all titled blocks below given element or in whole document. * @param topElement optional, the container to search for titled borders, default is document.body. */ that.init = function(topElement) { topElement = topElement || document.body; var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS); for (var i = titledBlocks.length - 1; i >= 0; i--) { var titledBlock = titledBlocks[i]; var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */ if ( ! title ) /* by default take first child */ title = titledBlock.children[0]; if (title) positionTitle(titledBlock, title); } };
This sets the default in case no parameter has been given.
Then it searches for CSS class titled-border
below the top element.
For each of them, it retrieves the related title element and passes both for further processing to
positionTitle()
.
Position the Title on the Border
Here comes the hard layout work to be done. It is commented extensively, so try to read the comments and understand the source by them. That is what comments are made for. Functions used here may be implemented in the utils on bottom.
/** Moves the title into a div that is created as first child of the block. */ var positionTitle = function(titledBlock, title) { var titleHeight = title.offsetHeight; /* height including padding and border */ var titleStyle = window.getComputedStyle(title); /* need to calculate margins */ var titleMargins = getTopAndBottomMargins(titleStyle); var fullTitleHeight = titleHeight + titleMargins; var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle)); styleTitle(titleStyle, title, titledBlock); styleTitledBlock(titleHeight, titledBlock); titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */ title.parentElement.removeChild(title); /* move title from parent to titleContainer */ titleContainer.appendChild(title); };
This finds together some variables that are used to style the elements now.
Most important is the full height of the title. The negative half of this will be the top
offset.
Mind that the computed style is used here, because with element styles you might see only the inline-element margins.
Then a new element is created, to be used as wrapper for the title.
This is needed to align the title text left, center or right, given by its own text-align
CSS property.
The new element is styled in its own function, as are the border and the title.
Finally the new element is inserted as first child into the border-container, and the title element is moved into the new element.
/** Creates title container that allows alignment. */ var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) { var titleContainer = document.createElement("div"); /* need a new element */ titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */ titleContainer.style["background-color"] = "transparent"; /* let shine through border */ titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */ titleContainer.style.left = "0"; /* to enable alignment, stick to the left */ titleContainer.style.width = "100%"; /* to enable alignment, take whole width */ titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */ titleContainer.style["text-align"] = alignment; /* adopt alignment from title */ if (alignment !== "center") /* keep distance from border-corner */ titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px"; return titleContainer; }; /** Prepares title. */ var styleTitle = function(titleStyle, title, titledBlock) { var backgroundColor = titleStyle["background-color"]; if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */ title.style["background-color"] = findParentBackgroundColor(titledBlock); title.style["padding-left"] = "0.2em"; /* distance between text and border */ title.style["padding-right"] = "0.3em"; title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */ title.style["white-space"] = "nowrap"; /* do not break title into new line */ title.style["overflow"] = "hidden"; /* do not write into other elements */ }; /** Prepares bordering block. */ var styleTitledBlock = function(titleHeight, titledBlock) { titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */ titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */ titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */ };
The title-container needs to be absolutely positioned towards the relative border-container.
It goes upwards (negative top
) half of its height.
It must be transparent to let the border strike through, and it needs to be full-width to enable alignment.
Then it sets the alignment, avoiding
layout problems by using box-sizing: border-box.
The title styles must guarantee that the border does not strike through, using a background color.
This is retrieved from the parents when not declared on the element itself, default is white.
Important here is the display: inline-block
style. It makes any title that was defined
as block element shrink its width to what is really needed. Only that way I can align in its container block.
The border element finally needs to be positioned relatively, because it will be the coordinate system
for the absolute title-container. It must provide a margin-top
for the title to not slip out of the page on top,
and a padding-top
for the following child elements to not slip into the lower part of the title.
Utils
Here are the remaining functions that have been used in the code above.
var findParentBackgroundColor = function(element) { while (element && element !== document.documentElement) { var style = window.getComputedStyle(element); var backgroundColor = style["background-color"]; if (backgroundColor && ! isTransparent(backgroundColor)) return backgroundColor; element = element.parentElement; } return "white"; }; var isTransparent = function(color) { return color === "transparent" || color === "rgba(0, 0, 0, 0)"; }; var getAlignment = function(style) { var alignment = style["text-align"]; return ! alignment ? "left" : (alignment === "start") ? "left" : alignment; }; var getTopAndBottomMargins = function(style) { return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]); };
Mind that always the computed style is passed to these functions, not the element-style.
Remarkable may be the isTransparent()
function, amazingly there is no standard
for this. Also remarkable the text-align
property value "start" that I encountered on Webkit browsers.
Click here to see the whole source code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | "use strict"; /** * Creates an object with an init() function that can layout titled borders. * @param TITLED_BLOCK_CLASS optional, name of CSS class designating the bordered container, without leading dot. * @param BLOCK_TITLE_CLASS optional, name of CSS class designating the title, without leading dot. * @returns an object to call init(). */ var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS) { var that = {}; var findParentBackgroundColor = function(element) { while (element && element !== document.documentElement) { var style = window.getComputedStyle(element); var backgroundColor = style["background-color"]; if (backgroundColor && ! isTransparent(backgroundColor)) return backgroundColor; element = element.parentElement; } return "white"; }; var isTransparent = function(color) { return color === "transparent" || color === "rgba(0, 0, 0, 0)"; }; var getAlignment = function(style) { var alignment = style["text-align"]; return ! alignment ? "left" : (alignment === "start") ? "left" : alignment; }; var getTopAndBottomMargins = function(style) { return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]); }; /** Creates title container that allows alignment. */ var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) { var titleContainer = document.createElement("div"); /* need a new element */ titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */ titleContainer.style["background-color"] = "transparent"; /* let shine through border */ titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */ titleContainer.style.left = "0"; /* to enable alignment, stick to the left */ titleContainer.style.width = "100%"; /* to enable alignment, take whole width */ titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */ titleContainer.style["text-align"] = alignment; /* adopt alignment from title */ if (alignment !== "center") /* keep distance from border-corner */ titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px"; return titleContainer; }; /** Prepares title. */ var styleTitle = function(titleStyle, title, titledBlock) { var backgroundColor = titleStyle["background-color"]; if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */ title.style["background-color"] = findParentBackgroundColor(titledBlock); title.style["padding-left"] = "0.2em"; /* distance between text and border */ title.style["padding-right"] = "0.3em"; title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */ title.style["white-space"] = "nowrap"; /* do not break title into new line */ title.style["overflow"] = "hidden"; /* do not write into other elements */ }; /** Prepares bordering block. */ var styleTitledBlock = function(titleHeight, titledBlock) { titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */ titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */ titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */ }; /** Moves the title into a div that is created as first child of the block. */ var positionTitle = function(titledBlock, title) { var titleHeight = title.offsetHeight; /* height including padding and border */ var titleStyle = window.getComputedStyle(title); /* need to calculate margins */ var titleMargins = getTopAndBottomMargins(titleStyle); var fullTitleHeight = titleHeight + titleMargins; var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle)); styleTitle(titleStyle, title, titledBlock); styleTitledBlock(titleHeight, titledBlock); titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */ title.parentElement.removeChild(title); /* move title from parent to titleContainer */ titleContainer.appendChild(title); }; /** * Initializes all titled blocks below given element or in whole document. * @param topElement optional, the container to search for titled borders, default is document.body. */ that.init = function(topElement) { topElement = topElement || document.body; var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS); for (var i = titledBlocks.length - 1; i >= 0; i--) { var titledBlock = titledBlocks[i]; var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */ if ( ! title ) /* by default take first child */ title = titledBlock.children[0]; if (title) positionTitle(titledBlock, title); } }; return that; }; /* initialize all titled borders */ titledBorderFactory("titled-border", "border-title").init(); |
Keine Kommentare:
Kommentar veröffentlichen