How can I use the CSS :target selector to create a reusable modal box

Viewed 74

I have a large collection of proverbs from an endangered language and I'd like to use a modal box for each to make different transcriptions available, without cluttering the page. I've created a div element that expands to show the optional information when clicked on using the CSS :target selector, and I'd like to be able to use that code over and over again for each proverb without creating an unique class or ID for each proverb. The problem is that clicking on a proverb in the middle of the list only expands the first div box. Is there a way to get the div box to open for each proverb that's clicked on without creating a unique expandable div for each proverb (I have hundreds of them)?

Here's the code I used:

#modalBox {
  display: block;
  position: relative;
  text-align: left;
  overflow: hidden;
  background: #ffffff;
  transition: all 0.4s;
  padding: 5px;
  margin-bottom: 2rem;
  height: 2.5rem;
  width: 100%;
  max-width: 800px;
}

#modalBox:target {
  height: 13.5rem;
}
<div class="proverbs">
  <div id="modalBox">
    <a href="#modalBox"> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</a>
    <p id="data"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p id="data"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p id="data"><b>English:</b> The hasty cat makes blind kittens.</p>
    </p>
    <a href="#" class="modalclose">&times;</a>
  </div>
</div>

3 Answers

I'm not sure how you could do this cleanly without at least some JavaScript. Here is a vanilla implementation to get you started. Instead of targeting precise ids, we can use a class name instead, and let the interpreter figure out which id is the clicked one.

The JavaScript runs through the HTML, dynamically adding incrementing ids (e.g., modalBox0, modalBox1, etc.) and the corresponding link which opens each modal (e.g., href='#modalBox0', href='#modalBox1', etc.).

const modals = document.querySelectorAll('.modalBox');

modals.forEach((modal, index) => {
  const id = `modalBox${index}`;
  modal.id = id
  const link = modal.querySelector('a');
  link.setAttribute('href', `#${id}`)
})
.modalBox {
  display: block;
  position: relative;
  text-align: left;
  overflow: hidden;
  background: #ffffff;
  transition: all 0.4s;
  padding: 5px;
  margin-bottom: 2rem;
  height: 2.5rem;
  width: 100%;
  max-width: 800px;
}

.modalBox:target {
  height: 13.5rem;
}
<div class="proverbs">
  <div class="modalBox">
    <a href="#"> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</a>
    <p id="data"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p id="data"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p id="data"><b>English:</b> The hasty cat makes blind kittens.</p>
    <a href="#" class="modalclose">&times;</a>
  </div>

  <div class="modalBox">
    <a href="#"> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</a>
    <p id="data"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p id="data"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p id="data"><b>English:</b> The hasty cat makes blind kittens.</p>
    <a href="#" class="modalclose">&times;</a>
  </div>
</div>

jsFiddle

There are several CSS-first approaches to producing this effect (which do without javascript mostly or entirely).

But one approach barely relies even on CSS: it's an HTML-first solution.

The details / summary pair of elements are the perfect semantic markup for presenting this kind of information.


Working Example:

.modalBox {
  display: block;
  position: relative;
  width: min(98%, 800px);
  padding: 5px;
  overflow: hidden;
}

.modalBox summary {
  font-style: italic;
  cursor: pointer;
}

.modalBox p {
  transform: translateY(30vh);
  transition: transform 0.3s linear;
}

.modalBox[open] p {
  transform: translateY(0);
}
<div class="proverbs">
  <details class="modalBox">
    <summary> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</summary>
    <p data-lang="ipa"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p data-lang="it"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p data-lang="en"><b>English:</b> The hasty cat makes blind kittens.</p>
  </details>
  
  <details class="modalBox">
    <summary> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</summary>
    <p data-lang="ipa"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p data-lang="it"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p data-lang="en"><b>English:</b> The hasty cat makes blind kittens.</p>
  </details>
  
  <details class="modalBox">
    <summary> A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</summary>
    <p data-lang="ipa"><b>IPA:</b> a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</p>
    <p data-lang="it"><b>Italian:</b> La gatta frettolosa fa i gattini ciechi.</p>
    <p data-lang="en"><b>English:</b> The hasty cat makes blind kittens.</p>
  </details>
</div>


Further Reading:

With 100+ proverbs you'll need JavaScript. This example takes an array of objects and inserts the content when the user either clicks the #next or #back <button>. The only thing you need to change is the data:

const proverbs = [
  {
    main: `A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.`,
    ipa: `a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ`,
    it: `La gatta frettolosa fa i gattini ciechi.`,
    en: `The hasty cat makes blind kittens.`
  },...
];
/*
Structure is [{main: 'first line', ipa: 'line of ipa', it: 'line of Italian',
 en: 'line of English'}, {second proverb}, {third proverb},...]
*/

Although it uses #id because the HTMLFormElement interface uses #id, [name], or index references -- I used #id because the CSS selector is easier to write. There is just a single modal that handles an unlimited amount of those 4 lined proverbs, so #id shouldn't be a problem and if they are you can use [name] attribute instead.

Details are commented in example

// Reference <form>
const UI = document.forms.UI;
// Array of proverbs and translations
const proverbs = [
  {main: `A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.`,
   ipa: `a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ`,
   it: `La gatta frettolosa fa i gattini ciechi.`,
   en: `The hasty cat makes blind kittens.`},
   {main: `010101010101010101010101010101010101010101`, ipa: `02`, it: `03`, en: `04`},
   {main: `1111111111111111111111111111111111111111111111111`, ipa: `12`, it: `13`, en: `14`},
   {main: `2121212121212121212121212121212121212121212121`, ipa: `22`, it: `23`, en: `24`},
   {main: `3131313131313131313131313131313131313131313131`, ipa: `32`, it: `33`, en: `34`},
   {main: `4141414141414141414141414141414141414141414141`, ipa: `42`, it: `43`, en: `44`},
   {main: `5151515151515151515151515151515151515151515151`, ipa: `52`, it: `53`, en: `54`}
];
// Define counter
let count = 0; 
// Bind <form> to the click event
UI.onclick = clickHandler;
// Event handler passes Event Object by default
function clickHandler(e) {
  // Reference <dialog>
  const modal = document.querySelector('.proverbs');
  // Reference what the user clicked
  const clk = e.target;
  // reference all <button>s and the <fieldset>
  const IO = this.elements;
  // If the user clicked #open, open <dialog>
  if (clk.matches('#open')) {
    clk.style.display = 'none';
    modal.showModal();
  }
  // If the user clicked #close, close <dialog>
  if (clk.matches('#close')) {
    IO.open.style.display = 'inline-flex';
    modal.close();
  }
  /*
  If the user clicked #next...
  ...increment count...
  ...if count is more than the total length of proverbs array...
  ...make count 0 (loop over to the start)...
  ...call move() pass count and IO
  */
  if (clk.matches('#next')) {
    ++count;
    if (count > (proverbs.length-1)) count = 0;
    move(count, IO);
  }
  // For the sake of brevity - it's the opposite of #next
  if (clk.matches('#back')) {
    --count;
    if (count < 0) count = proverbs.length -1;
    move(count, IO);
  }
}
// Pass in the counter and form control reference (IO) 
function move(i, fc) {
  // Reference <ul>
  const list = document.querySelector('.list');
  // htmlString of <legend> with new content
  const main = `<legend>${proverbs[i].main}</legend>`;
  // htmlstring of the new content of <ul>
  const items = `
        <li><b>IPA: </b>${proverbs[i].ipa}</li>
        <li><b>Italian: </b>${proverbs[i].it}</li>
        <li><b>English: </b>${proverbs[i].en}</li>`;
  // Remove the old <legend> and <li>
  fc.content.firstElementChild.remove();
  list.replaceChildren();
  // Insert the htmlStrings
  fc.content.insertAdjacentHTML('afterbegin', main);
  list.insertAdjacentHTML('beforeend', items);
}
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@300&family=Raleway:wght@500&display=swap');

html {
  font: 500 2ch/1.2 'Oswald'
}

dialog {
  position: relative;
  padding-top: 25px;
  border-radius: 8px;
  box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, 
              rgba(0, 0, 0, 0.23) 0px 3px 6px;
}

#content {
  border-radius: 4px;
}

legend {
  font-family: 'Raleway';
  font-weight: 900;
  margin-bottom: -8px;
}

#close {
  position: absolute;
  right: 4px;
  top: 4px;
  height: 1rem;
  font-size: 1.2rem;
}

ul {
  margin-left: 0px;
}

li {
  margin-bottom: 8px;
}

button {
  display: inline-flex;
  align-items: center;
  border: 2px outset rgb(227, 227, 227);
  border-radius: 4px;
  font: inherit;
  font-variant: small-caps;
  cursor: pointer;
}

.btn {
  float: right;
}

#back {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-right-radius: 0px;
  border-bottom-right-radius: 0px;
}

#next {
  border-top-left-radius: 0px;
  border-bottom-left-radius: 0px;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
<form id='UI'>
  <button id='open' type='button'>Open</button>
  <dialog class="proverbs">
    <button id="close" type='button'>&times;</button>
    <fieldset id='content'>
      <legend>A (g)attë mbrèšarolë facë i (g)attarièddë cëcatë.</legend>
      <ul class='list'>
        <li><b>IPA: </b>a atːə mˌbɾɪʃəˈɾolə fat͡ʃ i ˈatːaɾiˌɛdːə t͡ʃə katʰ</li>
        <li><b>Italian: </b>La gatta frettolosa fa i gattini ciechi.</li>
        <li><b>English: </b>The hasty cat makes blind kittens.</li>
      </ul>
      <button id='next' class='btn' type='button'>Next</button>
      <button id='back' class='btn' type='button'>Back</button>
    </fieldset>
  </dialog>
</form>

Related