Pen Settings

HTML

CSS

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

JavaScript

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

Behavior

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.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

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

Visit your global Editor Settings.

HTML

              
                <template id="data-card">
    <article class="card" data-card tabindex="0" id="">
        <header>
            <div>
                <h2></h2>
                <p data-title></p>
                <p data-pronouns></p>
            </div>
            <img src="" alt="" width="90">
        </header>
        <p></p>
        <footer>
            <div data-del-container>
                <p>Actions:</p>
                <button data-del-button>Delete <span class="visually-hidden" data-name></span></button>
            </div>
            <div data-confirm-container
                 role="alertdialog"
                 hidden>
                <h3 data-confirm-heading class="visually-hidden">Delete confirmation</h3>
                <p data-confirm-description>Are you sure you want to permanently delete <span data-name></span>?</p>
                <button data-cancel-button type="reset">Cancel</button>
                <button data-confirm-button>Yes, delete <span data-name></span></button>
            </div>
        </footer>
    </article>
</template>

<main>
    <h1>UX people</h1>
    <p>Some people that deal with UX things.</p>
    <div id="card-conttainer" role="feed">
        
    </div>
</main>
              
            
!

CSS

              
                [role="feed"] {
    display: grid;
    grid-template-rows: auto;
    grid-gap: 1.5em;
}

@media (min-width: 35em) {
    [role="feed"] {
        grid-template-columns: repeat(2, 1fr);
    }
}

.card {
    display: grid;
    grid-template-rows: auto 1fr auto;
    padding: calc(1em - 3px);
    border: 3px solid transparent;
    border-radius: .5em;
    box-shadow: 0 .25em .5em rgba(0,0,0,.5);
}

.card:focus {
    border-color: purple;
    outline: none;
}

.card > header {
    display: grid;
    grid-template-areas: "avatar data";
    grid-template-columns: auto 1fr;
    grid-column-gap: 1em;
    padding-bottom: .5em;
    border-bottom: 2px solid purple;
    
    // Overide base template defaults
    justify-content: unset;
    align-items: unset;
}

.card > header h2 {
    margin: 0;
    font-size: 1.25em;
}

.card > header p {
    margin: 0;
}

.card > header p[data-pronouns] {
    font-style: italic;
}

.card > header img {
    grid-area: avatar;
    border-radius: 3px;
}

.card > header div {
    grid-area: data;
}

.card > footer {
    margin-top: 1em;
    padding-top: .5em;
    border-top: 1px solid #333;
}

.card > footer p {
    margin: 0;
}

.card button {
    margin-top: .5em;
}

.card button + button {
    margin-left: .5em;
}

.visually-hidden {
    position: absolute;
    clip: rect(0,0,0,0);
}

[hidden] {
    display: none;
}

button {
  display: inline-block;
  position: relative;
  padding: 0.4em 0.7em;
  border: 1px solid hsl(0, 71%, 49%);
  border-radius: 5px;
  box-shadow: 0 1px 2px hsl(0, 27%, 55%);
  color: #fff;
  font: inherit;
  background-color: hsl(0, 82%, 51%);
  background-image: linear-gradient(to bottom, hsl(0, 82%, 53%), hsl(0, 82%, 47%));
}

button:hover {
  border-color: hsl(0, 71%, 29%);
  background-color: hsl(0, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(0, 82%, 33%), hsl(0, 82%, 27%));
  cursor: default;
}

button:focus {
  outline: none;
}

button:focus::before {
  position: absolute;
  z-index: -1;

  /* button border width - outline width - offset */
  top: calc(-1px - 3px - 3px);
  right: calc(-1px - 3px - 3px);
  bottom: calc(-1px - 3px - 3px);
  left: calc(-1px - 3px - 3px);
  border: 3px solid hsl(0, 71%, 49%);

  /* button border radius + outline width + offset */
  border-radius: calc(5px + 3px + 3px);
  content: '';
}

button:active {
  border-color: hsl(0, 71%, 49%);
  background-color: hsl(0, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(0, 82%, 53%), hsl(0, 82%, 47%));
  box-shadow: inset 0 3px 5px 1px hsl(0, 82%, 30%);
}

button[type="reset"] {
  border-color: hsl(0, 0%, 49%);
  box-shadow: 0 1px 2px hsl(0, 0%, 55%);
  color: #333;
  background-color: hsl(0, 0%, 93%);
  background-image: linear-gradient(to bottom, hsl(0, 0%, 93%), hsl(0, 0%, 87%));
}

button[type="reset"]:hover {
  border-color: hsl(0, 0%, 29%);
  background-color: hsl(0, 0%, 31%);
  background-image: linear-gradient(to bottom, hsl(0, 0%, 73%), hsl(0, 0%, 67%));
}

button[type="reset"]:focus::before {
  border-color: hsl(0, 0%, 49%);
}

button svg {
  margin: 0.15em auto -0.15em;
  height: 1em;
  width: 1em;
  pointer-events: none;
}
              
            
!

JS

              
                let data = [
    {
        fullName: 'Zoë Bijl',
        shortName: 'Zoë',
        title: 'Excellent Earth Person',
        pronouns: 'they/them',
        description: 'Zoë is an editor of W3C’s WAI-ARIA APG and they sporadically write on their blog. They work as an accessibility engineer for CrowdStrike.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Zoe.JPEG',
            alt: 'Portrait of Zoë with a dinosaur hat'
        }
    },
    {
        fullName: 'Daniela Remogne',
        shortName: 'Daniela',
        title: 'Engineering Manager',
        pronouns: 'she/her',
        description: '#JavaScript #EmberJS #CyberSecurity #WomanWhoCode #WomenInCyberSecurity #Coffee #Foodie',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Daniela.jpg',
            alt: 'Portrait of Daniela with a baret'
        }
    },
    {
        fullName: 'Michal Bryxí',
        shortName: 'Michal',
        title: 'Engineer III',
        pronouns: 'he/him',
        description: '- bike 🚴 - run 🏃🏻‍ - climb 🧗 - travel 🌍 - enjoy life 💚 IT guy with the need to live fully.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Michal.jpg',
            alt: 'Portrait of Michal with a coast in the background'
        }
    },
    {
        fullName: 'Tatiana Mac',
        shortName: 'Tatiana',
        title: 'Inclusive & Accessible Designer',
        pronouns: 'she/her',
        description: 'Editor @AListApart — Teacher @skillshare (https://skl.sh/2HQoyXo ) — Sartorialist — Traveller',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Tatiana.jpg',
            alt: 'Portrait of Tatiana looking fabulous'
        }
    },
    {
        fullName: 'Natalie Patrice Tucker',
        shortName: 'Natalie',
        title: 'Accessibility Evangelist',
        pronouns: 'mostly she',
        description: 'Web Developer Committed to a Web that works for everyone #a11y.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Natalie.jpg',
            alt: 'Portrait of Patrice'
        }
    },
    {
        fullName: 'Alex Graul',
        shortName: 'Alex',
        title: 'Director of UX',
        pronouns: 'he/him',
        description: 'Data visualisation & information interfaces. Expect intermittent otters.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Alex.jpg',
            alt: 'Portrait of Alex'
        }
    },
    {
        fullName: 'Aurora Suriel Melchor',
        shortName: 'Aurora',
        title: 'Visual Scribe',
        pronouns: 'she/her',
        description: 'I draw words and write drawings. UX designer, Visual Scribe and occasional Comicker.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Aurora.jpg',
            alt: 'Portrait of Aurora'
        }
    },
    {
        fullName: 'JaEun Jemma Ku',
        shortName: 'Jemma',
        title: 'Accessible UX Designer & Developer',
        pronouns: 'she/her',
        description: '@w3c ARIA APG co-editor and co- facilitator, Principal investigator for @a11yfirsteditor, A passionate tennis player.',
        avatar: {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/154571/Jemma.jpg',
            alt: 'Portrait of Jemma'
        }
    }
]

/* Template
{
        fullName: '',
        shortName: '',
        title: '',
        pronouns: '',
        description: '',
        avatar: {
            src: '',
            alt: ''
        }
    }
*/

class Card {
    constructor(data, id, templateId, index) {
        this.data = data
        this.index = index
        this.container = document.getElementById(id)
        this.template = document.getElementById(templateId)
        this.card = document.importNode(this.template.content, true)
        this.setID()
        this.fillData()
        this.getElements()
        this.bind()
        this.addCard()
    }
    
    setID() {
        this.randomNumber = this.index + this.getRandomInt() * this.getRandomInt()
        this.element = this.card.querySelector('[data-card]')
        this.element.id = `card-${this.randomNumber}`
    }
    
    fillData() {
        let fullName = this.card.querySelector('h2')
        fullName.textContent = this.data.fullName
        
        let title = this.card.querySelector('[data-title]')
        title.textContent = this.data.title
        
        let pronouns = this.card.querySelector('[data-pronouns]')
        pronouns.textContent = this.data.pronouns
        
        let avatar = this.card.querySelector('img')
        avatar.src = this.data.avatar.src
        avatar.alt = this.data.avatar.alt
        
        let description = this.card.querySelector('header + p')
        description.textContent = this.data.description
        
        let name = this.card.querySelectorAll('[data-name]')
        name.forEach(name => name.textContent = this.data.shortName)
    }
    
    getElements() {
        this.delContainer = this.card.querySelector('[data-del-container]')
        this.delButton = this.card.querySelector('[data-del-button]')
        this.confirmContainer = this.card.querySelector('[data-confirm-container]')
        this.confirmButton = this.card.querySelector('[data-confirm-button]')
        this.cancelButton = this.card.querySelector('[data-cancel-button]')
    }
    
    bind() {
        this.boundEsc = this.escEventHandler.bind(this)
        this.delButton.addEventListener('click', this.askConfirmation.bind(this))
    }

    askConfirmation() {
        this.delContainer.setAttribute('hidden', 'hidden')
        this.confirmContainer.removeAttribute('hidden', 'hidden')
        this.cancelButton.focus()
        this.cancelButton.addEventListener('click', this.cancel.bind(this))
        this.confirmButton.addEventListener('click', this.delete.bind(this))
        window.addEventListener('keydown', this.boundEsc)
    }
    
    escEventHandler(event) {
        let key = event.key
        if (key === 'Escape') {
            this.cancel()
        }
    }
    
    delete() {
        this.container.removeChild(this.element)
    }
    
    cancel() {
        this.confirmContainer.setAttribute('hidden', 'hidden')
        this.delContainer.removeAttribute('hidden', 'hidden')
        this.delButton.focus()
        window.removeEventListener(this.boundEsc)
    }
    
    setUpAria() {
        let headingID = `heading-${this.randomNumber}`
        let descriptionID = `description-${this.randomNumber}`
        let heading = this.card.querySelector('[data-confirm-heading]')
        let description = this.card.querySelector('[data-confirm-description]')
        
        heading.setAttribute('id', headingID)
        description.setAttribute('id', descriptionID)
        this.confirmContainer.setAttribute('aria-labelledby', headingID)
        this.confirmContainer.setAttribute('aria-describedby', descriptionID)
        this.confirmButton.setAttribute('aria-controls', this.element.id)
    }
    
    addCard() {
        this.container.append(this.card)
    }
    
    getRandomInt(max = 42) {
       return Math.floor(Math.random() * Math.floor(max));
    }
}

data.forEach((data, index) => new Card(data, 'card-conttainer', 'data-card', index))

// NOTE: The keyboard support for the feed role is incomplete.
// For full implementation information see: https://w3c.github.io/aria-practices/#feed
class Feed {
    constructor(element, itemSelector) {
        this.element = element
        this.items = this.element.children
        
        this.setIndex()
        this.bind()
    }
    
    setIndex() {
        Array.from(this.items).forEach((item, index) => item.index = index)
    }
    
    bind() {
        Array.from(this.items).forEach(item => item.addEventListener('keydown', this.keyEventHandler.bind(this)))
    }
    
    keyEventHandler(event) {
        let target = event.target
        let key = event.key
        
        switch(key) {
            case 'PageUp':
                event.preventDefault()
                this.focusItem(target, -1)
                break;
            case 'PageDown':
                event.preventDefault()
                this.focusItem(target, 1)
                break;
        }
    }
    
    focusItem(currentElement, direction) {
        let targetIndex = currentElement.index + direction
        if (targetIndex < 0 || targetIndex > this.items.length) {
            return
        }
        this.items[targetIndex].focus()
    }
}

Array.from(document.querySelectorAll('[role="feed"]')).forEach(element => new Feed(element))
              
            
!
999px

Console