I'm trying to use interlinear glossing to show the structure of a sentence to an audience without requiring them to learn the language in question.

Are there any tools for quickly creating an interlinear gloss and getting the corresponding HTML (or other markup snippet)?

Ideally, I wouldn't have to write raw HTML to achieve this, but one does what one must do.

  • 2
    I don't know anything off the top of my head, but I'll write you up a solution tonight :). Would you be amenable to having the markup show up like this? <span class="line"><span class="text"><span class="source">Je</span><span class="gloss">I</span> <span class="text"><span class="source">mange</span><span class="gloss">eat</span></span></span>. I can also write up a Javascript precompiler to turn a more human-readable format like I (je) eat (mange) into the above so that you don't need to touch HTML or CSS.
    – Steven
    Sep 13, 2011 at 21:20
  • Yes, that is exactly the sort of thing I had in mind. Sep 13, 2011 at 21:22
  • @Steven: make that an answer!
    – Mitch
    Sep 13, 2011 at 21:34
  • @Mitch: I definitely will as soon as I write it up. I didn't want to collect "speculation votes" in the meantime.
    – Steven
    Sep 13, 2011 at 21:36
  • 1
    It would appear that Nat and Peter Olson's responses fully answer the question without my having to invent something new, so I won't go to that length for the time being. If their methods are lacking in some way for you, please let me know, and I'll definitely be able to help.
    – Steven
    Sep 14, 2011 at 1:13

7 Answers 7


This is a very important question, I think. It's central to digitization of linguistic materials on the web. I've been working on some (open source) software tools for doing this sort of thing for a while now, and while it's in too much of a messy state to be of immediate use to you, I hope it's okay if I write at some length about the approach to marking up glosses that I've found most useful, after a lot of experimentation.

I've found that the most important thing to get right in marking up glosses is to make sure that the structure makes sense. Usually, the goal of glossing is to annotate words (and sometimes morphemes) with translations for the semantics and labels for the grammatical categories. So let's take a simple example Spanish hablo as in Yo hablo español. This might be glossed:

yo habl-o español
I speak-1sg.PRES Spanish

Note that there lots of variations of how one might gloss this sentence, depending on the linguistic topic at hand and the level of specificity. For instance, there might be a citation tier, an orthographic tier, a phonetic tier, a morphological tier, and a free translation:

(1) Snifty 2012
Yo hablo español.
jo ˈaβl-o espaˈɲol
I speak-1sg.PRES Spanish
"I speak Spanish."

Some linguists work with far more tiers -- multiple orthographies, tone, whatever. But for the purposes of discussion let's stick with something simple -- morphologically delimited orthography, morphological analysis, and a free translation.

Now the first thought in my mind when I set out to start digitizing such thing was "well, it's interlinear glossing, so I'm going to be inter-calating lines." So, something like:

<div class=gloss>
  <p class=sentence lang=es>yo habl-o español</p>
  <p class=morphological>I speak-1sg.PRES Spanish</p>
  <p class=free lang=en>I speak Spanish.</p>

This seems reasonable enough at first. (And in fact some online projects have used quite a similar form of markup, check out the source of this Gothic text.) But there are difficulties in glossing if we get beyond a short, simple example sentence. The most glaring problem is that things don't line up right. What if the example sentence isn't Yo hablo español, but rather Qhichwa Simi Hamut'ana Kuraq Sunturtaq ninmi, huklla simi, Qusqu llaqtap rimayninmi huklla allin qhichwa simi, nispa.

The fact of the matter is that things will get ugly fast. Consider:

  • What do we do to make sure words and their tiered annotations align vertically across the various lines?
  • How do we handle line wraps?

One horrid thought was to use tables -- one could argue that a gloss is ''kind of'' tabular, I guess. At least vertical alignment could be handled. So:

    <td>yo</td>    <td>habl-o</td>    <td>español</td>
    <td>I</td>    <td>speak-1sg.PRES</td>    <td>Spanish</td>
    <td colspan="3">I speak Spanish.</td>

All this for a three-word sentence? This is just awful. You have to keep track of which td corresponds to each of the other tds in all of the other trs... and the free translation "row" isn't really a row at all. Imagine trying to set that Runasimi sentence from above... Blech!

And it's this sort of problem that I think motivates the key insight about marking up glosses:

A word and its annotations should be siblings in the markup, and anything above the level of a word should be represented as a list of word-level items.

We can view such a structure, as a nested HTML list:

    1. yo
    2. I
    1. habl-o
    2. speak-1sg.PRES
    1. español
    2. Spanish

Although it looks quite weird in this format (especially with this site's default CSS), this is actually, I think, the "right" structure for capturing what's really going on in a so-called interlinear gloss. Here's how markup for that might look:

<ol class=sentence>
    <ol class=word>
      <li lang=es>yo</li>
      <li lang=en_MORPH>I</li>

    <ol class=word>
      <li lang=es>habl-o</li>
      <li lang=en_MORPH>speak-1sg.PRES</li>

    <ol class=word>
      <li lang=es>español</li> 
      <li lang=en_MORPH>Spanish</li>


Now, this might not intuitively look much like a gloss at first blush, at least not visually. But the structure is right. And in fact, with a little CSS, we can render these nested lists in a way that looks exactly like a traditional gloss, and which behaves correctly.

We can't mess with CSS in the posts within this site itself, but you can check out this jsfiddle to see what I'm talking about.

div.wrapper {
    background: hsla(55, 99%, 80%, 50);
    padding: 5px;
    overflow: auto;
    margin: 1em 0;
ol.sentence {
    display: inline;
ol.sentence li[lang=es]{
    font-weight: bold;
ol.sentence li[lang=en_MORPH]{
    font-family: monospace;
ol.word {
    padding: .5em;
    margin-bottom: .5em;
    background: white;
    border: 1px dotted #ccc;
    display: inline-block;

You'll notice that the very same markup of that three-word sentence wraps correctly if we place it inside a wrapper with a fixed width.

There's a lot more that could be said about this topic. Specifically, I haven't answered the OP's question with regard a tool for generating such markup, but I think something along the lines of the nifty tool that Nat put together above could solve the problem.

I think it would be worth having a discussion about more generic tools for editing, saving, searching, and otherwise manipulating and sharing glosses on the web. (I've been trying to wrap my head around backbone.js as a tool to do just this sort of thing, but the learning curve is pretty steep.)

  • The disadvantage of approaches such as the one you propose is that it doesn't work well with text-based browsers or screen readers. I'm using a similar approach, which looks like this in Lynx.
    – bb94
    Jan 30, 2021 at 23:04

I don't know of any tools to do this, but in HTML5 there is an awesome ruby element.

For example, if I wanted to give an English translation above the Spanish sentence Quiero comer una caja de caracoles, I could use this HTML markup:

    <rb>Quiero</rb><rt>I want</rt>
    <rb>comer</rb><rt>to eat</rt>

You can see a few samples of this in action here.

This is not directly supported in all browsers, but that can easily be amended with a CSS stylesheet.

  • 1
    +1 for semantically valid elements.
    – TRiG
    Sep 14, 2011 at 10:20
  • 6
    Ruby is an excellent solution for the case where the glosses only have one tier, but what about the (very common, in linguistics case) where there are two or more tiers (say, phonetic, morphological, and word-for-word)?
    – pat
    Sep 18, 2011 at 0:10

I came across a solution here, which has several versions of interlinear glosses as accomplished through a combination of simple HTML, CSS, and javascript.

The simplest version requires no javascript, and relies on the following: HTML "unit":

<div class="unit">
    <p class="gk">οὕτως</p>
    <p class="en">such</p>

The magic here comes in the CSS, which can be abstracted as follows

div.unit {
    float: left;
    margin-bottom: 1em;
p.en {
    margin: 0em;
    padding: 0em 0.5em;
p.gk {
    margin: 0em;
    padding: 0em 0.5em;

Essentially, the idea is to provide both word-sets with the same amount of padding, but to contain them within a div that automatically resizes to the size of the largest element.

That said, I briefly threw something together based on a combination of this link's answer and Steven Xu's comment above, and you can find it here

The sample is from John 3:16, as from the link, but you can replace it with any text of the form LANG1(LANG2) . Note that currently the script requires you to have the open paren adjacent to the last letter of the source text, but does allow for multiword source and target. I am open to any suggestions to improve this tool. Please note it is very basic but it gets the job done!

PS: The current CSS is static, but could potentially be dynamic and based on user-supplied options. A later version?

  • 4
    I was going to post this (as it's my own blog post) but amazing to see someone beat me to it :-) Sep 18, 2011 at 16:32

I use just html and css.

Here's the html.

<div class="interlinear">
        <div class="orig">det ordet</div>
        <div class="intlin">
            <span class="orig">det</span>
            <span class="morph">det</span>
            <span class="trans">1s.neuter.definite</span>
        <div class="intlin">
            <span class="orig">ordet</span>
            <span class="morph">ord -et</span>
            <span class="trans">word -1s.neuter.definite</span>
        <div class="freetrans">the word</div>

Here's the css:

.interlinear {clear: both; text-align: left;}
.intlin {float: left; margin: 0.25em; text-align: left;}
.intlin .morph, .intlin .trans, .intlin .orig, .interlinear {display: block; margin: 2px 0;}
.intlin .orig {font-weight: bold;}
.interlinear .freetrans { font-style: italic;}
.interlinear div.orig { font-weight: bold; text-align: left;}
.interlinear .morph { color: #10a }

Since it seems impossible to highlight inside a pre-tag in this variety of markdown, below is just the magic bits of the css:

.interlinear {clear: both}
.intlin {float: left}
.intlin .morph, .intlin .trans, .intlin .orig, .interlinear {display: block}

Somewhere I have several tools that can generate this from a SIL Toolbox interlinear via xml-export and xslt.


I have written a jQuery wrapper method to align interlinear glosses.

The nice thing about this approach is that it minimizes the HTML markup that you need to put into the lines of your linguistic data. For example, something like:

<div class="align-me">
  <div>les chiens dormaient</div>
  <div>le-s chien-s dorm-ai-ent</div>
  <div>DET-PL dog-PL sleep-IMPF-3PL</div>

could be aligned by calling:


I have not yet extensively debugged this, but here's the code. Comments/suggestions welcome:

(function($) {
    // IGT - Interlinear Gloss Text -- this wrapped set method correctly aligns
    //  words in interlinear gloss text format into columns.  Each element of
    //  the set is expected to contain two or more elements and the text of
    //  each of these sub-elements is considered to be the line.  In the
    //  following example, 'les chiens', 'le-s chien-s' and 'DET-PL dog-PL' are
    //  the lines:
    //    <div class="align-me">
    //      <div>les chiens</div>
    //      <div>le-s chien-s</div>
    //      <div>DET-PL dog-PL</div>
    //    </div>
    //  Basic usage:
    //    $('div.align-me').igt();
    //  Usage with options:
    //    $('div.align-me').igt({buffer: 20, lineGroupBuffer: 5, indent: 60,
    //                          minLineWidthAsPerc: 75});

    $.fn.igt = function (options) {

        // Align words in each element of the wrapped set
        $(this).each(function () {

            var container = $(this);

            // Each child is a line whose words may need alignment
            var children = $(this).children();

            // spanWidths holds the width of each span in each line; it will
            //  look something like [[49, 32, 40], [66, 49, 40], [61, 99, 25]]
            var spanWidths = [];  

            // colWidths holds the width of each column, i.e., the width of the
            //  longest <span>-wrapped word with index x, e.g., [66, 99, 40]
            var colWidths = [];

            // lineHeights holds height of each line
            var lineHeights = []; 

            // OPTIONS //

            if (options === undefined) options = {};

            // Number of pixels to put between each span
            var buffer = (options.buffer === undefined) ? 30: options.buffer;

            // Number of pixels to put between groups of lines ("lineGroups")
            var lineGroupBuffer = (options.lineGroupBuffer === undefined) ?
                                    10 : options.lineGroupBuffer;

            // Number of pixels to indent each subsequent line
            var indent = (options.indent === undefined) ? 40 : options.indent;

            // Minimum width of a line as a percentage of the container's width
            var minLineWidthAsPerc = (options.minLineWidthAsPerc === undefined) ?
                                    50 : options.minLineWidthAsPerc;

            // Line Group Class: class to give to line groups
            var lineGroupClass = (options.lineGroupClass === undefined) ?
                            'old-form-igt-line-group': options.lineGroupClass;

            // FUNCTIONS //

            // Spanify -- input: line of text; output: line with each word
            //  enclosed in a span tag
            function spanify(elementText) {
                return $.map(elementText.replace(/\s\s+/g, ' ').split(' '),
                    function (word) {
                        return '<span style="white-space: nowrap;">' + word +

            // Get Greatest Width -- return the greatest width among the words
            //  in the same 'column'.
            function getGreatestWidth(widths, index, spanIndex) {
                if (colWidths[spanIndex] === undefined) {

                    // E.g., from [[49, 32, 40], [66, 49], [61, 99, 25]],
                    //  return [40, 25] (assuming spanIndex = 2)
                    widths = $.map(widths, function (widthSet) {
                        if (widthSet.length == widths[index].length)
                            return widthSet[spanIndex];
                            return 0;

                    result = Math.max.apply(Math, widths); // Get the widest
                    colWidths[spanIndex] = result;  // Remember for later
                    return result;

                } else {
                    // We know max width of this column from previous iterations
                    return colWidths[spanIndex];

            // Set Width -- set the width of the span to the width of the widest
            //  span in the same column PLUS the buffer.
            function setWidth(index, spanIndex, span) {
                greatestWidth = getGreatestWidth(spanWidths, index, spanIndex);
                $(span).css({display: 'inline-block',
                            width: greatestWidth + buffer});

            // Sum -- sum all integers in an array (c'mon Javascript!)
            function sum(array) {
                result = 0;
                for (i = 0;i < array.length;i += 1)
                    result += array[i];
                return result;

            // Get New Max Width: get the max width of a "line group" based on
            //  the current max width, indent and minLineWidthAsPerc
            function getNewMaxWidth(currentMaxWidth, minLineWidth) {
                if ((currentMaxWidth - indent) > minLineWidth) {
                    return currentMaxWidth - indent;
                } else {
                    // Sorry, we can't reduce the width any further
                    return currentMaxWidth;


            // Enclose each word of each child in span tags, record the width
            //  of each such span tag and the height of each line
            children.each(function (index, child) {
                // wrap words in spans

                // record the width of each span
                var widths = [];
                $('span', child).each(function (index, span) {

                // Record the height of each line
                lineHeights.push($($('span', child)[0]).height());


            // linesToColumnify is an array of indices representing the lines
            //  whose span-wrapped words need to be aligned.  Such lines have
            //  a word count that is greater than one and equal to that of all
            //  subsequent lines.
            var linesToColumnify = [];
            $.each(spanWidths, function (index, line) {
                // isColumnable returns true if the last line has more than two
                //  words and all lines from this one on down have the same word
                //  count
                function isColumnable(index, line) {
                    if (spanWidths[spanWidths.length - 1].length < 2)
                        return false;
                    for (var i = index + 1; i < spanWidths.length; i++) {
                        if (spanWidths[i].length !== spanWidths[index].length)
                            return false;
                    return true;
                if (isColumnable(index, line)) linesToColumnify.push(index);

            // Set the width of each span tag to the width of the longest span
            //  tag in the same 'column' plus the buffer
            children.each(function (index, child) {
                // Only alter the width of spans inside of columnable lines
                if (linesToColumnify.indexOf(index) !== -1) {
                    $('span', child).each(function (spanIndex, span) {
                        setWidth(index, spanIndex, span);

            // If the container's height is not equal to the sum of the line
            //  heights, we have lines wrapping and need to fix that by breaking
            //  the lines into multiple lines.
            var containerHeight = $(this).height();
            if (containerHeight !== sum(lineHeights)) {
                var containerWidth = $(this).width();
                var minLineWidth = Math.round(minLineWidthAsPerc / 100 *

                // Create the lineGroups list of objects; this tells us the max
                //  width of each line and, indirectly via the spanWidths object,
                //  the slice of <span>-wrapped words we want in each line.
                var lineGroups = [{maxWidth: containerWidth, indent: 0,
                                 spanWidths: []}];
                $.each(colWidths, function (index, width) {
                    lineGroup = lineGroups[lineGroups.length - 1];
                    if ((sum(lineGroup.spanWidths) + width + buffer)
                        < lineGroup.maxWidth) {
                        lineGroup.spanWidths.push(width + buffer);
                    } else {

                            getNewMaxWidth(lineGroup.maxWidth, minLineWidth),
                            spanWidths: [width + buffer]});

                // Create a new container that has the lines broken up,
                //  grouped and indented appropriately.
                var newContainer = $('<div>');
                var begin = 0;
                previousIndent = 0;
                $.each(lineGroups, function (index, lineGroup) {
                    var topMarg = (index !== 0) ? lineGroupBuffer : 'auto';
                    var currentIndent = ((lineGroup.maxWidth -
                        (index * indent)) < minLineWidth) ? previousIndent :
                        (index * indent);
                    previousIndent = currentIndent;

                    var lineGroupDiv = $('<div>')
                                            .css({'margin-left': currentIndent,
                                                'margin-top': topMarg});
                    var end = begin + lineGroup.spanWidths.length;
                        function (index, line) {
                                    .html($(line).children().slice(begin, end)));
                    begin = end;

                // Replace the container's children with those of the new container


Pat's answer works well and explains a lot, but I'd like to add something:

Logically, the most suitable HTML element for interlinear glosses is the not-very-well-known DL element ("description list"): The DL element wraps a list of definitions or descriptions, each consisting of one DT element ("description term") which defines a term that should be explained, and one or more DD elements ("description definition") which provide that explanation. Applied to an interlinear gloss, the DT element corresponds to the word in the target language, and the various DD elements correspond to various levels of glossing, e.g. morpheme breakdown, morpheme-by-morpheme gloss, word-by-word translation etc.

Based on this logic, I have been using the following structure for interlinear glosses in a HTML environment for several years (Lezgian example from the Leipzig Glossing Rules):

<dl class="gloss">
<dl class="gloss">
<dl class="gloss">
<dl class="gloss">
<dl class="gloss">
<dl class="gloss">
<div class="glend">Now their farm will not stay behind forever.</div> 

The final DIV.glend element contains a free translation of the whole sentence.

The corresponding CSS code looks something like the following:

dl.gloss {
  margin:0 0.5em 0 0;
dl.gloss dt {
dl.gloss dd {
  margin-left:0 !important;
.glend {
  • When I did this, I found it more useful to outright generate a fake table using the css display property, as that automatically aligned the cells into columns.
    – Circeus
    Jul 12, 2017 at 21:49

You can do lovely interlinear glosses with several Latex packages (gb4e and covington, but I think there are others as well). It is possible to convert Latex to HTML, but I have never tried it, so I'm not sure how well that would work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.