Styling

The number one goal of Picobel is to expose the functionality of the HTML <audio> tag to the styling power of CSS. By converting that tag into more descrete elements (buttons, divs, spans, etc.), Picobel lets you flex your CSS muscles in whatever way you want to. This is where the magic happens!

Note: if writing your own CSS is not something you're interested in, then you can use one of Picobel's pre-made themes. The themes give you cross-browser consistency and let you choose which bits of functionality to include, but sidestep the whole style-it-yourself part (but who are we kidding? The reason you're here is because you want to style it yourself!).

Overview

Prefix

All class names are prexifed by the theme option specified when Picobel is initialised. So running picobel({ theme: "foo" }) would result in all class names being prefixed with the string "foo" (e.g. foo__play-pause).

BEM

Picobel class names follow the Block Element Modifier (BEM) methodology.

The Skeleton Theme

Often you might want to style the look-and-feel of an audio element without wanting to substantially change the behaviour of the more complex elements (such as the loading indicator or the volume and progress range sliders). For this purpose, the Skeleton theme has been provided to use as a jumping-off point for your own styles.

Skeleton theme

The markup

These are example of the markup generated by Picobel for each <audio> element it finds on a page. Because the components option allows different components to be composed in arbitrary orders, the examples have been broken down by component.

Note: for all following examples, THEME will represent whatever string value you passed as the theme option when Picobel was initialised. If no theme value is provided, the default value is picobel (e.g. THEME__loader would be picobel__loader).

root

<div class="picobel picobel--index-0 THEME" data-picobel-index="0">
    <!-- children -->
</div>

The outer element that contains all the components for an instance of Picobel. Can also include the modifiers playing, muted, and loading to indicate the current status of the player.

The data-picobel-index value and the picobel--index- class name both mark the index number of the audio player within the DOM. The first player on the page will be index 0, the second player on the page will be index 1, and so on...

Note: Indexes are calculated per context, so if for some (unlikely) reason you had multiple instances of Picobel running on the same page, each context would have it's own incrementing indexes - i.e. there would be repetition of index values on the page.

loading indicator

<div class="THEME__loader"></div>
/* Example CSS to only show the loader element when the player is in the "loading" state */
.THEME__loader {
    display: none;
    /* loader styles */
}

.THEME.loading .THEME__loader {
    display: block;
}

playPause

<button class="THEME__play-pause" type="button">
    <span class="THEME__play-pause__text">Play</span>
</button>

mute

<button class="THEME__mute" type="button">Mute</button>

volume

See the Pseudo Range Markup section for a full explaination of the <input type="range"> markup.

<div class="THEME__volume">
    <label class="THEME__volume-label" for="THEME__volume-slider__range--0" >
        <span class="THEME__volume-label-inner">
            <span class="THEME__volume-label-key">Volume </span>
            <span class="THEME__volume-label-value">10</span>
        </span>
    </label>
    <div class="THEME__volume-slider__wrapper">
        <div class="THEME__volume-slider__replacement">
            <div class="THEME__volume-slider__background"></div>
            <div class="THEME__volume-slider__indicator"></div>
            <div class="THEME__volume-slider__playhead"></div>
        </div>
        <input
            class="THEME__volume-slider__range"
            id="THEME__volume-slider__range--0"
            type="range"
            min="0"
            max="1"
            value="1"
            step="0.1"
        />
    </div>
</div>

title

<span class="THEME__title">FILE_NAME</span>

If the title attribute has been set on the <audio> element, then the title component will show this value. Otherwise the filename and extension will be use (e.g. audio-file.mp3).

artist

<span class="THEME__artist">ARTIST_NAME</span>

If the data-picobel-artist value has been set on the <audio> element, then the artist component will show this value. Otherwise the component will not be rendered.

duration

<span class="THEME__duration">1:42</span>

timer

<span class="THEME__timer">0:00</span>

progress

See the Pseudo Range Markup section for a full explaination of the <input type="range"> markup.

<div class="THEME__progress">
    <label class="THEME__progress-label" for="THEME__progress-slider__range--0">
        <span class="THEME__progress-label-inner">Progress</span>
    </label>
    <div class="THEME__progress-slider__wrapper">
        <div class="THEME__progress-slider__replacement">
            <div class="THEME__progress-slider__background"></div>
            <div class="THEME__progress-slider__indicator"></div>
            <div class="THEME__progress-slider__playhead"></div>
        </div>
        <input
            class="THEME__progress-slider__range"
            id="THEME__progress-slider__range--0"
            type="range"
            min="0"
            max="100"
            value="0"
        />
    </div>
</div>

Pseudo Range markup

While it is possible in some circumstances to style a range input (<input type="range">) using CSS alone, for deeper support and more consistency Picobel adds extra DOM elements to create a "pseudo" range element.

This approach is used in the pre-made themes and is the recommended approach when creating new themes.

The general concept is to add a new DOM node for each part you might want to style (e.g. the range "track", the "thumb", and the currently selected portion of the track), and then overlay them with an invisible HTML range elmenent to handle the functionality (and retain the baked-in keyboard accessibility).

There are two range components in Picobel: volume and progress. Here is some example CSS for the progress slider:

/*
It can be useful to set variables to ensure consistency of
the overall height of the interactive area and the height
of the visible "track"
*/
.THEME {
    --progress-height: 32px;
    --progress-inner-height: 30px;
}

/*
The wrapper that contains both pseudo and real range inputs
*/
.THEME__progress-slider__wrapper {
    height: var(--progress-height);
    font-size: 10px;
    line-height: 1;
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
}

/*
The wrapper for just the pseudo range input
*/
.THEME__progress-slider__replacement {
    position: relative;
    width: 100%;
    height: var(--progress-height);
    overflow: hidden;
}

/*
Because we're making the actual HTML input invisible to
screen readers, we need to ensure the replacement has a
visible "focus" style (the `focus` class is added by
Picobel when required)
*/
.THEME__progress-slider__replacement.focus {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
    border-radius: 2px;
    z-index: 3;
}

/*
The "track" for the pseudo range input
*/
.THEME__progress-slider__background {
    background: var(--white);
    border-top: 1px solid var(--black);
    border-bottom: 1px solid var(--black);
    height: var(--progress-inner-height);
}

/*
The "currently selected" portion of the track. Note the
width is `0` - this will be updated by Piocobel as the
audio file is played to accurately mark the percentage
of the file that has been played (in the case of the
`progress` slider. The same concept applies to the
`volume` slider too).
*/
.THEME__progress-slider__indicator {
    height: var(--progress-inner-height);
    position: absolute;
    top: 0;
    left: 0;
    background: var(--grey);
    width: 0%;
}

/*
The "thumb" of the range. A.k.a. the part that you
would touch if it was a physical control.
*/
.THEME__progress-slider__playhead {
    width: 8px;
    height: var(--progress-inner-height);
    position: absolute;
    top: 0;
    left: 0%;
    margin-left: -4px;
    background: var(--black);
}

/*
Hide dynamic elements until loading has finished
*/
.THEME.loading .THEME__progress-slider__indicator,
.THEME.loading .THEME__progress-slider__playhead {
    display: none;
}

/*
Make sure the real range element is the same size as the
pseudo one. Position it on-top of the fake and make it
invisible (so we can still get the functionality)
*/
.THEME__progress-slider__range {
    width: 100%;
    height: var(--progress-height);
    padding: 0;
    margin: 0;
    z-index: 4;
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
}

/*
Make sure the range Track is the right size and shape.
Needs prefixed versions to ensure cross-browser consistency.
*/
.THEME__progress-slider__range::-webkit-slider-runnable-track {
    width: 100%;
    height: var(--progress-height);
    cursor: pointer;
}
.THEME__progress-slider__range::-moz-range-track {
    width: 100%;
    height: var(--progress-height);
    cursor: pointer;
}
.THEME__progress-slider__range::-ms-track {
    width: 100%;
    height: var(--progress-height);
    cursor: pointer;
}